Building a CRM with Filament 3, Part 3: Projects

Building a CRM with Filament 3, Part 3: Projects


Now, so far our CRM doesn't accomplish too much as we can only manage a list of our client information.

We want to be able to track projects for our clients, categorize and tag those projects, and eventually track billable time in the form of tasks for those projects.

We will break this part up into a few sections, but first we need a place to create projects for our clients.

Lets create a projects table:

1php artisan make:migration create_projects_table
1Schema::create('projects', function (Blueprint $table) {
2 $table->id();
3 $table->foreignIdFor(Customer::class);
4 $table->string('name');
5 $table->text('description')->nullable();
6 $table->boolean('billable')->default(false);
7 $table->timestamps();
8 $table->softDeletes();
9});

We don't need too much info so far, later our categories and tags will be many to many polymorphic relationships and will be stored elsewhere.

After we migrate, we can create out Project model:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\Relations\BelongsTo;
8use Illuminate\Database\Eloquent\Relations\BelongsToMany;
9use Illuminate\Database\Eloquent\SoftDeletes;
10 
11class Project extends Model
12{
13 use HasFactory,
14 SoftDeletes;
15 
16 protected $fillable = [
17 'customer_id',
18 'name',
19 'description',
20 'billable',
21 ];
22 
23 protected $casts = [
24 'billable' => 'boolean',
25 ];
26 
27 public function customer(): BelongsTo
28 {
29 return $this->belongsTo(Customer::class);
30 }
31 
32 public function users(): BelongsToMany
33 {
34 return $this->belongsToMany(User::class);
35 }
36}

Once we have our Project model, we can use Filament to generate a resource for us:

1php artisan make:filament-resource Project --generate

We should now have a Filament/ProjectResource folder as well as a Filament/ProjectResource.php file.

We will leave the folder alone for now, but the file should have been pre-generated by Filament and look something like this (I added a few extras such as users and customer):

1<?php
2 
3namespace App\Filament\Resources;
4 
5use App\Filament\Resources\ProjectResource\Pages;
6use App\Models\Project;
7use Filament\Forms\Components\Select;
8use Filament\Forms\Components\Textarea;
9use Filament\Forms\Components\TextInput;
10use Filament\Forms\Components\Toggle;
11use Filament\Forms\Form;
12use Filament\Resources\Resource;
13use Filament\Tables;
14use Filament\Tables\Table;
15use Illuminate\Database\Eloquent\Builder;
16use Illuminate\Database\Eloquent\SoftDeletingScope;
17 
18class ProjectResource extends Resource
19{
20 protected static ?string $model = Project::class;
21 
22 protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
23 
24 public static function form(Form $form): Form
25 {
26 return $form
27 ->schema([
28 TextInput::make('name')
29 ->columnSpanFull()
30 ->required(),
31 Textarea::make('comments')
32 ->columnSpanFull(),
33 Select::make('customer_id')
34 ->searchable()
35 ->preload()
36 ->required()
37 ->relationship(name: 'customer', titleAttribute: 'name'),
38 Select::make('user_id')
39 ->searchable()
40 ->preload()
41 ->placeholder('Search for people...')
42 ->required()
43 ->multiple()
44 ->relationship(name: 'users', titleAttribute: 'name'),
45 Toggle::make('billable'),
46 ]);
47 }
48 
49 public static function table(Table $table): Table
50 {
51 return $table
52 ->columns([
53 Tables\Columns\TextColumn::make('customer.name'),
54 Tables\Columns\TextColumn::make('name'),
55 Tables\Columns\IconColumn::make('billable')
56 ->boolean(),
57 ])
58 ->filters([
59 Tables\Filters\TrashedFilter::make(),
60 ])
61 ->actions([
62 Tables\Actions\EditAction::make(),
63 ]);
64 }
65 
66 public static function getPages(): array
67 {
68 return [
69 'index' => Pages\ListProjects::route('/'),
70 'create' => Pages\CreateProject::route('/create'),
71 'edit' => Pages\EditProject::route('/{record}/edit'),
72 ];
73 }
74 
75 public static function getEloquentQuery(): Builder
76 {
77 return parent::getEloquentQuery()
78 ->withoutGlobalScopes([
79 SoftDeletingScope::class,
80 ]);
81 }
82}

We should now have a working CRUD interface that looks like this:

Project CRUD

And a table that looks like this:

Project Table

In the next section we will be turning the CRUD into a wizard that looks like this:

Project Wizard

As well as a more informative table with filters:

Project Table 2

Project Table Filters

Stay tuned for part four where we will add polymorphic tags and categories to our projects as well as turn the form into a wizard.

Anthony Rappa

By Anthony Rappa

Hello! I'm a full stack developer from Long Island, New York. Working mainly with Laravel, Tailwind, Livewire, and Alpine.js (TALL Stack). I share everything I know about these tools and more, as well as any useful resources I find from the community. You can find me on GitHub and LinkedIn.