count(); return $count > 0 ? (string) $count : null; } public static function getNavigationBadgeColor(): ?string { $count = ApprovalQueue::where('status', 'pending')->count(); return $count > 10 ? 'danger' : ($count > 0 ? 'warning' : 'success'); } public static function form(Form $form): Form { return $form->schema([]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('status') ->label('Status') ->sortable() ->badge() ->color(fn (string $state) => match ($state) { 'pending' => 'warning', 'confirmed' => 'success', 'change_requested' => 'danger', default => 'gray', }) ->formatStateUsing(fn (string $state) => match ($state) { 'pending' => 'Needs Review', 'confirmed' => 'Approved', 'change_requested' => 'Changes Needed', default => ucfirst($state), }), Tables\Columns\TextColumn::make('action') ->label('Type') ->badge() ->color(fn (string $state) => match ($state) { 'Create' => 'info', 'Update' => 'gray', default => 'gray', }) ->formatStateUsing(fn (string $state) => match ($state) { 'Create' => 'New Fundraiser', 'Update' => 'Edit', default => $state, }), Tables\Columns\TextColumn::make('appeal.name') ->label('Fundraiser Name') ->sortable() ->searchable() ->limit(50) ->tooltip(fn (ApprovalQueue $r) => $r->appeal?->name), Tables\Columns\TextColumn::make('appeal.user.name') ->label('Created By') ->sortable() ->searchable(), Tables\Columns\TextColumn::make('appeal.donationType.display_name') ->label('Cause') ->badge() ->color('success'), Tables\Columns\TextColumn::make('appeal.amount_to_raise') ->label('Goal') ->formatStateUsing(fn ($state) => $state ? '£' . number_format($state, 0) : '—') ->sortable(), Tables\Columns\TextColumn::make('ai_verdict') ->label('AI Review') ->getStateUsing(function (ApprovalQueue $record) { $extra = json_decode($record->extra_data, true); $ai = $extra['ai_review'] ?? null; if (!$ai) return '—'; return $ai['decision'] ?? '—'; }) ->badge() ->color(fn ($state) => match ($state) { 'approve' => 'success', 'reject' => 'danger', 'review' => 'warning', default => 'gray', }) ->formatStateUsing(fn ($state) => match ($state) { 'approve' => '✓ Safe', 'reject' => '✗ Flagged', 'review' => '? Uncertain', default => '—', }) ->tooltip(function (ApprovalQueue $record) { $extra = json_decode($record->extra_data, true); $ai = $extra['ai_review'] ?? null; if (!$ai) return 'No AI review yet'; return ($ai['summary'] ?? '') . "\n\nConfidence: " . round(($ai['confidence'] ?? 0) * 100) . '%'; }), Tables\Columns\TextColumn::make('message') ->label('Notes') ->limit(40) ->placeholder('—') ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('created_at') ->label('Submitted') ->since() ->sortable(), ]) ->filters([ Tables\Filters\SelectFilter::make('status') ->label('Status') ->options([ 'pending' => 'Needs Review', 'confirmed' => 'Approved', 'change_requested' => 'Changes Needed', ]) ->default('pending'), Tables\Filters\SelectFilter::make('action') ->label('Type') ->options([ 'Create' => 'New Fundraiser', 'Update' => 'Edit', ]), ]) ->actions([ Tables\Actions\Action::make('quick_approve') ->label('Approve') ->icon('heroicon-o-check-circle') ->color('success') ->requiresConfirmation() ->modalHeading('Approve this fundraiser?') ->modalDescription(fn (ApprovalQueue $r) => "This will make \"{$r->appeal?->name}\" live on the website.") ->visible(fn (ApprovalQueue $r) => $r->status === 'pending') ->action(function (ApprovalQueue $record) { app(ApprovalQueueService::class)->approveAppeal($record); Notification::make()->title('Fundraiser approved')->success()->send(); }), Tables\Actions\Action::make('quick_reject') ->label('Request Changes') ->icon('heroicon-o-x-circle') ->color('danger') ->visible(fn (ApprovalQueue $r) => $r->status === 'pending') ->form([ \Filament\Forms\Components\Textarea::make('message') ->label('What needs to change?') ->placeholder('Tell the fundraiser what to fix...') ->required() ->rows(3), ]) ->action(function (ApprovalQueue $record, array $data) { $record->update(['message' => $data['message']]); app(ApprovalQueueService::class)->requestChange($record); Notification::make()->title('Change request sent')->warning()->send(); }), Tables\Actions\Action::make('view_fundraiser') ->label('View') ->icon('heroicon-o-eye') ->url(fn (ApprovalQueue $r) => $r->appeal_id ? AppealResource::getUrl('edit', ['record' => $r->appeal_id]) : null) ->openUrlInNewTab(), ]) ->headerActions([ Tables\Actions\Action::make('ai_review_all') ->label('AI Review All Pending') ->icon('heroicon-o-sparkles') ->color('info') ->requiresConfirmation() ->modalHeading('Run AI Review on All Pending Fundraisers?') ->modalDescription('This will use AI to review all pending fundraisers. Obvious spam will be auto-rejected, clear fundraisers will be auto-approved, and uncertain ones will be flagged for your review.') ->modalSubmitActionLabel('Start AI Review') ->action(function () { try { $service = app(AIAppealReviewService::class); $stats = $service->reviewAllPending(); Notification::make() ->title('AI Review Complete') ->body( "Reviewed: {$stats['reviewed']}\n" . "✓ Auto-approved: {$stats['approved']}\n" . "✗ Auto-rejected: {$stats['rejected']}\n" . "? Needs your review: {$stats['flagged']}" ) ->success() ->persistent() ->send(); } catch (\Throwable $e) { Log::error('AI Review failed', ['error' => $e->getMessage()]); Notification::make() ->title('AI Review Failed') ->body($e->getMessage()) ->danger() ->send(); } }), Tables\Actions\Action::make('bulk_approve_safe') ->label('Approve All AI-Safe') ->icon('heroicon-o-check-badge') ->color('success') ->requiresConfirmation() ->modalHeading('Approve all AI-verified fundraisers?') ->modalDescription('This will approve all pending fundraisers that the AI marked as safe. Only high-confidence approvals will be processed.') ->visible(function () { return ApprovalQueue::where('status', 'pending') ->where('extra_data', 'like', '%"decision":"approve"%') ->exists(); }) ->action(function () { $items = ApprovalQueue::where('status', 'pending') ->where('extra_data', 'like', '%"decision":"approve"%') ->with('appeal') ->get(); $count = 0; foreach ($items as $item) { if ($item->appeal) { app(ApprovalQueueService::class)->approveAppeal($item); $count++; } } Notification::make() ->title("{$count} fundraisers approved") ->success() ->send(); }), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\BulkAction::make('bulk_approve') ->label('Approve Selected') ->icon('heroicon-o-check-circle') ->color('success') ->requiresConfirmation() ->action(function ($records) { $count = 0; foreach ($records as $record) { if ($record->status === 'pending' && $record->appeal) { app(ApprovalQueueService::class)->approveAppeal($record); $count++; } } Notification::make()->title("{$count} fundraisers approved")->success()->send(); }), Tables\Actions\BulkAction::make('bulk_reject') ->label('Reject Selected') ->icon('heroicon-o-x-circle') ->color('danger') ->requiresConfirmation() ->form([ \Filament\Forms\Components\Textarea::make('message') ->label('Rejection reason') ->required(), ]) ->action(function ($records, array $data) { $count = 0; foreach ($records as $record) { if ($record->status === 'pending') { $record->update(['message' => $data['message']]); app(ApprovalQueueService::class)->requestChange($record); $count++; } } Notification::make()->title("{$count} fundraisers rejected")->warning()->send(); }), ]), ]) ->defaultSort('created_at', 'desc') ->poll('30s'); } public static function getRelations(): array { return []; } public static function getPages(): array { return [ 'index' => Pages\ListApprovalQueues::route('/'), 'edit' => Pages\EditApprovalQueue::route('/{record}/edit'), ]; } }