Some checks failed
Main Confidence / confidence (push) Failing after 57s
## Summary - add a read-first governance inbox page at `/admin/governance/inbox` - aggregate assigned findings, intake, stale operations, alert-delivery failures, and review follow-up into one canonical routing surface - add focused coverage for inbox authorization, navigation context, page behavior, and section builder logic - include the Spec Kit artifacts for spec 250 ## Notes - branch is synced with `dev` - this PR supersedes #290 for the governance inbox work Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #291
164 lines
9.6 KiB
PHP
164 lines
9.6 KiB
PHP
<x-filament-panels::page>
|
|
@php
|
|
$scope = $this->appliedScope();
|
|
$sections = $this->sections();
|
|
$emptyState = $this->calmEmptyState();
|
|
@endphp
|
|
|
|
<x-filament::section>
|
|
<div class="flex flex-col gap-3">
|
|
<div class="inline-flex w-fit items-center gap-2 rounded-full border border-primary-200 bg-primary-50 px-3 py-1 text-xs font-medium text-primary-700 dark:border-primary-700/60 dark:bg-primary-950/40 dark:text-primary-300">
|
|
<x-filament::icon icon="heroicon-o-inbox-stack" class="h-3.5 w-3.5" />
|
|
Governance inbox
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<h1 class="text-2xl font-semibold tracking-tight text-gray-950 dark:text-white">
|
|
Governance inbox
|
|
</h1>
|
|
|
|
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">
|
|
This workspace decision surface routes you into the existing findings, operations, alerts, and review surfaces without introducing a second workflow state.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2 text-sm text-gray-600 dark:text-gray-300">
|
|
@if (filled($scope['workspace_label'] ?? null))
|
|
<span class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
|
|
Workspace: {{ $scope['workspace_label'] }}
|
|
</span>
|
|
@endif
|
|
|
|
<span class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
|
|
Scope: {{ $scope['family_label'] ?? 'All attention' }}
|
|
</span>
|
|
|
|
<span class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
|
|
Visible items: {{ $scope['total_count'] ?? 0 }}
|
|
</span>
|
|
|
|
@if (filled($scope['tenant_label'] ?? null))
|
|
<span class="inline-flex items-center rounded-full bg-warning-50 px-3 py-1 text-xs font-medium text-warning-700 dark:bg-warning-500/10 dark:text-warning-300">
|
|
Tenant: {{ $scope['tenant_label'] }}
|
|
</span>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
<a
|
|
href="{{ $this->pageUrl(['family' => null]) }}"
|
|
class="inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition {{ $this->family === null ? 'border-primary-300 bg-primary-50 text-primary-700 dark:border-primary-700/60 dark:bg-primary-950/40 dark:text-primary-300' : 'border-gray-200 text-gray-700 hover:border-gray-300 dark:border-gray-700 dark:text-gray-200 dark:hover:border-gray-600' }}"
|
|
>
|
|
All attention
|
|
<span class="rounded-full bg-black/5 px-2 py-0.5 text-xs dark:bg-white/10">{{ $scope['total_count'] ?? 0 }}</span>
|
|
</a>
|
|
|
|
@foreach ($this->availableFamilies() as $family)
|
|
<a
|
|
href="{{ $this->pageUrl(['family' => $family['key']]) }}"
|
|
class="inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition {{ $this->isActiveFamily($family['key']) ? 'border-primary-300 bg-primary-50 text-primary-700 dark:border-primary-700/60 dark:bg-primary-950/40 dark:text-primary-300' : 'border-gray-200 text-gray-700 hover:border-gray-300 dark:border-gray-700 dark:text-gray-200 dark:hover:border-gray-600' }}"
|
|
>
|
|
{{ $family['label'] }}
|
|
<span class="rounded-full bg-black/5 px-2 py-0.5 text-xs dark:bg-white/10">{{ $family['count'] }}</span>
|
|
</a>
|
|
@endforeach
|
|
</div>
|
|
|
|
@if ($this->hasTenantPrefilter())
|
|
<div class="flex flex-wrap items-center gap-3 text-sm text-gray-600 dark:text-gray-300">
|
|
<span>The inbox is currently filtered to one tenant.</span>
|
|
|
|
<a href="{{ $this->pageUrl(['tenant' => null]) }}" class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">
|
|
Clear tenant filter
|
|
</a>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</x-filament::section>
|
|
|
|
@if ($sections === [])
|
|
<x-filament::section>
|
|
<div class="flex flex-col gap-4 rounded-2xl border border-dashed border-gray-300 bg-gray-50/60 p-6 dark:border-gray-700 dark:bg-gray-900/40">
|
|
<div class="space-y-1">
|
|
<h2 class="text-base font-semibold text-gray-950 dark:text-white">{{ $emptyState['title'] }}</h2>
|
|
<p class="max-w-2xl text-sm leading-6 text-gray-600 dark:text-gray-300">{{ $emptyState['body'] }}</p>
|
|
</div>
|
|
|
|
@if (filled($emptyState['action_label'] ?? null) && filled($emptyState['action_url'] ?? null))
|
|
<div>
|
|
<x-filament::button tag="a" color="gray" href="{{ $emptyState['action_url'] }}">
|
|
{{ $emptyState['action_label'] }}
|
|
</x-filament::button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</x-filament::section>
|
|
@else
|
|
@foreach ($sections as $section)
|
|
<x-filament::section>
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
<div class="space-y-2">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<h2 class="text-base font-semibold text-gray-950 dark:text-white">{{ $section['label'] }}</h2>
|
|
<span class="inline-flex items-center 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">
|
|
{{ $section['count'] }}
|
|
</span>
|
|
</div>
|
|
|
|
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">{{ $section['summary'] }}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<x-filament::button tag="a" color="gray" href="{{ $section['dominant_action_url'] }}">
|
|
{{ $section['dominant_action_label'] }}
|
|
</x-filament::button>
|
|
</div>
|
|
</div>
|
|
|
|
@if ($section['count'] === 0)
|
|
<div class="rounded-2xl border border-dashed border-gray-300 bg-gray-50/60 p-5 text-sm leading-6 text-gray-600 dark:border-gray-700 dark:bg-gray-900/40 dark:text-gray-300">
|
|
{{ $section['empty_state'] }}
|
|
</div>
|
|
@else
|
|
<ul class="grid gap-3">
|
|
@foreach ($section['entries'] as $entry)
|
|
<li class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/60">
|
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
<div class="space-y-1.5">
|
|
@if (filled($entry['tenant_label'] ?? null))
|
|
<div class="text-xs font-medium uppercase tracking-[0.16em] text-gray-500 dark:text-gray-400">
|
|
{{ $entry['tenant_label'] }}
|
|
</div>
|
|
@endif
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<a href="{{ $entry['destination_url'] }}" class="text-sm font-semibold text-gray-950 hover:text-primary-600 dark:text-white dark:hover:text-primary-300">
|
|
{{ $entry['headline'] }}
|
|
</a>
|
|
|
|
<span class="inline-flex items-center 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">
|
|
{{ $entry['status_label'] }}
|
|
</span>
|
|
</div>
|
|
|
|
@if (filled($entry['subline'] ?? null))
|
|
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">{{ $entry['subline'] }}</p>
|
|
@endif
|
|
</div>
|
|
|
|
<div>
|
|
<x-filament::button tag="a" color="gray" size="sm" href="{{ $entry['destination_url'] }}">
|
|
Open source
|
|
</x-filament::button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
@endforeach
|
|
</ul>
|
|
@endif
|
|
</div>
|
|
</x-filament::section>
|
|
@endforeach
|
|
@endif
|
|
</x-filament-panels::page> |