withDefaults(new ActionSurfaceDefaults( moreGroupLabel: 'More', exportIsDefaultBulkActionForReadOnly: false, )) ->exempt(ActionSurfaceSlot::ListHeader, 'Inventory coverage stays read-only and uses KPI widgets instead of header actions.') ->exempt(ActionSurfaceSlot::InspectAffordance, 'Inventory coverage rows are runtime-derived metadata and intentionally omit inspect affordances.') ->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'Derived coverage rows do not expose row actions.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Derived coverage rows do not expose bulk actions.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state provides a clear-filters CTA to return to the full tenant coverage report.'); } public static function shouldRegisterNavigation(): bool { if (Filament::getCurrentPanel()?->getId() === 'admin') { return false; } return parent::shouldRegisterNavigation(); } public static function canAccess(): bool { $tenant = static::resolveTenantContextForCurrentPanel(); $user = auth()->user(); if (! $tenant instanceof Tenant || ! $user instanceof User) { return false; } /** @var CapabilityResolver $resolver */ $resolver = app(CapabilityResolver::class); return $resolver->isMember($user, $tenant) && $resolver->can($user, $tenant, Capabilities::TENANT_VIEW); } public function mount(): void { $this->mountInteractsWithTable(); } protected function getHeaderWidgets(): array { return [ InventoryKpiHeader::class, ]; } public function table(Table $table): Table { return $table ->persistFiltersInSession() ->persistSearchInSession() ->persistSortInSession() ->searchable() ->searchPlaceholder('Search by type or label') ->defaultSort('follow_up_priority') ->defaultPaginationPageOption(50) ->paginated(\App\Support\Filament\TablePaginationProfiles::customPage()) ->records(function ( ?string $sortColumn, ?string $sortDirection, ?string $search, array $filters, int $page, int $recordsPerPage ): LengthAwarePaginator { $rows = $this->filterRows( rows: $this->coverageRows(), search: $search, filters: $filters, ); $rows = $this->sortRows( rows: $rows, sortColumn: $sortColumn, sortDirection: $sortDirection, ); return $this->paginateRows( rows: $rows, page: $page, recordsPerPage: $recordsPerPage, ); }) ->columns([ TextColumn::make('coverage_state') ->label('Coverage state') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::InventoryCoverageState)) ->color(BadgeRenderer::color(BadgeDomain::InventoryCoverageState)) ->icon(BadgeRenderer::icon(BadgeDomain::InventoryCoverageState)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::InventoryCoverageState)) ->sortable(), TextColumn::make('label') ->label('Type') ->sortable() ->badge() ->formatStateUsing(function (?string $state, array $record): string { return TagBadgeCatalog::spec( TagBadgeDomain::PolicyType, $record['type'] ?? $state, )->label; }) ->color(function (?string $state, array $record): string { return TagBadgeCatalog::spec( TagBadgeDomain::PolicyType, $record['type'] ?? $state, )->color; }) ->icon(function (?string $state, array $record): ?string { return TagBadgeCatalog::spec( TagBadgeDomain::PolicyType, $record['type'] ?? $state, )->icon; }) ->iconColor(function (?string $state, array $record): ?string { $spec = TagBadgeCatalog::spec( TagBadgeDomain::PolicyType, $record['type'] ?? $state, ); return $spec->iconColor ?? $spec->color; }) ->wrap(), TextColumn::make('follow_up_guidance') ->label('Follow-up guidance') ->wrap() ->toggleable(), TextColumn::make('observed_item_count') ->label('Observed items') ->numeric() ->sortable(), TextColumn::make('category') ->badge() ->formatStateUsing(fn (?string $state): string => TagBadgeCatalog::spec(TagBadgeDomain::PolicyCategory, $state)->label) ->color(fn (?string $state): string => TagBadgeCatalog::spec(TagBadgeDomain::PolicyCategory, $state)->color) ->icon(fn (?string $state): ?string => TagBadgeCatalog::spec(TagBadgeDomain::PolicyCategory, $state)->icon) ->iconColor(function (?string $state): ?string { $spec = TagBadgeCatalog::spec(TagBadgeDomain::PolicyCategory, $state); return $spec->iconColor ?? $spec->color; }) ->toggleable() ->wrap(), TextColumn::make('restore') ->label('Restore') ->badge() ->formatStateUsing(function (?string $state): string { return filled($state) ? BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, $state)->label : 'Not provided'; }) ->color(function (?string $state): string { return filled($state) ? BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, $state)->color : 'gray'; }) ->icon(function (?string $state): ?string { return filled($state) ? BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, $state)->icon : 'heroicon-m-minus-circle'; }) ->iconColor(function (?string $state): ?string { if (! filled($state)) { return 'gray'; } $spec = BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, $state); return $spec->iconColor ?? $spec->color; }) ->toggleable(), IconColumn::make('dependencies') ->label('Dependencies') ->boolean() ->trueIcon('heroicon-m-check-circle') ->falseIcon('heroicon-m-minus-circle') ->trueColor('success') ->falseColor('gray') ->alignCenter() ->toggleable(), TextColumn::make('type') ->label('Type key') ->sortable() ->fontFamily(FontFamily::Mono) ->copyable() ->wrap() ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('segment') ->label('Segment') ->badge() ->formatStateUsing(fn (?string $state): string => $state === 'foundation' ? 'Foundation' : 'Policy') ->color(fn (?string $state): string => $state === 'foundation' ? 'gray' : 'info') ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('risk') ->label('Risk') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyRisk)) ->color(BadgeRenderer::color(BadgeDomain::PolicyRisk)) ->icon(BadgeRenderer::icon(BadgeDomain::PolicyRisk)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyRisk)) ->toggleable(isToggledHiddenByDefault: true), ]) ->filters($this->tableFilters()) ->emptyStateHeading('No coverage rows match this report') ->emptyStateDescription('Clear the current search or filters to return to the full tenant coverage report.') ->emptyStateIcon('heroicon-o-funnel') ->emptyStateActions([ Action::make('clear_filters') ->label('Clear filters') ->icon('heroicon-o-x-mark') ->color('gray') ->action(function (): void { $this->resetTable(); }), ]) ->actions([]) ->bulkActions([]); } /** * @return array */ protected function tableFilters(): array { $filters = [ SelectFilter::make('coverage_state') ->label('Coverage state') ->options([ 'succeeded' => BadgeCatalog::spec(BadgeDomain::InventoryCoverageState, 'succeeded')->label, 'failed' => BadgeCatalog::spec(BadgeDomain::InventoryCoverageState, 'failed')->label, 'skipped' => BadgeCatalog::spec(BadgeDomain::InventoryCoverageState, 'skipped')->label, 'unknown' => BadgeCatalog::spec(BadgeDomain::InventoryCoverageState, 'unknown')->label, ]), SelectFilter::make('category') ->label('Category') ->options($this->categoryFilterOptions()), ]; if ($this->restoreFilterOptions() !== []) { $filters[] = SelectFilter::make('restore') ->label('Restore mode') ->options($this->restoreFilterOptions()); } return $filters; } /** * @return Collection */ protected function coverageRows(): Collection { $truth = $this->coverageTruth(); if (! $truth instanceof TenantCoverageTruth) { return collect(); } return collect($truth->rows) ->mapWithKeys(static fn ($row): array => [ $row->key => $row->toArray(), ]); } /** * @param Collection> $rows * @param array $filters * @return Collection> */ protected function filterRows(Collection $rows, ?string $search, array $filters): Collection { $normalizedSearch = Str::lower(trim((string) $search)); $coverageState = $filters['coverage_state']['value'] ?? null; $category = $filters['category']['value'] ?? null; $restore = $filters['restore']['value'] ?? null; return $rows ->when( $normalizedSearch !== '', function (Collection $rows) use ($normalizedSearch): Collection { return $rows->filter(function (array $row) use ($normalizedSearch): bool { return str_contains(Str::lower((string) $row['type']), $normalizedSearch) || str_contains(Str::lower((string) $row['label']), $normalizedSearch); }); }, ) ->when( filled($coverageState), fn (Collection $rows): Collection => $rows->where('coverage_state', (string) $coverageState), ) ->when( filled($category), fn (Collection $rows): Collection => $rows->where('category', (string) $category), ) ->when( filled($restore), fn (Collection $rows): Collection => $rows->where('restore', (string) $restore), ); } /** * @param Collection> $rows * @return Collection> */ protected function sortRows(Collection $rows, ?string $sortColumn, ?string $sortDirection): Collection { $sortColumn = in_array($sortColumn, ['type', 'label', 'observed_item_count', 'coverage_state', 'follow_up_priority'], true) ? $sortColumn : null; if ($sortColumn === null) { return $rows; } $records = $rows->all(); uasort($records, function (array $left, array $right) use ($sortColumn, $sortDirection): int { $comparison = match ($sortColumn) { 'observed_item_count' => ((int) ($left[$sortColumn] ?? 0)) <=> ((int) ($right[$sortColumn] ?? 0)), 'follow_up_priority' => ((int) ($left[$sortColumn] ?? 0)) <=> ((int) ($right[$sortColumn] ?? 0)), default => strnatcasecmp( (string) ($left[$sortColumn] ?? ''), (string) ($right[$sortColumn] ?? ''), ), }; if ($comparison === 0 && $sortColumn === 'follow_up_priority') { $comparison = ((int) ($right['observed_item_count'] ?? 0)) <=> ((int) ($left['observed_item_count'] ?? 0)); } if ($comparison === 0) { $comparison = strnatcasecmp( (string) ($left['label'] ?? ''), (string) ($right['label'] ?? ''), ); } return $sortDirection === 'desc' ? ($comparison * -1) : $comparison; }); return collect($records); } /** * @param Collection> $rows */ protected function paginateRows(Collection $rows, int $page, int $recordsPerPage): LengthAwarePaginator { return new LengthAwarePaginator( items: $rows->forPage($page, $recordsPerPage), total: $rows->count(), perPage: $recordsPerPage, currentPage: $page, ); } /** * @return array */ protected function categoryFilterOptions(): array { return $this->coverageRows() ->pluck('category') ->filter(fn (mixed $category): bool => is_string($category) && $category !== '') ->unique() ->sort() ->mapWithKeys(function (string $category): array { return [ $category => TagBadgeCatalog::spec(TagBadgeDomain::PolicyCategory, $category)->label, ]; }) ->all(); } /** * @return array */ protected function restoreFilterOptions(): array { return $this->coverageRows() ->pluck('restore') ->filter(fn (mixed $restore): bool => is_string($restore) && $restore !== '') ->unique() ->sort() ->mapWithKeys(function (string $restore): array { return [ $restore => BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, $restore)->label, ]; }) ->all(); } /** * @return array */ public function coverageSummary(): array { $truth = $this->coverageTruth(); if (! $truth instanceof TenantCoverageTruth) { return []; } return [ 'supportedTypes' => $truth->supportedTypeCount, 'succeededTypes' => $truth->succeededTypeCount, 'followUpTypes' => $truth->followUpTypeCount, 'observedItems' => $truth->observedItemTotal, 'observedTypes' => $truth->observedTypeCount(), 'topFollowUpLabel' => $truth->topPriorityFollowUpRow()?->label, 'topFollowUpGuidance' => $truth->topPriorityFollowUpRow()?->followUpGuidance, 'hasCurrentCoverageResult' => $truth->hasCurrentCoverageResult, ]; } /** * @return array */ public function basisRunSummary(): array { $truth = $this->coverageTruth(); $tenant = static::resolveTenantContextForCurrentPanel(); $user = auth()->user(); if (! $truth instanceof TenantCoverageTruth || ! $tenant instanceof Tenant) { return []; } if (! $truth->basisRun instanceof OperationRun) { return [ 'title' => 'No current coverage basis', 'body' => $user instanceof User && $user->can(Capabilities::TENANT_INVENTORY_SYNC_RUN, $tenant) ? 'Run Inventory Sync from Inventory Items to establish current tenant coverage truth.' : 'A tenant operator with inventory sync permission must establish current tenant coverage truth.', 'badgeLabel' => null, 'badgeColor' => null, 'runUrl' => null, 'historyUrl' => null, 'inventoryItemsUrl' => InventoryItemResource::getUrl('index', tenant: $tenant), ]; } $badge = BadgeRenderer::spec(BadgeDomain::OperationRunOutcome, (string) $truth->basisRun->outcome); $canViewRun = $user instanceof User && Gate::forUser($user)->allows('view', $truth->basisRun); return [ 'title' => sprintf('Latest coverage-bearing sync completed %s.', $truth->basisCompletedAtLabel() ?? 'recently'), 'body' => $canViewRun ? 'Review the cited inventory sync to inspect provider or permission issues in detail.' : 'The coverage basis is current, but your role cannot open the cited run detail.', 'badgeLabel' => $badge->label, 'badgeColor' => $badge->color, 'runUrl' => $canViewRun ? route('admin.operations.view', ['run' => (int) $truth->basisRun->getKey()]) : null, 'historyUrl' => $canViewRun ? $this->inventorySyncHistoryUrl($tenant) : null, 'inventoryItemsUrl' => InventoryItemResource::getUrl('index', tenant: $tenant), ]; } protected function coverageTruth(): ?TenantCoverageTruth { if ($this->cachedCoverageTruth instanceof TenantCoverageTruth) { return $this->cachedCoverageTruth; } $tenant = static::resolveTenantContextForCurrentPanel(); if (! $tenant instanceof Tenant) { return null; } $this->cachedCoverageTruth = app(TenantCoverageTruthResolver::class)->resolve($tenant); return $this->cachedCoverageTruth; } private function inventorySyncHistoryUrl(Tenant $tenant): string { return route('admin.operations.index', [ 'tenant_id' => (int) $tenant->getKey(), 'tableFilters' => [ 'type' => [ 'value' => 'inventory_sync', ], ], ]); } }