schema([ // AI Review Summary (if available) Section::make('AI Review') ->icon('heroicon-o-sparkles') ->description('Automated compliance check results') ->schema([ Placeholder::make('ai_summary') ->label('') ->content(function () { $extra = json_decode($this->record->extra_data ?? '{}', true); $ai = $extra['ai_review'] ?? null; if (!$ai) { return new HtmlString( '
' . '

No AI review has been run yet. Click "Run AI Review" above to check this fundraiser.

' . '
' ); } $decision = $ai['decision'] ?? 'unknown'; $confidence = round(($ai['confidence'] ?? 0) * 100); $summary = $ai['summary'] ?? ''; $reasons = $ai['reasons'] ?? []; $flags = $ai['flags'] ?? []; $colorMap = ['approve' => 'green', 'reject' => 'red', 'review' => 'amber']; $iconMap = ['approve' => '✓', 'reject' => '✗', 'review' => '?']; $labelMap = ['approve' => 'Safe to Approve', 'reject' => 'Should Be Rejected', 'review' => 'Needs Your Judgment']; $color = $colorMap[$decision] ?? 'gray'; $icon = $iconMap[$decision] ?? '?'; $label = $labelMap[$decision] ?? 'Unknown'; $html = "
"; $html .= "
"; $html .= "{$icon}"; $html .= "
"; $html .= "

{$label}

"; $html .= "

Confidence: {$confidence}%

"; $html .= "
"; $html .= "

{$summary}

"; if (!empty($reasons)) { $html .= ""; } if (!empty($flags)) { $html .= "
"; foreach ($flags as $flag) { $html .= "{$flag}"; } $html .= "
"; } $html .= "
"; return new HtmlString($html); }), ]) ->collapsible(), // Fundraiser Details Section::make('Fundraiser Details') ->icon('heroicon-o-document-text') ->description('What the supporter submitted') ->schema([ Fieldset::make('Basic Info')->schema([ Placeholder::make('appeal_name') ->label('Fundraiser Name') ->content(fn () => $this->record->appeal?->name ?? '—'), Placeholder::make('appeal_owner') ->label('Created By') ->content(fn () => ($this->record->appeal?->user?->name ?? '—') . ' (' . ($this->record->appeal?->user?->email ?? '') . ')'), Placeholder::make('appeal_type') ->label('Cause') ->content(fn () => $this->record->appeal?->donationType?->display_name ?? '—'), Placeholder::make('appeal_target') ->label('Fundraising Goal') ->content(fn () => '£' . number_format($this->record->appeal?->amount_to_raise ?? 0, 0)), Placeholder::make('appeal_status') ->label('Current Status') ->content(function () { $status = $this->record->status; return match ($status) { 'pending' => new HtmlString('⏳ Waiting for Review'), 'confirmed' => new HtmlString('✓ Approved'), 'change_requested' => new HtmlString('✗ Changes Requested'), default => ucfirst($status), }; }), Placeholder::make('submission_type') ->label('Submission Type') ->content(function () { return match ($this->record->action) { 'Create' => 'Brand new fundraiser', 'Update' => 'Editing an existing fundraiser', default => $this->record->action, }; }), ])->columns(3), Placeholder::make('appeal_description') ->label('Description') ->content(fn () => $this->record->appeal?->description ?? '—') ->columnSpanFull(), Placeholder::make('appeal_story') ->label('Story') ->content(fn () => new HtmlString( '
' . ($this->record->appeal?->story ?? 'No story provided') . '
' )) ->columnSpanFull(), Placeholder::make('appeal_image') ->label('Cover Image') ->content(function () { $url = $this->record->appeal?->getPictureUrl(); if (!$url) return new HtmlString('No image'); return new HtmlString( "" ); }) ->columnSpanFull(), ]) ->collapsible(), // What Changed (for updates only) Section::make('What Changed') ->icon('heroicon-o-pencil-square') ->description('Fields modified since the last approved version') ->visible(fn () => $this->record->action === 'Update') ->schema([ Placeholder::make('changes_list') ->label('') ->content(function () { $changes = json_decode($this->record->extra_data ?? '[]', true); // Handle nested ai_review structure if (isset($changes['previous_extra_data'])) { $changes = json_decode($changes['previous_extra_data'] ?? '[]', true); } if (!is_array($changes) || empty($changes)) { return 'No specific changes recorded.'; } $friendlyNames = [ 'name' => 'Fundraiser name', 'description' => 'Description', 'story' => 'Story content', 'picture' => 'Cover image', 'amount_to_raise' => 'Fundraising goal', 'donation_type_id' => 'Cause', 'donation_country_id' => 'Country', 'is_in_memory' => 'In memory setting', 'in_memory_name' => 'Memorial name', 'is_visible' => 'Visibility', 'is_team_campaign' => 'Team campaign setting', 'is_accepting_members' => 'Member acceptance', 'is_custom_story' => 'Custom story setting', 'expires_at' => 'End date', 'parent_appeal_id' => 'Parent fundraiser', ]; $html = ''; return new HtmlString($html); }), ]) ->collapsible() ->collapsed(), ]); } public function getHeading(): string { return 'Review: ' . ($this->record->appeal?->name ?? 'Unknown Fundraiser'); } public function getSubheading(): string { return match ($this->record->status) { 'pending' => 'This fundraiser is waiting for your review. Check the details below and approve or request changes.', 'confirmed' => 'This fundraiser has been approved and is live on the website.', 'change_requested' => 'Changes have been requested. The fundraiser creator has been notified.', default => '', }; } protected function mutateFormDataBeforeFill(array $data): array { return $data; } protected function getHeaderActions(): array { return [ Action::make('ai_review') ->label('Run AI Review') ->icon('heroicon-o-sparkles') ->color('info') ->visible(fn () => $this->record->status === 'pending') ->action(function () { $service = app(AIAppealReviewService::class); $result = $service->review($this->record->appeal); $this->record->update([ 'extra_data' => json_encode([ 'ai_review' => $result, 'previous_extra_data' => $this->record->extra_data, ]), ]); $decisionLabels = [ 'approve' => 'AI recommends APPROVING', 'reject' => 'AI recommends REJECTING', 'review' => 'AI is UNCERTAIN — needs your judgment', ]; Notification::make() ->title($decisionLabels[$result['decision']] ?? 'Review complete') ->body($result['summary']) ->color(match ($result['decision']) { 'approve' => 'success', 'reject' => 'danger', default => 'warning', }) ->persistent() ->send(); $this->fillForm(); }), Action::make('approve') ->label('Approve Fundraiser') ->icon('heroicon-o-check-circle') ->color('success') ->requiresConfirmation() ->modalHeading('Approve this fundraiser?') ->modalDescription(fn () => "This will make \"{$this->record->appeal?->name}\" visible on the CharityRight website. The fundraiser creator will be notified by email.") ->modalSubmitActionLabel('Yes, Approve') ->visible(fn () => $this->record->status === 'pending') ->action(function () { app(ApprovalQueueService::class)->approveAppeal($this->record); Notification::make() ->title('Fundraiser approved! 🎉') ->body('The fundraiser is now live and the creator has been notified.') ->success() ->send(); return redirect()->route('filament.admin.resources.approval-queues.index'); }), Action::make('request_changes') ->label('Request Changes') ->icon('heroicon-o-pencil-square') ->color('warning') ->visible(fn () => $this->record->status === 'pending') ->form([ Textarea::make('message') ->label('What needs to change?') ->placeholder("Tell the fundraiser creator what to fix. Be specific and friendly.\n\nExample: \"Your story mentions a travel company — fundraisers must be about charitable causes only. Please rewrite your story to focus on the people you're helping.\"") ->required() ->rows(4), ]) ->action(function (array $data) { $this->record->update(['message' => $data['message']]); app(ApprovalQueueService::class)->requestChange($this->record); Notification::make() ->title('Change request sent') ->body('The fundraiser creator has been notified and asked to make changes.') ->warning() ->send(); return redirect()->route('filament.admin.resources.approval-queues.index'); }), Action::make('view_appeal') ->label('Open Fundraiser') ->icon('heroicon-o-arrow-top-right-on-square') ->color('gray') ->url(fn () => $this->record->appeal_id ? AppealResource::getUrl('edit', ['record' => $this->record->appeal_id]) : null) ->openUrlInNewTab(), ]; } protected function getSavedNotification(): ?Notification { return null; // We handle notifications in our custom actions } }