Now that we have Employees - they usually have to perform specific Tasks with our Customers. For example, they might need to make a phone call to them or send over some documents. For that, we can build a Task system with a calendar view like this:
In this lesson, we will do the following:
- Create Task Model and Database
- Add the Create Task button to the Customer list
- Add Task list to the Customer page (view page)
- Add Task Resource with Tabs
- Add a Calendar page for Tasks
Create Task Model and Database
Let's start with our Models and Database structure:
Migration
use App\Models\Customer;use App\Models\User; // ... Schema::create('tasks', function (Blueprint $table) { $table->id(); $table->foreignIdFor(Customer::class)->constrained(); $table->foreignIdFor(User::class)->nullable()->constrained(); $table->text('description'); $table->date('due_date')->nullable(); $table->boolean('is_completed')->default(false); $table->timestamps();});
Then, we can fill our Model:
app/Models/Task.php
use Illuminate\Database\Eloquent\Relations\BelongsTo; class Task extends Model{ protected $fillable = [ 'customer_id', 'user_id', 'description', 'due_date', 'is_completed', ]; protected $casts = [ 'due_date' => 'date', 'is_completed' => 'boolean', ]; public function customer(): BelongsTo { return $this->belongsTo(Customer::class); } public function employee(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); }}
That's it. We have our base structure for the Task Model and Database.
Add Create Task Button to the Customer list
Next, we want to add a button to create a new task for each of our customers:
app/Filament/Resources/CustomerResource.php
// ... return $table // ... ->actions([ // ... Tables\Actions\Action::make('Add Task') ->icon('heroicon-s-clipboard-document') ->form([ Forms\Components\RichEditor::make('description') ->required(), Forms\Components\Select::make('user_id') ->preload() ->searchable() ->relationship('employee', 'name'), Forms\Components\DatePicker::make('due_date') ->native(false), ]) ->action(function (Customer $customer, array $data) { $customer->tasks()->create($data); Notification::make() ->title('Task created successfully') ->success() ->send(); }) ]) // ...
Before we load the page, we need to add a relationship to our Customer Model:
app/Models/Customer.php
// ... public function tasks(): HasMany{ return $this->hasMany(Task::class);} // ...
Now we can load the page and see these buttons:
Clicking on them will open a modal with a form to create a new Task:
Add Task List to the Customer Page
Now that we can create tasks, we cannot know what tasks are assigned to the Customer. Let's solve that by adding it to our Customer View page:
app/Filament/Resources/CustomerResource.php
use Filament\Infolists\Components\Tabs;use Filament\Infolists\Components\Actions\Action; // ... return $infolist ->schema([ // ... Section::make('Pipeline Stage History and Notes') ->schema([ ViewEntry::make('pipelineStageLogs') ->label('') ->view('infolists.components.pipeline-stage-history-list') ]) ->collapsible() ->collapsible(), Tabs::make('Tasks') ->tabs([ Tabs\Tab::make('Completed') ->badge(fn($record) => $record->completedTasks->count()) ->schema([ RepeatableEntry::make('completedTasks') ->hiddenLabel() ->schema([ TextEntry::make('description') ->html() ->columnSpanFull(), TextEntry::make('employee.name') ->hidden(fn($state) => is_null($state)), TextEntry::make('due_date') ->hidden(fn($state) => is_null($state)) ->date(), ]) ->columns() ]), Tabs\Tab::make('Incomplete') ->badge(fn($record) => $record->incompleteTasks->count()) ->schema([ RepeatableEntry::make('incompleteTasks') ->hiddenLabel() ->schema([ TextEntry::make('description') ->html() ->columnSpanFull(), TextEntry::make('employee.name') ->hidden(fn($state) => is_null($state)), TextEntry::make('due_date') ->hidden(fn($state) => is_null($state)) ->date(), TextEntry::make('is_completed') ->formatStateUsing(function ($state) { return $state ? 'Yes' : 'No'; }) ->suffixAction( Action::make('complete') ->button() ->requiresConfirmation() ->modalHeading('Mark task as completed?') ->modalDescription('Are you sure you want to mark this task as completed?') ->action(function (Task $record) { $record->is_completed = true; $record->save(); Notification::make() ->title('Task marked as completed') ->success() ->send(); }) ), ]) ->columns(3) ]) ]) ->columnSpanFull(), ]); // ...
With this, we expect two new relationships in our Customer Model:
app/Models/Customer.php
// ... public function completedTasks(): HasMany{ return $this->hasMany(Task::class)->where('is_completed', true);} public function incompleteTasks(): HasMany{ return $this->hasMany(Task::class)->where('is_completed', false);} // ...
These relationships will load specific information for displaying our RepeatableEntry
fields. Treat them as a way to filter the data.
Now, loading our Customer view - we can see the Tasks section:
In our incomplete tab - we can see the button to mark the task as completed:
Add Task Resource with Tabs
Creating tasks from the Customer page is nice - we should have a separate page for that. Let's create a new Resource for that:
php artisan make:filament-resource Task --generate
By default, Filament guessed the fields, but it's not entirely accurate:
Even our Create form is not quite right:
Let's fix both of these to be up to our standards. First, we will fix the table to show the correct information:
Note: We have replaced the whole table
app/Filament/Resources/TaskResource.php
use Filament\Notifications\Notification; // ... return $table ->columns([ Tables\Columns\TextColumn::make('customer.first_name') ->formatStateUsing(function ($record) { return $record->customer->first_name . ' ' . $record->customer->last_name; }) ->searchable(['first_name', 'last_name']) ->sortable(), Tables\Columns\TextColumn::make('employee.name') ->label('Employee') ->searchable() ->sortable(), Tables\Columns\TextColumn::make('description') ->html(), Tables\Columns\TextColumn::make('due_date') ->date() ->sortable(), Tables\Columns\IconColumn::make('is_completed') ->boolean(), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('updated_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ // ]) ->actions([ Tables\Actions\EditAction::make(), Tables\Actions\Action::make('Complete') ->hidden(fn(Task $record) => $record->is_completed) ->icon('heroicon-m-check-badge') ->modalHeading('Mark task as completed?') ->modalDescription('Are you sure you want to mark this task as completed?') ->action(function (Task $record) { $record->is_completed = true; $record->save(); Notification::make() ->title('Task marked as completed') ->success() ->send(); }) ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), ]), ]) ->defaultSort(function ($query) { return $query->orderBy('due_date', 'asc') ->orderBy('id', 'desc'); }); // ...
This fixed a couple of things:
- Employee name display
- Customer name display
- Added default sort by due date and id
- Added
Complete
action to the table
Here's what this looks like now:
Next, we need to fix our form:
Note: Once again, we have replaced the whole form
app/Filament/Resources/TaskResource.php
use App\Models\Customer; // ... return $form ->schema([ Forms\Components\Select::make('customer_id') ->searchable() ->relationship('customer') ->getOptionLabelFromRecordUsing(fn(Customer $record) => $record->first_name . ' ' . $record->last_name) ->searchable(['first_name', 'last_name']) ->required(), Forms\Components\Select::make('user_id') ->preload() ->searchable() ->relationship('employee', 'name'), Forms\Components\RichEditor::make('description') ->required() ->maxLength(65535) ->columnSpanFull(), Forms\Components\DatePicker::make('due_date'), Forms\Components\Toggle::make('is_completed') ->required(), ]); // ...
This fixes the following:
- Customer select field is now searchable by first/last names and displays the full name
- Employee select field is now searchable by name
- Description is now a RichEditor
Here's what this looks like now:
Adding Tabs to the Task Resource
Now that our table is fixed, we can add a couple of tabs to our Task Resource:
- My Tasks - filter for employees to only see their tasks
- All Tasks - displays all tasks in the system
- Completed Tasks - displays only completed tasks
- Incomplete Tasks - displays only incomplete tasks
app/Filament/Resources/TaskResource/Pages/ListTasks.php
use App\Models\Task;use Filament\Resources\Components\Tab; // ... public function getTabs(): array{ $tabs = []; if (!auth()->user()->isAdmin()) { $tabs[] = Tab::make('My Tasks') ->badge(Task::where('user_id', auth()->id())->count()) ->modifyQueryUsing(function ($query) { return $query->where('user_id', auth()->id()); }); } $tabs[] = Tab::make('All Tasks') ->badge(Task::count()); $tabs[] = Tab::make('Completed Tasks') ->badge(Task::where('is_completed', true)->count()) ->modifyQueryUsing(function ($query) { return $query->where('is_completed', true); }); $tabs[] = Tab::make('Incomplete Tasks') ->badge(Task::where('is_completed', false)->count()) ->modifyQueryUsing(function ($query) { return $query->where('is_completed', false); }); return $tabs;}
Here's what this looks like for admins:
And for employees:
Add a Calendar Page for Tasks
Last on our list is a custom page using Filament FullCalendar plugin. Let's install the plugin via composer:
composer require saade/filament-fullcalendar:^3.0
Then we need to register it in our AdminPanelProvider
:
app/Filament/Providers/AdminPanelProvider.php
use Saade\FilamentFullCalendar\FilamentFullCalendarPlugin; // ... return $panel // ... ->authMiddleware([ Authenticate::class, ]) ->plugins([ FilamentFullCalendarPlugin::make() ]); // ...
Once that is done, we can create a livewire page:
php artisan make:filament-page TaskCalendar
This should create the file app/Filament/Pages/TaskCalendar.php
. Next, we need to create a widget for it:
php artisan make:filament-widget TaskCalendar
Make sure that your settings are correct:
Note: We are aiming to create a livewire widget here!
Once that is done, you should have a new file app/Livewire/TaskCalendarWidget.php
, which means we can add the widget to our page:
resources/views/filament/pages/task-calendar.blade.php
<x-filament-panels::page> @livewire(App\Livewire\TaskCalendarWidget::class)</x-filament-panels::page>
Now, all we have to do - is modify our widget itself:
app/Livewire/TaskCalendarWidget.php
use Filament\Widgets\Widget;use App\Filament\Resources\TaskResource;use App\Models\Task;use Saade\FilamentFullCalendar\Data\EventData;use Saade\FilamentFullCalendar\Widgets\FullCalendarWidget; class TaskCalendarWidget extends Widgetclass TaskCalendarWidget extends FullCalendarWidget{ protected static string $view = 'livewire.task-calendar-widget'; public function fetchEvents(array $fetchInfo): array { return Task::query() ->where('due_date', '>=', $fetchInfo['start']) ->where('due_date', '<=', $fetchInfo['end']) ->when(!auth()->user()->isAdmin(), function ($query) { return $query->where('user_id', auth()->id()); }) ->get() ->map( fn(Task $task) => EventData::make() ->id($task->id) ->title(strip_tags($task->description)) ->start($task->due_date) ->end($task->due_date) ->url(TaskResource::getUrl('edit', [$task->id])) ->toArray() ) ->toArray(); }}
Since we removed the $view
from our widget, we can delete this file resources/views/livewire/task-calendar-widget.blade.php
Now we can go to our page and see the calendar:
That's it. We have a working Task system now with a calendar view. If you wish to modify what the calendar does, you can read the plugin documentation.
Question how do we go about changing the time for the task the only thing coming up is 12a in front of every task?
If you need the time - you need to have
dateTime
column on your tasks and not justdate
. That is indeed a limitation on the calendar integration - it sets 12am as default date if none is provided.If you add this: ->allDay(true)
to the EventData::make() you will render them without time, so it won't display 12a or 00 in front of the events.
Helo, thanks for wonderful course. I hit a problem I am not able to solve. In CustomerResource Infolist on Tabs with Tasks there is ->suffixAction at is_completed TextEntry. If I use the action with the closure: ->action(function (Task $record) { $record->is_completed = true; $record->save(); i got an axception, that the model Customer instead of model Task was provided to the closure. It seems it injects the $record of the main record of the infolist = Customer and not the actual record of the repeatable entry.
Hi guys,
I just found out that something strange happens with table action form.
When we select employee in the form, somehow it automatically updates the $customer employee_id to selected user_id...
->action(function (Customer $customer, array $data) { dd($customer, Customer::find($customer->id)); })
It is happening when we use ->relationship('employee', 'name'), to fix it we can change it to ->options(User::all()->pluck('name', 'id')) but it is strange it is doing that in the background.
Hi, I'm not sure I fully understood the issue you are facing here.
Does this happen on an action when you have more than one row selected? What about just a single row selected, is it working as expected?
It happens on this form:
Tables\Actions\Action::make('Add Task') ->icon('heroicon-s-clipboard-document') ->form([ Forms\Components\RichEditor::make('description') ->required(), Forms\Components\Select::make('user_id') ->preload() ->searchable() ->relationship('employee', 'name'), Forms\Components\DatePicker::make('due_date') ->native(false),
Its per record
Hmm, I have not experienced this. Could you dump the
$data
usingdd($data)
and see what it returns? As it shouldn't set the employee id, unless you have an employee ID on the form...Or maybe something happens if you have more than 1 record selected and try to create a task - not sure
Its not problem in the data. Forms\Components\Select::make('user_id') ->preload() ->searchable() ->relationship('employee', 'name')
This like auto-populates employee_id when it is using relationship, if you dd($customer) you can see it changes it when the select changes..
This is weird. I don't see such behaviour, and I don't remember it being like this...
I new and learing. If I use UTC time on the app and have a timezone field for my user, how can I have the text column adjust from UTC to customer's time zone? This is probably easy to do but i've searched and couldn't find anything specific to filament. Right now I have a text column that shows the timestamp from my database, I really need to recalucate the proper time before displaying. Any help will be greatly appreciated. Thank you.
Hi, then this course would help you:
//course/laravel-user-timezones?mtm_campaign=search-results-course
Thank you very much. Please when i add this to CustomerResource below i'm encountering an error.
Tables\Actions\Action::make('Add Task') ->icon('heroicon-s-clipboard-document') ->form([ Forms\Components\RichEditor::make('description') ->required(), Forms\Components\Select::make('user_id') ->preload() ->searchable() ->relationship('employee', 'name'), Forms\Components\DatePicker::make('due_date') ->native(false),
What is the error?
I've fixed that. thank you.
O lecture fifteen. Im havig difficultyu in creating a Calendar widget. It shows
Unable to find component: [App\Livewire\TaskCalendarWidget]
after following your procedures. Thanks for quick response.
It seems that you don't have the widget class declared correctly. Please check the file that's below the
blade
code snippetResolved. Thank you so much.