*/ private const FAMILY_ORDER = [ 'assigned_findings', 'intake_findings', 'stale_operations', 'alert_delivery_failures', 'review_follow_up', ]; public function __construct( private TenantBackupHealthResolver $backupHealthResolver, private RestoreSafetyResolver $restoreSafetyResolver, private TenantTriageReviewStateResolver $tenantTriageReviewStateResolver, private TenantReviewRegisterService $tenantReviewRegisterService, ) {} /** * @param array $authorizedTenants * @param array $visibleFindingTenants * @param array $reviewTenants * @return array{ * sections: list>, * available_families: list, * family_counts: array, * total_count: int, * } */ public function build( User $user, Workspace $workspace, array $authorizedTenants, array $visibleFindingTenants, array $reviewTenants, bool $canViewAlerts, ?Tenant $selectedTenant = null, ?string $selectedFamily = null, ?CanonicalNavigationContext $navigationContext = null, ): array { $authorizedTenantsById = $this->indexTenants($authorizedTenants); $visibleFindingTenantsById = $this->indexTenants($visibleFindingTenants); $reviewTenantsById = $this->indexTenants($reviewTenants); $allSections = []; $availableFamilies = []; $familyCounts = []; if ($visibleFindingTenantsById !== []) { $assignedSection = $this->assignedFindingsSection( user: $user, visibleFindingTenants: $visibleFindingTenantsById, selectedTenant: $selectedTenant, navigationContext: $navigationContext, ); $allSections[$assignedSection['key']] = $assignedSection; $availableFamilies[] = [ 'key' => $assignedSection['key'], 'label' => $assignedSection['label'], 'count' => $assignedSection['count'], ]; $familyCounts[$assignedSection['key']] = $assignedSection['count']; $intakeSection = $this->intakeFindingsSection( visibleFindingTenants: $visibleFindingTenantsById, selectedTenant: $selectedTenant, navigationContext: $navigationContext, ); $allSections[$intakeSection['key']] = $intakeSection; $availableFamilies[] = [ 'key' => $intakeSection['key'], 'label' => $intakeSection['label'], 'count' => $intakeSection['count'], ]; $familyCounts[$intakeSection['key']] = $intakeSection['count']; } if ($authorizedTenantsById !== []) { $operationsSection = $this->operationsSection( workspace: $workspace, authorizedTenants: $authorizedTenantsById, selectedTenant: $selectedTenant, navigationContext: $navigationContext, ); $allSections[$operationsSection['key']] = $operationsSection; $availableFamilies[] = [ 'key' => $operationsSection['key'], 'label' => $operationsSection['label'], 'count' => $operationsSection['count'], ]; $familyCounts[$operationsSection['key']] = $operationsSection['count']; } if ($canViewAlerts) { $alertsSection = $this->alertsSection( workspace: $workspace, authorizedTenants: $authorizedTenantsById, selectedTenant: $selectedTenant, navigationContext: $navigationContext, ); $allSections[$alertsSection['key']] = $alertsSection; $availableFamilies[] = [ 'key' => $alertsSection['key'], 'label' => $alertsSection['label'], 'count' => $alertsSection['count'], ]; $familyCounts[$alertsSection['key']] = $alertsSection['count']; } if ($reviewTenantsById !== []) { $reviewSection = $this->reviewFollowUpSection( user: $user, workspace: $workspace, reviewTenants: $reviewTenantsById, selectedTenant: $selectedTenant, navigationContext: $navigationContext, ); $allSections[$reviewSection['key']] = $reviewSection; $availableFamilies[] = [ 'key' => $reviewSection['key'], 'label' => $reviewSection['label'], 'count' => $reviewSection['count'], ]; $familyCounts[$reviewSection['key']] = $reviewSection['count']; } $sections = []; foreach (self::FAMILY_ORDER as $familyKey) { $section = $allSections[$familyKey] ?? null; if (! is_array($section)) { continue; } if ($selectedFamily !== null) { if ($familyKey === $selectedFamily) { $sections[] = $section; } continue; } if ((int) ($section['count'] ?? 0) > 0) { $sections[] = $section; } } return [ 'sections' => $sections, 'available_families' => $availableFamilies, 'family_counts' => $familyCounts, 'total_count' => array_sum($familyCounts), ]; } /** * @param array $tenants * @return array */ private function indexTenants(array $tenants): array { $indexed = []; foreach ($tenants as $tenant) { $indexed[(int) $tenant->getKey()] = $tenant; } return $indexed; } /** * @param array $visibleFindingTenants * @return array */ private function assignedFindingsSection( User $user, array $visibleFindingTenants, ?Tenant $selectedTenant, ?CanonicalNavigationContext $navigationContext, ): array { $baseQuery = $this->assignedFindingsQuery($user, $visibleFindingTenants, $selectedTenant); $count = (clone $baseQuery)->count(); $overdueCount = (clone $baseQuery) ->whereNotNull('due_at') ->where('due_at', '<', now()) ->count(); $entries = $this->orderedAssignedFindingsQuery(clone $baseQuery) ->limit(self::PREVIEW_LIMIT) ->get() ->map(fn (Finding $finding): array => $this->findingEntry($finding, 'assigned_findings', $navigationContext, 10)) ->all(); return [ 'key' => 'assigned_findings', 'label' => 'Assigned findings', 'count' => $count, 'summary' => $this->assignedFindingsSummary($count, $overdueCount), 'dominant_action_label' => 'Open my findings', 'dominant_action_url' => $this->appendQuery( MyFindingsInbox::getUrl( panel: 'admin', parameters: array_filter([ 'tenant' => $selectedTenant?->external_id, ], static fn (mixed $value): bool => is_string($value) && $value !== ''), ), $navigationContext?->toQuery() ?? [], ), 'entries' => $entries, 'empty_state' => $selectedTenant instanceof Tenant ? 'No assigned findings match this tenant filter right now.' : 'No assigned findings are visible right now.', ]; } /** * @param array $visibleFindingTenants * @return array */ private function intakeFindingsSection( array $visibleFindingTenants, ?Tenant $selectedTenant, ?CanonicalNavigationContext $navigationContext, ): array { $baseQuery = $this->intakeFindingsQuery($visibleFindingTenants, $selectedTenant); $count = (clone $baseQuery)->count(); $needsTriageCount = (clone $baseQuery) ->whereIn('status', [Finding::STATUS_NEW, Finding::STATUS_REOPENED]) ->count(); $entries = $this->orderedIntakeFindingsQuery(clone $baseQuery) ->limit(self::PREVIEW_LIMIT) ->get() ->map(fn (Finding $finding): array => $this->findingEntry($finding, 'intake_findings', $navigationContext, 20)) ->all(); return [ 'key' => 'intake_findings', 'label' => 'Findings intake', 'count' => $count, 'summary' => $this->intakeFindingsSummary($count, $needsTriageCount), 'dominant_action_label' => 'Open findings intake', 'dominant_action_url' => $this->appendQuery( FindingsIntakeQueue::getUrl( panel: 'admin', parameters: array_filter([ 'tenant' => $selectedTenant?->external_id, 'view' => $needsTriageCount > 0 ? 'needs_triage' : null, ], static fn (mixed $value): bool => $value !== null && $value !== ''), ), $navigationContext?->toQuery() ?? [], ), 'entries' => $entries, 'empty_state' => $selectedTenant instanceof Tenant ? 'No intake findings match this tenant filter right now.' : 'No intake findings are visible right now.', ]; } /** * @param array $authorizedTenants * @return array */ private function operationsSection( Workspace $workspace, array $authorizedTenants, ?Tenant $selectedTenant, ?CanonicalNavigationContext $navigationContext, ): array { $terminalQuery = $this->terminalOperationsQuery($workspace, $authorizedTenants, $selectedTenant); $staleQuery = $this->staleOperationsQuery($workspace, $authorizedTenants, $selectedTenant); $terminalCount = (clone $terminalQuery)->count(); $staleCount = (clone $staleQuery)->count(); $entries = array_merge( (clone $terminalQuery)->latest('completed_at')->latest('id')->limit(self::PREVIEW_LIMIT)->get()->all(), (clone $staleQuery)->latest('created_at')->latest('id')->limit(self::PREVIEW_LIMIT)->get()->all(), ); $entries = collect($entries) ->unique(fn (OperationRun $run): int => (int) $run->getKey()) ->sortBy([ fn (OperationRun $run): int => $run->problemClass() === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP ? 0 : 1, fn (OperationRun $run): int => -1 * (int) $run->getKey(), ]) ->take(self::PREVIEW_LIMIT) ->map(fn (OperationRun $run): array => $this->operationEntry($run, $navigationContext)) ->values() ->all(); $dominantProblemClass = $terminalCount > 0 ? OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP : OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION; return [ 'key' => 'stale_operations', 'label' => 'Operations follow-up', 'count' => $terminalCount + $staleCount, 'summary' => $this->operationsSummary($terminalCount, $staleCount), 'dominant_action_label' => $terminalCount > 0 ? 'Open terminal follow-up' : 'Open stale operations', 'dominant_action_url' => OperationRunLinks::index( tenant: $selectedTenant, context: $navigationContext, problemClass: $dominantProblemClass, ), 'entries' => $entries, 'empty_state' => $selectedTenant instanceof Tenant ? 'No stale or terminal follow-up operations match this tenant filter right now.' : 'No stale or terminal follow-up operations are visible right now.', ]; } /** * @param array $authorizedTenants * @return array */ private function alertsSection( Workspace $workspace, array $authorizedTenants, ?Tenant $selectedTenant, ?CanonicalNavigationContext $navigationContext, ): array { $baseQuery = $this->alertsQuery($workspace, $authorizedTenants, $selectedTenant); $count = (clone $baseQuery)->count(); $entries = (clone $baseQuery) ->latest('created_at') ->latest('id') ->limit(self::PREVIEW_LIMIT) ->get() ->map(fn (AlertDelivery $delivery): array => $this->alertEntry($delivery, $navigationContext)) ->all(); return [ 'key' => 'alert_delivery_failures', 'label' => 'Alert delivery failures', 'count' => $count, 'summary' => $this->alertsSummary($count), 'dominant_action_label' => 'Open alert deliveries', 'dominant_action_url' => $this->appendQuery( AlertDeliveryResource::getUrl(panel: 'admin'), array_replace_recursive( $navigationContext?->toQuery() ?? [], [ 'tableFilters' => array_filter([ 'status' => ['value' => AlertDelivery::STATUS_FAILED], 'tenant_id' => $selectedTenant instanceof Tenant ? ['value' => (string) $selectedTenant->getKey()] : null, ], static fn (mixed $value): bool => $value !== null), ], ), ), 'entries' => $entries, 'empty_state' => $selectedTenant instanceof Tenant ? 'No failed alert deliveries match this tenant filter right now.' : 'No failed alert deliveries are visible right now.', ]; } /** * @param array $reviewTenants * @return array */ private function reviewFollowUpSection( User $user, Workspace $workspace, array $reviewTenants, ?Tenant $selectedTenant, ?CanonicalNavigationContext $navigationContext, ): array { $tenantIds = $selectedTenant instanceof Tenant ? [(int) $selectedTenant->getKey()] : array_keys($reviewTenants); $backupHealthByTenant = $this->backupHealthResolver->assessMany($tenantIds); $recoveryEvidenceByTenant = $this->restoreSafetyResolver->dashboardRecoveryEvidenceForTenants($tenantIds, $backupHealthByTenant); $resolved = $this->tenantTriageReviewStateResolver->resolveMany( workspaceId: (int) $workspace->getKey(), tenantIds: $tenantIds, backupHealthByTenant: $backupHealthByTenant, recoveryEvidenceByTenant: $recoveryEvidenceByTenant, ); $latestPublishedReviews = $this->tenantReviewRegisterService ->latestPublishedQuery($user, $workspace) ->get() ->keyBy('tenant_id') ->all(); $rawEntries = []; foreach ($tenantIds as $tenantId) { $tenant = $reviewTenants[$tenantId] ?? null; $rows = $resolved['rows'][$tenantId] ?? null; if (! $tenant instanceof Tenant || ! is_array($rows)) { continue; } foreach ([PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH, PortfolioArrivalContextToken::FAMILY_RECOVERY_EVIDENCE] as $family) { $row = $rows[$family] ?? null; if (! is_array($row) || ($row['current_concern_present'] ?? false) !== true) { continue; } $derivedState = $row['derived_state'] ?? null; if (! in_array($derivedState, [ TenantTriageReview::STATE_FOLLOW_UP_NEEDED, TenantTriageReview::DERIVED_STATE_CHANGED_SINCE_REVIEW, ], true)) { continue; } $rawEntries[] = $this->reviewEntry( tenant: $tenant, family: $family, row: $row, latestPublishedReview: $latestPublishedReviews[$tenantId] ?? null, navigationContext: $navigationContext, ); } } usort($rawEntries, function (array $left, array $right): int { $leftRank = (int) ($left['urgency_rank'] ?? 0); $rightRank = (int) ($right['urgency_rank'] ?? 0); if ($leftRank !== $rightRank) { return $leftRank <=> $rightRank; } return strcmp((string) ($left['headline'] ?? ''), (string) ($right['headline'] ?? '')); }); $followUpCount = collect($rawEntries) ->where('status_label', 'Follow-up needed') ->count(); $changedCount = collect($rawEntries) ->where('status_label', 'Changed since review') ->count(); return [ 'key' => 'review_follow_up', 'label' => 'Review follow-up', 'count' => count($rawEntries), 'summary' => $this->reviewSummary($followUpCount, $changedCount), 'dominant_action_label' => 'Open review follow-up', 'dominant_action_url' => $selectedTenant instanceof Tenant ? $this->appendQuery(CustomerReviewWorkspace::tenantPrefilterUrl($selectedTenant), $navigationContext?->toQuery() ?? []) : $this->appendQuery(TenantResource::getUrl(panel: 'admin'), array_replace_recursive( $navigationContext?->toQuery() ?? [], [ 'backup_posture' => [ TenantBackupHealthAssessment::POSTURE_ABSENT, TenantBackupHealthAssessment::POSTURE_STALE, TenantBackupHealthAssessment::POSTURE_DEGRADED, ], 'recovery_evidence' => [ TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED, TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_UNVALIDATED, ], 'review_state' => [ TenantTriageReview::STATE_FOLLOW_UP_NEEDED, TenantTriageReview::DERIVED_STATE_CHANGED_SINCE_REVIEW, ], 'triage_sort' => TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST, ], )), 'entries' => array_slice($rawEntries, 0, self::PREVIEW_LIMIT), 'empty_state' => $selectedTenant instanceof Tenant ? 'No review follow-up is visible for this tenant filter right now.' : 'No review follow-up is visible right now.', ]; } /** * @param array $visibleFindingTenants */ private function assignedFindingsQuery(User $user, array $visibleFindingTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder { $tenantIds = $selectedTenant instanceof Tenant ? [(int) $selectedTenant->getKey()] : array_keys($visibleFindingTenants); return Finding::query() ->with(['tenant', 'ownerUser:id,name', 'assigneeUser:id,name']) ->withSubjectDisplayName() ->whereIn('tenant_id', $tenantIds === [] ? [-1] : $tenantIds) ->where('assignee_user_id', (int) $user->getKey()) ->whereIn('status', Finding::openStatusesForQuery()); } private function orderedAssignedFindingsQuery(\Illuminate\Database\Eloquent\Builder $query): \Illuminate\Database\Eloquent\Builder { return $query ->orderByRaw( 'case when due_at is not null and due_at < ? then 0 when reopened_at is not null then 1 else 2 end asc', [now()], ) ->orderByRaw('case when due_at is null then 1 else 0 end asc') ->orderBy('due_at') ->orderByDesc('id'); } /** * @param array $visibleFindingTenants */ private function intakeFindingsQuery(array $visibleFindingTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder { $tenantIds = $selectedTenant instanceof Tenant ? [(int) $selectedTenant->getKey()] : array_keys($visibleFindingTenants); return Finding::query() ->with(['tenant', 'ownerUser:id,name', 'assigneeUser:id,name']) ->withSubjectDisplayName() ->whereIn('tenant_id', $tenantIds === [] ? [-1] : $tenantIds) ->whereNull('assignee_user_id') ->whereIn('status', Finding::openStatusesForQuery()); } private function orderedIntakeFindingsQuery(\Illuminate\Database\Eloquent\Builder $query): \Illuminate\Database\Eloquent\Builder { return $query ->orderByRaw( "case when due_at is not null and due_at < ? then 0 when status = ? then 1 when status = ? then 2 else 3 end asc", [now(), Finding::STATUS_REOPENED, Finding::STATUS_NEW], ) ->orderByRaw('case when due_at is null then 1 else 0 end asc') ->orderBy('due_at') ->orderByDesc('id'); } /** * @param array $authorizedTenants */ private function terminalOperationsQuery(Workspace $workspace, array $authorizedTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder { return $this->operationsBaseQuery($workspace, $authorizedTenants, $selectedTenant) ->terminalFollowUp(); } /** * @param array $authorizedTenants */ private function staleOperationsQuery(Workspace $workspace, array $authorizedTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder { return $this->operationsBaseQuery($workspace, $authorizedTenants, $selectedTenant) ->activeStaleAttention(); } /** * @param array $authorizedTenants */ private function operationsBaseQuery(Workspace $workspace, array $authorizedTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder { $tenantIds = array_keys($authorizedTenants); return OperationRun::query() ->with('tenant') ->where('workspace_id', (int) $workspace->getKey()) ->where(function ($query) use ($selectedTenant, $tenantIds): void { if ($selectedTenant instanceof Tenant) { $query->where('tenant_id', (int) $selectedTenant->getKey()); return; } $query ->whereIn('tenant_id', $tenantIds === [] ? [-1] : $tenantIds) ->orWhereNull('tenant_id'); }); } /** * @param array $authorizedTenants */ private function alertsQuery(Workspace $workspace, array $authorizedTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder { $tenantIds = array_keys($authorizedTenants); return AlertDelivery::query() ->with('tenant') ->where('workspace_id', (int) $workspace->getKey()) ->where('status', AlertDelivery::STATUS_FAILED) ->where(function ($query) use ($selectedTenant, $tenantIds): void { if ($selectedTenant instanceof Tenant) { $query->where('tenant_id', (int) $selectedTenant->getKey()); return; } $query ->whereIn('tenant_id', $tenantIds === [] ? [-1] : $tenantIds) ->orWhereNull('tenant_id'); }); } /** * @return array */ private function findingEntry(Finding $finding, string $familyKey, ?CanonicalNavigationContext $navigationContext, int $baseUrgencyRank): array { $sublineParts = array_values(array_filter([ $finding->owner_user_id !== null ? 'Owner: '.FindingResource::accountableOwnerDisplayFor($finding) : null, FindingExceptionResource::relativeTimeDescription($finding->due_at) ?? FindingResource::dueAttentionLabelFor($finding), $finding->reopened_at !== null ? 'Reopened' : null, ])); return [ 'family_key' => $familyKey, 'source_model' => Finding::class, 'source_key' => (string) $finding->getKey(), 'tenant_id' => $finding->tenant ? (int) $finding->tenant->getKey() : null, 'tenant_label' => $finding->tenant?->name, 'headline' => $finding->resolvedSubjectDisplayName() ?? 'Finding #'.$finding->getKey(), 'subline' => $sublineParts === [] ? null : implode(' • ', $sublineParts), 'urgency_rank' => $baseUrgencyRank + ($finding->due_at?->isPast() === true ? 0 : 1) + ($finding->reopened_at !== null ? 0 : 1), 'status_label' => Str::of((string) $finding->status)->replace('_', ' ')->title()->value(), 'destination_url' => $this->appendQuery( FindingResource::getUrl('view', ['record' => $finding], panel: 'tenant', tenant: $finding->tenant), $navigationContext?->toQuery() ?? [], ), 'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox', ]; } /** * @return array */ private function operationEntry(OperationRun $run, ?CanonicalNavigationContext $navigationContext): array { $problemClass = $run->problemClass(); return [ 'family_key' => 'stale_operations', 'source_model' => OperationRun::class, 'source_key' => (string) $run->getKey(), 'tenant_id' => $run->tenant ? (int) $run->tenant->getKey() : null, 'tenant_label' => $run->tenant?->name, 'headline' => $problemClass === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP ? 'Terminal follow-up operation' : 'Stale active operation', 'subline' => OperationRunLinks::identifier($run), 'urgency_rank' => $problemClass === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP ? 0 : 1, 'status_label' => $problemClass === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP ? 'Terminal follow-up' : 'Stale', 'destination_url' => OperationRunLinks::tenantlessView($run, $navigationContext), 'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox', ]; } /** * @return array */ private function alertEntry(AlertDelivery $delivery, ?CanonicalNavigationContext $navigationContext): array { $payload = is_array($delivery->payload) ? $delivery->payload : []; $headline = is_string($payload['title'] ?? null) && $payload['title'] !== '' ? (string) $payload['title'] : 'Failed alert delivery'; $sublineParts = array_values(array_filter([ is_string($delivery->last_error_message) && $delivery->last_error_message !== '' ? $delivery->last_error_message : null, is_string($delivery->event_type) && $delivery->event_type !== '' ? $delivery->event_type : null, ])); return [ 'family_key' => 'alert_delivery_failures', 'source_model' => AlertDelivery::class, 'source_key' => (string) $delivery->getKey(), 'tenant_id' => $delivery->tenant ? (int) $delivery->tenant->getKey() : null, 'tenant_label' => $delivery->tenant?->name, 'headline' => $headline, 'subline' => $sublineParts === [] ? null : implode(' • ', $sublineParts), 'urgency_rank' => 0, 'status_label' => 'Failed', 'destination_url' => $this->appendQuery( AlertDeliveryResource::getUrl('view', ['record' => $delivery], panel: 'admin'), $navigationContext?->toQuery() ?? [], ), 'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox', ]; } /** * @param array $row * @return array */ private function reviewEntry( Tenant $tenant, string $family, array $row, mixed $latestPublishedReview, ?CanonicalNavigationContext $navigationContext, ): array { $state = (string) ($row['derived_state'] ?? TenantTriageReview::DERIVED_STATE_NOT_REVIEWED); $familyLabel = $family === PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH ? 'Backup health' : 'Recovery evidence'; $headline = $state === TenantTriageReview::STATE_FOLLOW_UP_NEEDED ? $familyLabel.' needs review follow-up' : $familyLabel.' changed since review'; $sublineParts = array_values(array_filter([ is_string($row['reviewed_by_user_name'] ?? null) && $row['reviewed_by_user_name'] !== '' ? 'Last review: '.$row['reviewed_by_user_name'] : null, isset($row['reviewed_at']) && $row['reviewed_at'] !== null ? 'Reviewed '.optional($row['reviewed_at'])->toDateTimeString() : null, ])); $destinationUrl = $latestPublishedReview !== null ? TenantReviewResource::tenantScopedUrl('view', ['record' => $latestPublishedReview], $tenant, 'tenant') : CustomerReviewWorkspace::tenantPrefilterUrl($tenant); return [ 'family_key' => 'review_follow_up', 'source_model' => TenantTriageReview::class, 'source_key' => (string) $tenant->getKey().':'.$family, 'tenant_id' => (int) $tenant->getKey(), 'tenant_label' => $tenant->name, 'headline' => $headline, 'subline' => $sublineParts === [] ? null : implode(' • ', $sublineParts), 'urgency_rank' => $state === TenantTriageReview::STATE_FOLLOW_UP_NEEDED ? 0 : 1, 'status_label' => $state === TenantTriageReview::STATE_FOLLOW_UP_NEEDED ? 'Follow-up needed' : 'Changed since review', 'destination_url' => $this->appendQuery($destinationUrl, $navigationContext?->toQuery() ?? []), 'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox', ]; } private function assignedFindingsSummary(int $count, int $overdueCount): string { if ($count === 0) { return 'No assigned findings are visible in the current scope.'; } if ($overdueCount > 0) { return sprintf( '%d assigned finding%s remain open. %d %s overdue.', $count, $count === 1 ? '' : 's', $overdueCount, $overdueCount === 1 ? 'is' : 'are', ); } return sprintf( '%d assigned finding%s remain open in the visible scope.', $count, $count === 1 ? '' : 's', ); } private function intakeFindingsSummary(int $count, int $needsTriageCount): string { if ($count === 0) { return 'No intake findings are visible in the current scope.'; } return sprintf( '%d unassigned finding%s remain in intake. %d still need first triage.', $count, $count === 1 ? '' : 's', $needsTriageCount, ); } private function operationsSummary(int $terminalCount, int $staleCount): string { if ($terminalCount + $staleCount === 0) { return 'No stale or terminal follow-up operations are visible in the current scope.'; } if ($terminalCount > 0 && $staleCount > 0) { return sprintf( '%d terminal follow-up operation%s and %d stale active run%s need monitoring attention.', $terminalCount, $terminalCount === 1 ? '' : 's', $staleCount, $staleCount === 1 ? '' : 's', ); } if ($terminalCount > 0) { return sprintf( '%d terminal follow-up operation%s need monitoring attention.', $terminalCount, $terminalCount === 1 ? '' : 's', ); } return sprintf( '%d stale active run%s need monitoring attention.', $staleCount, $staleCount === 1 ? '' : 's', ); } private function alertsSummary(int $count): string { if ($count === 0) { return 'No failed alert deliveries are visible in the current scope.'; } return sprintf( '%d failed alert delivery attempt%s remain visible in this workspace.', $count, $count === 1 ? '' : 's', ); } private function reviewSummary(int $followUpCount, int $changedCount): string { $total = $followUpCount + $changedCount; if ($total === 0) { return 'No review follow-up is visible in the current scope.'; } return sprintf( '%d review concern%s need attention. %d marked follow-up needed and %d changed since review.', $total, $total === 1 ? '' : 's', $followUpCount, $changedCount, ); } /** * @param array $query */ private function appendQuery(string $url, array $query): string { if ($query === []) { return $url; } $separator = str_contains($url, '?') ? '&' : '?'; return $url.$separator.http_build_query($query); } }