User relation manager to add: * - a "Privacy Requests" row action, since a User's erasure/export requests * can't be shown as a nested relation manager two levels deep (Customer -> * User -> Requests isn't a shape Filament relation managers support) — a * modal listing that specific User's requests is the practical alternative. * - a "Request Erasure" row action — the User-scoped panel entry point, * mirroring Modules\Core\Privacy\Filament\Extensions\ * CustomerErasureActionsExtension on the Customer side, including the same * "erase immediately" checkbox for a staff-triggered urgent request. * See docs/privacy.md "User-scope vs Customer-scope". */ class UserRelationManager extends CoreUserRelationManager { public function getDefaultTable(Table $table): Table { $table = parent::getDefaultTable($table); return $table->recordActions([ ...$table->getActions(), Action::make('privacyRequests') ->label('Privacy Requests') ->icon('heroicon-o-shield-exclamation') ->modalHeading(fn (Model $record) => "Privacy requests for {$record->name}") ->modalSubmitAction(false) ->modalCancelActionLabel('Close') ->schema(fn (Model $record) => $this->requestsInfolist($record)), Action::make('requestErasure') ->label('Request Erasure') ->icon('heroicon-o-shield-exclamation') ->color('danger') ->requiresConfirmation() ->modalDescription('Opens a cancellable grace-period erasure request for this individual — deactivates their login and detaches them from every linked Customer account once it completes. No Customer account\'s own data is affected.') ->schema([ Checkbox::make('immediate') ->label('Erase immediately (skip the 30-day grace period)') ->helperText('Staff-only, for a formal legal request or regulator inquiry that genuinely requires urgency — not a routine deletion. Runs synchronously, cannot be cancelled once submitted.') ->default(false), ]) ->action(function (Model $record, array $data) { $privacyService = app(PrivacyService::class); $staff = auth('staff')->user(); if ($data['immediate']) { $privacyService->requestImmediateErasureForUser($record, $staff); Notification::make() ->title('User erased') ->body('Erasure ran immediately — see the Erasure Requests list for the outcome.') ->success() ->send(); return; } $privacyService->requestErasureForUser($record, $staff); Notification::make() ->title('Erasure requested') ->body('The grace period starts now, and this user\'s login is deactivated immediately — see the Erasure Requests list.') ->success() ->send(); }), ]); } private function requestsInfolist(Model $record): array { return [ TextEntry::make('erasure_heading') ->label('') ->state('Erasure Requests'), RepeatableEntry::make('erasureRequests') ->label('') ->state(fn () => $record->erasureRequests()->latest()->get()) ->schema([ TextEntry::make('status')->formatStateUsing(fn ($state) => ucfirst($state->value)), TextEntry::make('scheduled_for')->dateTime(), TextEntry::make('requestedBy')->label('Requested by')->state( fn (DataErasureRequest $record) => DataErasureRequest::displayNameFor($record->requestedBy) ), ]) ->columns(3), TextEntry::make('export_heading') ->label('') ->state('Export Requests'), RepeatableEntry::make('exportRequests') ->label('') ->state(fn () => $record->exportRequests()->latest()->get()) ->schema([ TextEntry::make('status')->formatStateUsing(fn ($state) => ucfirst($state->value)), TextEntry::make('created_at')->label('Requested at')->dateTime(), ]) ->columns(2), ]; } }