## Summary - rename the tenant dashboard operations KPI to attention-first wording and keep the primary header CTA derived from the highest-priority recommended action - restyle the `Operations requiring attention` card to match the existing neutral dashboard card language while keeping only a subtle per-item attention accent - replace technical operation identifiers on the dashboard with calmer timing/copy, including provider-consent follow-up messaging for blocked permission posture checks - refresh the local Spec Kit artifacts for spec 273 so the branch documentation matches the implemented attention-only dashboard scope ## Validation - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Dashboard/TenantDashboardProductizationSummaryTest.php tests/Feature/Dashboard/TenantDashboardProductizationActionsTest.php tests/Feature/Dashboard/TenantDashboardProductizationAuthorizationTest.php tests/Feature/Filament/DashboardKpisWidgetTest.php tests/Feature/Filament/TenantDashboardDbOnlyTest.php tests/Browser/Dashboard/TenantDashboardProductizationSmokeTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #338
304 lines
19 KiB
PHP
304 lines
19 KiB
PHP
@php
|
|
$overviewSecondaryListStackClasses = 'flex flex-col gap-2';
|
|
$overviewSecondaryListRowBaseClasses = 'min-w-0 rounded-xl border p-4 shadow-sm';
|
|
$overviewSecondaryListRowSurfaceClasses = 'border-gray-200 bg-white/80 dark:border-white/10 dark:bg-white/5';
|
|
$overviewSecondaryListInteractiveClasses = 'transition duration-150 hover:shadow-md hover:ring-1 hover:ring-gray-950/5 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 dark:hover:ring-white/10';
|
|
@endphp
|
|
|
|
<div
|
|
@if ($pollingInterval)
|
|
wire:poll.{{ $pollingInterval }}
|
|
@endif
|
|
data-testid="tenant-dashboard-overview"
|
|
class="grid w-full min-w-0 gap-6 xl:grid-cols-12"
|
|
style="grid-column: 1 / -1;"
|
|
>
|
|
<!-- Left Column (Main) -->
|
|
<div data-testid="tenant-dashboard-overview-main" class="flex w-full min-w-0 flex-col gap-6 xl:col-span-8">
|
|
<!-- Recommended Actions -->
|
|
<x-filament::section :heading="__('localization.dashboard.overview.section_recommended_actions')">
|
|
@if ($recommendedActions === [])
|
|
<div data-testid="tenant-dashboard-recommended-actions-empty" class="rounded-xl border border-success-200 bg-success-50/80 p-5 dark:border-success-800 dark:bg-success-500/10">
|
|
<div class="text-sm font-semibold text-success-700 dark:text-success-300">{{ __('localization.dashboard.overview.empty_recommended_actions_headline') }}</div>
|
|
<p class="mt-2 text-sm leading-6 text-success-700/90 dark:text-success-200/90">
|
|
{{ __('localization.dashboard.overview.empty_recommended_actions_summary') }}
|
|
</p>
|
|
</div>
|
|
@else
|
|
<div data-testid="tenant-dashboard-recommended-actions" class="grid min-w-0 gap-4">
|
|
@foreach (array_slice($recommendedActions, 0, 3) as $index => $action)
|
|
<div data-testid="tenant-dashboard-recommended-action" data-action-key="{{ $action['key'] }}" class="flex min-w-0 items-start gap-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5 max-sm:flex-col max-sm:items-stretch">
|
|
<div data-testid="tenant-dashboard-recommended-action-priority" class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-200">
|
|
{{ $index + 1 }}
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
@if (filled($action['icon'] ?? null))
|
|
<x-filament::icon
|
|
data-testid="tenant-dashboard-recommended-action-icon"
|
|
data-action-key="{{ $action['key'] }}"
|
|
data-icon="{{ $action['icon'] }}"
|
|
:icon="$action['icon']"
|
|
class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500"
|
|
/>
|
|
@endif
|
|
|
|
<h3 class="min-w-0 text-sm font-semibold text-gray-950 dark:text-white">{{ $action['title'] }}</h3>
|
|
</div>
|
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
<span class="font-medium text-gray-700 dark:text-gray-300">Reason:</span> {{ $action['reason'] }}
|
|
</p>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-500">
|
|
<span class="font-medium text-gray-700 dark:text-gray-300">Impact:</span> {{ $action['impact'] }}
|
|
</p>
|
|
</div>
|
|
@if (filled($action['actionUrl'] ?? null))
|
|
<div class="shrink-0 max-sm:ml-0 sm:ml-4">
|
|
<x-filament::button data-testid="tenant-dashboard-secondary-action" :href="$action['actionUrl']" tag="a" color="gray" size="sm">
|
|
{{ $action['actionLabel'] ?? 'Review' }}
|
|
</x-filament::button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</x-filament::section>
|
|
|
|
<!-- Governance Status -->
|
|
<x-filament::section :heading="__('localization.dashboard.overview.section_governance_status')">
|
|
<div class="{{ $overviewSecondaryListStackClasses }}">
|
|
@foreach ($governanceStatus as $status)
|
|
@php
|
|
$isGovernanceStatusInteractive = filled($status['actionUrl'] ?? null);
|
|
$governanceStatusClasses = $isGovernanceStatusInteractive
|
|
? "{$overviewSecondaryListRowBaseClasses} {$overviewSecondaryListRowSurfaceClasses} {$overviewSecondaryListInteractiveClasses} flex items-start justify-between gap-4"
|
|
: "{$overviewSecondaryListRowBaseClasses} {$overviewSecondaryListRowSurfaceClasses} flex items-start justify-between gap-4";
|
|
@endphp
|
|
|
|
@if ($isGovernanceStatusInteractive)
|
|
<a
|
|
data-testid="tenant-dashboard-governance-status"
|
|
data-overview-row-style="secondary-list-row"
|
|
data-status-key="{{ $status['key'] ?? '' }}"
|
|
data-governance-interactive="true"
|
|
href="{{ $status['actionUrl'] }}"
|
|
class="{{ $governanceStatusClasses }}"
|
|
>
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
@if (filled($status['icon'] ?? null))
|
|
<x-filament::icon
|
|
data-testid="tenant-dashboard-governance-status-icon"
|
|
data-status-key="{{ $status['key'] ?? '' }}"
|
|
data-icon="{{ $status['icon'] }}"
|
|
:icon="$status['icon']"
|
|
class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500"
|
|
/>
|
|
@endif
|
|
|
|
<div class="truncate text-sm font-semibold text-gray-900 dark:text-white">{{ $status['label'] }}</div>
|
|
</div>
|
|
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ $status['description'] }}</div>
|
|
</div>
|
|
<div class="ml-4 shrink-0 pt-0.5">
|
|
<x-filament::badge :color="$status['tone']">{{ $status['value'] }}</x-filament::badge>
|
|
</div>
|
|
</a>
|
|
@else
|
|
<div
|
|
data-testid="tenant-dashboard-governance-status"
|
|
data-overview-row-style="secondary-list-row"
|
|
data-status-key="{{ $status['key'] ?? '' }}"
|
|
data-governance-interactive="false"
|
|
class="{{ $governanceStatusClasses }}"
|
|
>
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
@if (filled($status['icon'] ?? null))
|
|
<x-filament::icon
|
|
data-testid="tenant-dashboard-governance-status-icon"
|
|
data-status-key="{{ $status['key'] ?? '' }}"
|
|
data-icon="{{ $status['icon'] }}"
|
|
:icon="$status['icon']"
|
|
class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500"
|
|
/>
|
|
@endif
|
|
|
|
<div class="truncate text-sm font-semibold text-gray-900 dark:text-white">{{ $status['label'] }}</div>
|
|
</div>
|
|
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ $status['description'] }}</div>
|
|
</div>
|
|
<div class="ml-4 shrink-0 pt-0.5">
|
|
<x-filament::badge :color="$status['tone']">{{ $status['value'] }}</x-filament::badge>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
@endforeach
|
|
</div>
|
|
</x-filament::section>
|
|
|
|
@if ($activeOperationSummary)
|
|
<div
|
|
data-testid="tenant-dashboard-operations-attention-summary"
|
|
class="min-w-0 rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5"
|
|
>
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ $activeOperationSummary['title'] }}</div>
|
|
<x-filament::badge :color="$activeOperationSummary['tone']">{{ $activeOperationSummary['count'] }}</x-filament::badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex shrink-0 flex-wrap items-center gap-2 sm:justify-end">
|
|
<x-filament::button
|
|
data-testid="tenant-dashboard-operations-attention-secondary-action"
|
|
tag="a"
|
|
:href="$activeOperationSummary['secondaryActionUrl']"
|
|
size="sm"
|
|
color="gray"
|
|
>
|
|
{{ $activeOperationSummary['secondaryActionLabel'] }}
|
|
</x-filament::button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3">
|
|
@foreach ($activeOperationSummary['items'] ?? [] as $operation)
|
|
<div data-testid="tenant-dashboard-operations-attention-item" class="rounded-xl border border-gray-200 border-l-4 border-l-warning-400 bg-gray-50/70 p-4 dark:border-white/10 dark:border-l-warning-500 dark:bg-white/5">
|
|
<div class="flex items-start justify-between gap-4 max-sm:flex-col max-sm:items-stretch">
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
@if (filled($operation['icon'] ?? null))
|
|
<x-filament::icon
|
|
data-testid="tenant-dashboard-operations-attention-item-icon"
|
|
data-icon="{{ $operation['icon'] }}"
|
|
:icon="$operation['icon']"
|
|
class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500"
|
|
/>
|
|
@endif
|
|
|
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ $operation['title'] }}</div>
|
|
|
|
@if (filled($operation['attentionLabel'] ?? null))
|
|
<x-filament::badge color="warning">{{ $operation['attentionLabel'] }}</x-filament::badge>
|
|
@endif
|
|
</div>
|
|
|
|
<p class="mt-2 text-sm leading-6 text-gray-700 dark:text-gray-300">{{ $operation['outcomeSentence'] }}</p>
|
|
|
|
@if (filled($operation['timingLabel'] ?? null))
|
|
<div class="mt-2 text-xs font-medium text-gray-500 dark:text-gray-400">{{ $operation['timingLabel'] }}</div>
|
|
@endif
|
|
|
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400"><span class="font-medium text-gray-700 dark:text-gray-300">{{ __('localization.dashboard.overview.label_reason') }}:</span> {{ $operation['reason'] }}</p>
|
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400"><span class="font-medium text-gray-700 dark:text-gray-300">{{ __('localization.dashboard.overview.label_impact') }}:</span> {{ $operation['impact'] }}</p>
|
|
</div>
|
|
|
|
<div class="shrink-0 max-sm:ml-0 sm:ml-4">
|
|
<x-filament::button
|
|
data-testid="tenant-dashboard-operations-attention-item-action"
|
|
tag="a"
|
|
:href="$operation['primaryActionUrl']"
|
|
size="sm"
|
|
>
|
|
{{ $operation['primaryActionLabel'] }}
|
|
</x-filament::button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Right Column (Aside) -->
|
|
<div data-testid="tenant-dashboard-overview-aside" class="flex w-full min-w-0 flex-col gap-6 xl:col-span-4">
|
|
@foreach ($readinessCards as $card)
|
|
@php
|
|
$cardMeta = array_values(array_filter($card['meta'] ?? []));
|
|
$headline = $card['headline'] ?? null;
|
|
$cardProgress = array_values(array_filter($card['progress'] ?? []));
|
|
@endphp
|
|
<div data-testid="tenant-dashboard-readiness-card" data-readiness-key="{{ $card['key'] }}" class="min-w-0 rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div class="min-w-0">
|
|
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ $card['title'] }}</div>
|
|
@if (filled($headline))
|
|
<div class="mt-1 text-base font-semibold text-gray-950 dark:text-white">{{ $headline }}</div>
|
|
@else
|
|
<div class="mt-1 text-base font-semibold text-gray-950 dark:text-white">{{ $card['status'] }}</div>
|
|
@endif
|
|
</div>
|
|
<x-filament::badge :color="$card['tone']">{{ $card['status'] }}</x-filament::badge>
|
|
</div>
|
|
|
|
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-400">{{ $card['body'] }}</p>
|
|
|
|
@if ($cardProgress !== [])
|
|
<div class="mt-3 space-y-3">
|
|
@foreach ($cardProgress as $progress)
|
|
@php
|
|
$progressBarColor = match ($progress['tone'] ?? 'primary') {
|
|
'success' => 'var(--success-500)',
|
|
'warning' => 'var(--warning-500)',
|
|
'danger' => 'var(--danger-500)',
|
|
default => 'var(--primary-500)',
|
|
};
|
|
@endphp
|
|
|
|
<div data-progress-key="{{ $progress['key'] }}" class="space-y-1.5">
|
|
<div class="flex items-center justify-between gap-3 text-xs">
|
|
<span class="truncate text-gray-500 dark:text-gray-400">{{ $progress['label'] }}</span>
|
|
<span class="shrink-0 font-medium text-gray-700 dark:text-gray-200">{{ $progress['valueLabel'] }}</span>
|
|
</div>
|
|
|
|
<div class="overflow-hidden rounded-full bg-gray-100 dark:bg-white/10" style="height: 0.5rem;">
|
|
<div
|
|
class="block h-full rounded-full"
|
|
role="progressbar"
|
|
aria-label="{{ $progress['label'] }}"
|
|
aria-valuemin="0"
|
|
aria-valuemax="100"
|
|
aria-valuenow="{{ $progress['percent'] }}"
|
|
aria-valuetext="{{ $progress['valueLabel'] }}"
|
|
style="width: {{ $progress['percent'] }}%; background-color: {{ $progressBarColor }};"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
@if ($cardMeta !== [])
|
|
<div class="mt-3 grid gap-2 text-xs">
|
|
@foreach ($cardMeta as $item)
|
|
<div class="flex items-center justify-between gap-3 rounded-lg bg-gray-50/80 px-3 py-2 text-gray-600 dark:bg-white/5 dark:text-gray-300">
|
|
<span class="truncate text-gray-500 dark:text-gray-400">{{ $item['label'] }}</span>
|
|
<span class="shrink-0 font-medium text-gray-700 dark:text-gray-200">{{ $item['value'] }}</span>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
@if (filled($card['actionLabel'] ?? null))
|
|
<div class="mt-4">
|
|
@if (filled($card['actionUrl'] ?? null))
|
|
<x-filament::button data-testid="tenant-dashboard-secondary-action" tag="a" :href="$card['actionUrl']" size="sm" color="gray">
|
|
{{ $card['actionLabel'] }}
|
|
</x-filament::button>
|
|
@else
|
|
<x-filament::button data-testid="tenant-dashboard-secondary-action" size="sm" color="gray" disabled>
|
|
{{ $card['actionLabel'] }}
|
|
</x-filament::button>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div> |