|null */ private ?array $outputGuidanceStateCache = null; public function mount(int|string $record): void { parent::mount($record); $this->auditCustomerWorkspaceOpen(); } protected function resolveRecord(int|string $key): Model { return EnvironmentReviewResource::resolveScopedRecordOrFail($key); } protected function authorizeAccess(): void { $tenant = EnvironmentReviewResource::panelTenantContext(); $record = $this->getRecord(); $user = auth()->user(); if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment || ! $record instanceof EnvironmentReview) { abort(404); } if ((int) $record->managed_environment_id !== (int) $tenant->getKey()) { abort(404); } if (! $user->canAccessTenant($tenant)) { abort(404); } if (! $user->can('view', $record)) { abort(403); } } protected function getHeaderActions(): array { if ($this->isCustomerWorkspaceView()) { return [ $this->openCurrentRenderedReportAction(), ]; } $secondaryActions = $this->secondaryLifecycleActions(); return array_values(array_filter([ $this->primaryLifecycleAction(), Actions\ActionGroup::make($secondaryActions) ->label('More') ->icon('heroicon-m-ellipsis-vertical') ->color('gray') ->visible(fn (): bool => $secondaryActions !== []), Actions\ActionGroup::make([ $this->archiveReviewAction(), ]) ->label('Danger') ->icon('heroicon-o-archive-box') ->color('danger') ->visible(fn (): bool => ! $this->isCustomerWorkspaceView() && ! $this->record->statusEnum()->isTerminal()), ])); } private function primaryLifecycleAction(): ?Actions\Action { return match ($this->primaryLifecycleActionName()) { 'refresh_review' => $this->refreshReviewAction(), 'publish_review' => $this->publishReviewAction(), 'create_next_review' => $this->createNextReviewAction(), 'export_executive_pack' => $this->exportExecutivePackAction(), 'open_successor_review' => $this->openSuccessorReviewAction(), default => null, }; } private function primaryLifecycleActionName(): ?string { if ($this->isCustomerWorkspaceView()) { return null; } $mappedPrimaryActionName = $this->mappedPrimaryLifecycleActionName(); if ($mappedPrimaryActionName !== null) { return $mappedPrimaryActionName; } if ((string) $this->record->status === EnvironmentReviewStatus::Published->value) { return 'export_executive_pack'; } if ((string) $this->record->status === EnvironmentReviewStatus::Ready->value) { return 'publish_review'; } if ($this->record->isMutable()) { return 'refresh_review'; } return null; } /** * @return list */ private function secondaryLifecycleActions(): array { return array_values(array_filter(array_map( fn (string $name): ?Actions\Action => match ($name) { 'refresh_review' => $this->refreshReviewAction(), 'publish_review' => $this->publishReviewAction(), 'export_executive_pack' => $this->exportExecutivePackAction(), 'create_next_review' => $this->createNextReviewAction(), 'open_successor_review' => $this->openSuccessorReviewAction(), default => null, }, $this->secondaryLifecycleActionNames(), ))); } /** * @return array */ private function secondaryLifecycleActionNames(): array { if ($this->isCustomerWorkspaceView()) { return []; } $names = []; if ($this->record->isMutable()) { $names[] = 'refresh_review'; $names[] = 'publish_review'; } if (in_array((string) $this->record->status, [ EnvironmentReviewStatus::Ready->value, EnvironmentReviewStatus::Published->value, ], true)) { $names[] = 'export_executive_pack'; } if ($this->record->isPublished()) { $names[] = 'create_next_review'; } return array_values(array_filter( $names, fn (string $name): bool => $name !== $this->primaryLifecycleActionName(), )); } private function refreshReviewAction(): Actions\Action { $rule = GovernanceActionCatalog::rule('refresh_review'); return UiEnforcement::forAction( Actions\Action::make('refresh_review') ->label($rule->canonicalLabel) ->icon('heroicon-o-arrow-path') ->color('primary') ->hidden(fn (): bool => ! $this->record->isMutable()) ->requiresConfirmation() ->modalHeading($rule->modalHeading) ->modalDescription($rule->modalDescription) ->action(function () use ($rule): void { $user = auth()->user(); if (! $user instanceof User) { abort(403); } try { app(EnvironmentReviewService::class)->refresh($this->record, $user); } catch (\Throwable $throwable) { Notification::make()->danger()->title('Unable to refresh review')->body($throwable->getMessage())->send(); return; } $this->record->refresh(); $this->record->loadMissing(['tenant', 'sections', 'evidenceSnapshot', 'currentExportReviewPack', 'operationRun']); $this->outputGuidanceStateCache = null; Notification::make() ->success() ->title($rule->successTitle) ->body($this->refreshReviewFeedbackBody()) ->send(); }), ) ->requireCapability(Capabilities::ENVIRONMENT_REVIEW_MANAGE) ->apply(); } private function publishReviewAction(): Actions\Action { $rule = GovernanceActionCatalog::rule('publish_review'); return UiEnforcement::forAction( Actions\Action::make('publish_review') ->label($rule->canonicalLabel) ->icon('heroicon-o-check-badge') ->color('primary') ->hidden(fn (): bool => ! $this->record->isMutable()) ->disabled(fn (): bool => ! $this->canPublishCurrentReview()) ->tooltip(fn (): ?string => ! $this->canPublishCurrentReview() ? __('localization.review.resolve_review_blockers_before_publishing') : null) ->requiresConfirmation() ->modalHeading($rule->modalHeading) ->modalDescription($rule->modalDescription) ->form([ Textarea::make('publish_reason') ->label('Publication reason') ->rows(4) ->required() ->maxLength(2000), ]) ->action(function (array $data) use ($rule): void { $user = auth()->user(); if (! $user instanceof User) { abort(403); } try { app(EnvironmentReviewLifecycleService::class)->publish( $this->record, $user, (string) ($data['publish_reason'] ?? ''), ); } catch (\Throwable $throwable) { Notification::make()->danger()->title('Unable to publish review')->body($throwable->getMessage())->send(); return; } $this->refreshFormData(['status', 'published_at', 'published_by_user_id', 'summary']); $this->outputGuidanceStateCache = null; Notification::make()->success()->title($rule->successTitle)->send(); }), ) ->requireCapability(Capabilities::ENVIRONMENT_REVIEW_MANAGE) ->preserveVisibility() ->preserveDisabled() ->apply(); } private function exportExecutivePackAction(): Actions\Action { $action = UiEnforcement::forAction( Actions\Action::make('export_executive_pack') ->label('Export executive pack') ->icon('heroicon-o-arrow-down-tray') ->color('primary') ->hidden(fn (): bool => ! in_array((string) $this->record->status, [ EnvironmentReviewStatus::Ready->value, EnvironmentReviewStatus::Published->value, ], true)) ->disabled(fn (): bool => EnvironmentReviewResource::reviewPackGenerationBlocked($this->record->tenant)) ->action(fn (): mixed => EnvironmentReviewResource::executeExport($this->record)), ) ->requireCapability(Capabilities::ENVIRONMENT_REVIEW_MANAGE) ->preserveVisibility() ->preserveDisabled() ->apply(); $action->tooltip(fn (): ?string => EnvironmentReviewResource::reviewPackGenerationActionTooltip($this->record->tenant)); return $action; } private function createNextReviewAction(): Actions\Action { return UiEnforcement::forAction( Actions\Action::make('create_next_review') ->label(__('localization.review.create_next_review')) ->icon('heroicon-o-document-duplicate') ->color('primary') ->hidden(fn (): bool => ! $this->record->isPublished()) ->requiresConfirmation() ->modalHeading(__('localization.review.create_next_review_heading')) ->modalDescription(__('localization.review.create_next_review_description')) ->modalSubmitActionLabel(__('localization.review.create_next_review_confirm')) ->action(function (): void { $user = auth()->user(); if (! $user instanceof User) { abort(403); } try { $nextReview = app(EnvironmentReviewLifecycleService::class)->createNextReview($this->record, $user); } catch (\Throwable $throwable) { Notification::make()->danger()->title('Unable to create next review')->body($throwable->getMessage())->send(); return; } Notification::make()->success()->title(__('localization.review.create_next_review_success'))->send(); $this->redirect(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $nextReview], $nextReview->tenant)); }), ) ->requireCapability(Capabilities::ENVIRONMENT_REVIEW_MANAGE) ->preserveVisibility() ->apply(); } private function openSuccessorReviewAction(): Actions\Action { return Actions\Action::make('open_successor_review') ->label(__('localization.review.open_successor_review')) ->icon('heroicon-o-arrow-top-right-on-square') ->color('primary') ->visible(fn (): bool => $this->successorReviewUrl() !== null) ->url(fn (): ?string => $this->successorReviewUrl()); } private function archiveReviewAction(): Actions\Action { $rule = GovernanceActionCatalog::rule('archive_review'); return UiEnforcement::forAction( Actions\Action::make('archive_review') ->label($rule->canonicalLabel) ->icon('heroicon-o-archive-box') ->color('danger') ->hidden(fn (): bool => $this->record->statusEnum()->isTerminal()) ->requiresConfirmation() ->modalHeading($rule->modalHeading) ->modalDescription($rule->modalDescription) ->form([ Textarea::make('archive_reason') ->label('Archive reason') ->rows(4) ->required() ->maxLength(2000), ]) ->action(function (array $data) use ($rule): void { $user = auth()->user(); if (! $user instanceof User) { abort(403); } app(EnvironmentReviewLifecycleService::class)->archive( $this->record, $user, (string) ($data['archive_reason'] ?? ''), ); $this->refreshFormData(['status', 'archived_at']); $this->outputGuidanceStateCache = null; Notification::make()->success()->title($rule->successTitle)->send(); }), ) ->requireCapability(Capabilities::ENVIRONMENT_REVIEW_MANAGE) ->preserveVisibility() ->apply(); } private function openCurrentRenderedReportAction(): Actions\Action { return Actions\Action::make('open_current_rendered_report') ->label(fn (): string => EnvironmentReviewResource::renderedReportActionLabelFor($this->record)) ->icon('heroicon-o-document-text') ->color('primary') ->disabled(fn (): bool => $this->currentRenderedReportUrl() === null) ->tooltip(fn (): ?string => $this->currentRenderedReportUnavailableReason()) ->url(fn (): ?string => $this->currentRenderedReportUrl()) ->openUrlInNewTab(); } private function currentRenderedReportUrl(): ?string { return EnvironmentReviewResource::currentRenderedReportUrlFor($this->record); } private function currentRenderedReportUnavailableReason(): ?string { if ($this->currentRenderedReportUrl() !== null) { return null; } $pack = $this->record->currentExportReviewPack; $tenant = $this->record->tenant; $user = auth()->user(); if (! $pack instanceof ReviewPack) { return __('localization.review.customer_review_pack_missing'); } if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment || ! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) { return __('localization.review.customer_review_pack_forbidden'); } if ($pack->status !== ReviewPackStatus::Ready->value) { return __('localization.review.customer_review_pack_not_ready'); } if ($pack->expires_at !== null && $pack->expires_at->isPast()) { return __('localization.review.customer_review_pack_expired'); } return __('localization.review.customer_review_pack_unavailable'); } /** * @return array */ private function outputGuidanceState(): array { return $this->outputGuidanceStateCache ??= EnvironmentReviewResource::outputGuidanceState($this->record); } private function canPublishCurrentReview(): bool { return app(EnvironmentReviewReadinessGate::class)->canPublish($this->record); } private function refreshReviewFeedbackBody(): string { return data_get($this->outputGuidanceState(), 'resolution_case.primary_action.key') === 'publish_review' ? __('localization.review.refresh_review_feedback_ready') : __('localization.review.refresh_review_feedback_blocked'); } private function mappedPrimaryLifecycleActionName(): ?string { $actionName = data_get($this->outputGuidanceState(), 'resolution_case.primary_action.action_name'); return is_string($actionName) && in_array($actionName, [ 'refresh_review', 'publish_review', 'create_next_review', 'open_successor_review', ], true) ? $actionName : null; } private function successorReviewUrl(): ?string { $url = data_get($this->outputGuidanceState(), 'resolution_case.primary_action.url'); $actionName = data_get($this->outputGuidanceState(), 'resolution_case.primary_action.action_name'); if ($actionName !== 'open_successor_review' || ! is_string($url) || trim($url) === '') { return null; } return trim($url); } private function isCustomerWorkspaceView(): bool { return request()->boolean(CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY); } private function auditCustomerWorkspaceOpen(): void { if (! $this->isCustomerWorkspaceView()) { return; } $user = auth()->user(); $tenant = $this->record->tenant; if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment) { return; } app(WorkspaceAuditLogger::class)->log( workspace: $tenant->workspace, action: AuditActionId::EnvironmentReviewOpened, context: [ 'metadata' => [ 'review_id' => (int) $this->record->getKey(), 'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE, 'tenant_filter_id' => request()->query('tenant_filter_id'), 'interpretation_version' => $this->record->controlInterpretationVersion(), ], ], actor: $user, resourceType: 'environment_review', resourceId: (string) $this->record->getKey(), targetLabel: sprintf('ManagedEnvironment review #%d', (int) $this->record->getKey()), tenant: $tenant, ); } }