name; } public static function getGlobalSearchResultDetails(Model $record): array { $donationCount = $record->donations()->count(); $totalDonated = $record->donations() ->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at')) ->sum('amount') / 100; $activeSG = $record->scheduledGivingDonations()->where('is_active', true)->count(); $details = [ 'Email' => $record->email, ]; if ($record->phone) { $details['Phone'] = $record->phone; } if ($donationCount > 0) { $details['Donations'] = $donationCount . ' (£' . number_format($totalDonated, 0) . ' total)'; } if ($activeSG > 0) { $details['Monthly Giving'] = $activeSG . ' active'; } return $details; } public static function getGlobalSearchResultActions(Model $record): array { return [ GlobalSearchAction::make('edit') ->label('Open Donor Profile') ->url(static::getUrl('edit', ['record' => $record])), ]; } public static function getGlobalSearchEloquentQuery(): Builder { return parent::getGlobalSearchEloquentQuery()->latest('created_at'); } public static function getGlobalSearchResultsLimit(): int { return 10; } // ─── Form (Edit screen) ────────────────────────────────────── public static function form(Form $form): Form { return $form ->schema([ Select::make('user_id') ->relationship('user', 'email') ->searchable() ->live() ->helperText('The website login account linked to this donor (if any).') ->disabled(), Grid::make()->schema([ Section::make('Personal Details')->schema([ Select::make('title') ->required() ->options(config('donate.titles')) ->columnSpan(1), TextInput::make('first_name') ->required() ->maxLength(255) ->columnSpan(2), TextInput::make('last_name') ->required() ->maxLength(255) ->columnSpan(2), ]) ->columns(5) ->columnSpan(1), Section::make('Contact Information')->schema([ TextInput::make('email') ->email() ->required() ->disabled(fn (\Filament\Forms\Get $get) => (bool) $get('user_id')) ->live() ->maxLength(255) ->copyable(), TextInput::make('phone') ->tel() ->maxLength(32) ->copyable(), ]) ->columns() ->columnSpan(1), ]), ]); } // ─── Table (List screen) ───────────────────────────────────── // This is the "Donor Lookup" — staff search by any field and // immediately see key context: are they a monthly giver? how many // donations? This helps them triage before even clicking in. public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('name') ->label('Donor') ->searchable(['first_name', 'last_name']) ->sortable(['first_name']) ->weight('bold') ->description(fn (Customer $record): ?string => $record->email), TextColumn::make('phone') ->label('Phone') ->searchable() ->copyable() ->placeholder('—'), TextColumn::make('confirmed_total') ->label('Total Donated') ->getStateUsing(function (Customer $record) { return $record->donations() ->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at')) ->sum('amount') / 100; }) ->money('gbp') ->sortable(query: function (Builder $query, string $direction) { $query->withSum([ 'donations as confirmed_total' => fn ($q) => $q->whereHas('donationConfirmation', fn ($q2) => $q2->whereNotNull('confirmed_at')) ], 'amount')->orderBy('confirmed_total', $direction); }) ->color(fn ($state) => $state >= 1000 ? 'success' : null) ->weight(fn ($state) => $state >= 1000 ? 'bold' : null), TextColumn::make('donations_count') ->label('Donations') ->counts('donations') ->sortable() ->badge() ->color(fn ($state) => match(true) { $state >= 50 => 'success', $state >= 10 => 'info', $state > 0 => 'gray', default => 'danger', }), TextColumn::make('monthly_giving') ->label('Monthly') ->getStateUsing(function (Customer $record) { $active = $record->scheduledGivingDonations()->where('is_active', true)->first(); if (!$active) return null; return '£' . number_format($active->total_amount, 0) . '/mo'; }) ->badge() ->color('success') ->placeholder('—'), TextColumn::make('gift_aid') ->label('Gift Aid') ->getStateUsing(function (Customer $record) { return $record->donations() ->whereHas('donationPreferences', fn ($q) => $q->where('is_gift_aid', true)) ->exists() ? 'Yes' : null; }) ->badge() ->color('success') ->placeholder('—'), TextColumn::make('last_donation') ->label('Last Donation') ->getStateUsing(function (Customer $record) { $last = $record->donations() ->whereHas('donationConfirmation', fn ($q) => $q->whereNotNull('confirmed_at')) ->latest() ->first(); return $last?->created_at; }) ->since() ->placeholder('Never'), ]) ->filters([ Filter::make('has_donations') ->label('Has donated') ->toggle() ->query(fn (Builder $q) => $q->has('donations')), Filter::make('monthly_supporter') ->label('Monthly supporter') ->toggle() ->query(fn (Builder $q) => $q->whereHas( 'scheduledGivingDonations', fn ($q2) => $q2->where('is_active', true) )), Filter::make('gift_aid') ->label('Gift Aid donors') ->toggle() ->query(fn (Builder $q) => $q->whereHas( 'donations', fn ($q2) => $q2->whereHas('donationPreferences', fn ($q3) => $q3->where('is_gift_aid', true)) )), Filter::make('major_donor') ->label('Major donors (£1000+)') ->toggle() ->query(function (Builder $q) { $q->whereHas('donations', function ($q2) { $q2->whereHas('donationConfirmation', fn ($q3) => $q3->whereNotNull('confirmed_at')); }, '>=', 1) ->withSum([ 'donations as total_confirmed' => fn ($q2) => $q2->whereHas('donationConfirmation', fn ($q3) => $q3->whereNotNull('confirmed_at')) ], 'amount') ->having('total_confirmed', '>=', 100000); }), Filter::make('incomplete_donations') ->label('Has incomplete donations') ->toggle() ->query(fn (Builder $q) => $q->whereHas( 'donations', fn ($q2) => $q2->whereDoesntHave('donationConfirmation', fn ($q3) => $q3->whereNotNull('confirmed_at')) ->where('created_at', '>=', now()->subDays(30)) )), Filter::make('recent') ->label('Joined last 30 days') ->toggle() ->query(fn (Builder $q) => $q->where('created_at', '>=', now()->subDays(30))), ]) ->actions([ EditAction::make() ->label('Open') ->icon('heroicon-o-arrow-right'), ]) ->defaultSort('created_at', 'desc') ->searchPlaceholder('Search by name, email, or phone...'); } public static function getRelations(): array { return [ DonationsRelationManager::class, ScheduledGivingDonationsRelationManager::class, AddressesRelationManager::class, InternalNotesRelationManager::class, ]; } public static function getPages(): array { return [ 'index' => ListCustomers::route('/'), 'edit' => EditCustomer::route('/{record}/edit'), ]; } }