defaultSort('created_at', 'desc') ->columns([ ImageColumn::make('thumbnail_path') ->label(__('admin.moderation.table.photo')) ->disk('public') ->visibility('public') ->circular() ->getStateUsing(fn (Photo $record) => $record->thumbnail_path ?: $record->file_path), Tables\Columns\TextColumn::make('event.name') ->label(__('admin.moderation.table.event')) ->searchable() ->limit(30), Tables\Columns\TextColumn::make('event.tenant.name') ->label(__('admin.moderation.table.tenant')) ->limit(30), Tables\Columns\TextColumn::make('guest_name') ->label(__('admin.moderation.table.uploader')) ->searchable() ->limit(20), Tables\Columns\TextColumn::make('status') ->label(__('admin.moderation.table.status')) ->badge() ->color(fn (?string $state) => match ($state) { 'approved' => 'success', 'rejected' => 'danger', 'hidden' => 'gray', default => 'warning', }) ->formatStateUsing(fn (?string $state) => self::statusLabels()[$state] ?? '—') ->sortable(), Tables\Columns\TextColumn::make('security_scan_status') ->label(__('admin.moderation.table.security_scan')) ->badge() ->color(fn (?string $state) => match ($state) { 'clean', 'skipped', 'stripped' => 'success', 'infected' => 'danger', 'error' => 'warning', default => 'gray', }) ->formatStateUsing(fn (?string $state) => self::securityScanLabels()[$state] ?? '—') ->toggleable(), Tables\Columns\TextColumn::make('ingest_source') ->label(__('admin.moderation.table.ingest_source')) ->badge() ->color('gray') ->formatStateUsing(fn (?string $state) => self::ingestSourceLabels()[$state] ?? '—') ->toggleable(), Tables\Columns\TextColumn::make('created_at') ->label(__('admin.moderation.table.uploaded_at')) ->since() ->sortable(), Tables\Columns\TextColumn::make('moderator.name') ->label(__('admin.moderation.table.moderated_by')) ->placeholder('—') ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('moderated_at') ->label(__('admin.moderation.table.moderated_at')) ->since() ->placeholder('—') ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ SelectFilter::make('status') ->label(__('admin.moderation.filters.status')) ->options(self::statusLabels()) ->default('pending'), SelectFilter::make('ingest_source') ->label(__('admin.moderation.filters.ingest_source')) ->options(self::ingestSourceLabels()) ->default(Photo::SOURCE_GUEST_PWA), SelectFilter::make('security_scan_status') ->label(__('admin.moderation.filters.security_scan_status')) ->options(self::securityScanLabels()), SelectFilter::make('tenant_id') ->label(__('admin.common.tenant')) ->options(fn () => Tenant::query()->orderBy('name')->pluck('name', 'id')->toArray()) ->searchable(), SelectFilter::make('event_id') ->label(__('admin.common.event')) ->options(fn () => Event::query()->orderBy('name')->pluck('name', 'id')->toArray()) ->searchable(), Filter::make('created_at') ->label(__('admin.moderation.filters.uploaded_at')) ->form([ DatePicker::make('from')->label(__('admin.common.from')), DatePicker::make('until')->label(__('admin.common.until')), ]) ->query(function (Builder $query, array $data): Builder { return $query ->when( $data['from'] ?? null, fn (Builder $query, string $date): Builder => $query->whereDate('created_at', '>=', $date) ) ->when( $data['until'] ?? null, fn (Builder $query, string $date): Builder => $query->whereDate('created_at', '<=', $date) ); }), ]) ->recordActions([ ViewAction::make(), Action::make('approve') ->label(__('admin.moderation.actions.approve')) ->color('success') ->icon(Heroicon::OutlinedCheckCircle) ->visible(fn (Photo $record) => $record->status === 'pending') ->form([ self::moderationNotesField(false), ]) ->requiresConfirmation() ->action(fn (Photo $record, array $data) => self::applyModeration($record, 'approved', $data['moderation_notes'] ?? null)), Action::make('reject') ->label(__('admin.moderation.actions.reject')) ->color('danger') ->icon(Heroicon::OutlinedXCircle) ->visible(fn (Photo $record) => $record->status === 'pending') ->form([ self::moderationNotesField(true), ]) ->requiresConfirmation() ->action(fn (Photo $record, array $data) => self::applyModeration($record, 'rejected', $data['moderation_notes'] ?? null)), Action::make('hide') ->label(__('admin.moderation.actions.hide')) ->color('gray') ->icon(Heroicon::OutlinedEyeSlash) ->visible(fn (Photo $record) => $record->status !== 'hidden') ->form([ self::moderationNotesField(false), ]) ->requiresConfirmation() ->action(fn (Photo $record, array $data) => self::applyModeration($record, 'hidden', $data['moderation_notes'] ?? null)), ]) ->toolbarActions([ BulkActionGroup::make([ BulkAction::make('approve') ->label(__('admin.moderation.actions.approve_selected')) ->icon(Heroicon::OutlinedCheckCircle) ->color('success') ->form([ self::moderationNotesField(false), ]) ->requiresConfirmation() ->action(fn (Collection $records, array $data) => self::applyModerationToRecords($records, 'approved', $data['moderation_notes'] ?? null)), BulkAction::make('reject') ->label(__('admin.moderation.actions.reject_selected')) ->icon(Heroicon::OutlinedXCircle) ->color('danger') ->form([ self::moderationNotesField(true), ]) ->requiresConfirmation() ->action(fn (Collection $records, array $data) => self::applyModerationToRecords($records, 'rejected', $data['moderation_notes'] ?? null)), BulkAction::make('hide') ->label(__('admin.moderation.actions.hide_selected')) ->icon(Heroicon::OutlinedEyeSlash) ->color('gray') ->form([ self::moderationNotesField(false), ]) ->requiresConfirmation() ->action(fn (Collection $records, array $data) => self::applyModerationToRecords($records, 'hidden', $data['moderation_notes'] ?? null)), ]), ]); } private static function moderationNotesField(bool $required): Textarea { return Textarea::make('moderation_notes') ->label(__('admin.moderation.fields.moderation_notes')) ->maxLength(1000) ->rows(3) ->required($required); } private static function applyModeration(Photo $record, string $status, ?string $notes): void { $record->update([ 'status' => $status, 'moderation_notes' => $notes, 'moderated_at' => now(), 'moderated_by' => Filament::auth()->id(), ]); app(SuperAdminAuditLogger::class)->record( 'photo.'.$status, $record, SuperAdminAuditLogger::fieldsMetadata([ 'status', 'moderation_notes', 'moderated_at', 'moderated_by', ]), source: self::class ); } private static function applyModerationToRecords(Collection $records, string $status, ?string $notes): int { $moderatedAt = now(); $moderatedBy = Filament::auth()->id(); $updated = Photo::query() ->whereIn('id', $records->pluck('id')) ->where('status', 'pending') ->update([ 'status' => $status, 'moderation_notes' => $notes, 'moderated_at' => $moderatedAt, 'moderated_by' => $moderatedBy, ]); $logger = app(SuperAdminAuditLogger::class); foreach ($records as $record) { $logger->record( 'photo.'.$status, $record, SuperAdminAuditLogger::fieldsMetadata([ 'status', 'moderation_notes', 'moderated_at', 'moderated_by', ]), source: self::class ); } return $updated; } private static function statusLabels(): array { return [ 'pending' => __('admin.moderation.status.pending'), 'approved' => __('admin.moderation.status.approved'), 'rejected' => __('admin.moderation.status.rejected'), 'hidden' => __('admin.moderation.status.hidden'), ]; } private static function ingestSourceLabels(): array { return [ Photo::SOURCE_GUEST_PWA => __('admin.moderation.ingest_sources.guest_pwa'), Photo::SOURCE_TENANT_ADMIN => __('admin.moderation.ingest_sources.tenant_admin'), Photo::SOURCE_PHOTOBOOTH => __('admin.moderation.ingest_sources.photobooth'), Photo::SOURCE_SPARKBOOTH => __('admin.moderation.ingest_sources.sparkbooth'), Photo::SOURCE_UNKNOWN => __('admin.moderation.ingest_sources.unknown'), ]; } private static function securityScanLabels(): array { return [ 'pending' => __('admin.moderation.security_scan.pending'), 'clean' => __('admin.moderation.security_scan.clean'), 'infected' => __('admin.moderation.security_scan.infected'), 'skipped' => __('admin.moderation.security_scan.skipped'), 'stripped' => __('admin.moderation.security_scan.stripped'), 'error' => __('admin.moderation.security_scan.error'), ]; } }