*/ public array $supportDiagnosticsAuditKeys = []; private ?TenantDashboardSummary $dashboardSummary = null; public static function getNavigationLabel(): string { return __('localization.dashboard.tenant_title'); } public function getTitle(): string | Htmlable { $tenant = Filament::getTenant(); if (! $tenant instanceof Tenant) { return __('localization.dashboard.tenant_title'); } $summary = $this->dashboardSummary(); if (! $summary instanceof TenantDashboardSummary) { return (string) $tenant->name; } return new HtmlString(sprintf( '%s%s', e((string) $tenant->name), e($this->posturePillClasses((string) ($summary->posture['tone'] ?? 'gray'))), e((string) ($summary->posture['status'] ?? '')), )); } public function getSubheading(): string | Htmlable | null { return __('localization.dashboard.overview.page_subheading'); } /** * @param array $parameters */ public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null, bool $shouldGuessMissingParameters = false): string { return parent::getUrl($parameters, $isAbsolute, $panel ?? 'tenant', $tenant, $shouldGuessMissingParameters); } /** * @return array | WidgetConfiguration> */ public function getWidgets(): array { return [ TenantDashboardContextChips::class, DashboardKpis::class, TenantDashboardOverview::class, ]; } public function getColumns(): int|array { return ['default' => 1, 'xl' => 12]; } /** * @return array */ protected function getHeaderActions(): array { $actions = []; if ($primaryAction = $this->primaryFollowUpHeaderAction()) { $actions[] = $primaryAction; } $moreActions = array_values(array_filter([ $this->secondaryHeaderAction(), $this->requestSupportAction(), $this->openSupportDiagnosticsAction(), ])); if ($moreActions !== []) { $actions[] = ActionGroup::make($moreActions) ->label(__('localization.dashboard.more_actions')) ->icon('heroicon-o-ellipsis-horizontal') ->color('gray'); } return $actions; } private function primaryFollowUpHeaderAction(): ?Action { $payload = $this->primaryFollowUpHeaderPayload(); if (! is_array($payload)) { return $this->governanceInboxHeaderAction(); } return $this->summaryHeaderAction( name: 'primaryFollowUp', payload: $payload, color: 'primary', icon: 'heroicon-o-bolt', ); } private function secondaryHeaderAction(): ?Action { $payload = $this->secondaryHeaderPayload(); if (! is_array($payload)) { return null; } return $this->summaryHeaderAction( name: 'reviewOutput', payload: $payload, color: 'gray', icon: 'heroicon-o-document-duplicate', ); } /** * @return array|null */ private function primaryFollowUpHeaderPayload(): ?array { $summary = $this->dashboardSummary(); if (! $summary instanceof TenantDashboardSummary) { return null; } $payload = $summary->recommendedActions[0] ?? null; return is_array($payload) && filled($payload['actionLabel'] ?? null) ? $payload : null; } /** * @return array|null */ private function secondaryHeaderPayload(): ?array { $summary = $this->dashboardSummary(); if (! $summary instanceof TenantDashboardSummary) { return null; } $primaryPayload = $this->primaryFollowUpHeaderPayload(); foreach ($summary->readinessCards as $payload) { if (! is_array($payload) || ! filled($payload['actionLabel'] ?? null)) { continue; } if (! in_array($payload['key'] ?? null, ['customer_safe_output', 'current_review'], true)) { continue; } if ( is_array($primaryPayload) && ($payload['actionLabel'] ?? null) === ($primaryPayload['actionLabel'] ?? null) && ($payload['actionUrl'] ?? null) === ($primaryPayload['actionUrl'] ?? null) ) { continue; } return $payload; } return null; } private function governanceInboxHeaderAction(): ?Action { $tenant = Filament::getTenant(); $user = auth()->user(); if (! $tenant instanceof Tenant || ! $user instanceof User || ! $user->canAccessTenant($tenant)) { return null; } return Action::make('primaryFollowUp') ->label(__('localization.dashboard.overview.action_open_governance_inbox')) ->icon('heroicon-o-inbox-stack') ->color('primary') ->url(GovernanceInbox::getUrl(panel: 'admin').'?'.http_build_query([ 'tenant_id' => (int) $tenant->getKey(), ])); } /** * @param array $payload */ private function summaryHeaderAction(string $name, array $payload, string $color, string $icon): ?Action { $label = $payload['actionLabel'] ?? null; $url = $payload['actionUrl'] ?? null; $helperText = $payload['helperText'] ?? null; if (! is_string($label) || $label === '') { return null; } if ((! is_string($url) || $url === '') && (! is_string($helperText) || $helperText === '')) { return null; } $action = Action::make($name) ->label($label) ->icon($icon) ->color($color); if (is_string($url) && $url !== '') { $action->url($url); } else { $action->disabled(); } if (is_string($helperText) && $helperText !== '') { $action->tooltip($helperText); } return $action; } private function dashboardSummary(): ?TenantDashboardSummary { if ($this->dashboardSummary instanceof TenantDashboardSummary) { return $this->dashboardSummary; } $tenant = Filament::getTenant(); $user = auth()->user(); if (! $tenant instanceof Tenant || ! $user instanceof User) { return null; } $this->dashboardSummary = app(TenantDashboardSummaryBuilder::class)->build($tenant, $user); return $this->dashboardSummary; } private function posturePillClasses(string $tone): string { return match ($tone) { 'success' => 'inline-flex items-center rounded-full border border-success-200 bg-success-50 px-3 py-1 text-sm font-medium text-success-700 shadow-sm dark:border-success-800 dark:bg-success-500/10 dark:text-success-300', 'danger' => 'inline-flex items-center rounded-full border border-danger-200 bg-danger-50 px-3 py-1 text-sm font-medium text-danger-700 shadow-sm dark:border-danger-800 dark:bg-danger-500/10 dark:text-danger-300', 'warning' => 'inline-flex items-center rounded-full border border-warning-200 bg-warning-50 px-3 py-1 text-sm font-medium text-warning-700 shadow-sm dark:border-warning-800 dark:bg-warning-500/10 dark:text-warning-300', default => 'inline-flex items-center rounded-full border border-gray-200 bg-white px-3 py-1 text-sm font-medium text-gray-700 shadow-sm dark:border-white/10 dark:bg-white/5 dark:text-gray-300', }; } public function authorizeTenantSupportRequest(): void { $this->resolveCurrentTenantForCapability(Capabilities::SUPPORT_REQUESTS_CREATE); } private function requestSupportAction(): Action { $action = Action::make('requestSupport') ->label(__('localization.dashboard.request_support')) ->icon('heroicon-o-paper-airplane') ->color('gray') ->slideOver() ->stickyModalHeader() ->modalHeading(__('localization.dashboard.support_request_heading')) ->modalDescription(__('localization.dashboard.support_request_description')) ->modalSubmitActionLabel(__('localization.dashboard.submit_request')) ->form([ Placeholder::make('included_context') ->label(__('localization.dashboard.included_context')) ->content(fn (): string => $this->tenantSupportRequestAttachmentSummary()) ->columnSpanFull(), Placeholder::make('latest_external_handoff') ->label(__('localization.dashboard.latest_external_handoff')) ->content(fn (): string => $this->tenantLatestSupportRequestHandoffSummary()) ->columnSpanFull(), Select::make('external_handoff_mode') ->label(__('localization.dashboard.external_handoff_mode')) ->options(fn (): array => $this->supportHandoffModeOptions()) ->default(SupportRequest::EXTERNAL_HANDOFF_MODE_INTERNAL_ONLY) ->helperText(fn (): string => $this->supportDeskTargetAvailable() ? __('localization.dashboard.external_handoff_mode_helper_available') : __('localization.dashboard.external_handoff_mode_helper_unavailable')) ->required() ->live() ->native(false), Placeholder::make('handoff_mutation_scope') ->label(__('localization.dashboard.handoff_mutation_scope')) ->content(fn (Get $get): string => $this->externalHandoffMutationScope($get('external_handoff_mode'))) ->columnSpanFull(), TextInput::make('external_ticket_reference') ->label(__('localization.dashboard.external_ticket_reference')) ->helperText(__('localization.dashboard.external_ticket_reference_helper')) ->required(fn (Get $get): bool => $get('external_handoff_mode') === SupportRequest::EXTERNAL_HANDOFF_MODE_LINK_EXISTING_TICKET) ->visible(fn (Get $get): bool => $this->supportDeskTargetAvailable() && $get('external_handoff_mode') === SupportRequest::EXTERNAL_HANDOFF_MODE_LINK_EXISTING_TICKET), TextInput::make('external_ticket_url') ->label(__('localization.dashboard.external_ticket_url')) ->helperText(__('localization.dashboard.external_ticket_url_helper')) ->url() ->visible(fn (Get $get): bool => $this->supportDeskTargetAvailable() && $get('external_handoff_mode') === SupportRequest::EXTERNAL_HANDOFF_MODE_LINK_EXISTING_TICKET) ->columnSpanFull(), Select::make('severity') ->label(__('localization.dashboard.severity')) ->options(SupportRequest::severityOptions()) ->default(SupportRequest::SEVERITY_NORMAL) ->required() ->native(false), TextInput::make('summary') ->label(__('localization.dashboard.summary')) ->required() ->columnSpanFull(), Textarea::make('reproduction_notes') ->label(__('localization.dashboard.reproduction_notes')) ->rows(4) ->columnSpanFull(), TextInput::make('contact_name') ->label(__('localization.dashboard.contact_name')) ->default(fn (): ?string => $this->resolveDashboardActor()->name), TextInput::make('contact_email') ->label(__('localization.dashboard.contact_email')) ->email() ->default(fn (): ?string => $this->resolveDashboardActor()->email), ]) ->action(function (array $data): void { $actor = $this->resolveDashboardActor(); $tenant = $this->resolveCurrentTenantForCapability(Capabilities::SUPPORT_REQUESTS_CREATE); $supportRequest = app(SupportRequestSubmissionService::class)->submitForTenant($tenant, $actor, $data); Notification::make() ->title(__('localization.dashboard.support_request_submitted')) ->body($this->supportRequestNotificationBody($supportRequest)) ->when( $supportRequest->hasExternalHandoffFailure(), fn (Notification $notification): Notification => $notification->warning(), fn (Notification $notification): Notification => $notification->success(), ) ->when( $supportRequest->external_ticket_url !== null, fn (Notification $notification): Notification => $notification->actions([ Action::make('openExternalTicket') ->label(__('localization.dashboard.open_external_ticket')) ->url((string) $supportRequest->external_ticket_url, shouldOpenInNewTab: true), ]), ) ->send(); }); return UiEnforcement::forAction($action) ->requireCapability(Capabilities::SUPPORT_REQUESTS_CREATE) ->apply(); } private function openSupportDiagnosticsAction(): Action { $action = Action::make('openSupportDiagnostics') ->label(__('localization.dashboard.open_support_diagnostics')) ->icon('heroicon-o-lifebuoy') ->color('gray') ->modal() ->slideOver() ->stickyModalHeader() ->modalHeading(__('localization.dashboard.support_diagnostics')) ->modalDescription(__('localization.dashboard.support_diagnostics_description')) ->modalSubmitAction(false) ->modalCancelAction(fn (Action $action): Action => $action->label(__('localization.dashboard.close'))) ->mountUsing(function (): void { $this->auditTenantSupportDiagnosticsOpen(); }) ->modalContent(fn (): View => view('filament.modals.support-diagnostic-bundle', [ 'bundle' => $this->tenantSupportDiagnosticBundle(), ])); return UiEnforcement::forAction($action) ->requireCapability(Capabilities::SUPPORT_DIAGNOSTICS_VIEW) ->apply(); } /** * @return array */ public function tenantSupportDiagnosticBundle(): array { $user = $this->resolveDashboardActor(); $tenant = $this->resolveCurrentTenantForCapability(Capabilities::SUPPORT_DIAGNOSTICS_VIEW); return app(SupportDiagnosticBundleBuilder::class)->forTenant($tenant, $user); } private function auditTenantSupportDiagnosticsOpen(): void { $user = $this->resolveDashboardActor(); $tenant = $this->resolveCurrentTenantForCapability(Capabilities::SUPPORT_DIAGNOSTICS_VIEW); $this->recordSupportDiagnosticsOpened( tenant: $tenant, bundle: $this->tenantSupportDiagnosticBundle(), user: $user, ); } /** * @param array $bundle */ private function recordSupportDiagnosticsOpened(Tenant $tenant, array $bundle, User $user): void { $auditKey = 'tenant:'.$tenant->getKey(); if (in_array($auditKey, $this->supportDiagnosticsAuditKeys, true)) { return; } app(WorkspaceAuditLogger::class)->logSupportDiagnosticsOpened( tenant: $tenant, contextType: 'tenant', bundle: $bundle, actor: $user, ); app(ProductTelemetryRecorder::class)->record( eventName: ProductUsageEventCatalog::SUPPORT_DIAGNOSTICS_OPENED, workspaceId: (int) $tenant->workspace_id, tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), subjectType: 'tenant', subjectId: (int) $tenant->getKey(), metadata: [ 'source_surface' => 'tenant_dashboard', ], ); $this->supportDiagnosticsAuditKeys[] = $auditKey; } private function resolveDashboardActor(): User { $user = auth()->user(); if (! $user instanceof User) { abort(404); } return $user; } private function resolveCurrentTenantForCapability(string $capability): Tenant { $user = $this->resolveDashboardActor(); $tenant = Filament::getTenant(); if (! $tenant instanceof Tenant) { abort(404); } $resolver = app(CapabilityResolver::class); if (! $resolver->isMember($user, $tenant)) { abort(404); } if (! $resolver->can($user, $tenant, $capability)) { abort(403); } return $tenant; } private function tenantSupportRequestAttachmentSummary(): string { $tenant = Filament::getTenant(); $user = auth()->user(); if (! $tenant instanceof Tenant || ! $user instanceof User) { return 'Only canonical redacted tenant context will be attached.'; } $resolver = app(CapabilityResolver::class); if (! $resolver->isMember($user, $tenant)) { return 'Only canonical redacted tenant context will be attached.'; } return $resolver->can($user, $tenant, Capabilities::SUPPORT_DIAGNOSTICS_VIEW) ? 'A redacted diagnostic snapshot and the canonical tenant context will be attached.' : 'Only the canonical redacted tenant context will be attached because you cannot view support diagnostics.'; } private function tenantLatestSupportRequestHandoffSummary(): string { $tenant = $this->resolveCurrentTenantForCapability(Capabilities::SUPPORT_REQUESTS_CREATE); $user = $this->resolveDashboardActor(); $summary = app(SupportRequestSubmissionService::class)->latestTenantHandoffSummary($tenant, $user); return $this->formatLatestHandoffSummary($summary); } /** * @return array */ private function supportHandoffModeOptions(): array { if (! $this->supportDeskTargetAvailable()) { return [ SupportRequest::EXTERNAL_HANDOFF_MODE_INTERNAL_ONLY => __('localization.dashboard.handoff_mode_internal_only'), ]; } return [ SupportRequest::EXTERNAL_HANDOFF_MODE_INTERNAL_ONLY => __('localization.dashboard.handoff_mode_internal_only'), SupportRequest::EXTERNAL_HANDOFF_MODE_CREATE_EXTERNAL_TICKET => __('localization.dashboard.handoff_mode_create_external_ticket'), SupportRequest::EXTERNAL_HANDOFF_MODE_LINK_EXISTING_TICKET => __('localization.dashboard.handoff_mode_link_existing_ticket'), ]; } private function supportDeskTargetAvailable(): bool { return app(ExternalSupportDeskHandoffService::class)->targetIsConfigured(); } private function externalHandoffMutationScope(mixed $mode): string { return match ($mode) { SupportRequest::EXTERNAL_HANDOFF_MODE_CREATE_EXTERNAL_TICKET => __('localization.dashboard.mutation_scope_external_create'), SupportRequest::EXTERNAL_HANDOFF_MODE_LINK_EXISTING_TICKET => __('localization.dashboard.mutation_scope_external_link'), default => __('localization.dashboard.mutation_scope_internal_only'), }; } /** * @param array|null $summary */ private function formatLatestHandoffSummary(?array $summary): string { if ($summary === null) { return __('localization.dashboard.latest_external_handoff_none'); } $internalReference = (string) $summary['internal_reference']; if (($summary['has_failure'] ?? false) === true) { return __('localization.dashboard.latest_external_handoff_failed', [ 'reference' => $internalReference, 'failure' => (string) $summary['external_handoff_failure_summary'], ]); } if (($summary['has_external_link'] ?? false) === true) { return __('localization.dashboard.latest_external_handoff_linked', [ 'reference' => $internalReference, 'external' => (string) $summary['external_ticket_reference'], ]); } return __('localization.dashboard.latest_external_handoff_internal_only', [ 'reference' => $internalReference, ]); } private function supportRequestNotificationBody(SupportRequest $supportRequest): string { return match ($supportRequest->externalHandoffOutcome()) { SupportRequest::HANDOFF_OUTCOME_EXTERNAL_TICKET_CREATED => __('localization.dashboard.support_request_submitted_created', [ 'reference' => $supportRequest->internal_reference, 'external' => $supportRequest->external_ticket_reference, ]), SupportRequest::HANDOFF_OUTCOME_EXTERNAL_TICKET_LINKED => __('localization.dashboard.support_request_submitted_linked', [ 'reference' => $supportRequest->internal_reference, 'external' => $supportRequest->external_ticket_reference, ]), SupportRequest::HANDOFF_OUTCOME_EXTERNAL_HANDOFF_FAILED => __('localization.dashboard.support_request_submitted_failed', [ 'reference' => $supportRequest->internal_reference, 'failure' => $supportRequest->external_handoff_failure_summary, ]), default => __('localization.dashboard.support_request_submitted_internal_only', [ 'reference' => $supportRequest->internal_reference, ]), }; } }