TenantAtlas/apps/platform/resources/views/filament/widgets/dashboard/tenant-dashboard-overview.blade.php
ahmido 2952e5ad3e feat: polish tenant dashboard operations attention UX (#338)
## 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
2026-05-07 16:55:17 +00:00

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>