|null */ public ?array $arrivalState = null; protected static bool $isLazy = false; protected int|string|array $columnSpan = 'full'; protected string $view = 'filament.widgets.tenant.triage-arrival-continuity'; public function mount(): void { $this->arrivalState = PortfolioArrivalContextToken::decode( request()->query(PortfolioArrivalContextToken::QUERY_PARAMETER), ); } /** * @return array */ protected function getViewData(): array { $tenant = Filament::getTenant(); if (! $tenant instanceof Tenant) { return ['context' => null, 'reviewState' => null]; } $context = $this->resolveArrivalContext($tenant); if ($context === null) { return ['context' => null, 'reviewState' => null]; } return [ 'context' => $context, 'reviewState' => $this->currentReviewStateFor($tenant, $context->concernFamily), ]; } public function markReviewedAction(): Action { return UiEnforcement::forAction( Action::make('markReviewed') ->label('Mark reviewed') ->icon('heroicon-o-check-circle') ->color('success') ->requiresConfirmation() ->modalHeading('Mark reviewed') ->modalDescription($this->reviewModalDescription(TenantTriageReview::STATE_REVIEWED)) ->visible(fn (): bool => $this->canShowReviewActions()) ->action(function (TenantTriageReviewService $service): void { $this->handleReviewMutation(TenantTriageReview::STATE_REVIEWED, $service); }), ) ->preserveVisibility() ->requireCapability(Capabilities::TENANT_TRIAGE_REVIEW_MANAGE) ->apply(); } public function markFollowUpNeededAction(): Action { return UiEnforcement::forAction( Action::make('markFollowUpNeeded') ->label('Mark follow-up needed') ->icon('heroicon-o-exclamation-triangle') ->color('warning') ->requiresConfirmation() ->modalHeading('Mark follow-up needed') ->modalDescription($this->reviewModalDescription(TenantTriageReview::STATE_FOLLOW_UP_NEEDED)) ->visible(fn (): bool => $this->canShowReviewActions()) ->action(function (TenantTriageReviewService $service): void { $this->handleReviewMutation(TenantTriageReview::STATE_FOLLOW_UP_NEEDED, $service); }), ) ->preserveVisibility() ->requireCapability(Capabilities::TENANT_TRIAGE_REVIEW_MANAGE) ->apply(); } private function canShowReviewActions(): bool { $tenant = Filament::getTenant(); if (! $tenant instanceof Tenant) { return false; } $context = $this->resolveArrivalContext($tenant); if ($context === null) { return false; } return ($this->currentReviewStateFor($tenant, $context->concernFamily)['current_concern_present'] ?? false) === true; } private function reviewModalDescription(string $targetManualState): \Closure { return function () use ($targetManualState): string { $tenant = Filament::getTenant(); if (! $tenant instanceof Tenant) { return 'This triage session is no longer available.'; } $context = $this->resolveArrivalContext($tenant); if ($context === null) { return 'This triage session is no longer available.'; } $reviewState = $this->currentReviewStateFor($tenant, $context->concernFamily); if (($reviewState['current_concern_present'] ?? false) !== true) { return 'This triage session no longer points at a current concern.'; } $currentLabel = BadgeRenderer::spec( BadgeDomain::TenantTriageReviewState, (string) ($reviewState['derived_state'] ?? TenantTriageReview::DERIVED_STATE_NOT_REVIEWED), )->label; $targetLabel = BadgeRenderer::spec(BadgeDomain::TenantTriageReviewState, $targetManualState)->label; return implode("\n\n", [ 'Concern family: '.$this->concernFamilyLabel($context->concernFamily), 'Current review state: '.$currentLabel, 'Target state: '.$targetLabel, 'Scope: TenantPilot only. This updates shared triage progress and does not change backup posture or recovery evidence.', ]); }; } private function handleReviewMutation(string $targetManualState, TenantTriageReviewService $service): void { $tenant = Filament::getTenant(); if (! $tenant instanceof Tenant) { return; } $context = $this->resolveArrivalContext($tenant); if ($context === null) { Notification::make() ->title('No triage session available') ->warning() ->send(); return; } $reviewState = $this->currentReviewStateFor($tenant, $context->concernFamily); if (($reviewState['current_concern_present'] ?? false) !== true) { Notification::make() ->title('No current concern to update') ->body('This arrival context no longer maps to an active concern.') ->warning() ->send(); return; } $backupHealth = app(TenantBackupHealthResolver::class)->assess($tenant); $recoveryEvidence = app(RestoreSafetyResolver::class)->dashboardRecoveryEvidence($tenant); $actor = auth()->user(); $review = match ($targetManualState) { TenantTriageReview::STATE_REVIEWED => $service->markReviewed( tenant: $tenant, concernFamily: $context->concernFamily, backupHealth: $backupHealth, recoveryEvidence: $recoveryEvidence, actor: $actor instanceof User ? $actor : null, ), TenantTriageReview::STATE_FOLLOW_UP_NEEDED => $service->markFollowUpNeeded( tenant: $tenant, concernFamily: $context->concernFamily, backupHealth: $backupHealth, recoveryEvidence: $recoveryEvidence, actor: $actor instanceof User ? $actor : null, ), default => null, }; if (! $review instanceof TenantTriageReview) { return; } Notification::make() ->title('Review state updated') ->body(sprintf( '%s is now %s for %s.', $tenant->name, BadgeRenderer::spec(BadgeDomain::TenantTriageReviewState, $review->current_state)->label, $this->concernFamilyLabel($context->concernFamily), )) ->success() ->send(); } /** * @return array|null */ private function currentReviewStateFor(Tenant $tenant, string $concernFamily): ?array { $backupHealth = app(TenantBackupHealthResolver::class)->assess($tenant); $recoveryEvidence = app(RestoreSafetyResolver::class)->dashboardRecoveryEvidence($tenant); return app(TenantTriageReviewStateResolver::class)->resolveMany( workspaceId: (int) $tenant->workspace_id, tenantIds: [(int) $tenant->getKey()], backupHealthByTenant: [(int) $tenant->getKey() => $backupHealth], recoveryEvidenceByTenant: [(int) $tenant->getKey() => $recoveryEvidence], )['rows'][(int) $tenant->getKey()][$concernFamily] ?? null; } private function concernFamilyLabel(string $concernFamily): string { return match ($concernFamily) { PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH => 'Backup health', PortfolioArrivalContextToken::FAMILY_RECOVERY_EVIDENCE => 'Recovery evidence', default => 'Portfolio concern', }; } private function resolveArrivalContext(Tenant $tenant): ?PortfolioArrivalContext { return app(PortfolioArrivalContextResolver::class)->resolveState($tenant, $this->arrivalState); } }