TenantAtlas/resources/views/filament/infolists/entries/baseline-snapshot-groups.blade.php
ahmido 3c3daae405 feat: normalize operator outcome taxonomy (#186)
## Summary
- introduce a shared operator outcome taxonomy with semantic axes, severity bands, and next-action policy
- apply the taxonomy to operations, evidence/review completeness, baseline semantics, and restore semantics
- harden badge rendering, tenant-safe filtering/search behavior, and operator-facing summary/notification wording
- add the spec kit artifacts, reference documentation, and regression coverage for diagnostic-vs-primary state handling

## Testing
- focused Pest coverage for taxonomy registry and badge guardrails
- operations presentation and notification tests
- evidence, baseline, restore, and tenant-scope regression tests

## Notes
- Livewire v4.0+ compliance is preserved in the existing Filament v5 stack
- panel provider registration remains unchanged in bootstrap/providers.php
- no new globally searchable resource was added; adopted resources remain tenant-safe and out of global search where required
- no new destructive action family was introduced; existing actions keep their current authorization and confirmation behavior
- no new frontend asset strategy was introduced; existing deploy flow with filament:assets remains unchanged

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #186
2026-03-22 12:13:34 +00:00

147 lines
8.9 KiB
PHP

@php
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use Illuminate\Support\Carbon;
$groups = isset($groups) ? $groups : (isset($getState) ? $getState() : []);
$groups = is_array($groups) ? $groups : [];
$formatTimestamp = static function (?string $value): string {
if (! is_string($value) || trim($value) === '') {
return '—';
}
try {
return Carbon::parse($value)->toDayDateTimeString();
} catch (\Throwable) {
return $value;
}
};
@endphp
<div class="space-y-4">
@if ($groups === [])
<div class="rounded-lg border border-dashed border-gray-300 px-4 py-6 text-sm text-gray-600 dark:border-gray-700 dark:text-gray-300">
No snapshot items were captured for this baseline snapshot.
</div>
@else
@foreach ($groups as $group)
@php
$fidelitySpec = BadgeRenderer::spec(BadgeDomain::BaselineSnapshotFidelity, $group['fidelity'] ?? null);
$gapSpec = BadgeRenderer::spec(BadgeDomain::BaselineSnapshotGapStatus, data_get($group, 'gapSummary.badge_state'));
$messages = data_get($group, 'gapSummary.messages', []);
@endphp
<x-filament::section
:heading="$group['label'] ?? ($group['policyType'] ?? 'Policy type')"
:description="$group['coverageHint'] ?? null"
collapsible
:collapsed="(bool) ($group['initiallyCollapsed'] ?? true)"
>
<x-slot name="headerEnd">
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ (int) ($group['itemCount'] ?? 0) }} {{ \Illuminate\Support\Str::plural('item', (int) ($group['itemCount'] ?? 0)) }}
</span>
<x-filament::badge :color="$fidelitySpec->color" :icon="$fidelitySpec->icon" size="sm">
{{ $fidelitySpec->label }}
</x-filament::badge>
<x-filament::badge :color="$gapSpec->color" :icon="$gapSpec->icon" size="sm">
{{ $gapSpec->label }}
</x-filament::badge>
</div>
</x-slot>
<div class="space-y-4">
@if (! empty($group['renderingError']))
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-900/40 dark:text-gray-200">
{{ $group['renderingError'] }}
</div>
@endif
@if (is_array($messages) && $messages !== [])
<div class="rounded-lg border px-4 py-3 text-sm {{ data_get($group, 'gapSummary.has_gaps') ? 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-200' : 'border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-700 dark:bg-gray-900/40 dark:text-gray-200' }}">
<div class="font-medium">{{ data_get($group, 'gapSummary.has_gaps') ? 'Coverage gaps' : 'Diagnostic notes' }}</div>
<ul class="mt-2 list-disc space-y-1 pl-5">
@foreach ($messages as $message)
<li>{{ $message }}</li>
@endforeach
</ul>
</div>
@endif
<div class="space-y-3">
@foreach (($group['items'] ?? []) as $item)
@php
$itemFidelitySpec = BadgeRenderer::spec(BadgeDomain::BaselineSnapshotFidelity, $item['fidelity'] ?? null);
$itemGapSpec = BadgeRenderer::spec(BadgeDomain::BaselineSnapshotGapStatus, data_get($item, 'gapSummary.badge_state'));
@endphp
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900">
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div class="space-y-1">
<div class="text-sm font-semibold text-gray-900 dark:text-white">
{{ $item['label'] ?? 'Snapshot item' }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $item['typeLabel'] ?? 'Policy type' }}
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
{{ $item['referenceStatus'] ?? 'Captured metadata' }}
</span>
<x-filament::badge :color="$itemFidelitySpec->color" :icon="$itemFidelitySpec->icon" size="sm">
{{ $itemFidelitySpec->label }}
</x-filament::badge>
<x-filament::badge :color="$itemGapSpec->color" :icon="$itemGapSpec->icon" size="sm">
{{ $itemGapSpec->label }}
</x-filament::badge>
</div>
</div>
<div class="mt-3 grid gap-2 text-sm text-gray-700 dark:text-gray-200 md:grid-cols-3">
<div>
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Identity</div>
<div class="mt-1 break-all">{{ $item['identityHint'] ?? '—' }}</div>
</div>
<div>
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Observed</div>
<div class="mt-1">{{ $formatTimestamp($item['observedAt'] ?? null) }}</div>
</div>
<div>
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Source reference</div>
<div class="mt-1">{{ $item['sourceReference'] ?? '—' }}</div>
</div>
</div>
@if (is_array(data_get($item, 'gapSummary.messages')) && data_get($item, 'gapSummary.messages') !== [])
<div class="mt-3 rounded-lg border px-3 py-2 text-xs {{ data_get($item, 'gapSummary.has_gaps') ? 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-200' : 'border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-700 dark:bg-gray-900/40 dark:text-gray-200' }}">
{{ implode(' ', data_get($item, 'gapSummary.messages', [])) }}
</div>
@endif
@if (($item['structuredAttributes'] ?? []) !== [])
<dl class="mt-4 grid gap-3 md:grid-cols-2">
@foreach (($item['structuredAttributes'] ?? []) as $attribute)
<div class="rounded-lg border border-gray-100 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/40">
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{{ $attribute['label'] ?? 'Attribute' }}
</dt>
<dd class="mt-1 text-sm text-gray-800 dark:text-gray-100">
{{ $attribute['value'] ?? '—' }}
</dd>
</div>
@endforeach
</dl>
@endif
</div>
@endforeach
</div>
</div>
</x-filament::section>
@endforeach
@endif
</div>