Currently, we have Customers and Pipeline Stages in our system. Still, there is no easy way to move our Customers within the Pipeline while saving its history. Let's fix that by adding a table Action:
In this lesson, we will do the following:
- Create a
CustomerPipelineStage
Model to save the history of the Customer's Pipeline Stage changes and any comments added. - Add a custom Table Action to move customers to other pipeline stages.
- Add creating and updating action Observers to our Customer Model to save the history.
Creating Logs Model and Table
Our CustomerPipelineStage
Model will be a simple table with the following fields:
-
customer_id
- the Customer ID -
pipeline_stage_id
(nullable) - the Pipeline Stage ID. It is nullable to allow notes without status change. -
user_id
(nullable) - the User who made the change. It is nullable to allow system-triggered changes to be logged. -
notes
(nullable, text) - any notes added to the change
Let's create the Migration:
Migration
use App\Models\Customer;use App\Models\PipelineStage;use App\Models\User; // ... Schema::create('customer_pipeline_stages', function (Blueprint $table) { $table->id(); $table->foreignIdFor(Customer::class)->constrained(); $table->foreignIdFor(PipelineStage::class)->nullable()->constrained(); $table->foreignIdFor(User::class)->nullable()->constrained(); $table->text('notes')->nullable(); $table->timestamps();});
Next, we will create the Model:
app/Models/CustomerPipelineStage.php
use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo; class CustomerPipelineStage extends Model{ protected $fillable = [ 'customer_id', 'pipeline_stage_id', 'user_id', 'notes' ]; public function user(): BelongsTo { return $this->belongsTo(User::class); } public function customer(): BelongsTo { return $this->belongsTo(Customer::class); } public function pipelineStage(): BelongsTo { return $this->belongsTo(PipelineStage::class); }}
And finally, we will add the relationship to our Customer
Model:
app/Models/Customer.php
use Illuminate\Database\Eloquent\Relations\HasMany; // ... public function pipelineStageLogs(): HasMany{ return $this->hasMany(CustomerPipelineStage::class);}
That's it. We can run:
php artisan migrate:fresh --seed
And have the Database table ready for our customer logs.
Creating Table Action
Now that we have our Database and Models ready, we can implement the table Action:
use Filament\Notifications\Notification; // ... public static function table(Table $table): Table{ return $table ->columns([ // ... ]) ->filters([ // ... ]) ->actions([ Tables\Actions\EditAction::make(), Tables\Actions\Action::make('Move to Stage') ->icon('heroicon-m-pencil-square') ->form([ Forms\Components\Select::make('pipeline_stage_id') ->label('Status') ->options(PipelineStage::pluck('name', 'id')->toArray()) ->default(function (Customer $record) { $currentPosition = $record->pipelineStage->position; return PipelineStage::where('position', '>', $currentPosition)->first()?->id; }), Forms\Components\Textarea::make('notes') ->label('Notes') ]) ->action(function (Customer $customer, array $data): void { $customer->pipeline_stage_id = $data['pipeline_stage_id']; $customer->save(); $customer->pipelineStageLogs()->create([ 'pipeline_stage_id' => $data['pipeline_stage_id'], 'notes' => $data['notes'], 'user_id' => auth()->id() ]); Notification::make() ->title('Customer Pipeline Updated') ->success() ->send(); }), ]) ->bulkActions([ // ... ]);} // ...
Few things to note here:
- Our select will, by default, retrieve the next Pipeline Stage in the list so that we don't have to select it manually.
- Our
notes
are being written into a temporary field that will be removed from the Model at the update event. - We are sending a notification to the User after the update.
Here's what this looks like in the UI:
Creating Pipeline Stage Logger
The last thing to do is to create the Observers to log all the Pipeline Stage changes:
app/Models/Customer.php
// ... public static function booted(): void{ self::created(function (Customer $customer) { $customer->pipelineStageLogs()->create([ 'pipeline_stage_id' => $customer->pipeline_stage_id, 'user_id' => auth()->check() ? auth()->id() : null ]); });} // ...
This is going to listen for create event and do the following:
- Create a new
CustomerPipelineStage
record with thepipeline_stage_id
anduser_id
(if logged in) from the Customer.
That's it. In the next lesson, we will build the table filters.
before Creating Table Action you need to run php artisan migrate:fresh --seed the Creating Table Action it's done in the app\Filament\Resources\CustomerResource.php file
Updated to better reflect that migration is needed!
Could someone explain what the observer is doing here? Is it logging the pipeline change somewhere other than in the model? I have not used observers much or at all.
Thanks
Observers are meant to observe Models events (ex. created, updated). In our case, we are observing the
Created
event and creating new log item from this. This allows you to create the Customer from anywhere usingCustomer::create();
and it will always have the first log item createdSo this is equilavent to a MySQL trigger ?