Integrates latest TenantPilot platform changes from `platform-dev` into `dev`. This PR was created by agent on user request; do not merge automatically. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #302
423 lines
17 KiB
PHP
423 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Pages;
|
|
|
|
use App\Filament\Widgets\Tenant\TenantTriageArrivalContinuity;
|
|
use App\Filament\Widgets\Dashboard\BaselineCompareNow;
|
|
use App\Filament\Widgets\Dashboard\DashboardKpis;
|
|
use App\Filament\Widgets\Dashboard\NeedsAttention;
|
|
use App\Filament\Widgets\Dashboard\RecentDriftFindings;
|
|
use App\Filament\Widgets\Dashboard\RecentOperations;
|
|
use App\Filament\Widgets\Dashboard\RecoveryReadiness;
|
|
use App\Models\SupportRequest;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\Audit\WorkspaceAuditLogger;
|
|
use App\Services\Auth\CapabilityResolver;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\ProductTelemetry\ProductTelemetryRecorder;
|
|
use App\Support\ProductTelemetry\ProductUsageEventCatalog;
|
|
use App\Support\Rbac\UiEnforcement;
|
|
use App\Support\SupportDiagnostics\SupportDiagnosticBundleBuilder;
|
|
use App\Support\SupportRequests\ExternalSupportDeskHandoffService;
|
|
use App\Support\SupportRequests\SupportRequestSubmissionService;
|
|
use Filament\Actions\Action;
|
|
use Filament\Facades\Filament;
|
|
use Filament\Forms\Components\Placeholder;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Dashboard;
|
|
use Filament\Schemas\Components\Utilities\Get;
|
|
use Filament\Widgets\Widget;
|
|
use Filament\Widgets\WidgetConfiguration;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
class TenantDashboard extends Dashboard
|
|
{
|
|
/**
|
|
* @var list<string>
|
|
*/
|
|
public array $supportDiagnosticsAuditKeys = [];
|
|
|
|
public function getTitle(): string
|
|
{
|
|
return __('localization.dashboard.tenant_title');
|
|
}
|
|
|
|
/**
|
|
* @param array<mixed> $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<class-string<Widget> | WidgetConfiguration>
|
|
*/
|
|
public function getWidgets(): array
|
|
{
|
|
return [
|
|
TenantTriageArrivalContinuity::class,
|
|
RecoveryReadiness::class,
|
|
DashboardKpis::class,
|
|
NeedsAttention::class,
|
|
BaselineCompareNow::class,
|
|
RecentDriftFindings::class,
|
|
RecentOperations::class,
|
|
];
|
|
}
|
|
|
|
public function getColumns(): int|array
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
/**
|
|
* @return array<Action>
|
|
*/
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
$this->requestSupportAction(),
|
|
$this->openSupportDiagnosticsAction(),
|
|
];
|
|
}
|
|
|
|
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<string, mixed>
|
|
*/
|
|
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<string, mixed> $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<string, string>
|
|
*/
|
|
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<string, mixed>|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,
|
|
]),
|
|
};
|
|
}
|
|
}
|