name; } public static function getGlobalSearchResultDetails(Model $record): array { $pct = $record->amount_to_raise > 0 ? round($record->amount_raised / $record->amount_to_raise * 100) : 0; return [ 'Progress' => '£' . number_format($record->amount_raised / 100, 0) . ' / £' . number_format($record->amount_to_raise / 100, 0) . " ({$pct}%)", 'Status' => $record->is_accepting_donations ? 'Live' : 'Closed', 'Owner' => $record->user?->name ?? '—', ]; } public static function getGlobalSearchResultActions(Model $record): array { return [ GlobalSearchAction::make('edit') ->label('Open Fundraiser') ->url(static::getUrl('edit', ['record' => $record])), ]; } public static function getGlobalSearchEloquentQuery(): Builder { return parent::getGlobalSearchEloquentQuery() ->with('user') ->latest('created_at'); } // ─── Form ──────────────────────────────────────────────────── public static function form(Form $form): Form { return $form ->schema([ Fieldset::make('Connections')->schema([ Select::make('parent_appeal_id') ->label('Parent Fundraiser') ->relationship('parent', 'name', modifyQueryUsing: fn ($query) => $query->whereNull('parent_appeal_id')) ->required(false) ->searchable(), Select::make('user_id') ->label('Page Owner') ->searchable() ->relationship('user', 'name') ->required(), ]), Section::make('General Info')->schema([ Fieldset::make('Info')->schema([ TextInput::make('name') ->label('Fundraiser Name') ->required() ->maxLength(255), TextInput::make('slug') ->label('URL Slug') ->required() ->maxLength(255) ->unique( table: 'appeals', column: 'slug', ignorable: $form->getRecord(), modifyRuleUsing: fn ($rule) => $rule->whereNull('deleted_at') ) ->disabled(), TextInput::make('description') ->label('Short Description') ->required() ->maxLength(512), ]), Fieldset::make('Target & Allocation')->schema([ TextInput::make('amount_to_raise') ->label('Fundraising Target') ->numeric() ->minValue(150) ->maxValue(999_999_999) ->prefix('£') ->required(), TextInput::make('amount_raised') ->label('Amount Raised So Far') ->numeric() ->prefix('£') ->disabled(), Select::make('donation_type_id') ->label('Cause') ->relationship('donationType', 'display_name') ->searchable() ->preload() ->live() ->required(), Select::make('donation_country_id') ->label('Country') ->required() ->visible(function (\Filament\Forms\Get $get) { $donationTypeId = $get('donation_type_id'); if (!($donationType = DonationType::find($donationTypeId))) { return false; } return $donationType->donationCountries()->count() > 1; }) ->options(function (\Filament\Forms\Get $get) { $donationTypeId = $get('donation_type_id'); if (!($donationType = DonationType::find($donationTypeId))) { return []; } return $donationType->donationCountries->pluck('name', 'id')->toArray(); }) ->live(), ]), Fieldset::make('Settings')->schema([ Toggle::make('is_visible') ->label('Visible on website'), Toggle::make('is_in_memory') ->label('In memory of someone') ->live(), TextInput::make('in_memory_name') ->label('In memory of') ->required(fn (\Filament\Forms\Get $get) => $get('is_in_memory')) ->visible(fn (\Filament\Forms\Get $get) => $get('is_in_memory')), Toggle::make('is_accepting_donations') ->label('Accepting donations'), Toggle::make('is_team_campaign') ->label('Team fundraiser'), Toggle::make('is_accepting_members') ->label('Accepting team members') ->live() ->visible(fn (\Filament\Forms\Get $get) => $get('is_team_campaign')), ]), ]) ->collapsible() ->collapsed(), Section::make('Content')->schema([ FileUpload::make('picture') ->label('Cover Image') ->required() ->columnSpanFull(), RichEditor::make('story') ->label('Fundraiser Story') ->required() ->minLength(150) ->columnSpanFull(), ]) ->collapsible() ->collapsed(), ]); } // ─── Table ──────────────────────────────────────────────────── // Designed for the "Fundraiser Nurture" journey: // Jasmine opens this page and immediately sees which fundraisers // need attention, which are succeeding, which are stale. public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('name') ->label('Fundraiser') ->searchable() ->sortable() ->weight('bold') ->limit(40) ->description(fn (Appeal $a) => $a->user?->name ? 'by ' . $a->user->name : null) ->tooltip(fn (Appeal $a) => $a->name), TextColumn::make('status_label') ->label('Status') ->getStateUsing(function (Appeal $a) { if ($a->status === 'pending') return 'Pending Review'; if (!$a->is_accepting_donations) return 'Closed'; return 'Live'; }) ->badge() ->color(fn ($state) => match ($state) { 'Live' => 'success', 'Pending Review' => 'warning', 'Closed' => 'gray', default => 'gray', }), TextColumn::make('progress') ->label('Progress') ->getStateUsing(function (Appeal $a) { $raised = $a->amount_raised / 100; $target = $a->amount_to_raise / 100; $pct = $target > 0 ? round($raised / $target * 100) : 0; return '£' . number_format($raised, 0) . ' / £' . number_format($target, 0) . " ({$pct}%)"; }) ->color(function (Appeal $a) { $pct = $a->amount_to_raise > 0 ? $a->amount_raised / $a->amount_to_raise : 0; if ($pct >= 1) return 'success'; if ($pct >= 0.5) return 'info'; if ($pct > 0) return 'warning'; return 'danger'; }) ->weight('bold'), TextColumn::make('donationType.display_name') ->label('Cause') ->badge() ->color('info') ->toggleable(), TextColumn::make('nurture_status') ->label('Needs Attention?') ->getStateUsing(function (Appeal $a) { if ($a->status !== 'confirmed') return null; if (!$a->is_accepting_donations) return null; $raised = $a->amount_raised; $target = $a->amount_to_raise; $age = $a->created_at?->diffInDays(now()) ?? 0; if ($raised == 0 && $age > 7) return '🔴 No donations yet'; if ($raised > 0 && $raised >= $target * 0.8 && $raised < $target) return '🟡 Almost there!'; if ($raised >= $target) return '🟢 Target reached!'; if ($raised > 0 && $age > 30) return '🟠 Slowing down'; return null; }) ->placeholder('—') ->wrap(), TextColumn::make('created_at') ->label('Created') ->since() ->sortable() ->description(fn (Appeal $a) => $a->created_at?->format('d M Y')), ]) ->filters([]) ->actions([ Action::make('view_page') ->label('View Page') ->icon('heroicon-o-eye') ->color('gray') ->url(fn (Appeal $a) => 'https://www.charityright.org.uk/fundraiser/' . $a->slug) ->openUrlInNewTab(), ActionGroup::make([ EditAction::make(), Action::make('email_owner') ->label('Email Owner') ->icon('heroicon-o-envelope') ->url(fn (Appeal $a) => $a->user?->email ? 'mailto:' . $a->user->email : null) ->visible(fn (Appeal $a) => (bool) $a->user?->email) ->openUrlInNewTab(), Action::make('send_to_engage') ->label('Send to Engage') ->icon('heroicon-o-arrow-up-on-square') ->action(function (Appeal $appeal) { dispatch(new SendAppeal($appeal)); Notification::make()->title('Sent to Engage')->success()->send(); }), Action::make('appeal_owner') ->label('Page Owner Details') ->icon('heroicon-o-user') ->modalContent(fn (Appeal $appeal): View => view('filament.fields.appeal-owner', ['appeal' => $appeal])) ->modalWidth(MaxWidth::Large) ->modalSubmitAction(false) ->modalCancelAction(false), ]), ]) ->bulkActions([ BulkAction::make('send_to_engage') ->label('Send to Engage') ->icon('heroicon-o-arrow-up-on-square') ->action(function ($records) { foreach ($records as $appeal) { dispatch(new SendAppeal($appeal)); } Notification::make()->title(count($records) . ' sent to Engage')->success()->send(); }), BulkAction::make('send_to_wp') ->label('Sync to WordPress') ->icon('heroicon-o-globe-alt') ->action(function ($records) { foreach ($records as $appeal) { dispatch(new SyncAppeal($appeal)); } Notification::make()->title(count($records) . ' synced to WordPress')->success()->send(); }), ]) ->defaultSort('created_at', 'desc') ->searchPlaceholder('Search by fundraiser name...'); } public static function getRelations(): array { return [ AppealDonationsRelationManager::class, AppealChildrenRelationManager::class, ]; } public static function getPages(): array { return [ 'index' => ListAppeals::route('/'), 'edit' => EditAppeal::route('/{record}/edit'), ]; } }