satisfy(ActionSurfaceSlot::ListHeader, 'Header actions provide a single Clear filters action for the customer review workspace.') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value) ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The customer review workspace remains scan-first and does not expose bulk actions.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state keeps exactly one Clear filters CTA when filters are active.') ->exempt(ActionSurfaceSlot::DetailHeader, 'Row navigation opens the latest published review detail instead of an inline canonical detail panel.'); } public static function getNavigationGroup(): string { return __('localization.review.reporting'); } public static function getNavigationLabel(): string { return __('localization.review.customer_reviews'); } public function getTitle(): string { return __('localization.review.customer_review_workspace'); } public static function tenantPrefilterUrl(Tenant $tenant): string { $tenantIdentifier = filled($tenant->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(); $this->auditWorkspaceOpen(); } protected function getHeaderActions(): array { $actions = []; $governanceContext = $this->incomingGovernanceContext(); if ($governanceContext?->backLinkUrl !== null) { $actions[] = Action::make('return_to_governance_inbox') ->label($governanceContext->backLinkLabel ?? 'Back to governance inbox') ->icon('heroicon-o-arrow-left') ->color('gray') ->url($governanceContext->backLinkUrl); } $actions[] = Action::make('clear_filters') ->label(__('localization.review.clear_filters')) ->icon('heroicon-o-x-mark') ->color('gray') ->visible(fn (): bool => $this->hasActiveFilters()) ->action(function (): void { $this->clearWorkspaceFilters(); }); return $actions; } 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(__('localization.review.tenant'))->searchable()->sortable(), TextColumn::make('latest_review') ->label(__('localization.review.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('control_readiness') ->label(__('localization.review.control_readiness')) ->badge() ->getStateUsing(fn (Tenant $record): string => $this->controlReadinessLabel($record)) ->color(fn (Tenant $record): string => $this->controlReadinessColor($record)) ->description(fn (Tenant $record): string => $this->controlReadinessDescription($record)) ->wrap(), TextColumn::make('evidence_basis') ->label(__('localization.review.evidence_basis')) ->getStateUsing(fn (Tenant $record): string => $this->controlEvidenceBasisSummary($record)) ->wrap(), TextColumn::make('recommended_next_action') ->label(__('localization.review.recommended_next_action')) ->getStateUsing(fn (Tenant $record): string => $this->controlRecommendedNextAction($record)) ->wrap(), TextColumn::make('published_at') ->label(__('localization.review.published')) ->getStateUsing(fn (Tenant $record): ?string => $this->latestPublishedAt($record)?->toDateTimeString()) ->dateTime() ->placeholder('—'), ]) ->filters([ SelectFilter::make('tenant_id') ->label(__('localization.review.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(__('localization.review.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), ]) ->bulkActions([]) ->emptyStateHeading(__('localization.review.no_released_customer_reviews')) ->emptyStateDescription(fn (): string => $this->hasActiveFilters() ? __('localization.review.clear_filters_description') : __('localization.review.no_released_customer_reviews_description')) ->emptyStateActions([ Action::make('clear_filters_empty') ->label(__('localization.review.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 auditWorkspaceOpen(): void { $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return; } app(WorkspaceAuditLogger::class)->log( workspace: $workspace, action: AuditActionId::CustomerReviewWorkspaceOpened, context: [ 'metadata' => [ 'source_surface' => self::SOURCE_SURFACE, 'tenant_filter_id' => $this->currentTenantFilterId(), 'entitled_tenant_count' => count($this->authorizedTenants()), 'interpretation_version' => $this->currentTenantFilterInterpretationVersion(), 'interpretation_versions' => $this->visibleInterpretationVersions(), ], ], actor: $user, resourceType: 'customer_review_workspace', resourceId: (string) $workspace->getKey(), targetLabel: __('localization.review.customer_review_workspace'), ); } 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; } $query = array_filter( array_replace( [self::DETAIL_CONTEXT_QUERY_KEY => 1], [ 'source_surface' => self::SOURCE_SURFACE, 'tenant_filter_id' => $this->currentTenantFilterId(), 'interpretation_version' => $review->controlInterpretationVersion(), ], $this->navigationContext()?->toQuery() ?? [], ), static fn (mixed $value): bool => $value !== null && $value !== '', ); return $this->appendQuery(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant, 'tenant'), $query); } 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 ?? __('localization.review.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 __('localization.review.no_published_review_available'); } $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.' '.__('localization.review.terminal_outcomes').': '.$findingOutcomeSummary.'.'); } private function controlReadinessLabel(Tenant $tenant): string { $control = $this->primaryControlSummary($tenant); if ($control === null) { return __('localization.review.control_readiness_unmapped'); } $label = $control['readiness_label'] ?? null; return is_string($label) && trim($label) !== '' ? $label : ComplianceEvidenceMappingV1::readinessLabel((string) ($control['readiness_bucket'] ?? 'review_recommended')); } private function controlReadinessColor(Tenant $tenant): string { return match ((string) ($this->primaryControlSummary($tenant)['readiness_bucket'] ?? 'unmapped')) { 'follow_up_required' => 'warning', 'review_recommended' => 'info', 'evidence_on_record' => 'success', default => 'gray', }; } private function controlReadinessDescription(Tenant $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof TenantReview) { return __('localization.review.no_published_review_available'); } $controls = $review->controlInterpretationControls(); $version = $review->controlInterpretationVersion(); $displayLabel = $review->controlInterpretation()['display_label'] ?? null; $prefixParts = array_values(array_filter([ is_string($displayLabel) && trim($displayLabel) !== '' ? $displayLabel : null, $version !== null ? __('localization.review.interpretation_version_short', ['version' => $version]) : null, ])); $prefix = $prefixParts === [] ? '' : implode(' · ', $prefixParts).' '; if ($controls === []) { return $prefix.__('localization.review.control_readiness_unmapped_description'); } $summary = collect($controls) ->take(2) ->map(function (array $control): string { $name = is_string($control['control_name'] ?? null) ? $control['control_name'] : __('localization.review.control'); $label = is_string($control['readiness_label'] ?? null) ? $control['readiness_label'] : ComplianceEvidenceMappingV1::readinessLabel((string) ($control['readiness_bucket'] ?? 'review_recommended')); return $name.': '.$label; }) ->implode(' · '); $remaining = count($controls) - 2; if ($remaining > 0) { $summary .= ' · '.__('localization.review.additional_controls', ['count' => $remaining]); } $limitations = $this->controlLimitationSummary($review); return trim($prefix.$summary.($limitations !== null ? ' '.$limitations : '')); } private function controlEvidenceBasisSummary(Tenant $tenant): string { $control = $this->primaryControlSummary($tenant); if ($control === null) { return __('localization.review.control_evidence_unmapped'); } $summary = $control['evidence_basis_summary'] ?? null; return is_string($summary) && trim($summary) !== '' ? $summary : __('localization.review.control_evidence_unavailable'); } private function controlRecommendedNextAction(Tenant $tenant): string { $control = $this->primaryControlSummary($tenant); if ($control === null) { return __('localization.review.control_recommendation_unmapped'); } $action = $control['recommended_next_action'] ?? null; return is_string($action) && trim($action) !== '' ? $action : __('localization.review.no_action_needed'); } /** * @return array|null */ private function primaryControlSummary(Tenant $tenant): ?array { $review = $this->latestPublishedReview($tenant); if (! $review instanceof TenantReview) { return null; } $controls = collect($review->controlInterpretationControls()); return $controls ->sortBy(static fn (array $control): int => match ((string) ($control['readiness_bucket'] ?? '')) { 'follow_up_required' => 0, 'review_recommended' => 1, 'evidence_on_record' => 2, default => 3, }) ->first(); } private function controlLimitationSummary(TenantReview $review): ?string { $counts = $review->controlInterpretationLimitationCounts(); if ($counts === []) { return null; } $labels = collect($counts) ->filter(static fn (int $count): bool => $count > 0) ->keys() ->map(static fn (string $flag): string => ComplianceEvidenceMappingV1::limitationLabel($flag)) ->values() ->all(); return $labels === [] ? null : __('localization.review.control_limitations_summary', ['limitations' => implode(', ', $labels)]); } private function findingSummary(Tenant $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof TenantReview) { return __('localization.review.no_published_review_available'); } $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 __('localization.review.no_findings_recorded'); } if ($terminalOutcomes === null) { return __('localization.review.findings_count_summary', ['count' => $findingCount]); } return __('localization.review.findings_count_with_outcomes', [ 'count' => $findingCount, 'outcomes' => $terminalOutcomes, ]); } private function acceptedRiskSummary(Tenant $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof TenantReview) { return __('localization.review.no_published_review_available'); } $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); $countSummary = match (true) { $statusMarkedCount === 0 => __('localization.review.no_accepted_risks_recorded'), $warningCount > 0 => __('localization.review.accepted_risks_need_follow_up', ['warnings' => $warningCount, 'total' => $statusMarkedCount]), $validGovernedCount > 0 => __('localization.review.accepted_risks_governed', ['count' => $validGovernedCount]), default => __('localization.review.accepted_risks_on_record', ['count' => $statusMarkedCount]), }; $accountability = $this->acceptedRiskAccountability($tenant); return $accountability === null ? $countSummary : $countSummary.' '.$accountability; } private function reviewPackAvailability(Tenant $tenant): string { if (! $this->latestPublishedReview($tenant) instanceof TenantReview) { return __('localization.review.no_published_review_available'); } $pack = $this->latestReviewPack($tenant); $user = auth()->user(); if (! $pack instanceof ReviewPack) { return __('localization.review.no_current_review_pack'); } if (! $user instanceof User || ! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) { return __('localization.review.review_pack_access_unavailable'); } if ($pack->status !== ReviewPackStatus::Ready->value) { return __('localization.review.review_pack_unavailable'); } if ($pack->expires_at !== null && $pack->expires_at->isPast()) { return __('localization.review.review_pack_expired'); } return __('localization.review.review_pack_available'); } private function evidenceProofAvailability(Tenant $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof TenantReview) { return __('localization.review.no_published_review_available'); } $snapshot = $review->evidenceSnapshot; $user = auth()->user(); if (! $snapshot instanceof EvidenceSnapshot) { return __('localization.review.evidence_proof_absent'); } if (! $user instanceof User || ! $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) { return __('localization.review.evidence_proof_access_unavailable'); } if ((string) $snapshot->status === 'expired' || ($snapshot->expires_at !== null && $snapshot->expires_at->isPast())) { return __('localization.review.evidence_proof_expired'); } return __('localization.review.evidence_proof_available'); } /** * @return list */ private function visibleInterpretationVersions(): array { $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return []; } return app(TenantReviewRegisterService::class) ->latestPublishedQuery($user, $workspace) ->get() ->map(static fn (TenantReview $review): ?string => $review->controlInterpretationVersion()) ->filter() ->unique() ->values() ->all(); } private function currentTenantFilterInterpretationVersion(): ?string { $tenantId = $this->currentTenantFilterId(); if ($tenantId === null) { return null; } $tenant = $this->authorizedTenants()[$tenantId] ?? null; if (! $tenant instanceof Tenant) { return null; } return $tenant->tenantReviews()->published() ->latest('published_at') ->latest('generated_at') ->latest('id') ->first() ?->controlInterpretationVersion(); } private function acceptedRiskAccountability(Tenant $tenant): ?string { $exception = FindingException::query() ->with(['owner', 'approver', 'currentDecision']) ->where('workspace_id', (int) $tenant->workspace_id) ->where('tenant_id', (int) $tenant->getKey()) ->current() ->orderByRaw("case when current_validity_state in ('valid', 'expiring') then 0 else 1 end") ->latest('approved_at') ->latest('requested_at') ->latest('id') ->first(); if (! $exception instanceof FindingException) { return null; } $accountable = $exception->owner?->name ?? $exception->approver?->name; $decisionType = $exception->currentDecision?->decision_type; $reviewDue = $exception->review_due_at ?? $exception->expires_at; $reason = is_string($exception->request_reason) ? trim($exception->request_reason) : ''; $parts = []; if (is_string($accountable) && trim($accountable) !== '') { $parts[] = $reviewDue === null ? __('localization.review.accepted_risk_accountable', ['name' => $accountable]) : __('localization.review.accepted_risk_accountable_until', [ 'name' => $accountable, 'date' => $reviewDue->toDateString(), ]); } elseif (is_string($decisionType) && trim($decisionType) !== '') { $parts[] = __('localization.review.accepted_risk_partial_accountability'); } if ($reason !== '') { $parts[] = __('localization.review.accepted_risk_reason', [ 'reason' => Str::limit($reason, 160), ]); } return $parts === [] ? null : implode(' ', $parts); } private function navigationContext(): ?CanonicalNavigationContext { return CanonicalNavigationContext::fromRequest(request()); } private function incomingGovernanceContext(): ?CanonicalNavigationContext { $context = $this->navigationContext(); return $context?->sourceSurface === 'governance.inbox' ? $context : null; } /** * @param array $query */ private function appendQuery(string $url, array $query): string { if ($query === []) { return $url; } return $url.(str_contains($url, '?') ? '&' : '?').http_build_query($query); } }