Real-World Scenario
You’re building a client portal for a marketing agency. Each client (team) needs to:- View their projects and campaigns
- See invoices and make payments
- Access reports and analytics
- Communicate with the agency team
- Upload and download files
The Business Model
Agency Structure:- Agency staff use the admin panel to manage all clients
- Each client gets their own team/workspace
- Clients can invite their own team members
- Each team is billed separately (subscription per client)
- Dedicated workspace for their company
- All their projects, files, and reports in one place
- Team collaboration (invite colleagues)
- Secure client portal experience
Database Structure
1. Projects Table
Each client has projects (campaigns, websites, etc.):Copy
php artisan make:migration create_projects_table
Copy
Schema::create('projects', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->text('description')->nullable();
$table->enum('type', ['website', 'seo', 'social_media', 'content', 'branding']);
$table->enum('status', ['proposal', 'in_progress', 'review', 'completed', 'on_hold']);
$table->date('start_date')->nullable();
$table->date('deadline')->nullable();
$table->decimal('budget', 10, 2)->nullable();
$table->timestamps();
$table->index(['team_id', 'status']);
});
2. Invoices Table
Track billing for each client:Copy
php artisan make:migration create_invoices_table
Copy
Schema::create('invoices', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
$table->string('invoice_number')->unique();
$table->decimal('amount', 10, 2);
$table->date('issue_date');
$table->date('due_date');
$table->enum('status', ['draft', 'sent', 'paid', 'overdue', 'cancelled'])->default('draft');
$table->text('notes')->nullable();
$table->timestamp('paid_at')->nullable();
$table->timestamps();
$table->index(['team_id', 'status']);
$table->index('invoice_number');
});
3. Reports Table
Monthly performance reports for clients:Copy
php artisan make:migration create_reports_table
Copy
Schema::create('reports', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
$table->foreignId('project_id')->nullable()->constrained()->nullOnDelete();
$table->string('title');
$table->enum('type', ['monthly', 'quarterly', 'campaign', 'custom']);
$table->date('period_start');
$table->date('period_end');
$table->json('metrics')->nullable(); // Store analytics data
$table->string('pdf_path')->nullable();
$table->timestamps();
$table->index(['team_id', 'period_start']);
});
4. Files Table
Document storage for each client:Copy
php artisan make:migration create_client_files_table
Copy
Schema::create('client_files', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
$table->foreignId('project_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('uploaded_by')->constrained('users');
$table->string('name');
$table->string('file_path');
$table->string('mime_type');
$table->unsignedBigInteger('size'); // bytes
$table->enum('category', ['contract', 'asset', 'report', 'other'])->default('other');
$table->timestamps();
$table->index(['team_id', 'category']);
});
Complete Model Implementation
Project Model with Business Logic
Copy
<?php
namespace App\Models;
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Project extends Model
{
protected $fillable = [
'team_id',
'name',
'description',
'type',
'status',
'start_date',
'deadline',
'budget',
];
protected $casts = [
'start_date' => 'date',
'deadline' => 'date',
'budget' => 'decimal:2',
];
// Automatic team scoping
protected static function booted(): void
{
static::addGlobalScope('team', function (Builder $builder) {
if (Filament::getTenant()) {
$builder->where('team_id', Filament::getTenant()->id);
}
});
static::creating(function (Project $project) {
if (!$project->team_id && Filament::getTenant()) {
$project->team_id = Filament::getTenant()->id;
}
});
}
// Relationships
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
public function reports(): HasMany
{
return $this->hasMany(Report::class);
}
public function files(): HasMany
{
return $this->hasMany(ClientFile::class);
}
// Business Logic Methods
public function isOverdue(): bool
{
return $this->deadline &&
$this->deadline->isPast() &&
!in_array($this->status, ['completed', 'on_hold']);
}
public function getDaysUntilDeadlineAttribute(): ?int
{
return $this->deadline ? now()->diffInDays($this->deadline, false) : null;
}
public function getStatusColorAttribute(): string
{
return match($this->status) {
'completed' => 'success',
'in_progress' => 'primary',
'review' => 'warning',
'on_hold' => 'danger',
default => 'gray',
};
}
}
Invoice Model with Payment Tracking
Copy
<?php
namespace App\Models;
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Invoice extends Model
{
protected $fillable = [
'team_id',
'invoice_number',
'amount',
'issue_date',
'due_date',
'status',
'notes',
'paid_at',
];
protected $casts = [
'amount' => 'decimal:2',
'issue_date' => 'date',
'due_date' => 'date',
'paid_at' => 'datetime',
];
protected static function booted(): void
{
static::addGlobalScope('team', function (Builder $builder) {
if (Filament::getTenant()) {
$builder->where('team_id', Filament::getTenant()->id);
}
});
static::creating(function (Invoice $invoice) {
if (!$invoice->team_id && Filament::getTenant()) {
$invoice->team_id = Filament::getTenant()->id;
}
// Auto-generate invoice number
if (!$invoice->invoice_number) {
$invoice->invoice_number = 'INV-' . strtoupper(uniqid());
}
});
// Auto-update status when paid
static::updating(function (Invoice $invoice) {
if ($invoice->isDirty('paid_at') && $invoice->paid_at) {
$invoice->status = 'paid';
}
});
}
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
// Business Logic
public function markAsPaid(): void
{
$this->update([
'status' => 'paid',
'paid_at' => now(),
]);
}
public function isOverdue(): bool
{
return $this->status !== 'paid' &&
$this->due_date->isPast();
}
public function getDaysOverdueAttribute(): ?int
{
if (!$this->isOverdue()) {
return null;
}
return now()->diffInDays($this->due_date);
}
}
Filament Resources
Project Resource (Client View)
Copy
<?php
namespace App\Filament\App\Resources;
use App\Models\Project;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class ProjectResource extends Resource
{
protected static ?string $model = Project::class;
protected static ?string $navigationIcon = 'heroicon-o-folder';
protected static ?int $navigationSort = 1;
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable()
->weight('bold'),
Tables\Columns\BadgeColumn::make('type')
->colors([
'primary' => 'website',
'success' => 'seo',
'warning' => 'social_media',
'info' => 'content',
'secondary' => 'branding',
]),
Tables\Columns\BadgeColumn::make('status')
->colors([
'secondary' => 'proposal',
'primary' => 'in_progress',
'warning' => 'review',
'success' => 'completed',
'danger' => 'on_hold',
]),
Tables\Columns\TextColumn::make('deadline')
->date('M d, Y')
->sortable()
->color(fn (Project $record) =>
$record->isOverdue() ? 'danger' : null
)
->description(fn (Project $record) =>
$record->isOverdue() ? 'Overdue!' : null
),
Tables\Columns\TextColumn::make('budget')
->money('USD')
->sortable(),
])
->filters([
Tables\Filters\SelectFilter::make('status')
->multiple()
->options([
'proposal' => 'Proposal',
'in_progress' => 'In Progress',
'review' => 'Review',
'completed' => 'Completed',
'on_hold' => 'On Hold',
]),
Tables\Filters\SelectFilter::make('type')
->multiple()
->options([
'website' => 'Website',
'seo' => 'SEO',
'social_media' => 'Social Media',
'content' => 'Content',
'branding' => 'Branding',
]),
])
->actions([
Tables\Actions\ViewAction::make(),
])
->defaultSort('created_at', 'desc');
}
public static function canCreate(): bool
{
return false; // Agency creates projects, not clients
}
}
Invoice Resource (Client View)
Copy
<?php
namespace App\Filament\App\Resources;
use App\Models\Invoice;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class InvoiceResource extends Resource
{
protected static ?string $model = Invoice::class;
protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
protected static ?int $navigationSort = 2;
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('invoice_number')
->searchable()
->sortable()
->copyable(),
Tables\Columns\TextColumn::make('amount')
->money('USD')
->sortable()
->weight('bold'),
Tables\Columns\TextColumn::make('issue_date')
->date('M d, Y')
->sortable(),
Tables\Columns\TextColumn::make('due_date')
->date('M d, Y')
->sortable()
->color(fn (Invoice $record) =>
$record->isOverdue() ? 'danger' : null
),
Tables\Columns\BadgeColumn::make('status')
->colors([
'secondary' => 'draft',
'primary' => 'sent',
'success' => 'paid',
'danger' => 'overdue',
'warning' => 'cancelled',
]),
])
->filters([
Tables\Filters\SelectFilter::make('status')
->options([
'sent' => 'Sent',
'paid' => 'Paid',
'overdue' => 'Overdue',
]),
])
->actions([
Tables\Actions\Action::make('pay')
->icon('heroicon-o-credit-card')
->color('success')
->visible(fn (Invoice $record) =>
in_array($record->status, ['sent', 'overdue'])
)
->url(fn (Invoice $record) =>
route('stripe.invoice.pay', $record)
),
Tables\Actions\Action::make('download')
->icon('heroicon-o-arrow-down-tray')
->url(fn (Invoice $record) =>
route('invoice.download', $record)
)
->openUrlInNewTab(),
Tables\Actions\ViewAction::make(),
])
->defaultSort('issue_date', 'desc');
}
public static function canCreate(): bool
{
return false; // Agency creates invoices
}
public static function canEdit(): bool
{
return false; // Clients can't edit invoices
}
}
Dashboard Widgets
Client Dashboard Widget
Copy
<?php
namespace App\Filament\App\Widgets;
use App\Models\Invoice;
use App\Models\Project;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class ClientDashboardStats extends BaseWidget
{
protected function getStats(): array
{
$activeProjects = Project::where('status', 'in_progress')->count();
$totalProjects = Project::count();
$completedProjects = Project::where('status', 'completed')->count();
$unpaidInvoices = Invoice::whereIn('status', ['sent', 'overdue'])->sum('amount');
$overdueInvoices = Invoice::where('status', 'overdue')->count();
return [
Stat::make('Active Projects', $activeProjects)
->description("{$completedProjects} completed")
->descriptionIcon('heroicon-o-check-circle')
->color('primary')
->chart([3, 5, 4, 6, 7, 5, $activeProjects]),
Stat::make('Outstanding Balance', '$' . number_format($unpaidInvoices, 2))
->description($overdueInvoices > 0 ? "{$overdueInvoices} overdue" : 'All current')
->descriptionIcon($overdueInvoices > 0 ? 'heroicon-o-exclamation-triangle' : 'heroicon-o-check')
->color($overdueInvoices > 0 ? 'danger' : 'success'),
Stat::make('This Month', 'Reports Ready')
->description('View your latest performance reports')
->descriptionIcon('heroicon-o-document-chart-bar')
->color('info')
->url(route('filament.app.resources.reports.index')),
];
}
}
Why This Example is Better
Real Business Logic
- ✅ Overdue detection for projects and invoices
- ✅ Automatic invoice numbering
- ✅ Payment tracking
- ✅ Status color coding
- ✅ Client-specific permissions
Complete Data Model
- ✅ Projects, Invoices, Reports, Files
- ✅ Proper relationships
- ✅ Indexed for performance
- ✅ Cascade deletes handled
Production-Ready Features
- ✅ Client can’t create/edit projects or invoices
- ✅ Payment integration hooks
- ✅ PDF download functionality
- ✅ File categorization
- ✅ Date-based filtering
UX Considerations
- ✅ Visual indicators for overdue items
- ✅ Quick actions (Pay, Download)
- ✅ Search and filters
- ✅ Responsive dashboard
- ✅ Clear navigation
Next Steps
- CRM System Example - Customer relationship management
- SaaS Platform Example - Subscription-based SaaS
- Learning Platform Example - Educational courses

