isConfirmed() ? '✓' : '✗'; return $status . ' £' . number_format($record->amount / 100, 2) . ' — ' . ($record->customer?->name ?? 'Unknown donor'); } public static function getGlobalSearchResultDetails(Model $record): array { return [ 'Cause' => $record->donationType?->display_name ?? '—', 'Date' => $record->created_at?->format('d M Y H:i'), 'Ref' => $record->reference_code ?? '—', 'Status' => $record->isConfirmed() ? 'Confirmed' : 'Incomplete', ]; } public static function getGlobalSearchResultActions(Model $record): array { return [ GlobalSearchAction::make('edit') ->label('View Donation') ->url(static::getUrl('edit', ['record' => $record])), ]; } public static function getGlobalSearchEloquentQuery(): Builder { return parent::getGlobalSearchEloquentQuery() ->with(['customer', 'donationType', 'donationConfirmation']) ->latest('created_at'); } public static function getGlobalSearchResultsLimit(): int { return 8; } // ─── Form (Edit/View screen) ───────────────────────────────── public static function form(Form $form): Form { return $form ->schema([ Section::make('Donation Details') ->collapsible() ->schema([ Grid::make(5)->schema([ Placeholder::make('Confirmed?') ->content(fn (Donation $record) => new HtmlString( $record->isConfirmed() ? '✓ Confirmed' : '✗ Incomplete' )), Placeholder::make('Amount') ->content(fn (Donation $record) => new HtmlString('' . Helpers::formatMoneyGlobal($record->donationTotal()) . '')), Placeholder::make('Admin Contribution') ->content(fn (Donation $record) => new HtmlString('' . Helpers::formatMoneyGlobal($record->donationAdminAmount()) . '')), Placeholder::make('Gift Aid?') ->content(fn (Donation $record) => new HtmlString( $record->isGiftAid() ? '✓ Yes' : 'No' )), Placeholder::make('Zakat?') ->content(fn (Donation $record) => new HtmlString( $record->isZakat() ? '✓ Yes' : 'No' )), ]), Fieldset::make('Donor') ->columns(3) ->schema([ Placeholder::make('name') ->content(fn (Donation $donation) => $donation->customer?->name ?? '—'), Placeholder::make('email') ->content(fn (Donation $donation) => $donation->customer?->email ?? '—'), Placeholder::make('phone') ->content(fn (Donation $donation) => strlen(trim($phone = $donation->customer?->phone ?? '')) > 0 ? $phone : new HtmlString('Not provided')), ]) ->visible(fn (Donation $donation) => (bool) $donation->customer), Fieldset::make('Address') ->columns(3) ->schema([ Placeholder::make('house') ->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->house ?? '')) ? $v : new HtmlString('')), Placeholder::make('street') ->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->street ?? '')) ? $v : new HtmlString('')), Placeholder::make('town') ->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->town ?? '')) ? $v : new HtmlString('')), Placeholder::make('state') ->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->state ?? '')) ? $v : new HtmlString('')), Placeholder::make('postcode') ->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->postcode ?? '')) ? $v : new HtmlString('')), Placeholder::make('country_code') ->content(fn (Donation $donation) => strlen(trim($v = $donation->address?->country_code ?? '')) ? $v : new HtmlString('')), ]) ->visible(fn (Donation $donation) => (bool) $donation->address), Fieldset::make('Allocation') ->schema([ Select::make('donation_type_id') ->label('Cause') ->relationship('donationType', 'display_name') ->required() ->disabled(), Select::make('donation_country_id') ->label('Country') ->relationship('donationCountry', 'name') ->required() ->disabled(), Select::make('appeal_id') ->label('Fundraiser') ->relationship('appeal', 'name') ->searchable() ->live() ->disabled() ->visible(fn (Donation $donation) => $donation->appeal_id !== null), Placeholder::make('ever_give_donation') ->content(fn (Donation $donation) => '£' . number_format( EverGiveDonation::where('donation_id', $donation->id)->value('amount') ?? 0, 2 ) )->visible(function (Donation $donation) { return EverGiveDonation::where('donation_id', $donation->id)->where('status', '!=', 'failed_to_send_to_ever_give_api')->first(); }), ]), ]), ]); } // ─── Table ──────────────────────────────────────────────────── // Designed for two workflows: // 1. "Find a specific donation" (search by donor name/email/ref) // 2. "What happened today?" (default sort, quick status scan) public static function table(Table $table): Table { return $table ->columns([ IconColumn::make('is_confirmed') ->label('') ->boolean() ->trueIcon('heroicon-o-check-circle') ->falseIcon('heroicon-o-x-circle') ->trueColor('success') ->falseColor('danger') ->tooltip(fn (Donation $d) => $d->isConfirmed() ? 'Payment confirmed' : 'Payment incomplete — may need follow-up'), TextColumn::make('created_at') ->label('Date') ->dateTime('d M Y H:i') ->sortable() ->description(fn (Donation $d) => $d->created_at?->diffForHumans()), TextColumn::make('customer.name') ->label('Donor') ->description(fn (Donation $d) => $d->customer?->email) ->searchable(query: function (Builder $query, string $search) { $query->whereHas('customer', fn ($q) => $q ->where('first_name', 'like', "%{$search}%") ->orWhere('last_name', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%") ); }) ->sortable(), TextColumn::make('amount') ->label('Amount') ->money('gbp', divideBy: 100) ->sortable() ->weight('bold'), TextColumn::make('donationType.display_name') ->label('Cause') ->badge() ->color('info') ->toggleable(), TextColumn::make('reoccurrence') ->label('Type') ->formatStateUsing(fn ($state) => match ((int) $state) { -1 => 'One-off', 2 => 'Monthly', default => DonationOccurrence::translate($state), }) ->badge() ->color(fn ($state) => match ((int) $state) { -1 => 'gray', 2 => 'success', default => 'warning', }), TextColumn::make('appeal.name') ->label('Fundraiser') ->toggleable() ->limit(25) ->placeholder('Direct'), TextColumn::make('reference_code') ->label('Ref') ->searchable() ->fontFamily('mono') ->copyable() ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('provider_reference') ->label('Payment Ref') ->searchable() ->fontFamily('mono') ->copyable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters(static::getTableFilters()) ->actions(static::getTableRowActions()) ->bulkActions(static::getBulkActions()) ->headerActions([ ExportAction::make('donations') ->visible(fn () => Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation')) ->label('Export') ->icon('heroicon-o-arrow-down-on-square') ->exporter(DonationExporter::class), ]) ->defaultSort('created_at', 'desc') ->searchPlaceholder('Search by donor name, email, or reference...') ->poll('30s'); } public static function getRelations(): array { return [ EventLogsRelationManager::class, InternalNotesRelationManager::class, ]; } public static function getPages(): array { return [ 'index' => ListDonations::route('/'), 'edit' => EditDonation::route('/{record}/edit'), ]; } // ─── Table Actions ─────────────────────────────────────────── // Quick actions that solve problems without navigating away. // "See a failed donation? → Resend receipt. See unknown donor? → Open profile." private static function getTableRowActions(): array { return [ Action::make('view_donor') ->label('Donor') ->icon('heroicon-o-user') ->color('gray') ->url(fn (Donation $d) => $d->customer_id ? CustomerResource::getUrl('edit', ['record' => $d->customer_id]) : null) ->visible(fn (Donation $d) => (bool) $d->customer_id) ->openUrlInNewTab(), ActionGroup::make([ ViewAction::make(), Action::make('send_receipt') ->label('Send Receipt') ->icon('heroicon-o-envelope') ->requiresConfirmation() ->modalDescription(fn (Donation $d) => 'Send receipt to ' . ($d->customer?->email ?? '?')) ->visible(fn (Donation $d) => $d->isConfirmed() && (Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation'))) ->action(function (Donation $donation) { try { Mail::to($donation->customer->email)->send(new DonationConfirmed($donation)); Notification::make()->title('Receipt sent to ' . $donation->customer->email)->success()->send(); } catch (Throwable $e) { Log::error($e); Notification::make()->title('Failed to send receipt')->body($e->getMessage())->danger()->send(); } }), Action::make('view_in_stripe') ->label('View in Stripe') ->icon('heroicon-o-arrow-top-right-on-square') ->visible(fn (Donation $d) => (bool) $d->provider_reference) ->url(fn (Donation $d) => 'https://dashboard.stripe.com/search?query=' . urlencode($d->provider_reference)) ->openUrlInNewTab(), Action::make('send_to_engage') ->label('Send to Engage') ->icon('heroicon-o-arrow-up-on-square') ->visible(fn (Donation $d) => $d->isConfirmed() && (Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation'))) ->action(function (Donation $donation) { dispatch(new SendDonation($donation)); Notification::make()->title('Sent to Engage')->success()->send(); }), Action::make('send_to_zapier') ->label('Send to Zapier') ->icon('heroicon-o-arrow-up-on-square') ->visible(fn (Donation $d) => $d->isConfirmed() && (Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation'))) ->action(function (Donation $donation) { dispatch(new SendCustomer($donation->customer)); Notification::make()->title('Sent to Zapier')->success()->send(); }), ]), ]; } private static function getBulkActions() { return BulkActionGroup::make([ BulkAction::make('send_receipt') ->label('Send Receipts') ->icon('heroicon-o-envelope') ->requiresConfirmation() ->visible(fn () => Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation')) ->action(function ($records) { $sent = 0; foreach ($records as $donation) { try { Mail::to($donation->customer->email)->send(new DonationConfirmed($donation)); $sent++; } catch (Throwable $e) { Log::error($e); } } Notification::make()->title($sent . ' receipts sent')->success()->send(); }), BulkAction::make('send_to_engage') ->label('Send to Engage') ->icon('heroicon-o-arrow-up-on-square') ->visible(fn () => Auth::user()?->hasRole('Superadmin') || Auth::user()?->hasPermissionTo('view-donation')) ->action(function ($records) { foreach ($records as $donation) { dispatch(new SendDonation($donation)); } Notification::make()->title(count($records) . ' sent to Engage')->success()->send(); }), ]); } // ─── Filters (drill-down within tabs) ─────────────────────── private static function getTableFilters(): array { return [ SelectFilter::make('donation_type_id') ->label('Cause') ->options(fn () => DonationType::orderBy('display_name')->pluck('display_name', 'id')) ->searchable(), SelectFilter::make('donation_country_id') ->label('Country') ->options(fn () => DonationCountry::orderBy('name')->pluck('name', 'id')) ->searchable(), Filter::make('date_range') ->form([ DatePicker::make('from')->label('From'), DatePicker::make('to')->label('To'), ]) ->query(function (Builder $query, array $data): Builder { return $query ->when($data['from'], fn (Builder $q) => $q->whereDate('created_at', '>=', $data['from'])) ->when($data['to'], fn (Builder $q) => $q->whereDate('created_at', '<=', $data['to'])); }) ->columns(2), ]; } }