TenantAtlas/app/Filament/Pages/Monitoring/EvidenceOverview.php
2026-03-23 01:06:25 +01:00

137 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages\Monitoring;
use App\Filament\Resources\EvidenceSnapshotResource;
use App\Models\EvidenceSnapshot;
use App\Models\Tenant;
use App\Models\User;
use App\Support\Badges\BadgeCatalog;
use App\Support\Badges\BadgeDomain;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
use App\Support\Workspaces\WorkspaceContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Pages\Page;
use Illuminate\Auth\AuthenticationException;
use UnitEnum;
class EvidenceOverview extends Page
{
protected static bool $isDiscovered = false;
protected static bool $shouldRegisterNavigation = false;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
protected static string|UnitEnum|null $navigationGroup = 'Monitoring';
protected static ?string $title = 'Evidence Overview';
protected string $view = 'filament.pages.monitoring.evidence-overview';
/**
* @var list<array<string, mixed>>
*/
public array $rows = [];
public ?int $tenantFilter = null;
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly)
->satisfy(ActionSurfaceSlot::ListHeader, 'The overview header exposes a clear-filters action when a tenant prefilter is active.')
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The overview exposes a single drill-down link per row without a More menu.')
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The overview does not expose bulk actions.')
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state explains the current scope and offers a clear-filters CTA.');
}
public function mount(): void
{
$user = auth()->user();
if (! $user instanceof User) {
throw new AuthenticationException;
}
$workspaceContext = app(WorkspaceContext::class);
$workspace = $workspaceContext->currentWorkspaceForMemberOrFail($user, request());
$workspaceId = (int) $workspace->getKey();
$accessibleTenants = $user->tenants()
->where('tenants.workspace_id', $workspaceId)
->orderBy('tenants.name')
->get()
->filter(fn (Tenant $tenant): bool => (int) $tenant->workspace_id === $workspaceId && $user->can('evidence.view', $tenant))
->values();
$this->tenantFilter = is_numeric(request()->query('tenant_id')) ? (int) request()->query('tenant_id') : null;
$tenantIds = $accessibleTenants->pluck('id')->map(static fn (mixed $id): int => (int) $id)->all();
$query = EvidenceSnapshot::query()
->with('tenant')
->where('workspace_id', $workspaceId)
->whereIn('tenant_id', $tenantIds)
->where('status', 'active')
->latest('generated_at');
if ($this->tenantFilter !== null) {
$query->where('tenant_id', $this->tenantFilter);
}
$snapshots = $query->get()->unique('tenant_id')->values();
$this->rows = $snapshots->map(function (EvidenceSnapshot $snapshot): array {
$truth = app(ArtifactTruthPresenter::class)->forEvidenceSnapshot($snapshot);
$freshnessSpec = BadgeCatalog::spec(BadgeDomain::GovernanceArtifactFreshness, $truth->freshnessState);
return [
'tenant_name' => $snapshot->tenant?->name ?? 'Unknown tenant',
'tenant_id' => (int) $snapshot->tenant_id,
'snapshot_id' => (int) $snapshot->getKey(),
'completeness_state' => (string) $snapshot->completeness_state,
'generated_at' => $snapshot->generated_at?->toDateTimeString(),
'missing_dimensions' => (int) (($snapshot->summary['missing_dimensions'] ?? 0)),
'stale_dimensions' => (int) (($snapshot->summary['stale_dimensions'] ?? 0)),
'artifact_truth' => [
'label' => $truth->primaryLabel,
'color' => $truth->primaryBadgeSpec()->color,
'icon' => $truth->primaryBadgeSpec()->icon,
'explanation' => $truth->primaryExplanation,
],
'freshness' => [
'label' => $freshnessSpec->label,
'color' => $freshnessSpec->color,
'icon' => $freshnessSpec->icon,
],
'next_step' => $truth->nextStepText(),
'view_url' => $snapshot->tenant
? EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant)
: null,
];
})->all();
}
/**
* @return array<Action>
*/
protected function getHeaderActions(): array
{
return [
Action::make('clear_filters')
->label('Clear filters')
->color('gray')
->visible(fn (): bool => $this->tenantFilter !== null)
->url(route('admin.evidence.overview')),
];
}
}