Next on our list - separating user roles. In our system, we need admins to manage the system settings and employees, while the employees themselves can only manage customers and nothing else:
In this lesson, we will do the following:
My Customers
- customers assigned to the employeeLet's start by creating our migration file:
Migration
Schema::create('roles', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps();});
Then, we can fill out our Model:
app/Models/Role.php
class Role extends Model{ protected $fillable = ['name'];}
Of course, we should also add some Seeders:
database/seeders/DatabaseSeeder.php
use App\Models\Role; public function run(): void{ $roles = [ 'Admin', 'Employee' ]; foreach ($roles as $role) { Role::create(['name' => $role]); } // ...}
That's it for our basic Role setup. We now have a table with two roles - Admin and Employee.
Next on our list - the User management. Let's start by adding a new column to the users' table and relating it to our Role model:
Migration
use App\Models\Role; // ... Schema::table('users', function (Blueprint $table) { $table->foreignIdFor(Role::class)->nullable()->constrained();});
Then, let's add a relationship and a simple isAdmin
check to our Model:
app/Models/User.php
use Illuminate\Database\Eloquent\Relations\BelongsTo; // ... protected $fillable = [ 'name', 'email', 'password', 'role_id', ]; public function role(): BelongsTo{ return $this->belongsTo(Role::class);} public function isAdmin(): bool{ if (!$this->relationLoaded('role')) { $this->load('role'); } return $this->role->name === 'Admin';}
Of course, we should modify our seeders:
database/seeders/DatabaseSeeder.php
public function run(): void{ $roles = [ 'Admin', 'Employee' ]; foreach ($roles as $role) { Role::create(['name' => $role]); } User::factory()->create([ 'name' => 'Test Admin', 'email' => 'admin@admin.com', 'role_id' => Role::where('name', 'Admin')->first()->id, ]); // We will seed 10 employees User::factory()->count(10)->create([ 'role_id' => Role::where('name', 'Employee')->first()->id, ]); // ...}
Then, we can finally create our CRUD resource:
php artisan make:filament-resource User --generate
This has created all the Resource files needed for our User management. Let's modify it:
app/Filament/Resources/UserResource.php
use Illuminate\Support\Facades\Hash; // ... class UserResource extends Resource{ protected static ?string $model = User::class; protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; protected static ?string $navigationGroup = 'Settings'; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\Select::make('role_id') ->searchable() ->preload() ->relationship('role', 'name'), Forms\Components\TextInput::make('name') ->required() ->maxLength(255), Forms\Components\TextInput::make('email') ->email() ->required() ->maxLength(255), Forms\Components\DateTimePicker::make('email_verified_at'), Forms\Components\TextInput::make('password') ->password() ->required() // https://filamentphp.com/docs/3.x/forms/advanced#auto-hashing-password-field ->dehydrateStateUsing(fn(string $state): string => Hash::make($state)) ->dehydrated(fn(?string $state): bool => filled($state)) ->required(fn(string $operation): bool => $operation === 'create') ->maxLength(255), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('role.name') ->numeric() ->sortable(), Tables\Columns\TextColumn::make('name') ->searchable(), Tables\Columns\TextColumn::make('email') ->searchable(), Tables\Columns\TextColumn::make('role.name') ->sortable(), Tables\Columns\TextColumn::make('email_verified_at') ->dateTime() ->sortable(), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('updated_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) // ... } // ...}
That's it! We can now load the Users page and see our users list:
Next on our list is a requirement for admins to assign employees to customers. This will allow an admin to see which employee is responsible for which customer:
Migration
use App\Models\User; // ... Schema::table('customers', function (Blueprint $table) { $table->foreignIdFor(User::class, 'employee_id')->nullable()->constrained('users');});
Then, in our Customer model, we can add a relationship:
app/Models/Customer.php
// ... protected $fillable = [ // ... 'pipeline_stage_id' 'pipeline_stage_id', 'employee_id',]; // ... public function employee(): BelongsTo{ return $this->belongsTo(User::class, 'employee_id');}
Last, we need to add a field to our Customer form:
app/Filament/Resources/CustomerResource.php
use App\Models\Role;use App\Models\User; // ... public static function form(Form $form): Form{ return $form ->schema([ Forms\Components\Section::make('Employee Information') ->schema([ Forms\Components\Select::make('employee_id') ->options(User::where('role_id', Role::where('name', 'Employee')->first()->id)->pluck('name', 'id')) ]) ->hidden(!auth()->user()->isAdmin()), Forms\Components\Section::make('Customer Details') ->schema([ // ... ]) // ... ]);} public static function table(Table $table): Table{ return $table ->columns([ Tables\Columns\TextColumn::make('employee.name') ->hidden(!auth()->user()->isAdmin()), Tables\Columns\TextColumn::make('first_name') ->label('Name') // ... ]) // ...}
This gives us a new field on our Customer form:
And once an Employee is selected - we can see that employee in our Customers list:
We need to add our Employee changes to our Customer History, as that is essential information to know in case of some mix-up. So, let's start by adding a new column to our history table:
Migration
use App\Models\User; // ... Schema::table('customer_pipeline_stages', function (Blueprint $table) { $table->foreignIdFor(User::class, 'employee_id')->nullable()->constrained('users');});
Then, we can add a relationship to our History model:
app/Models/CustomerPipelineStage.php
// ... protected $fillable = [ // ... 'notes' 'notes', 'employee_id']; // ... public function employee(): BelongsTo{ return $this->belongsTo(User::class, 'employee_id');}
Next, we need a way to add this information to our History. We can do this by using an observer on our Customer model:
app/Models/Customer.php
public static function booted(): void{ self::created(function (Customer $customer) { $customer->pipelineStageLogs()->create([ 'pipeline_stage_id' => $customer->pipeline_stage_id, 'employee_id' => $customer->employee_id, 'user_id' => auth()->check() ? auth()->id() : null ]); }); self::updated(function (Customer $customer) { $lastLog = $customer->pipelineStageLogs()->whereNotNull('employee_id')->latest()->first(); // Here, we will check if the employee has changed, and if so - add a new log if ($lastLog && $customer->employee_id !== $lastLog->employee_id) { $customer->pipelineStageLogs()->create([ 'employee_id' => $customer->employee_id, 'notes' => is_null($customer->employee_id) ? 'Employee removed' : '', 'user_id' => auth()->id() ]); } });}
Now, of course, we need to display this information in our History list:
resources/views/infolists/components/pipeline-stage-history-list.blade.php
<x-dynamic-component :component="$getEntryWrapperView()" :entry="$entry" class="grid grid-cols-[--cols-default] fi-in-component-ctn gap-6"> @foreach($getState() as $pipelineLog) <div class="mb-4"> <div class=""> <span class="font-bold">{{ $pipelineLog->user?->name ?? 'System' }}</span>, <span x-data="{}" x-tooltip="{ content: '{{ $pipelineLog->created_at }}', theme: $store.theme, }">{{ $pipelineLog->created_at->diffForHumans() }}</span> </div> <div class=""> <span class="font-bold">Pipeline Stage:</span> {{ $pipelineLog->pipelineStage->name }} </div> <div class="flex flex-col"> @if($pipelineLog->pipelineStage) <p> <span class="font-bold">Pipeline Stage:</span> {{ $pipelineLog->pipelineStage?->name }} </p> @endif @if($pipelineLog->employee) <p> <span class="font-bold">Assigned Employee:</span> {{ $pipelineLog->employee?->name }} </p> @endif </div> @if($pipelineLog->notes) <div class=""> <span class="font-bold">Note:</span> {{ $pipelineLog->notes }} </div> @endif </div> @endforeach</x-dynamic-component>
Now, if we update our Customer and assign an employee (or change it) - we should get a log entry like this:
That's it, now we have an entire history of our Customer changes.
Now that we can assign employees and see the History - we should work on our employees' access. Right now, if they were to access the panel - we would see everything:
To limit this, we will create a few policies:
php artisan make:policy CustomFieldPolicy --model=CustomFieldphp artisan make:policy LeadSourcePolicy --model=LeadSourcephp artisan make:policy PipelineStagePolicy --model=PipelineStagephp artisan make:policy TagPolicy --model=Tagphp artisan make:policy UserPolicy --model=User
Then, we can modify our policies:
Note: We will apply the same code to all policies. We only need the viewAny()
method at this point
app/Policies/CustomFieldPolicy.php
class CustomFieldPolicy{ public function viewAny(User $user): bool { return $user->isAdmin(); }}
app/Policies/LeadSourcePolicy.php
class LeadSourcePolicy{ public function viewAny(User $user): bool { return $user->isAdmin(); }}
app/Policies/PipelineStagePolicy.php
class PipelineStagePolicy{ public function viewAny(User $user): bool { return $user->isAdmin(); }}
app/Policies/TagPolicy.php
class TagPolicy{ public function viewAny(User $user): bool { return $user->isAdmin(); }}
app/Policies/UserPolicy.php
class UserPolicy{ public function viewAny(User $user): bool { return $user->isAdmin(); }}
Once this is done - we can refresh our page and see that our Employees have limited access:
Last on our list - we need to add a tab for our employees to see their customers. We will do this by adding a new tab to our Customers page:
app/Filament/Resources/CustomerResource/Pages/ListCustomers.php
public function getTabs(): array{ $tabs = []; $tabs['all'] = Tab::make('All Customers') ->badge(Customer::count()); if (!auth()->user()->isAdmin()) { $tabs['my'] = Tab::make('My Customers') ->badge(Customer::where('employee_id', auth()->id())->count()) ->modifyQueryUsing(function ($query) { return $query->where('employee_id', auth()->id()); }); } // ... return $tabs;}
Once this is added, our Customers will see a new tab:
In the next lesson, we will modify our Employee creation process to send an invitation to a custom registration page.