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 ->searchable() ->searchPlaceholder('Search by policy type or label') ->defaultSort('label') ->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('type') ->label('Type') ->sortable() ->fontFamily(FontFamily::Mono) ->copyable() ->wrap(), TextColumn::make('label') ->label('Label') ->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('risk') ->label('Risk') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyRisk)) ->color(BadgeRenderer::color(BadgeDomain::PolicyRisk)) ->icon(BadgeRenderer::icon(BadgeDomain::PolicyRisk)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyRisk)), TextColumn::make('restore') ->label('Restore') ->badge() ->state(fn (array $record): ?string => $record['restore']) ->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; }), TextColumn::make('category') ->badge() ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyCategory)) ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyCategory)) ->icon(TagBadgeRenderer::icon(TagBadgeDomain::PolicyCategory)) ->iconColor(TagBadgeRenderer::iconColor(TagBadgeDomain::PolicyCategory)) ->toggleable() ->wrap(), 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(), IconColumn::make('dependencies') ->label('Dependencies') ->boolean() ->trueIcon('heroicon-m-check-circle') ->falseIcon('heroicon-m-minus-circle') ->trueColor('success') ->falseColor('gray') ->alignCenter() ->toggleable(), ]) ->filters($this->tableFilters()) ->emptyStateHeading('No coverage entries match this view') ->emptyStateDescription('Clear the current search or filters to return to the full coverage matrix.') ->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('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 { $resolver = app(CoverageCapabilitiesResolver::class); $supported = $this->mapCoverageRows( rows: InventoryPolicyTypeMeta::supported(), segment: 'policy', sourceOrderOffset: 0, resolver: $resolver, ); return $supported->merge($this->mapCoverageRows( rows: InventoryPolicyTypeMeta::foundations(), segment: 'foundation', sourceOrderOffset: $supported->count(), resolver: $resolver, )); } /** * @param array> $rows * @return Collection */ protected function mapCoverageRows( array $rows, string $segment, int $sourceOrderOffset, CoverageCapabilitiesResolver $resolver ): Collection { return collect($rows) ->values() ->mapWithKeys(function (array $row, int $index) use ($resolver, $segment, $sourceOrderOffset): array { $type = (string) ($row['type'] ?? ''); if ($type === '') { return []; } $key = "{$segment}:{$type}"; $restore = $row['restore'] ?? null; $risk = $row['risk'] ?? 'n/a'; return [ $key => [ '__key' => $key, 'key' => $key, 'segment' => $segment, 'type' => $type, 'label' => (string) ($row['label'] ?? $type), 'category' => (string) ($row['category'] ?? 'Other'), 'dependencies' => $segment === 'policy' && $resolver->supportsDependencies($type), 'restore' => is_string($restore) ? $restore : null, 'risk' => is_string($risk) ? $risk : 'n/a', 'source_order' => $sourceOrderOffset + $index, ], ]; }); } /** * @param Collection> $rows * @param array $filters * @return Collection> */ protected function filterRows(Collection $rows, ?string $search, array $filters): Collection { $normalizedSearch = Str::lower(trim((string) $search)); $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($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'], true) ? $sortColumn : null; if ($sortColumn === null) { return $rows->sortBy('source_order'); } $records = $rows->all(); uasort($records, function (array $left, array $right) use ($sortColumn, $sortDirection): int { $comparison = strnatcasecmp( (string) ($left[$sortColumn] ?? ''), (string) ($right[$sortColumn] ?? ''), ); if ($comparison === 0) { $comparison = ((int) ($left['source_order'] ?? 0)) <=> ((int) ($right['source_order'] ?? 0)); } 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(); } }