user(); $draft = $user instanceof User ? $this->relatedOnboardingDraft($tenant, $user) : null; $lifecycle = $this->tenantOperabilityService->lifecycleFor($tenant); $lane = $surface->isOnboardingSurface() ? TenantInteractionLane::OnboardingWorkflow : TenantInteractionLane::AdministrativeManagement; $workspaceId = request() !== null ? $this->workspaceContext->currentWorkspaceId(request()) : null; $resumeOutcome = $draft instanceof TenantOnboardingSession ? $this->tenantOperabilityService->outcomeFor( tenant: $tenant, question: TenantOperabilityQuestion::ResumeOnboardingEligibility, actor: $user instanceof User ? $user : null, workspaceId: $workspaceId, lane: $lane, onboardingDraft: $draft, ) : null; return new TenantActionContext( tenant: $tenant, lifecycle: $lifecycle, surface: $surface, actor: $user instanceof User ? $user : null, workspaceId: $workspaceId, lane: $lane, relatedOnboardingDraft: $draft, relatedOnboardingIsResumable: $resumeOutcome?->allowed ?? false, hasRelatedOnboardingDraft: $draft instanceof TenantOnboardingSession, isArchived: $lifecycle->canRestore(), ); } /** * @return list */ public function catalogForTenant(Tenant $tenant, TenantActionSurface $surface, ?User $user = null): array { $context = $this->buildContext($tenant, $surface, $user); $actions = [ $this->viewAction(), ]; $primaryAction = $this->primaryActionForContext($context); if ($primaryAction instanceof TenantActionDescriptor) { $actions[] = $primaryAction; } $secondaryActions = [ $this->secondaryRelatedOnboardingActionForContext($context, $primaryAction), $this->secondaryLifecycleActionForContext($context, $primaryAction), ]; foreach ($secondaryActions as $secondaryAction) { if ($secondaryAction instanceof TenantActionDescriptor) { $actions[] = $secondaryAction; } } return array_values(array_filter($actions, static fn (mixed $action): bool => $action instanceof TenantActionDescriptor && $action->visible)); } public function lifecycleActionForTenant(Tenant $tenant): ?TenantActionDescriptor { return $this->lifecycleActionForContext($this->buildContext($tenant, TenantActionSurface::ContextMenu)); } public function relatedOnboardingActionForTenant(Tenant $tenant, TenantActionSurface $surface, ?User $user = null): ?TenantActionDescriptor { return $this->relatedOnboardingActionForContext($this->buildContext($tenant, $surface, $user)); } public function onboardingEntryDescriptor(int $resumableDraftCount): TenantActionDescriptor { return match (true) { $resumableDraftCount === 1 => new TenantActionDescriptor( key: 'resume_onboarding', family: TenantActionFamily::OnboardingWorkflow, label: 'Resume onboarding', icon: 'heroicon-m-arrow-path', group: 'primary', ), $resumableDraftCount > 1 => new TenantActionDescriptor( key: 'choose_onboarding_draft', family: TenantActionFamily::OnboardingWorkflow, label: 'Choose onboarding draft', icon: 'heroicon-m-queue-list', group: 'primary', ), default => new TenantActionDescriptor( key: 'add_tenant', family: TenantActionFamily::OnboardingWorkflow, label: 'Add tenant', icon: 'heroicon-m-plus', group: 'primary', ), }; } public function relatedOnboardingDraft(Tenant $tenant, ?User $user = null): ?TenantOnboardingSession { $user ??= auth()->user(); if (! $user instanceof User) { return null; } return TenantOnboardingSession::query() ->where('workspace_id', (int) $tenant->workspace_id) ->where('tenant_id', (int) $tenant->getKey()) ->orderByDesc('updated_at') ->get() ->first(fn (TenantOnboardingSession $draft): bool => Gate::forUser($user)->allows('view', $draft)); } private function viewAction(): TenantActionDescriptor { return new TenantActionDescriptor( key: 'view', family: TenantActionFamily::Neutral, label: 'View', icon: 'heroicon-o-eye', group: 'primary', ); } private function primaryActionForContext(TenantActionContext $context): ?TenantActionDescriptor { if ($context->relatedOnboardingIsResumable && $context->lifecycle->canResumeOnboarding()) { return $this->relatedOnboardingActionForContext($context, 'primary'); } return $this->lifecycleActionForContext($context, 'primary'); } private function secondaryLifecycleActionForContext( TenantActionContext $context, ?TenantActionDescriptor $primaryAction, ): ?TenantActionDescriptor { $action = $this->lifecycleActionForContext($context); if (! $action instanceof TenantActionDescriptor) { return null; } if ($primaryAction instanceof TenantActionDescriptor && $primaryAction->key === $action->key) { return null; } return $action; } private function secondaryRelatedOnboardingActionForContext( TenantActionContext $context, ?TenantActionDescriptor $primaryAction, ): ?TenantActionDescriptor { $action = $this->relatedOnboardingActionForContext($context); if (! $action instanceof TenantActionDescriptor) { return null; } if ($primaryAction instanceof TenantActionDescriptor && $primaryAction->key === $action->key) { return null; } return $action; } private function lifecycleActionForContext( TenantActionContext $context, string $group = 'overflow', ): ?TenantActionDescriptor { if (! $context->isGenericTenantManagementSurface()) { return null; } $restoreOutcome = $this->tenantOperabilityService->outcomeFor( tenant: $context->tenant, question: TenantOperabilityQuestion::RestoreEligibility, actor: $context->actor, workspaceId: $context->workspaceId, lane: $context->lane, ); if ($this->shouldExposeAction($restoreOutcome)) { return new TenantActionDescriptor( key: 'restore', family: TenantActionFamily::LifecycleManagement, label: 'Restore', icon: 'heroicon-o-arrow-uturn-left', destructive: true, requiresConfirmation: true, auditActionId: AuditActionId::TenantRestored, successNotificationTitle: 'Tenant restored', successNotificationBody: 'The tenant is available again in normal tenant management flows and can be selected as active context.', modalHeading: 'Restore tenant', modalDescription: 'Restore this archived tenant so it can be selected again in normal tenant management flows.', group: $group, ); } $archiveOutcome = $this->tenantOperabilityService->outcomeFor( tenant: $context->tenant, question: TenantOperabilityQuestion::ArchiveEligibility, actor: $context->actor, workspaceId: $context->workspaceId, lane: $context->lane, ); if ($this->shouldExposeAction($archiveOutcome)) { return new TenantActionDescriptor( key: 'archive', family: TenantActionFamily::LifecycleManagement, label: 'Archive', icon: 'heroicon-o-archive-box-x-mark', destructive: true, requiresConfirmation: true, auditActionId: AuditActionId::TenantArchived, successNotificationTitle: 'Tenant archived', successNotificationBody: 'The tenant remains available for inspection and audit history, but it is no longer selectable as active context.', modalHeading: 'Archive tenant', modalDescription: 'Archive this tenant to retain it for inspection and audit history while removing it from active management flows.', group: $group, ); } return null; } private function relatedOnboardingActionForContext( TenantActionContext $context, string $group = 'overflow', ): ?TenantActionDescriptor { $draft = $context->relatedOnboardingDraft; if (! $draft instanceof TenantOnboardingSession) { return null; } $resumeOutcome = $this->tenantOperabilityService->outcomeFor( tenant: $context->tenant, question: TenantOperabilityQuestion::ResumeOnboardingEligibility, actor: $context->actor, workspaceId: $context->workspaceId, lane: $context->lane, onboardingDraft: $draft, ); if ($resumeOutcome->allowed) { return new TenantActionDescriptor( key: 'related_onboarding', family: TenantActionFamily::OnboardingWorkflow, label: 'Resume onboarding', icon: 'heroicon-o-arrow-path', group: $group, ); } if ($draft->isCancelled()) { return new TenantActionDescriptor( key: 'related_onboarding', family: TenantActionFamily::OnboardingWorkflow, label: 'View cancelled onboarding draft', icon: 'heroicon-o-eye', group: $group, ); } if ($draft->isCompleted()) { return new TenantActionDescriptor( key: 'related_onboarding', family: TenantActionFamily::OnboardingWorkflow, label: 'View completed onboarding', icon: 'heroicon-o-eye', group: $group, ); } return new TenantActionDescriptor( key: 'related_onboarding', family: TenantActionFamily::OnboardingWorkflow, label: 'View related onboarding', icon: 'heroicon-o-eye', group: $group, ); } private function shouldExposeAction(TenantOperabilityOutcome $outcome): bool { return $outcome->allowed || $outcome->isDeniedForCapability(); } }