getId() !== 'admin') { return false; } return parent::shouldRegisterNavigation(); } public static function canViewAny(): bool { $user = auth()->user(); if (! $user instanceof User) { return false; } $workspace = self::resolveWorkspace(); if (! $workspace instanceof Workspace) { return false; } $resolver = app(\App\Services\Auth\WorkspaceCapabilityResolver::class); return $resolver->isMember($user, $workspace) && $resolver->can($user, $workspace, Capabilities::WORKSPACE_BASELINES_VIEW); } public static function canCreate(): bool { return false; } public static function canEdit(Model $record): bool { return false; } public static function canDelete(Model $record): bool { return false; } public static function canView(Model $record): bool { if (! $record instanceof BaselineSnapshot) { return false; } $workspace = self::resolveWorkspace(); if (! $workspace instanceof Workspace) { return false; } return self::canViewAny() && (int) $record->workspace_id === (int) $workspace->getKey(); } public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView) ->exempt(ActionSurfaceSlot::ListHeader, 'Snapshots are created by capture runs; no list-header actions.') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value) ->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'Snapshots are immutable; rows navigate directly to the detail page.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Snapshots are immutable; no bulk actions.') ->exempt(ActionSurfaceSlot::ListEmptyState, 'Empty-state CTA is intentionally omitted; snapshots appear after baseline captures.') ->satisfy(ActionSurfaceSlot::DetailHeader, 'Primary related navigation is surfaced in the view header.'); } public static function getEloquentQuery(): Builder { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); return parent::getEloquentQuery() ->with('baselineProfile') ->when( $workspaceId !== null, fn (Builder $query): Builder => $query->where('workspace_id', (int) $workspaceId), ) ->when( $workspaceId === null, fn (Builder $query): Builder => $query->whereRaw('1 = 0'), ); } public static function form(Schema $schema): Schema { return $schema; } public static function table(Table $table): Table { return $table ->defaultSort('captured_at', 'desc') ->paginated(\App\Support\Filament\TablePaginationProfiles::resource()) ->persistFiltersInSession() ->persistSearchInSession() ->persistSortInSession() ->columns([ TextColumn::make('id') ->label('Snapshot') ->formatStateUsing(static fn (?int $state): string => $state ? '#'.$state : '—') ->sortable(), TextColumn::make('baselineProfile.name') ->label('Baseline') ->wrap() ->searchable() ->placeholder('—'), TextColumn::make('captured_at') ->label('Captured') ->since() ->sortable(), TextColumn::make('fidelity_summary') ->label('Fidelity') ->getStateUsing(static fn (BaselineSnapshot $record): string => self::fidelitySummary($record)) ->wrap(), TextColumn::make('snapshot_state') ->label('State') ->badge() ->getStateUsing(static fn (BaselineSnapshot $record): string => self::stateLabel($record)) ->color(static fn (BaselineSnapshot $record): string => self::hasGaps($record) ? 'warning' : 'success'), ]) ->recordUrl(static fn (BaselineSnapshot $record): ?string => static::canView($record) ? static::getUrl('view', ['record' => $record]) : null) ->filters([ SelectFilter::make('baseline_profile_id') ->label('Baseline') ->options(static::baselineProfileOptions()) ->searchable(), SelectFilter::make('snapshot_state') ->label('State') ->options(static::snapshotStateOptions()) ->query(fn (Builder $query, array $data): Builder => static::applySnapshotStateFilter($query, $data['value'] ?? null)), FilterPresets::dateRange('captured_at', 'Captured', 'captured_at'), ]) ->actions([ static::primaryRelatedAction(), ]) ->bulkActions([]) ->emptyStateHeading('No baseline snapshots') ->emptyStateDescription('Capture a baseline snapshot to review evidence fidelity and compare tenants over time.') ->emptyStateIcon('heroicon-o-camera'); } public static function infolist(Schema $schema): Schema { return $schema; } private static function primaryRelatedAction(): Action { return Action::make('primary_drill_down') ->label(fn (BaselineSnapshot $record): string => static::primaryRelatedEntry($record)?->actionLabel ?? 'Open related record') ->url(fn (BaselineSnapshot $record): ?string => static::primaryRelatedEntry($record)?->targetUrl) ->hidden(fn (BaselineSnapshot $record): bool => ! (static::primaryRelatedEntry($record)?->isAvailable() ?? false)) ->color('gray'); } private static function primaryRelatedEntry(BaselineSnapshot $record): ?RelatedContextEntry { return app(RelatedNavigationResolver::class) ->primaryListAction(CrossResourceNavigationMatrix::SOURCE_BASELINE_SNAPSHOT, $record); } public static function getPages(): array { return [ 'index' => Pages\ListBaselineSnapshots::route('/'), 'view' => Pages\ViewBaselineSnapshot::route('/{record}'), ]; } /** * @return array */ private static function baselineProfileOptions(): array { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); if ($workspaceId === null) { return []; } return BaselineProfile::query() ->where('workspace_id', (int) $workspaceId) ->orderBy('name') ->pluck('name', 'id') ->all(); } /** * @return array */ private static function snapshotStateOptions(): array { return [ 'complete' => 'Complete', 'with_gaps' => 'Captured with gaps', ]; } public static function resolveWorkspace(): ?Workspace { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); if ($workspaceId === null) { return null; } return Workspace::query()->whereKey($workspaceId)->first(); } private static function summary(BaselineSnapshot $snapshot): array { return is_array($snapshot->summary_jsonb) ? $snapshot->summary_jsonb : []; } private static function fidelityCounts(BaselineSnapshot $snapshot): array { $summary = self::summary($snapshot); $counts = $summary['fidelity_counts'] ?? null; $counts = is_array($counts) ? $counts : []; $content = $counts['content'] ?? 0; $meta = $counts['meta'] ?? 0; return [ 'content' => is_numeric($content) ? (int) $content : 0, 'meta' => is_numeric($meta) ? (int) $meta : 0, ]; } private static function fidelitySummary(BaselineSnapshot $snapshot): string { $counts = self::fidelityCounts($snapshot); return sprintf('Content %d, Meta %d', (int) ($counts['content'] ?? 0), (int) ($counts['meta'] ?? 0)); } private static function gapsCount(BaselineSnapshot $snapshot): int { $summary = self::summary($snapshot); $gaps = $summary['gaps'] ?? null; $gaps = is_array($gaps) ? $gaps : []; $count = $gaps['count'] ?? 0; return is_numeric($count) ? (int) $count : 0; } private static function hasGaps(BaselineSnapshot $snapshot): bool { return self::gapsCount($snapshot) > 0; } private static function stateLabel(BaselineSnapshot $snapshot): string { return self::hasGaps($snapshot) ? 'Captured with gaps' : 'Complete'; } private static function applySnapshotStateFilter(Builder $query, mixed $value): Builder { if (! is_string($value) || trim($value) === '') { return $query; } $gapCountExpression = self::gapCountExpression($query); return match ($value) { 'complete' => $query->whereRaw("{$gapCountExpression} = 0"), 'with_gaps' => $query->whereRaw("{$gapCountExpression} > 0"), default => $query, }; } private static function gapCountExpression(Builder $query): string { return match ($query->getConnection()->getDriverName()) { 'sqlite' => "COALESCE(CAST(json_extract(summary_jsonb, '$.gaps.count') AS INTEGER), 0)", 'pgsql' => "COALESCE((summary_jsonb #>> '{gaps,count}')::int, 0)", default => "COALESCE(CAST(JSON_EXTRACT(summary_jsonb, '$.gaps.count') AS UNSIGNED), 0)", }; } }