external_id) ? (string) $tenant->external_id : (string) $tenant->getKey(); return static::getUrl(panel: 'admin').'?'.http_build_query([ 'tenant' => $tenantIdentifier, ]); } /** * @var array|null */ private ?array $authorizedTenants = null; public function mount(): void { $this->authorizePageAccess(); $this->applyRequestedTenantPrefilter(); $this->mountInteractsWithTable(); } protected function getHeaderActions(): array { return [ Action::make('clear_filters') ->label('Clear filters') ->icon('heroicon-o-x-mark') ->color('gray') ->visible(fn (): bool => $this->hasActiveFilters()) ->action(function (): void { $this->clearWorkspaceFilters(); }), ]; } public function table(Table $table): Table { return $table ->query(fn (): Builder => $this->workspaceQuery()) ->defaultSort('name') ->paginated(TablePaginationProfiles::customPage()) ->persistFiltersInSession() ->persistSearchInSession() ->persistSortInSession() ->recordUrl(fn (Tenant $record): ?string => $this->latestReviewUrl($record)) ->columns([ TextColumn::make('name')->label('Tenant')->searchable()->sortable(), TextColumn::make('latest_review') ->label('Latest review') ->badge() ->getStateUsing(fn (Tenant $record): string => $this->latestReviewStateLabel($record)) ->color(fn (Tenant $record): string => $this->latestReviewStateColor($record)) ->icon(fn (Tenant $record): ?string => $this->latestReviewStateIcon($record)) ->iconColor(fn (Tenant $record): ?string => $this->latestReviewStateIconColor($record)) ->description(fn (Tenant $record): ?string => $this->reviewOutcomeDescription($record)) ->wrap(), TextColumn::make('finding_summary') ->label('Key findings') ->getStateUsing(fn (Tenant $record): string => $this->findingSummary($record)) ->wrap(), TextColumn::make('accepted_risk_summary') ->label('Accepted risks') ->getStateUsing(fn (Tenant $record): string => $this->acceptedRiskSummary($record)) ->wrap(), TextColumn::make('published_at') ->label('Published') ->getStateUsing(fn (Tenant $record): ?string => $this->latestPublishedAt($record)?->toDateTimeString()) ->dateTime() ->placeholder('—'), TextColumn::make('review_pack_state') ->label('Review pack') ->getStateUsing(fn (Tenant $record): string => $this->reviewPackAvailability($record)), ]) ->filters([ SelectFilter::make('tenant_id') ->label('Tenant') ->options(fn (): array => $this->tenantFilterOptions()) ->default(fn (): ?string => $this->defaultTenantFilter()) ->query(function (Builder $query, array $data): Builder { $tenantId = $data['value'] ?? null; return is_numeric($tenantId) ? $query->whereKey((int) $tenantId) : $query; }) ->searchable(), ]) ->actions([ Action::make('open_latest_review') ->label('Open latest review') ->icon('heroicon-o-arrow-top-right-on-square') ->url(fn (Tenant $record): ?string => $this->latestReviewUrl($record)) ->visible(fn (Tenant $record): bool => $this->latestPublishedReview($record) instanceof TenantReview), Action::make('download_review_pack') ->label('Download review pack') ->icon('heroicon-o-arrow-down-tray') ->url(fn (Tenant $record): ?string => $this->latestReviewPackDownloadUrl($record)) ->openUrlInNewTab() ->visible(fn (Tenant $record): bool => is_string($this->latestReviewPackDownloadUrl($record))), ]) ->bulkActions([]) ->emptyStateHeading('No entitled tenants match this view') ->emptyStateDescription(fn (): string => $this->hasActiveFilters() ? 'Clear the current filters to return to the full customer review workspace for your entitled tenants.' : 'Adjust filters to return to the full customer review workspace for your entitled tenants.') ->emptyStateActions([ Action::make('clear_filters_empty') ->label('Clear filters') ->icon('heroicon-o-x-mark') ->color('gray') ->visible(fn (): bool => $this->hasActiveFilters()) ->action(fn (): mixed => $this->clearWorkspaceFilters()), ]); } /** * @return array */ public 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 = app(TenantReviewRegisterService::class)->authorizedTenants($user, $workspace); } private function authorizePageAccess(): void { $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User) { abort(403); } if (! $workspace instanceof Workspace) { throw new NotFoundHttpException; } $service = app(TenantReviewRegisterService::class); if (! $service->canAccessWorkspace($user, $workspace)) { throw new NotFoundHttpException; } if ($this->authorizedTenants() === []) { throw new NotFoundHttpException; } } private function workspaceQuery(): Builder { $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return Tenant::query()->whereRaw('1 = 0'); } return app(TenantReviewRegisterService::class)->customerWorkspaceTenantQuery($user, $workspace); } /** * @return array */ private function tenantFilterOptions(): array { return collect($this->authorizedTenants()) ->mapWithKeys(static fn (Tenant $tenant): array => [ (string) $tenant->getKey() => $tenant->name, ]) ->all(); } private function defaultTenantFilter(): ?string { $tenantId = app(WorkspaceContext::class)->lastTenantId(request()); return is_int($tenantId) && array_key_exists($tenantId, $this->authorizedTenants()) ? (string) $tenantId : null; } private function applyRequestedTenantPrefilter(): void { $requestedTenant = request()->query('tenant', request()->query('tenant_id')); 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->tableFilters['tenant_id']['value'] = (string) $tenant->getKey(); $this->tableDeferredFilters['tenant_id']['value'] = (string) $tenant->getKey(); return; } throw new NotFoundHttpException; } private function hasActiveFilters(): bool { return $this->currentTenantFilterId() !== null; } private function clearWorkspaceFilters(): void { app(WorkspaceContext::class)->clearLastTenantId(request()); $this->removeTableFilters(); } private function currentTenantFilterId(): ?int { $tenantFilter = data_get($this->tableFilters, 'tenant_id.value'); if (! is_numeric($tenantFilter)) { $tenantFilter = data_get(session()->get($this->getTableFiltersSessionKey(), []), 'tenant_id.value'); } return is_numeric($tenantFilter) ? (int) $tenantFilter : null; } private function workspace(): ?Workspace { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); return is_numeric($workspaceId) ? Workspace::query()->whereKey((int) $workspaceId)->first() : null; } private function latestPublishedReview(Tenant $tenant): ?TenantReview { $review = $tenant->tenantReviews->first(); return $review instanceof TenantReview ? $review : null; } private function latestReviewUrl(Tenant $tenant): ?string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof TenantReview) { return null; } return TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant, 'tenant').'?'.http_build_query([ self::DETAIL_CONTEXT_QUERY_KEY => 1, ]); } private function latestReviewPack(Tenant $tenant): ?ReviewPack { $review = $this->latestPublishedReview($tenant); $pack = $review?->currentExportReviewPack; return $pack instanceof ReviewPack ? $pack : null; } private function latestReviewPackDownloadUrl(Tenant $tenant): ?string { $user = auth()->user(); $pack = $this->latestReviewPack($tenant); if (! $user instanceof User || ! $pack instanceof ReviewPack) { return null; } if (! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) { return null; } if ($pack->status !== ReviewPackStatus::Ready->value) { return null; } if ($pack->expires_at !== null && $pack->expires_at->isPast()) { return null; } return app(ReviewPackService::class)->generateDownloadUrl($pack, [ 'source_surface' => self::SOURCE_SURFACE, ]); } private function latestPublishedAt(Tenant $tenant): ?\Illuminate\Support\Carbon { return $this->latestPublishedReview($tenant)?->published_at; } private function reviewTruth(Tenant $tenant): ?ArtifactTruthEnvelope { $review = $this->latestPublishedReview($tenant); return $review instanceof TenantReview ? app(ArtifactTruthPresenter::class)->forTenantReview($review) : null; } private function reviewOutcome(Tenant $tenant): ?CompressedGovernanceOutcome { $presenter = app(ArtifactTruthPresenter::class); $review = $this->latestPublishedReview($tenant); $truth = $this->reviewTruth($tenant); if (! $review instanceof TenantReview || ! $truth instanceof ArtifactTruthEnvelope) { return null; } return $presenter->compressedOutcomeFor($review, SurfaceCompressionContext::reviewRegister()) ?? $presenter->compressedOutcomeFromEnvelope($truth, SurfaceCompressionContext::reviewRegister()); } private function latestReviewStateLabel(Tenant $tenant): string { return $this->reviewOutcome($tenant)?->primaryLabel ?? 'No published review'; } private function latestReviewStateColor(Tenant $tenant): string { return $this->reviewOutcome($tenant)?->primaryBadge->color ?? 'gray'; } private function latestReviewStateIcon(Tenant $tenant): ?string { return $this->reviewOutcome($tenant)?->primaryBadge->icon; } private function latestReviewStateIconColor(Tenant $tenant): ?string { return $this->reviewOutcome($tenant)?->primaryBadge->iconColor; } private function reviewOutcomeDescription(Tenant $tenant): ?string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof TenantReview) { return 'No published review available yet'; } $primaryReason = $this->reviewOutcome($tenant)?->primaryReason; $summary = is_array($review->summary) ? $review->summary : []; $findingOutcomes = $summary['finding_outcomes'] ?? null; if (! is_array($findingOutcomes)) { return $primaryReason; } $findingOutcomeSummary = app(FindingOutcomeSemantics::class)->compactOutcomeSummary($findingOutcomes); if ($findingOutcomeSummary === null) { return $primaryReason; } return trim($primaryReason.' Terminal outcomes: '.$findingOutcomeSummary.'.'); } private function findingSummary(Tenant $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof TenantReview) { return 'No published review available yet'; } $summary = is_array($review->summary) ? $review->summary : []; $findingCount = (int) ($summary['finding_count'] ?? 0); $findingOutcomes = is_array($summary['finding_outcomes'] ?? null) ? $summary['finding_outcomes'] : []; $terminalOutcomes = app(FindingOutcomeSemantics::class)->compactOutcomeSummary($findingOutcomes); if ($findingCount === 0) { return 'No findings recorded in the published review.'; } if ($terminalOutcomes === null) { return sprintf('%d findings summarized in the published review.', $findingCount); } return sprintf('%d findings. Terminal outcomes: %s.', $findingCount, $terminalOutcomes); } private function acceptedRiskSummary(Tenant $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof TenantReview) { return 'No published review available yet'; } $summary = is_array($review->summary) ? $review->summary : []; $riskAcceptance = is_array($summary['risk_acceptance'] ?? null) ? $summary['risk_acceptance'] : []; $statusMarkedCount = (int) ($riskAcceptance['status_marked_count'] ?? 0); $validGovernedCount = (int) ($riskAcceptance['valid_governed_count'] ?? 0); $warningCount = (int) ($riskAcceptance['warning_count'] ?? 0); return match (true) { $statusMarkedCount === 0 => 'No accepted risks recorded.', $warningCount > 0 => sprintf('%d accepted risks need governance follow-up (%d total).', $warningCount, $statusMarkedCount), $validGovernedCount > 0 => sprintf('%d accepted risks are governed.', $validGovernedCount), default => sprintf('%d accepted risks are on record.', $statusMarkedCount), }; } private function reviewPackAvailability(Tenant $tenant): string { $pack = $this->latestReviewPack($tenant); if (! $pack instanceof ReviewPack) { return 'Unavailable'; } if ($pack->status !== ReviewPackStatus::Ready->value) { return 'Unavailable'; } if ($pack->expires_at !== null && $pack->expires_at->isPast()) { return 'Unavailable'; } return 'Available'; } }