|null */ private ?array $authorizedTenants = null; /** * @var array|null */ private ?array $visibleDecisionTenants = null; /** * @var array|null */ private ?array $registerPayload = null; /** * @var array|null */ private ?array $unfilteredRegisterPayload = null; /** * @var array>|null */ private ?array $rowPayloadByExceptionId = null; private ?Workspace $workspace = null; public ?int $tenantId = null; public string $registerState = 'open'; public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly, ActionSurfaceType::ReadOnlyRegistryReport) ->satisfy(ActionSurfaceSlot::ListHeader, 'Header controls keep tenant and register-state scope visible without introducing a second mutation surface.') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value) ->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The decision register keeps one dominant row action and avoids a More menu in v1.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The decision register is read-only and intentionally omits bulk actions.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'Filtered empty states stay truthful and provide one path back to the broader register scope.') ->exempt(ActionSurfaceSlot::DetailHeader, 'The register owns no local detail surface; existing exception detail remains the action owner.'); } public static function canAccess(): bool { if (Filament::getCurrentPanel()?->getId() !== 'admin') { return false; } $user = auth()->user(); if (! $user instanceof User) { return false; } $workspace = static::resolveWorkspaceFromRequest(); if (! $workspace instanceof Workspace) { return false; } $resolver = app(WorkspaceCapabilityResolver::class); if (! $resolver->isMember($user, $workspace)) { return false; } if (static::hasRequestedTenantPrefilter()) { return true; } $visibleTenants = static::resolveVisibleDecisionTenantsFor($user, $workspace); if ($visibleTenants === []) { return false; } if (request()->query('register_state') === 'recently_closed') { return true; } return (int) (app(GovernanceDecisionRegisterBuilder::class)->build( workspace: $workspace, visibleTenants: $visibleTenants, registerState: 'open', )['counts']['open'] ?? 0) > 0; } public function mount(): void { $this->mountInteractsWithTable(); $this->authorizeWorkspaceMembership(); $this->applyRequestedTenantPrefilter(); $this->registerState = $this->resolveRequestedRegisterState(); $this->ensureRegisterIsVisible(); } public function pageUrl(array $overrides = []): string { $selectedTenant = $this->selectedTenant(); $resolvedTenant = array_key_exists('tenant', $overrides) ? $overrides['tenant'] : ($selectedTenant?->getKey() !== null ? (string) $selectedTenant->getKey() : null); $resolvedRegisterState = array_key_exists('register_state', $overrides) ? $overrides['register_state'] : $this->registerState; return static::getUrl( panel: 'admin', parameters: array_filter([ 'tenant_id' => is_string($resolvedTenant) && $resolvedTenant !== '' ? $resolvedTenant : null, 'register_state' => is_string($resolvedRegisterState) && $resolvedRegisterState !== 'open' ? $resolvedRegisterState : null, ], static fn (mixed $value): bool => $value !== null && $value !== ''), ); } public function appliedScope(): array { return [ 'workspace_label' => $this->workspace()?->name, 'tenant_label' => $this->selectedTenant()?->name, 'register_state_label' => $this->registerStateLabel($this->registerState), 'visible_count' => $this->registerPayload()['counts'][$this->registerState] ?? 0, ]; } /** * @return list */ public function availableRegisterStates(): array { $counts = $this->registerPayload()['counts'] ?? ['open' => 0, 'recently_closed' => 0]; return [ [ 'key' => 'open', 'label' => 'Open decisions', 'count' => (int) ($counts['open'] ?? 0), ], [ 'key' => 'recently_closed', 'label' => 'Recently closed', 'count' => (int) ($counts['recently_closed'] ?? 0), ], ]; } public function hasTenantPrefilter(): bool { return $this->selectedTenant() instanceof Tenant; } public function isActiveRegisterState(string $registerState): bool { return $this->registerState === $registerState; } public function emptyStateHeading(): string { if ($this->tenantFilterAloneExcludesRows()) { return 'This tenant filter is hiding other visible decision follow-through'; } if ($this->registerState === 'recently_closed') { return 'No recently closed decisions match this filter right now.'; } return 'No open decisions match this filter right now.'; } public function emptyStateDescription(): string { if ($this->tenantFilterAloneExcludesRows()) { return 'The current tenant scope is calm, but other visible tenants in this workspace still have open governance decisions.'; } if ($this->registerState === 'recently_closed') { return 'Switch back to open decisions to continue the current follow-through lane, or widen the tenant scope if you were filtering the register.'; } return 'Try widening the tenant scope or switch to recently closed decisions if you are checking what was just finished.'; } public function emptyStateActionLabel(): ?string { if ($this->tenantFilterAloneExcludesRows()) { return 'Clear tenant filter'; } if ($this->registerState === 'recently_closed') { return 'Open current decisions'; } return null; } public function emptyStateActionUrl(): ?string { if ($this->tenantFilterAloneExcludesRows()) { return $this->pageUrl(['tenant' => null]); } if ($this->registerState === 'recently_closed') { return $this->pageUrl(['register_state' => 'open']); } return null; } public function table(Table $table): Table { return $table ->query($this->tableQuery()) ->defaultSort('review_due_at', 'asc') ->paginated(TablePaginationProfiles::resource()) ->persistFiltersInSession() ->persistSearchInSession() ->persistSortInSession() ->recordUrl(null) ->columns([ TextColumn::make('tenant.name') ->label('Tenant') ->searchable() ->sortable(), TextColumn::make('status') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingExceptionStatus)) ->color(BadgeRenderer::color(BadgeDomain::FindingExceptionStatus)) ->icon(BadgeRenderer::icon(BadgeDomain::FindingExceptionStatus)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingExceptionStatus)) ->sortable(), TextColumn::make('current_validity_state') ->label('Impact') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingRiskGovernanceValidity)) ->color(BadgeRenderer::color(BadgeDomain::FindingRiskGovernanceValidity)) ->icon(BadgeRenderer::icon(BadgeDomain::FindingRiskGovernanceValidity)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingRiskGovernanceValidity)), TextColumn::make('owner.name') ->label('Owner') ->placeholder('—') ->toggleable(), TextColumn::make('review_due_at') ->label('Review due') ->dateTime() ->since() ->placeholder('—') ->tooltip(fn (FindingException $record): ?string => $record->review_due_at?->toDayDateTimeString()) ->sortable(), TextColumn::make('proof_availability') ->label('Proof') ->state(function (FindingException $record): string { $referenceCount = (int) data_get($record->evidence_summary ?? [], 'reference_count', 0); return $referenceCount > 0 ? $referenceCount.' evidence linked' : 'No linked proof'; }) ->wrap(), TextColumn::make('next_action_label') ->label('Next action') ->state(fn (FindingException $record): ?string => $this->rowPayload($record)['next_action_label'] ?? null) ->visible(fn (): bool => $this->registerState === 'open') ->wrap(), TextColumn::make('closure_reason') ->label('Closure reason') ->state(fn (FindingException $record): ?string => $this->rowPayload($record)['closure_reason'] ?? null) ->placeholder('—') ->visible(fn (): bool => $this->registerState === 'recently_closed') ->wrap(), ]) ->actions([ Action::make('open_decision') ->label('Open decision') ->color('gray') ->url(fn (FindingException $record): ?string => $this->decisionUrl($record)), ]) ->emptyStateHeading($this->emptyStateHeading()) ->emptyStateDescription($this->emptyStateDescription()) ->emptyStateActions($this->emptyStateActions()); } /** * @return list */ private function emptyStateActions(): array { $label = $this->emptyStateActionLabel(); $url = $this->emptyStateActionUrl(); if (! is_string($label) || ! is_string($url)) { return []; } return [ Action::make('empty_state_scope_action') ->label($label) ->url($url), ]; } /** * @return Builder */ private function tableQuery(): Builder { $tenantIds = array_values(array_map( static fn (Tenant $tenant): int => (int) $tenant->getKey(), $this->currentScopeTenants(), )); $query = FindingException::query() ->where('workspace_id', (int) $this->workspace()?->getKey()) ->whereIn('tenant_id', $tenantIds) ->with(['tenant', 'owner', 'currentDecision']); if ($this->registerState === 'recently_closed') { return $query ->whereIn('status', [ FindingException::STATUS_REJECTED, FindingException::STATUS_REVOKED, FindingException::STATUS_SUPERSEDED, ]) ->whereHas('currentDecision', function (Builder $decisionQuery): void { $decisionQuery->where('decided_at', '>=', now()->startOfDay()->subDays(30)); }); } return $query ->whereNotIn('status', [ FindingException::STATUS_REJECTED, FindingException::STATUS_REVOKED, FindingException::STATUS_SUPERSEDED, ]); } private function authorizeWorkspaceMembership(): void { $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User) { abort(403); } if (! $workspace instanceof Workspace) { throw new NotFoundHttpException; } $resolver = app(WorkspaceCapabilityResolver::class); if (! $resolver->isMember($user, $workspace)) { throw new NotFoundHttpException; } } private function ensureRegisterIsVisible(): void { if ($this->visibleDecisionTenants() === []) { abort(403); } if ($this->tenantId !== null || $this->registerState !== 'open') { return; } if ((int) ($this->registerPayload()['counts']['open'] ?? 0) === 0) { abort(403); } } /** * @return array */ private function authorizedTenants(): array { if ($this->authorizedTenants !== null) { return $this->authorizedTenants; } $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return $this->authorizedTenants = []; } return $this->authorizedTenants = static::resolveAuthorizedTenantsFor($user, $workspace); } /** * @return array */ private function visibleDecisionTenants(): array { if ($this->visibleDecisionTenants !== null) { return $this->visibleDecisionTenants; } $user = auth()->user(); $workspace = $this->workspace(); $tenants = $this->authorizedTenants(); if (! $user instanceof User || ! $workspace instanceof Workspace || $tenants === []) { return $this->visibleDecisionTenants = []; } return $this->visibleDecisionTenants = static::resolveVisibleDecisionTenantsFor($user, $workspace, $tenants); } private function applyRequestedTenantPrefilter(): void { $requestedTenant = request()->query('tenant_id', request()->query('tenant')); if (! is_string($requestedTenant) && ! is_numeric($requestedTenant)) { return; } foreach ($this->authorizedTenants() as $tenant) { if ((string) $tenant->getKey() !== (string) $requestedTenant && (string) $tenant->external_id !== (string) $requestedTenant) { continue; } $this->tenantId = (int) $tenant->getKey(); return; } throw new NotFoundHttpException; } private function resolveRequestedRegisterState(): string { $registerState = request()->query('register_state'); if (! is_string($registerState)) { return 'open'; } return in_array($registerState, ['open', 'recently_closed'], true) ? $registerState : 'open'; } private static function hasRequestedTenantPrefilter(): bool { $requestedTenant = request()->query('tenant_id', request()->query('tenant')); return is_string($requestedTenant) || is_numeric($requestedTenant); } private static function resolveWorkspaceFromRequest(): ?Workspace { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); if (! is_int($workspaceId)) { return null; } return Workspace::query()->whereKey($workspaceId)->first(); } /** * @return array */ private static function resolveAuthorizedTenantsFor(User $user, Workspace $workspace): array { return $user->tenants() ->where('tenants.workspace_id', (int) $workspace->getKey()) ->where('tenants.status', 'active') ->orderBy('tenants.name') ->get(['tenants.id', 'tenants.name', 'tenants.external_id', 'tenants.workspace_id']) ->all(); } /** * @param array|null $authorizedTenants * @return array */ private static function resolveVisibleDecisionTenantsFor(User $user, Workspace $workspace, ?array $authorizedTenants = null): array { $tenants = $authorizedTenants ?? static::resolveAuthorizedTenantsFor($user, $workspace); if ($tenants === []) { return []; } $resolver = app(CapabilityResolver::class); $resolver->primeMemberships( $user, array_map(static fn (Tenant $tenant): int => (int) $tenant->getKey(), $tenants), ); return array_values(array_filter( $tenants, fn (Tenant $tenant): bool => $resolver->can($user, $tenant, Capabilities::FINDING_EXCEPTION_VIEW), )); } private function workspace(): ?Workspace { if ($this->workspace instanceof Workspace) { return $this->workspace; } return $this->workspace = static::resolveWorkspaceFromRequest(); } private function selectedTenant(): ?Tenant { if (! is_int($this->tenantId)) { return null; } foreach ($this->visibleDecisionTenants() as $tenant) { if ((int) $tenant->getKey() === $this->tenantId) { return $tenant; } } return null; } /** * @return array */ private function currentScopeTenants(): array { $selectedTenant = $this->selectedTenant(); if ($selectedTenant instanceof Tenant) { return [$selectedTenant]; } return $this->visibleDecisionTenants(); } /** * @return array */ private function registerPayload(): array { if (is_array($this->registerPayload)) { return $this->registerPayload; } $workspace = $this->workspace(); if (! $workspace instanceof Workspace) { return $this->registerPayload = [ 'rows' => [], 'counts' => ['open' => 0, 'recently_closed' => 0], ]; } return $this->registerPayload = app(GovernanceDecisionRegisterBuilder::class)->build( workspace: $workspace, visibleTenants: $this->currentScopeTenants(), registerState: $this->registerState, ); } /** * @return array */ private function unfilteredRegisterPayload(): array { if (is_array($this->unfilteredRegisterPayload)) { return $this->unfilteredRegisterPayload; } $workspace = $this->workspace(); if (! $workspace instanceof Workspace) { return $this->unfilteredRegisterPayload = [ 'rows' => [], 'counts' => ['open' => 0, 'recently_closed' => 0], ]; } return $this->unfilteredRegisterPayload = app(GovernanceDecisionRegisterBuilder::class)->build( workspace: $workspace, visibleTenants: $this->visibleDecisionTenants(), registerState: 'open', ); } /** * @return array */ private function rowPayload(FindingException $record): array { if (! is_array($this->rowPayloadByExceptionId)) { $this->rowPayloadByExceptionId = collect($this->registerPayload()['rows'] ?? []) ->keyBy('exception_id') ->all(); } return $this->rowPayloadByExceptionId[(int) $record->getKey()] ?? []; } private function tenantFilterAloneExcludesRows(): bool { if (! is_int($this->tenantId) || $this->registerState !== 'open') { return false; } if (($this->registerPayload()['rows'] ?? []) !== []) { return false; } return (int) ($this->unfilteredRegisterPayload()['counts']['open'] ?? 0) > 0; } private function registerStateLabel(string $registerState): string { return match ($registerState) { 'recently_closed' => 'Recently closed', default => 'Open decisions', }; } public function decisionUrl(FindingException $record): ?string { $tenant = $record->tenant; if (! $tenant instanceof Tenant) { return null; } return $this->appendQuery( FindingExceptionResource::getUrl('view', ['record' => $record], panel: 'tenant', tenant: $tenant), $this->navigationContext()->toQuery(), ); } private function navigationContext(): CanonicalNavigationContext { return CanonicalNavigationContext::forDecisionRegister( canonicalRouteName: static::getRouteName(Filament::getPanel('admin')), tenantId: $this->tenantId, backLinkUrl: $this->pageUrl(), ); } /** * @param array $query */ private function appendQuery(string $url, array $query): string { $queryString = http_build_query($query); if ($queryString === '') { return $url; } $separator = str_contains($url, '?') ? '&' : '?'; return $url.$separator.$queryString; } }