Automated PR created by Codex via Gitea API. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #460
1467 lines
52 KiB
PHP
1467 lines
52 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Pages\Governance;
|
|
|
|
use App\Filament\Concerns\CleansAdminTenantQueryParameter;
|
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Services\Auth\CapabilityResolver;
|
|
use App\Services\Auth\WorkspaceCapabilityResolver;
|
|
use App\Services\EnvironmentReviews\EnvironmentReviewRegisterService;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\GovernanceDecisions\GovernanceDecisionRegisterBuilder;
|
|
use App\Support\GovernanceInbox\GovernanceInboxSectionBuilder;
|
|
use App\Support\GovernanceInbox\ReviewPublicationResolutionInboxProvider;
|
|
use App\Support\ManagedEnvironmentLinks;
|
|
use App\Support\Navigation\CanonicalNavigationContext;
|
|
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
|
use App\Support\Navigation\WorkspaceHubNavigation;
|
|
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\ActionSurface\Enums\ActionSurfaceType;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use BackedEnum;
|
|
use Filament\Facades\Filament;
|
|
use Filament\Pages\Page;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
use UnitEnum;
|
|
|
|
class GovernanceInbox extends Page
|
|
{
|
|
use CleansAdminTenantQueryParameter;
|
|
|
|
private const int LANE_PREVIEW_LIMIT = 5;
|
|
|
|
/**
|
|
* @var list<array{key: string, label: string, description: string, empty_state: string}>
|
|
*/
|
|
private const array LANE_DEFINITIONS = [
|
|
[
|
|
'key' => 'needs_triage',
|
|
'label' => 'Needs triage',
|
|
'description' => 'Unassigned findings that still need a first operator path.',
|
|
'empty_state' => 'No unassigned findings need first triage in the current scope.',
|
|
],
|
|
[
|
|
'key' => 'requires_decision',
|
|
'label' => 'Requires decision',
|
|
'description' => 'Assigned governance work and review follow-up that still need operator judgment.',
|
|
'empty_state' => 'No operator-decision items are open in the current scope.',
|
|
],
|
|
[
|
|
'key' => 'risk_exception_review',
|
|
'label' => 'Risk / exception review',
|
|
'description' => 'Accepted-risk and exception records that need approval, renewal, closure, or support review.',
|
|
'empty_state' => 'No accepted-risk or exception records need review in the current scope.',
|
|
],
|
|
[
|
|
'key' => 'evidence_required',
|
|
'label' => 'Evidence required',
|
|
'description' => 'Governance items that are still missing the linked proof needed for follow-through.',
|
|
'empty_state' => 'No visible governance items are blocked by missing evidence in the current scope.',
|
|
],
|
|
[
|
|
'key' => 'blocked',
|
|
'label' => 'Blocked',
|
|
'description' => 'Technical follow-up is still blocked on failed runs or delivery issues.',
|
|
'empty_state' => 'No blocked governance follow-up is visible in the current scope.',
|
|
],
|
|
];
|
|
|
|
protected static bool $isDiscovered = false;
|
|
|
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-inbox-stack';
|
|
|
|
protected static string|UnitEnum|null $navigationGroup = 'Governance';
|
|
|
|
protected static ?string $navigationLabel = 'Governance inbox';
|
|
|
|
protected static ?int $navigationSort = 5;
|
|
|
|
protected static ?string $title = 'Governance Inbox';
|
|
|
|
protected static ?string $slug = 'governance/inbox';
|
|
|
|
protected string $view = 'filament.pages.governance.governance-inbox';
|
|
|
|
/**
|
|
* @var array<int, ManagedEnvironment>|null
|
|
*/
|
|
private ?array $authorizedTenants = null;
|
|
|
|
/**
|
|
* @var array<int, ManagedEnvironment>|null
|
|
*/
|
|
private ?array $visibleFindingTenants = null;
|
|
|
|
/**
|
|
* @var array<int, ManagedEnvironment>|null
|
|
*/
|
|
private ?array $reviewTenants = null;
|
|
|
|
/**
|
|
* @var array<string, mixed>|null
|
|
*/
|
|
private ?array $inboxPayload = null;
|
|
|
|
/**
|
|
* @var array<string, mixed>|null
|
|
*/
|
|
private ?array $unfilteredInboxPayload = null;
|
|
|
|
/**
|
|
* @var array<string, mixed>|null
|
|
*/
|
|
private ?array $reviewPublicationResolutionUnfilteredPayload = null;
|
|
|
|
/**
|
|
* @var array<string, mixed>|null
|
|
*/
|
|
private ?array $lanePayload = null;
|
|
|
|
/**
|
|
* @var array<string, mixed>|null
|
|
*/
|
|
private ?array $recentlyResolvedPayload = null;
|
|
|
|
private ?Workspace $workspace = null;
|
|
|
|
private ?bool $visibleAlertsFamily = null;
|
|
|
|
private ?bool $visibleFindingExceptionsFamily = null;
|
|
|
|
public ?int $tenantId = null;
|
|
|
|
public ?string $family = null;
|
|
|
|
public ?string $status = null;
|
|
|
|
public ?string $updated = null;
|
|
|
|
public function getSubheading(): ?string
|
|
{
|
|
return 'Daily operator queue for governance follow-up, accepted risk, evidence gaps, and review handoff.';
|
|
}
|
|
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly, ActionSurfaceType::ReadOnlyRegistryReport)
|
|
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions keep the workspace governance queue calm and read-only.')
|
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
|
|
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The governance inbox routes into existing source surfaces instead of exposing row-level secondary menus.')
|
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The governance inbox does not expose bulk actions.')
|
|
->satisfy(ActionSurfaceSlot::ListEmptyState, 'Empty states stay calm and explain what operator work appears here.')
|
|
->exempt(ActionSurfaceSlot::DetailHeader, 'The governance inbox owns no local detail surface or mutation workflow.');
|
|
}
|
|
|
|
public static function getNavigationGroup(): string
|
|
{
|
|
return WorkspaceHubNavigation::workspaceWideGroup(__('localization.navigation.governance'));
|
|
}
|
|
|
|
public static function getNavigationUrl(): string
|
|
{
|
|
return WorkspaceHubNavigation::environmentFilteredUrl(static::getUrl(panel: 'admin'));
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->authorizeWorkspaceMembership();
|
|
$this->applyRequestedTenantPrefilter();
|
|
$this->family = $this->resolveRequestedFamily();
|
|
$this->status = $this->family === ReviewPublicationResolutionInboxProvider::FAMILY_KEY
|
|
? $this->resolveRequestedReviewPublicationStatus()
|
|
: null;
|
|
$this->updated = $this->family === ReviewPublicationResolutionInboxProvider::FAMILY_KEY
|
|
? $this->resolveRequestedReviewPublicationUpdated()
|
|
: null;
|
|
$this->ensureAtLeastOneVisibleFamily();
|
|
$this->ensureRequestedFamilyIsVisible();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function appliedScope(): array
|
|
{
|
|
$selectedTenant = $this->selectedTenant();
|
|
$availableFamilies = collect($this->availableFamilies())->keyBy('key');
|
|
|
|
return [
|
|
'workspace_label' => $this->workspace()?->name,
|
|
'tenant_label' => $selectedTenant?->name,
|
|
'tenant_prefilter_source' => $selectedTenant instanceof ManagedEnvironment ? 'explicit_filter' : 'none',
|
|
'family_key' => $this->family,
|
|
'family_label' => $this->family !== null
|
|
? ($availableFamilies->get($this->family)['label'] ?? Str::headline($this->family))
|
|
: 'All source families',
|
|
'status_key' => $this->status,
|
|
'status_label' => $this->status !== null
|
|
? ReviewPublicationResolutionInboxProvider::statusLabel($this->status)
|
|
: 'All active statuses',
|
|
'updated_key' => $this->updated,
|
|
'updated_label' => ReviewPublicationResolutionInboxProvider::updatedLabel($this->updated),
|
|
'total_count' => (int) ($this->inboxPayload()['total_count'] ?? 0),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array{key: string, label: string, count: int}>
|
|
*/
|
|
public function availableFamilies(): array
|
|
{
|
|
return collect($this->inboxPayload()['available_families'] ?? [])
|
|
->map(fn (array $family): array => $this->normalizeFamily($family))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public function sections(): array
|
|
{
|
|
return collect($this->inboxPayload()['sections'] ?? [])
|
|
->map(fn (array $section): array => $this->normalizeSection($section))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* total_open_items: int,
|
|
* headline: string,
|
|
* counts: list<array{key: string, label: string, count: int, description: string, state: string, chip_label: string}>,
|
|
* active_counts: list<array{key: string, label: string, count: int, description: string, state: string, chip_label: string}>,
|
|
* clear_counts: list<array{key: string, label: string, count: int, description: string, state: string, chip_label: string}>,
|
|
* primary_action: array{label: string, url: string}|null,
|
|
* next_recommended_item: array<string, mixed>|null,
|
|
* review_ready_supported: bool,
|
|
* }
|
|
*/
|
|
public function operatorSummary(): array
|
|
{
|
|
return $this->lanePayload()['summary'];
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public function laneGroups(): array
|
|
{
|
|
return $this->lanePayload()['lanes'];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function recentlyResolved(): ?array
|
|
{
|
|
if (is_array($this->recentlyResolvedPayload)) {
|
|
return $this->recentlyResolvedPayload === [] ? null : $this->recentlyResolvedPayload;
|
|
}
|
|
|
|
if (! $this->hasVisibleFindingExceptionsFamily()) {
|
|
$this->recentlyResolvedPayload = [];
|
|
|
|
return null;
|
|
}
|
|
|
|
if ($this->family !== null && $this->family !== 'finding_exceptions') {
|
|
$this->recentlyResolvedPayload = [];
|
|
|
|
return null;
|
|
}
|
|
|
|
$workspace = $this->workspace();
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
$this->recentlyResolvedPayload = [];
|
|
|
|
return null;
|
|
}
|
|
|
|
$visibleTenants = $this->selectedTenant() instanceof ManagedEnvironment
|
|
? [$this->selectedTenant()]
|
|
: $this->authorizedTenants();
|
|
|
|
$payload = app(GovernanceDecisionRegisterBuilder::class)->build(
|
|
workspace: $workspace,
|
|
visibleTenants: $visibleTenants,
|
|
registerState: 'recently_closed',
|
|
);
|
|
|
|
$count = (int) ($payload['counts']['recently_closed'] ?? 0);
|
|
|
|
if ($count === 0) {
|
|
$this->recentlyResolvedPayload = [];
|
|
|
|
return null;
|
|
}
|
|
|
|
$selectedTenant = $this->selectedTenant();
|
|
$openUrl = DecisionRegister::getUrl(
|
|
panel: 'admin',
|
|
parameters: array_filter([
|
|
'environment_id' => $selectedTenant?->getKey(),
|
|
'register_state' => 'recently_closed',
|
|
], static fn (mixed $value): bool => $value !== null && $value !== ''),
|
|
);
|
|
|
|
$rows = collect($payload['rows'] ?? [])
|
|
->take(3)
|
|
->map(function (array $row) use ($openUrl): array {
|
|
return [
|
|
'title' => filled($row['tenant_name'] ?? null)
|
|
? (string) $row['tenant_name']
|
|
: 'Recently closed decision',
|
|
'reason' => filled($row['closure_reason'] ?? null)
|
|
? (string) $row['closure_reason']
|
|
: 'Decision closed in the register.',
|
|
'next_action_label' => 'Open decision register',
|
|
'next_action_url' => $openUrl,
|
|
];
|
|
})
|
|
->values()
|
|
->all();
|
|
|
|
return $this->recentlyResolvedPayload = [
|
|
'label' => 'Recently resolved',
|
|
'count' => $count,
|
|
'summary' => sprintf(
|
|
'%d recently closed decision%s remain available in the Decision Register for reference and audit follow-through.',
|
|
$count,
|
|
$count === 1 ? '' : 's',
|
|
),
|
|
'open_url' => $openUrl,
|
|
'open_label' => 'Open recently closed decisions',
|
|
'rows' => $rows,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{label: string, state: string, body: string}
|
|
*/
|
|
public function diagnosticsPanel(): array
|
|
{
|
|
return [
|
|
'label' => 'Diagnostics / source detail',
|
|
'state' => 'Collapsed',
|
|
'body' => 'Raw diagnostics, payloads, and support detail stay on authorized source surfaces. This inbox keeps the operator queue, proof links, and next actions on the first screen.',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function calmEmptyState(): array
|
|
{
|
|
if ($this->tenantFilterAloneExcludesRows()) {
|
|
return [
|
|
'title' => 'This environment filter is hiding other visible attention',
|
|
'body' => 'The current environment scope is calm, but other visible environments in this workspace still have governance work that needs follow-up.',
|
|
'action_label' => 'Clear environment filter',
|
|
'action_url' => $this->pageUrl(['environment_id' => null, 'family' => null]),
|
|
];
|
|
}
|
|
|
|
if ($this->familyFilterAloneExcludesRows()) {
|
|
return [
|
|
'title' => 'This source focus is hiding other governance work',
|
|
'body' => 'The current source-family focus is calm, but other repo-backed governance items remain open in this workspace.',
|
|
'action_label' => 'Show all source families',
|
|
'action_url' => $this->pageUrl(['family' => null, 'status' => null, 'updated' => null]),
|
|
];
|
|
}
|
|
|
|
if ($this->reviewPublicationResolutionFiltersAloneExcludeRows()) {
|
|
return [
|
|
'title' => 'These review publication filters are hiding active preparation work',
|
|
'body' => 'The current status or updated-date focus has no matching review publication preparation items, but other active preparation items remain visible in this scope.',
|
|
'action_label' => 'Clear review publication filters',
|
|
'action_url' => $this->pageUrl(['status' => null, 'updated' => null]),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'title' => 'No governance items need attention.',
|
|
'body' => 'Findings, decisions, accepted-risk reviews, evidence gaps, and review follow-ups will appear here when they need operator attention.',
|
|
'action_label' => null,
|
|
'action_url' => null,
|
|
];
|
|
}
|
|
|
|
public function hasTenantPrefilter(): bool
|
|
{
|
|
return $this->selectedTenant() instanceof ManagedEnvironment;
|
|
}
|
|
|
|
public function isActiveFamily(?string $familyKey): bool
|
|
{
|
|
return $this->family === $familyKey;
|
|
}
|
|
|
|
/**
|
|
* @return list<array{key: string|null, label: string, active: bool, url: string}>
|
|
*/
|
|
public function reviewPublicationStatusFilters(): array
|
|
{
|
|
return collect([null, ...ReviewPublicationResolutionInboxProvider::STATUS_FILTERS])
|
|
->map(fn (?string $status): array => [
|
|
'key' => $status,
|
|
'label' => $status === null
|
|
? 'All active statuses'
|
|
: ReviewPublicationResolutionInboxProvider::statusLabel($status),
|
|
'active' => $this->status === $status,
|
|
'url' => $this->pageUrl([
|
|
'family' => ReviewPublicationResolutionInboxProvider::FAMILY_KEY,
|
|
'status' => $status,
|
|
]).'#source-detail',
|
|
])
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return list<array{key: string|null, label: string, active: bool, url: string}>
|
|
*/
|
|
public function reviewPublicationUpdatedFilters(): array
|
|
{
|
|
return collect([null, ...ReviewPublicationResolutionInboxProvider::UPDATED_FILTERS])
|
|
->map(fn (?string $updated): array => [
|
|
'key' => $updated,
|
|
'label' => ReviewPublicationResolutionInboxProvider::updatedLabel($updated),
|
|
'active' => $this->updated === $updated,
|
|
'url' => $this->pageUrl([
|
|
'family' => ReviewPublicationResolutionInboxProvider::FAMILY_KEY,
|
|
'updated' => $updated,
|
|
]).'#source-detail',
|
|
])
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
public function hasReviewPublicationResolutionFocus(): bool
|
|
{
|
|
return $this->family === ReviewPublicationResolutionInboxProvider::FAMILY_KEY;
|
|
}
|
|
|
|
public function pageUrl(array $overrides = []): string
|
|
{
|
|
$selectedTenant = $this->selectedTenant();
|
|
$resolvedTenant = array_key_exists('environment_id', $overrides)
|
|
? $overrides['environment_id']
|
|
: ($selectedTenant?->getKey() !== null ? (string) $selectedTenant->getKey() : null);
|
|
$resolvedFamily = array_key_exists('family', $overrides)
|
|
? $overrides['family']
|
|
: $this->family;
|
|
$hasReviewPublicationFocus = $resolvedFamily === ReviewPublicationResolutionInboxProvider::FAMILY_KEY;
|
|
$resolvedStatus = $hasReviewPublicationFocus
|
|
? (array_key_exists('status', $overrides) ? $overrides['status'] : $this->status)
|
|
: null;
|
|
$resolvedUpdated = $hasReviewPublicationFocus
|
|
? (array_key_exists('updated', $overrides) ? $overrides['updated'] : $this->updated)
|
|
: null;
|
|
|
|
return static::getUrl(
|
|
panel: 'admin',
|
|
parameters: array_filter([
|
|
'environment_id' => (is_string($resolvedTenant) || is_numeric($resolvedTenant)) && (string) $resolvedTenant !== '' ? (string) $resolvedTenant : null,
|
|
'family' => is_string($resolvedFamily) && $resolvedFamily !== '' ? $resolvedFamily : null,
|
|
'status' => is_string($resolvedStatus) && $resolvedStatus !== '' ? $resolvedStatus : null,
|
|
'updated' => is_string($resolvedUpdated) && $resolvedUpdated !== '' ? $resolvedUpdated : null,
|
|
], static fn (mixed $value): bool => $value !== null && $value !== ''),
|
|
);
|
|
}
|
|
|
|
public function navigationContext(): CanonicalNavigationContext
|
|
{
|
|
return CanonicalNavigationContext::forGovernanceInbox(
|
|
canonicalRouteName: static::getRouteName(Filament::getPanel('admin')),
|
|
tenantId: $this->tenantId,
|
|
backLinkUrl: $this->pageUrl(),
|
|
familyKey: $this->family,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return \Illuminate\Support\Collection<int, array<string, mixed>>
|
|
*/
|
|
private function workbenchEntries(): \Illuminate\Support\Collection
|
|
{
|
|
return collect($this->sections())
|
|
->flatMap(function (array $section): array {
|
|
$entries = is_array($section['entries'] ?? null) ? $section['entries'] : [];
|
|
|
|
return array_map(function (array $entry) use ($section): array {
|
|
$entry['section_key'] = (string) ($section['key'] ?? $entry['family_key'] ?? 'governance');
|
|
$entry['section_label'] = (string) ($section['label'] ?? 'Governance item');
|
|
|
|
return $entry;
|
|
}, $entries);
|
|
})
|
|
->values();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function lanePayload(): array
|
|
{
|
|
if (is_array($this->lanePayload)) {
|
|
return $this->lanePayload;
|
|
}
|
|
|
|
$lanes = collect(self::LANE_DEFINITIONS)
|
|
->mapWithKeys(fn (array $definition): array => [
|
|
$definition['key'] => [
|
|
...$definition,
|
|
'anchor_id' => 'lane-'.$definition['key'],
|
|
'count' => 0,
|
|
'items' => [],
|
|
],
|
|
])
|
|
->all();
|
|
|
|
foreach ($this->workbenchEntries() as $entry) {
|
|
$operatorItem = $this->buildOperatorItem($entry);
|
|
$laneKey = (string) $operatorItem['lane_key'];
|
|
|
|
if (! array_key_exists($laneKey, $lanes)) {
|
|
continue;
|
|
}
|
|
|
|
$lanes[$laneKey]['items'][] = $operatorItem;
|
|
$lanes[$laneKey]['count']++;
|
|
}
|
|
|
|
foreach ($lanes as $key => $lane) {
|
|
$lanes[$key]['items'] = collect($lane['items'])
|
|
->sortBy([
|
|
fn (array $item): int => (int) ($item['urgency_rank'] ?? 999),
|
|
fn (array $item): string => (string) ($item['title'] ?? ''),
|
|
])
|
|
->take(self::LANE_PREVIEW_LIMIT)
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
$visibleLanes = collect(self::LANE_DEFINITIONS)
|
|
->map(fn (array $definition): array => $lanes[$definition['key']])
|
|
->filter(fn (array $lane): bool => (int) $lane['count'] > 0)
|
|
->values()
|
|
->all();
|
|
|
|
$summaryCounts = collect(self::LANE_DEFINITIONS)
|
|
->map(fn (array $definition): array => [
|
|
'key' => $definition['key'],
|
|
'label' => $definition['label'],
|
|
'count' => (int) ($lanes[$definition['key']]['count'] ?? 0),
|
|
'description' => $definition['description'],
|
|
'state' => (int) ($lanes[$definition['key']]['count'] ?? 0) > 0 ? 'active' : 'clear',
|
|
'chip_label' => (int) ($lanes[$definition['key']]['count'] ?? 0) > 0
|
|
? (string) ((int) ($lanes[$definition['key']]['count'] ?? 0))
|
|
: 'Clear',
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
$totalOpenItems = collect($summaryCounts)->sum('count');
|
|
$nextRecommendedItem = $this->nextRecommendedItem($visibleLanes);
|
|
|
|
return $this->lanePayload = [
|
|
'lanes' => $visibleLanes,
|
|
'summary' => [
|
|
'total_open_items' => $totalOpenItems,
|
|
'headline' => $totalOpenItems === 1
|
|
? '1 open governance item needs attention'
|
|
: sprintf('%d open governance items need attention', $totalOpenItems),
|
|
'counts' => $summaryCounts,
|
|
'active_counts' => collect($summaryCounts)->where('state', 'active')->values()->all(),
|
|
'clear_counts' => collect($summaryCounts)->where('state', 'clear')->values()->all(),
|
|
'primary_action' => $nextRecommendedItem['primary_action'] ?? null,
|
|
'next_recommended_item' => $nextRecommendedItem,
|
|
'review_ready_supported' => false,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $entry
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function buildOperatorItem(array $entry): array
|
|
{
|
|
$laneKey = $this->classifyLane($entry);
|
|
$tenant = $this->tenantForEntry($entry);
|
|
$primaryActionUrl = filled($entry['primary_action_url'] ?? null)
|
|
? (string) $entry['primary_action_url']
|
|
: (filled($entry['destination_url'] ?? null) ? (string) $entry['destination_url'] : null);
|
|
$primaryAction = $this->normalizeAction([
|
|
'label' => (string) ($entry['primary_action_label'] ?? 'Open source'),
|
|
'url' => $primaryActionUrl,
|
|
]);
|
|
|
|
return [
|
|
'lane_key' => $laneKey,
|
|
'lane_label' => $this->laneDefinition($laneKey)['label'],
|
|
'title' => (string) ($entry['headline'] ?? 'Governance item'),
|
|
'status_label' => (string) ($entry['status_label'] ?? 'Needs attention'),
|
|
'reason_heading' => $laneKey === 'blocked' ? 'Blocker' : 'Reason',
|
|
'reason_label' => (string) ($entry['reason_label'] ?? 'Reason unavailable'),
|
|
'impact_label' => (string) ($entry['impact_label'] ?? 'Impact unavailable'),
|
|
'source_label' => (string) ($entry['section_label'] ?? 'Source record'),
|
|
'environment_label' => filled($entry['tenant_label'] ?? null)
|
|
? (string) $entry['tenant_label']
|
|
: 'Workspace-wide',
|
|
'context_label' => filled($entry['subline'] ?? null)
|
|
? (string) $entry['subline']
|
|
: null,
|
|
'owner_label' => (string) ($entry['owner_label'] ?? 'Owner unavailable'),
|
|
'due_label' => (string) ($entry['due_label'] ?? 'Due date unavailable'),
|
|
'evidence_label' => (string) ($entry['evidence_label'] ?? 'Evidence unavailable'),
|
|
'exception_label' => (string) ($entry['exception_label'] ?? 'Accepted-risk state unavailable'),
|
|
'primary_action' => $primaryAction,
|
|
'secondary_actions' => $this->secondaryActionsForEntry($entry, $tenant, $primaryActionUrl),
|
|
'linked_records' => $this->linkedRecordsForEntry($entry, $tenant),
|
|
'urgency_rank' => (int) ($entry['urgency_rank'] ?? 999),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $entry
|
|
*/
|
|
private function classifyLane(array $entry): string
|
|
{
|
|
$familyKey = (string) ($entry['family_key'] ?? '');
|
|
|
|
return match ($familyKey) {
|
|
'intake_findings' => 'needs_triage',
|
|
'finding_exceptions' => 'risk_exception_review',
|
|
'stale_operations', 'alert_delivery_failures' => 'blocked',
|
|
ReviewPublicationResolutionInboxProvider::FAMILY_KEY => match ((string) ($entry['inbox_status'] ?? 'needs_attention')) {
|
|
'failed', 'blocked' => 'blocked',
|
|
'needs_attention', 'needs_recheck' => 'evidence_required',
|
|
default => 'requires_decision',
|
|
},
|
|
'assigned_findings' => (($entry['evidence_state'] ?? null) === 'missing')
|
|
? 'evidence_required'
|
|
: 'requires_decision',
|
|
'review_follow_up' => 'requires_decision',
|
|
default => 'requires_decision',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return array{key: string, label: string, description: string, empty_state: string}
|
|
*/
|
|
private function laneDefinition(string $laneKey): array
|
|
{
|
|
foreach (self::LANE_DEFINITIONS as $definition) {
|
|
if ($definition['key'] === $laneKey) {
|
|
return $definition;
|
|
}
|
|
}
|
|
|
|
return self::LANE_DEFINITIONS[1];
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $visibleLanes
|
|
*/
|
|
private function nextRecommendedItem(array $visibleLanes): ?array
|
|
{
|
|
foreach ($visibleLanes as $lane) {
|
|
$items = is_array($lane['items'] ?? null) ? $lane['items'] : [];
|
|
|
|
foreach ($items as $item) {
|
|
if (! is_array($item)) {
|
|
continue;
|
|
}
|
|
|
|
$primaryAction = $this->normalizeAction($item['primary_action'] ?? null);
|
|
|
|
if ($primaryAction === null) {
|
|
continue;
|
|
}
|
|
|
|
return [
|
|
'headline' => $this->recommendedActionHeadline($item, $primaryAction),
|
|
'lane_key' => (string) ($lane['key'] ?? $item['lane_key'] ?? 'requires_decision'),
|
|
'lane_label' => (string) ($lane['label'] ?? $item['lane_label'] ?? 'Requires decision'),
|
|
'lane_url' => filled($lane['anchor_id'] ?? null) ? '#'.$lane['anchor_id'] : null,
|
|
'title' => (string) ($item['title'] ?? 'Governance item'),
|
|
'status_label' => (string) ($item['status_label'] ?? 'Needs attention'),
|
|
'environment_label' => (string) ($item['environment_label'] ?? 'Workspace-wide'),
|
|
'reason_heading' => (string) ($item['reason_heading'] ?? 'Reason'),
|
|
'reason_label' => (string) ($item['reason_label'] ?? 'Reason unavailable'),
|
|
'impact_label' => (string) ($item['impact_label'] ?? 'Impact unavailable'),
|
|
'primary_action' => $primaryAction,
|
|
];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $item
|
|
* @param array{label: string, url: string} $primaryAction
|
|
*/
|
|
private function recommendedActionHeadline(array $item, array $primaryAction): string
|
|
{
|
|
$title = (string) ($item['title'] ?? 'Governance item');
|
|
$actionLabel = (string) ($primaryAction['label'] ?? 'Open source');
|
|
$lowerAction = Str::lower($actionLabel);
|
|
|
|
if (Str::startsWith($title, 'Finding #') && Str::contains($lowerAction, 'finding')) {
|
|
$verb = trim((string) Str::of($actionLabel)->before(' finding'));
|
|
|
|
if ($verb !== '') {
|
|
return Str::ucfirst($verb).' '.$title;
|
|
}
|
|
}
|
|
|
|
if (Str::startsWith($title, 'Terminal follow-up operation') && Str::contains($lowerAction, 'operation')) {
|
|
return 'Open terminal operation proof';
|
|
}
|
|
|
|
return $actionLabel.': '.$title;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $entry
|
|
* @return list<array{label: string, url: string}>
|
|
*/
|
|
private function secondaryActionsForEntry(array $entry, ?ManagedEnvironment $tenant, ?string $primaryActionUrl): array
|
|
{
|
|
$familyKey = (string) ($entry['family_key'] ?? '');
|
|
$actions = [];
|
|
$environmentId = is_numeric($entry['managed_environment_id'] ?? null)
|
|
? (int) $entry['managed_environment_id']
|
|
: null;
|
|
|
|
foreach ($this->normalizeLinks($entry['secondary_actions'] ?? []) as $action) {
|
|
$this->appendUniqueLink($actions, $action['label'], $action['url'], [$primaryActionUrl]);
|
|
}
|
|
|
|
if (in_array($familyKey, ['assigned_findings', 'intake_findings', 'finding_exceptions'], true)) {
|
|
$this->appendUniqueLink(
|
|
$actions,
|
|
'Open decision register',
|
|
$this->decisionRegisterUrlFor($tenant),
|
|
[$primaryActionUrl],
|
|
);
|
|
}
|
|
|
|
if (in_array($familyKey, ['assigned_findings', 'intake_findings', 'review_follow_up'], true)) {
|
|
$this->appendUniqueLink(
|
|
$actions,
|
|
'Open evidence overview',
|
|
$this->evidenceOverviewUrlFor($tenant),
|
|
[$primaryActionUrl],
|
|
);
|
|
}
|
|
|
|
if ($familyKey === 'review_follow_up' && $tenant instanceof ManagedEnvironment) {
|
|
$this->appendUniqueLink(
|
|
$actions,
|
|
'Open Customer Review Workspace',
|
|
CustomerReviewWorkspace::environmentFilterUrl($tenant),
|
|
[$primaryActionUrl],
|
|
);
|
|
}
|
|
|
|
if ($familyKey === ReviewPublicationResolutionInboxProvider::FAMILY_KEY && $tenant instanceof ManagedEnvironment) {
|
|
$this->appendUniqueLink(
|
|
$actions,
|
|
'Open environment',
|
|
ManagedEnvironmentLinks::viewUrl($tenant),
|
|
[$primaryActionUrl],
|
|
);
|
|
}
|
|
|
|
if (in_array($familyKey, ['stale_operations', 'alert_delivery_failures', 'assigned_findings', 'intake_findings'], true) && $tenant instanceof ManagedEnvironment) {
|
|
$this->appendUniqueLink(
|
|
$actions,
|
|
'Open environment',
|
|
ManagedEnvironmentLinks::viewUrl($tenant),
|
|
[$primaryActionUrl],
|
|
);
|
|
}
|
|
|
|
if ($familyKey === 'finding_exceptions') {
|
|
$this->appendUniqueLink(
|
|
$actions,
|
|
'Open source finding',
|
|
$entry['destination_url'] ?? null,
|
|
[$primaryActionUrl],
|
|
);
|
|
}
|
|
|
|
if ($environmentId === null && filled($entry['destination_url'] ?? null)) {
|
|
$this->appendUniqueLink(
|
|
$actions,
|
|
'Open source',
|
|
$entry['destination_url'] ?? null,
|
|
[$primaryActionUrl],
|
|
);
|
|
}
|
|
|
|
return array_slice($actions, 0, 3);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $entry
|
|
* @return list<array{label: string, url: string}>
|
|
*/
|
|
private function linkedRecordsForEntry(array $entry, ?ManagedEnvironment $tenant): array
|
|
{
|
|
$familyKey = (string) ($entry['family_key'] ?? '');
|
|
$records = [];
|
|
|
|
foreach ($this->normalizeLinks($entry['linked_records'] ?? []) as $record) {
|
|
$this->appendUniqueLink($records, $record['label'], $record['url']);
|
|
}
|
|
|
|
$this->appendUniqueLink($records, 'Source record', $entry['destination_url'] ?? null);
|
|
$this->appendUniqueLink($records, 'Evidence path', $entry['evidence_path_url'] ?? null);
|
|
|
|
if (in_array($familyKey, ['assigned_findings', 'intake_findings', 'finding_exceptions'], true)) {
|
|
$this->appendUniqueLink($records, 'Decision register', $this->decisionRegisterUrlFor($tenant));
|
|
}
|
|
|
|
if ($familyKey === 'review_follow_up' && $tenant instanceof ManagedEnvironment) {
|
|
$this->appendUniqueLink($records, 'Customer Review Workspace', CustomerReviewWorkspace::environmentFilterUrl($tenant));
|
|
}
|
|
|
|
if ($familyKey === 'stale_operations') {
|
|
$this->appendUniqueLink($records, 'Operation proof', $entry['destination_url'] ?? null);
|
|
}
|
|
|
|
return array_slice($records, 0, 4);
|
|
}
|
|
|
|
/**
|
|
* @return list<array{label: string, url: string}>
|
|
*/
|
|
private function normalizeLinks(mixed $links): array
|
|
{
|
|
if (! is_array($links)) {
|
|
return [];
|
|
}
|
|
|
|
$normalized = [];
|
|
|
|
foreach ($links as $link) {
|
|
if (! is_array($link)) {
|
|
continue;
|
|
}
|
|
|
|
$label = $link['label'] ?? null;
|
|
$url = $link['url'] ?? null;
|
|
|
|
if (! is_string($label) || $label === '' || ! is_string($url) || $url === '') {
|
|
continue;
|
|
}
|
|
|
|
$normalized[] = [
|
|
'label' => $label,
|
|
'url' => $url,
|
|
];
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
/**
|
|
* @param list<array{label: string, url: string}> $links
|
|
* @param list<string|null> $ignoredUrls
|
|
*/
|
|
private function appendUniqueLink(array &$links, string $label, mixed $url, array $ignoredUrls = []): void
|
|
{
|
|
if (! is_string($url) || $url === '' || in_array($url, $ignoredUrls, true)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($links as $existingLink) {
|
|
if (($existingLink['url'] ?? null) === $url) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
$links[] = [
|
|
'label' => $label,
|
|
'url' => $url,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $family
|
|
* @return array{key: string, label: string, count: int}
|
|
*/
|
|
private function normalizeFamily(array $family): array
|
|
{
|
|
$key = (string) ($family['key'] ?? 'governance');
|
|
|
|
return [
|
|
'key' => $key,
|
|
'label' => filled($family['label'] ?? null)
|
|
? (string) $family['label']
|
|
: Str::headline($key),
|
|
'count' => (int) ($family['count'] ?? 0),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $section
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function normalizeSection(array $section): array
|
|
{
|
|
$key = (string) ($section['key'] ?? 'governance');
|
|
$label = filled($section['label'] ?? null)
|
|
? (string) $section['label']
|
|
: Str::headline($key);
|
|
|
|
return [
|
|
'key' => $key,
|
|
'label' => $label,
|
|
'count' => (int) ($section['count'] ?? 0),
|
|
'summary' => filled($section['summary'] ?? null)
|
|
? (string) $section['summary']
|
|
: 'No summary is available for this source family.',
|
|
'dominant_action' => $this->normalizeAction([
|
|
'label' => (string) ($section['dominant_action_label'] ?? 'Open '.$label),
|
|
'url' => $section['dominant_action_url'] ?? null,
|
|
]),
|
|
'dominant_action_label' => (string) ($section['dominant_action_label'] ?? 'Open '.$label),
|
|
'dominant_action_url' => is_string($section['dominant_action_url'] ?? null)
|
|
? (string) $section['dominant_action_url']
|
|
: null,
|
|
'entries' => collect($section['entries'] ?? [])
|
|
->filter(fn (mixed $entry): bool => is_array($entry))
|
|
->map(fn (array $entry): array => $this->normalizeSourceEntry($entry))
|
|
->values()
|
|
->all(),
|
|
'empty_state' => filled($section['empty_state'] ?? null)
|
|
? (string) $section['empty_state']
|
|
: 'No source records are visible in the current scope.',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $entry
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function normalizeSourceEntry(array $entry): array
|
|
{
|
|
return [
|
|
...$entry,
|
|
'headline' => filled($entry['headline'] ?? null)
|
|
? (string) $entry['headline']
|
|
: 'Governance item',
|
|
'status_label' => filled($entry['status_label'] ?? null)
|
|
? (string) $entry['status_label']
|
|
: 'Needs attention',
|
|
'destination_url' => is_string($entry['destination_url'] ?? null)
|
|
? (string) $entry['destination_url']
|
|
: null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{label: string, url: string}|null
|
|
*/
|
|
private function normalizeAction(mixed $action): ?array
|
|
{
|
|
if (! is_array($action)) {
|
|
return null;
|
|
}
|
|
|
|
$url = $action['url'] ?? null;
|
|
|
|
if (! is_string($url) || $url === '') {
|
|
return null;
|
|
}
|
|
|
|
$label = filled($action['label'] ?? null)
|
|
? (string) $action['label']
|
|
: 'Open source';
|
|
|
|
return [
|
|
'label' => $label,
|
|
'url' => $url,
|
|
];
|
|
}
|
|
|
|
private function tenantForEntry(array $entry): ?ManagedEnvironment
|
|
{
|
|
$environmentId = is_numeric($entry['managed_environment_id'] ?? null)
|
|
? (int) $entry['managed_environment_id']
|
|
: null;
|
|
|
|
if (! is_int($environmentId)) {
|
|
return null;
|
|
}
|
|
|
|
foreach ($this->authorizedTenants() as $tenant) {
|
|
if ((int) $tenant->getKey() === $environmentId) {
|
|
return $tenant;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function decisionRegisterUrlFor(?ManagedEnvironment $tenant = null): string
|
|
{
|
|
return DecisionRegister::getUrl(
|
|
panel: 'admin',
|
|
parameters: array_filter([
|
|
'environment_id' => $tenant?->getKey(),
|
|
], static fn (mixed $value): bool => $value !== null && $value !== ''),
|
|
);
|
|
}
|
|
|
|
private function evidenceOverviewUrlFor(?ManagedEnvironment $tenant = null): string
|
|
{
|
|
return route('admin.evidence.overview', array_filter([
|
|
'environment_id' => $tenant?->getKey(),
|
|
], static fn (mixed $value): bool => $value !== null && $value !== ''));
|
|
}
|
|
|
|
private function authorizeWorkspaceMembership(): void
|
|
{
|
|
$user = auth()->user();
|
|
$workspace = $this->workspace();
|
|
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
throw new NotFoundHttpException;
|
|
}
|
|
|
|
$resolver = app(WorkspaceCapabilityResolver::class);
|
|
|
|
if (! $resolver->isMember($user, $workspace)) {
|
|
throw new NotFoundHttpException;
|
|
}
|
|
}
|
|
|
|
private function ensureAtLeastOneVisibleFamily(): void
|
|
{
|
|
if (
|
|
$this->hasVisibleOperationsFamily()
|
|
|| $this->visibleFindingTenants() !== []
|
|
|| $this->hasVisibleFindingExceptionsFamily()
|
|
|| $this->reviewTenants() !== []
|
|
|| $this->hasVisibleAlertsFamily()
|
|
) {
|
|
return;
|
|
}
|
|
|
|
abort(403);
|
|
}
|
|
|
|
private function ensureRequestedFamilyIsVisible(): void
|
|
{
|
|
if ($this->family === null) {
|
|
return;
|
|
}
|
|
|
|
if (in_array($this->family, collect($this->availableFamilies())->pluck('key')->all(), true)) {
|
|
return;
|
|
}
|
|
|
|
throw new NotFoundHttpException;
|
|
}
|
|
|
|
private function hasVisibleOperationsFamily(): bool
|
|
{
|
|
return $this->authorizedTenants() !== [];
|
|
}
|
|
|
|
private function hasVisibleAlertsFamily(): bool
|
|
{
|
|
if (is_bool($this->visibleAlertsFamily)) {
|
|
return $this->visibleAlertsFamily;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$workspace = $this->workspace();
|
|
|
|
if (! $user instanceof User || ! $workspace instanceof Workspace) {
|
|
return $this->visibleAlertsFamily = false;
|
|
}
|
|
|
|
return $this->visibleAlertsFamily = app(WorkspaceCapabilityResolver::class)->can($user, $workspace, Capabilities::ALERTS_VIEW);
|
|
}
|
|
|
|
private function hasVisibleFindingExceptionsFamily(): bool
|
|
{
|
|
if (is_bool($this->visibleFindingExceptionsFamily)) {
|
|
return $this->visibleFindingExceptionsFamily;
|
|
}
|
|
|
|
if ($this->authorizedTenants() === []) {
|
|
return $this->visibleFindingExceptionsFamily = false;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$workspace = $this->workspace();
|
|
|
|
if (! $user instanceof User || ! $workspace instanceof Workspace) {
|
|
return $this->visibleFindingExceptionsFamily = false;
|
|
}
|
|
|
|
return $this->visibleFindingExceptionsFamily = app(WorkspaceCapabilityResolver::class)
|
|
->can($user, $workspace, Capabilities::FINDING_EXCEPTION_APPROVE);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, ManagedEnvironment>
|
|
*/
|
|
private function visibleFindingTenants(): array
|
|
{
|
|
if ($this->visibleFindingTenants !== null) {
|
|
return $this->visibleFindingTenants;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$tenants = $this->authorizedTenants();
|
|
|
|
if (! $user instanceof User || $tenants === []) {
|
|
return $this->visibleFindingTenants = [];
|
|
}
|
|
|
|
$resolver = app(CapabilityResolver::class);
|
|
$resolver->primeMemberships(
|
|
$user,
|
|
array_map(static fn (ManagedEnvironment $tenant): int => (int) $tenant->getKey(), $tenants),
|
|
);
|
|
|
|
return $this->visibleFindingTenants = array_values(array_filter(
|
|
$tenants,
|
|
fn (ManagedEnvironment $tenant): bool => $resolver->can($user, $tenant, Capabilities::TENANT_FINDINGS_VIEW),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @return array<int, ManagedEnvironment>
|
|
*/
|
|
private function reviewTenants(): array
|
|
{
|
|
if ($this->reviewTenants !== null) {
|
|
return $this->reviewTenants;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$workspace = $this->workspace();
|
|
|
|
if (! $user instanceof User || ! $workspace instanceof Workspace) {
|
|
return $this->reviewTenants = [];
|
|
}
|
|
|
|
$service = app(EnvironmentReviewRegisterService::class);
|
|
|
|
if (! $service->canAccessWorkspace($user, $workspace)) {
|
|
return $this->reviewTenants = [];
|
|
}
|
|
|
|
return $this->reviewTenants = $service->authorizedTenants($user, $workspace);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, ManagedEnvironment>
|
|
*/
|
|
private function authorizedTenants(): array
|
|
{
|
|
if ($this->authorizedTenants !== null) {
|
|
return $this->authorizedTenants;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$workspace = $this->workspace();
|
|
|
|
if (! $user instanceof User || ! $workspace instanceof Workspace) {
|
|
return $this->authorizedTenants = [];
|
|
}
|
|
|
|
return $this->authorizedTenants = $user->accessibleManagedEnvironmentsQuery((int) $workspace->getKey())
|
|
->where('managed_environments.lifecycle_status', 'active')
|
|
->orderBy('managed_environments.name')
|
|
->get(['managed_environments.id', 'managed_environments.name', 'managed_environments.slug', 'managed_environments.workspace_id'])
|
|
->all();
|
|
}
|
|
|
|
private function applyRequestedTenantPrefilter(): void
|
|
{
|
|
$workspace = $this->workspace();
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
return;
|
|
}
|
|
|
|
$filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
|
|
|
|
if (! $filter instanceof WorkspaceHubEnvironmentFilter) {
|
|
return;
|
|
}
|
|
|
|
$environmentId = $filter->environmentId();
|
|
|
|
foreach ($this->authorizedTenants() as $tenant) {
|
|
if ((int) $tenant->getKey() === $environmentId) {
|
|
$this->tenantId = $environmentId;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new NotFoundHttpException;
|
|
}
|
|
|
|
private function resolveRequestedFamily(): ?string
|
|
{
|
|
$family = request()->query('family');
|
|
|
|
if (! is_string($family)) {
|
|
return null;
|
|
}
|
|
|
|
return in_array($family, [
|
|
'assigned_findings',
|
|
'intake_findings',
|
|
'finding_exceptions',
|
|
'stale_operations',
|
|
'alert_delivery_failures',
|
|
'review_follow_up',
|
|
ReviewPublicationResolutionInboxProvider::FAMILY_KEY,
|
|
], true) ? $family : null;
|
|
}
|
|
|
|
private function resolveRequestedReviewPublicationStatus(): ?string
|
|
{
|
|
$status = request()->query('status');
|
|
|
|
if (! is_string($status)) {
|
|
return null;
|
|
}
|
|
|
|
return in_array($status, ReviewPublicationResolutionInboxProvider::STATUS_FILTERS, true) ? $status : null;
|
|
}
|
|
|
|
private function resolveRequestedReviewPublicationUpdated(): ?string
|
|
{
|
|
$updated = request()->query('updated');
|
|
|
|
if (! is_string($updated)) {
|
|
return null;
|
|
}
|
|
|
|
return in_array($updated, ReviewPublicationResolutionInboxProvider::UPDATED_FILTERS, true) ? $updated : null;
|
|
}
|
|
|
|
private function workspace(): ?Workspace
|
|
{
|
|
if ($this->workspace instanceof Workspace) {
|
|
return $this->workspace;
|
|
}
|
|
|
|
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
|
|
|
if (! is_int($workspaceId)) {
|
|
return null;
|
|
}
|
|
|
|
return $this->workspace = Workspace::query()->whereKey($workspaceId)->first();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function inboxPayload(): array
|
|
{
|
|
if (is_array($this->inboxPayload)) {
|
|
return $this->inboxPayload;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$workspace = $this->workspace();
|
|
|
|
if (! $user instanceof User || ! $workspace instanceof Workspace) {
|
|
return $this->inboxPayload = [
|
|
'sections' => [],
|
|
'available_families' => [],
|
|
'family_counts' => [],
|
|
'total_count' => 0,
|
|
];
|
|
}
|
|
|
|
return $this->inboxPayload = app(GovernanceInboxSectionBuilder::class)->build(
|
|
user: $user,
|
|
workspace: $workspace,
|
|
authorizedTenants: $this->authorizedTenants(),
|
|
visibleFindingTenants: $this->visibleFindingTenants(),
|
|
reviewTenants: $this->reviewTenants(),
|
|
canViewAlerts: $this->hasVisibleAlertsFamily(),
|
|
canViewFindingExceptions: $this->hasVisibleFindingExceptionsFamily(),
|
|
selectedTenant: $this->selectedTenant(),
|
|
selectedFamily: $this->family,
|
|
selectedReviewPublicationStatus: $this->status,
|
|
selectedReviewPublicationUpdated: $this->updated,
|
|
navigationContext: $this->navigationContext(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function unfilteredInboxPayload(): array
|
|
{
|
|
if (is_array($this->unfilteredInboxPayload)) {
|
|
return $this->unfilteredInboxPayload;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$workspace = $this->workspace();
|
|
|
|
if (! $user instanceof User || ! $workspace instanceof Workspace) {
|
|
return $this->unfilteredInboxPayload = [
|
|
'sections' => [],
|
|
'available_families' => [],
|
|
'family_counts' => [],
|
|
'total_count' => 0,
|
|
];
|
|
}
|
|
|
|
return $this->unfilteredInboxPayload = app(GovernanceInboxSectionBuilder::class)->build(
|
|
user: $user,
|
|
workspace: $workspace,
|
|
authorizedTenants: $this->authorizedTenants(),
|
|
visibleFindingTenants: $this->visibleFindingTenants(),
|
|
reviewTenants: $this->reviewTenants(),
|
|
canViewAlerts: $this->hasVisibleAlertsFamily(),
|
|
canViewFindingExceptions: $this->hasVisibleFindingExceptionsFamily(),
|
|
selectedTenant: null,
|
|
selectedFamily: null,
|
|
selectedReviewPublicationStatus: null,
|
|
selectedReviewPublicationUpdated: null,
|
|
navigationContext: $this->navigationContext(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function reviewPublicationResolutionUnfilteredPayload(): array
|
|
{
|
|
if (is_array($this->reviewPublicationResolutionUnfilteredPayload)) {
|
|
return $this->reviewPublicationResolutionUnfilteredPayload;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$workspace = $this->workspace();
|
|
|
|
if (! $user instanceof User || ! $workspace instanceof Workspace) {
|
|
return $this->reviewPublicationResolutionUnfilteredPayload = [
|
|
'sections' => [],
|
|
'available_families' => [],
|
|
'family_counts' => [],
|
|
'total_count' => 0,
|
|
];
|
|
}
|
|
|
|
return $this->reviewPublicationResolutionUnfilteredPayload = app(GovernanceInboxSectionBuilder::class)->build(
|
|
user: $user,
|
|
workspace: $workspace,
|
|
authorizedTenants: $this->authorizedTenants(),
|
|
visibleFindingTenants: $this->visibleFindingTenants(),
|
|
reviewTenants: $this->reviewTenants(),
|
|
canViewAlerts: $this->hasVisibleAlertsFamily(),
|
|
canViewFindingExceptions: $this->hasVisibleFindingExceptionsFamily(),
|
|
selectedTenant: $this->selectedTenant(),
|
|
selectedFamily: ReviewPublicationResolutionInboxProvider::FAMILY_KEY,
|
|
selectedReviewPublicationStatus: null,
|
|
selectedReviewPublicationUpdated: null,
|
|
navigationContext: $this->navigationContext(),
|
|
);
|
|
}
|
|
|
|
private function selectedTenant(): ?ManagedEnvironment
|
|
{
|
|
if (! is_int($this->tenantId)) {
|
|
return null;
|
|
}
|
|
|
|
foreach ($this->authorizedTenants() as $tenant) {
|
|
if ((int) $tenant->getKey() === $this->tenantId) {
|
|
return $tenant;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function tenantFilterAloneExcludesRows(): bool
|
|
{
|
|
if (! is_int($this->tenantId) || $this->family !== null || $this->status !== null || $this->updated !== null) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->laneGroups() !== []) {
|
|
return false;
|
|
}
|
|
|
|
return (int) ($this->unfilteredInboxPayload()['total_count'] ?? 0) > 0;
|
|
}
|
|
|
|
private function familyFilterAloneExcludesRows(): bool
|
|
{
|
|
if ($this->family === null) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->status !== null || $this->updated !== null) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->laneGroups() !== []) {
|
|
return false;
|
|
}
|
|
|
|
return (int) ($this->unfilteredInboxPayload()['total_count'] ?? 0) > 0;
|
|
}
|
|
|
|
private function reviewPublicationResolutionFiltersAloneExcludeRows(): bool
|
|
{
|
|
if ($this->family !== ReviewPublicationResolutionInboxProvider::FAMILY_KEY) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->status === null && $this->updated === null) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->laneGroups() !== []) {
|
|
return false;
|
|
}
|
|
|
|
return (int) data_get(
|
|
$this->reviewPublicationResolutionUnfilteredPayload(),
|
|
'family_counts.'.ReviewPublicationResolutionInboxProvider::FAMILY_KEY,
|
|
0,
|
|
) > 0;
|
|
}
|
|
}
|