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:
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.
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:
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:
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:
Complete
action to the tableHere'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:
Here's what this looks like now:
Now that our table is fixed, we can add a couple of tabs to our Task Resource:
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:
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.