## Summary - add baseline compare evidence gap detail modeling and a dedicated Livewire table surface - extend baseline compare landing and operation run detail surfaces to expose evidence gap details and stats - add spec artifacts for feature 162 and expand feature coverage with focused Filament and baseline tests ## Notes - branch: `162-baseline-gap-details` - commit: `a92dd812` - working tree was clean after push ## Validation - tests were not run in this step Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #192
192 lines
6.8 KiB
PHP
192 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Livewire;
|
|
|
|
use App\Support\Badges\TagBadgeDomain;
|
|
use App\Support\Badges\TagBadgeRenderer;
|
|
use App\Support\Baselines\BaselineCompareEvidenceGapDetails;
|
|
use App\Support\Filament\TablePaginationProfiles;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
use Filament\Tables\Table;
|
|
use Filament\Tables\TableComponent;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Str;
|
|
|
|
class BaselineCompareEvidenceGapTable extends TableComponent
|
|
{
|
|
/**
|
|
* @var list<array{
|
|
* __id: string,
|
|
* reason_code: string,
|
|
* reason_label: string,
|
|
* policy_type: string,
|
|
* subject_key: string,
|
|
* search_text: string
|
|
* }>
|
|
*/
|
|
public array $gapRows = [];
|
|
|
|
public string $context = 'default';
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $buckets
|
|
*/
|
|
public function mount(array $buckets = [], string $context = 'default'): void
|
|
{
|
|
$this->gapRows = BaselineCompareEvidenceGapDetails::tableRows($buckets);
|
|
$this->context = $context;
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->queryStringIdentifier('baselineCompareEvidenceGap'.Str::studly($this->context))
|
|
->defaultSort('reason_label')
|
|
->defaultPaginationPageOption(10)
|
|
->paginated(TablePaginationProfiles::picker())
|
|
->searchable()
|
|
->searchPlaceholder(__('baseline-compare.evidence_gap_search_placeholder'))
|
|
->records(function (
|
|
?string $sortColumn,
|
|
?string $sortDirection,
|
|
?string $search,
|
|
array $filters,
|
|
int $page,
|
|
int $recordsPerPage
|
|
): LengthAwarePaginator {
|
|
$rows = $this->filterRows(
|
|
rows: collect($this->gapRows),
|
|
search: $search,
|
|
filters: $filters,
|
|
);
|
|
|
|
$rows = $this->sortRows(
|
|
rows: $rows,
|
|
sortColumn: $sortColumn,
|
|
sortDirection: $sortDirection,
|
|
);
|
|
|
|
return $this->paginateRows(
|
|
rows: $rows,
|
|
page: $page,
|
|
recordsPerPage: $recordsPerPage,
|
|
);
|
|
})
|
|
->filters([
|
|
SelectFilter::make('reason_code')
|
|
->label(__('baseline-compare.evidence_gap_reason'))
|
|
->options(BaselineCompareEvidenceGapDetails::reasonFilterOptions($this->gapRows)),
|
|
SelectFilter::make('policy_type')
|
|
->label(__('baseline-compare.evidence_gap_policy_type'))
|
|
->options(BaselineCompareEvidenceGapDetails::policyTypeFilterOptions($this->gapRows)),
|
|
])
|
|
->striped()
|
|
->deferLoading(! app()->runningUnitTests())
|
|
->columns([
|
|
TextColumn::make('reason_label')
|
|
->label(__('baseline-compare.evidence_gap_reason'))
|
|
->searchable()
|
|
->sortable()
|
|
->wrap(),
|
|
TextColumn::make('policy_type')
|
|
->label(__('baseline-compare.evidence_gap_policy_type'))
|
|
->badge()
|
|
->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType))
|
|
->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType))
|
|
->icon(TagBadgeRenderer::icon(TagBadgeDomain::PolicyType))
|
|
->iconColor(TagBadgeRenderer::iconColor(TagBadgeDomain::PolicyType))
|
|
->searchable()
|
|
->sortable()
|
|
->wrap(),
|
|
TextColumn::make('subject_key')
|
|
->label(__('baseline-compare.evidence_gap_subject_key'))
|
|
->searchable()
|
|
->sortable()
|
|
->wrap()
|
|
->extraAttributes(['class' => 'font-mono text-xs']),
|
|
])
|
|
->actions([])
|
|
->bulkActions([])
|
|
->emptyStateHeading(__('baseline-compare.evidence_gap_table_empty_heading'))
|
|
->emptyStateDescription(__('baseline-compare.evidence_gap_table_empty_description'));
|
|
}
|
|
|
|
public function render(): View
|
|
{
|
|
return view('livewire.baseline-compare-evidence-gap-table');
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, array<string, mixed>> $rows
|
|
* @param array<string, mixed> $filters
|
|
* @return Collection<int, array<string, mixed>>
|
|
*/
|
|
private function filterRows(Collection $rows, ?string $search, array $filters): Collection
|
|
{
|
|
$normalizedSearch = Str::lower(trim((string) $search));
|
|
$reasonCode = $filters['reason_code']['value'] ?? null;
|
|
$policyType = $filters['policy_type']['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['search_text'] ?? '')), $normalizedSearch);
|
|
});
|
|
}
|
|
)
|
|
->when(
|
|
filled($reasonCode),
|
|
fn (Collection $rows): Collection => $rows->where('reason_code', (string) $reasonCode)
|
|
)
|
|
->when(
|
|
filled($policyType),
|
|
fn (Collection $rows): Collection => $rows->where('policy_type', (string) $policyType)
|
|
)
|
|
->values();
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, array<string, mixed>> $rows
|
|
* @return Collection<int, array<string, mixed>>
|
|
*/
|
|
private function sortRows(Collection $rows, ?string $sortColumn, ?string $sortDirection): Collection
|
|
{
|
|
if (! filled($sortColumn)) {
|
|
return $rows;
|
|
}
|
|
|
|
$direction = Str::lower((string) ($sortDirection ?? 'asc')) === 'desc' ? 'desc' : 'asc';
|
|
|
|
return $rows->sortBy(
|
|
fn (array $row): string => (string) ($row[$sortColumn] ?? ''),
|
|
SORT_NATURAL | SORT_FLAG_CASE,
|
|
$direction === 'desc'
|
|
)->values();
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, array<string, mixed>> $rows
|
|
*/
|
|
private function paginateRows(Collection $rows, int $page, int $recordsPerPage): LengthAwarePaginator
|
|
{
|
|
$perPage = max(1, $recordsPerPage);
|
|
$currentPage = max(1, $page);
|
|
$total = $rows->count();
|
|
$items = $rows->forPage($currentPage, $perPage)->values();
|
|
|
|
return new LengthAwarePaginator(
|
|
$items,
|
|
$total,
|
|
$perPage,
|
|
$currentPage,
|
|
);
|
|
}
|
|
}
|