TenantAtlas/apps/platform/resources/views/filament/pages/cross-tenant-compare.blade.php
ahmido 61feb48d8a
Some checks failed
Main Confidence / confidence (push) Failing after 54s
chore(platform): merge platform-dev into dev (#308)
Integrates latest TenantPilot platform changes from `platform-dev` into `dev`.

Refresh method in this update: merge from `origin/dev` into `platform-dev` on explicit user request.

This PR was created by agent on user request; do not merge automatically.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #308
2026-04-30 07:52:08 +00:00

208 lines
13 KiB
PHP

<x-filament-panels::page>
@php
$preview = is_array($preview ?? null) ? $preview : null;
$preflight = is_array($preflight ?? null) ? $preflight : null;
$previewSummary = is_array($preview['summary'] ?? null) ? $preview['summary'] : [];
$preflightSummary = is_array($preflight['summary'] ?? null) ? $preflight['summary'] : [];
$blockedReasonCounts = is_array($preflight['blockedReasonCounts'] ?? null) ? $preflight['blockedReasonCounts'] : [];
$sourceTenantName = data_get($preview, 'selection.sourceTenantName');
$targetTenantName = data_get($preview, 'selection.targetTenantName');
$selectedPolicyTypes = data_get($preview, 'selection.policyTypes', []);
@endphp
<x-filament::section heading="Cross-tenant compare">
<x-slot name="description">
Compare one authorized source tenant to one authorized target tenant from a canonical workspace surface. Preview stays read only and promotion remains preflight only.
</x-slot>
<div class="space-y-4">
<div class="rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
<form wire:submit.prevent="applySelection" class="space-y-4">
{{ $this->form }}
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div class="flex-1 rounded-xl border border-dashed border-gray-300 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-900/40">
<div class="text-sm font-semibold text-gray-950 dark:text-white">Shareable compare scope</div>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
Source tenant, target tenant, and governed-subject filters live on the URL so the same compare preview can be reopened or shared.
</p>
</div>
<div class="flex flex-wrap items-center gap-3 lg:shrink-0">
<x-filament::button type="submit" color="primary" size="sm" wire:loading.attr="disabled" wire:target="applySelection,generatePromotionPreflight">
Run compare preview
</x-filament::button>
@if ($this->hasActiveSelection())
<x-filament::button tag="a" :href="$this->clearSelectionUrl()" color="gray" size="sm">
Clear selection
</x-filament::button>
@endif
</div>
</div>
</form>
</div>
@if (filled($selectionMessage))
<div class="rounded-2xl border border-warning-200 bg-warning-50 px-4 py-3 text-sm text-warning-900 dark:border-warning-500/30 dark:bg-warning-500/10 dark:text-warning-100">
{{ $selectionMessage }}
</div>
@endif
@if ($preview === null)
<div class="rounded-2xl border border-dashed border-gray-300 bg-gray-50/70 px-5 py-6 text-sm text-gray-600 dark:border-gray-700 dark:bg-gray-900/40 dark:text-gray-300">
Choose a source tenant and a target tenant to build a compare preview. The source and target must be different tenants inside the active workspace.
</div>
@endif
</div>
</x-filament::section>
@if ($preview !== null)
<x-filament::section heading="Compare preview">
<x-slot name="description">
Decision-first summary of governed subjects. Raw payloads stay on the existing tenant and baseline surfaces.
</x-slot>
<div class="space-y-4" data-testid="cross-tenant-compare-preview">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div class="space-y-2">
<div class="flex flex-wrap items-center gap-2">
<x-filament::badge color="info" size="sm">Source tenant: {{ $sourceTenantName }}</x-filament::badge>
<x-filament::badge color="gray" size="sm">Target tenant: {{ $targetTenantName }}</x-filament::badge>
@foreach ($selectedPolicyTypes as $policyType)
<x-filament::badge color="gray" size="sm">{{ $this->stateLabel($policyType) }}</x-filament::badge>
@endforeach
</div>
<div class="flex flex-wrap items-center gap-3 text-sm">
@if (filled($this->sourceTenantUrl()))
<a href="{{ $this->sourceTenantUrl() }}" class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">
Open source tenant
</a>
@endif
@if (filled($this->targetTenantUrl()))
<a href="{{ $this->targetTenantUrl() }}" class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">
Open target tenant
</a>
@endif
</div>
<p class="text-sm text-gray-600 dark:text-gray-300">
The preview groups governed subjects into reproducible compare states so you can decide whether the target is aligned, missing, blocked, or needs manual review.
</p>
</div>
<dl class="grid gap-3 sm:grid-cols-3 xl:w-[28rem]">
@foreach (['match', 'different', 'missing', 'ambiguous', 'blocked', 'total'] as $state)
<div class="rounded-2xl border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
<dt class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $this->stateLabel($state) }}</dt>
<dd class="mt-1 text-2xl font-semibold text-gray-950 dark:text-white">{{ (int) ($previewSummary[$state] ?? 0) }}</dd>
</div>
@endforeach
</dl>
</div>
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
<div class="grid grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_minmax(0,0.8fr)] gap-3 border-b border-gray-200 px-4 py-3 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:border-gray-800 dark:text-gray-400">
<div>Governed subject</div>
<div>Reasoning</div>
<div>Compare state</div>
</div>
<div class="divide-y divide-gray-200 dark:divide-gray-800">
@foreach (data_get($preview, 'subjects', []) as $subject)
<div class="grid grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_minmax(0,0.8fr)] gap-3 px-4 py-4" data-testid="cross-tenant-compare-subject">
<div class="space-y-2">
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ data_get($subject, 'displayName') }}</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
<span>{{ $this->stateLabel((string) data_get($subject, 'policyType', 'unknown')) }}</span>
@if (filled(data_get($subject, 'subjectKey')))
<x-filament::badge color="gray" size="sm">{{ data_get($subject, 'subjectKey') }}</x-filament::badge>
@endif
</div>
</div>
<div class="flex flex-wrap gap-2">
@forelse (data_get($subject, 'reasonCodes', []) as $reasonCode)
<x-filament::badge color="gray" size="sm">{{ $this->reasonLabel((string) $reasonCode) }}</x-filament::badge>
@empty
<span class="text-sm text-gray-500 dark:text-gray-400">No blocking reason.</span>
@endforelse
</div>
<div class="flex items-start justify-start xl:justify-end">
<x-filament::badge :color="$this->stateColor((string) data_get($subject, 'state', 'unknown'))" size="sm">
{{ $this->stateLabel((string) data_get($subject, 'state', 'unknown')) }}
</x-filament::badge>
</div>
</div>
@endforeach
</div>
</div>
</div>
</x-filament::section>
@endif
@if ($preflight !== null)
<x-filament::section heading="Promotion preflight">
<x-slot name="description">
Read-only readiness view. No target mutation, queue dispatch, or operation run is created in this slice.
</x-slot>
<div class="space-y-4" data-testid="cross-tenant-preflight">
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
@foreach (['ready', 'blocked', 'manual_mapping_required', 'total'] as $state)
<div class="rounded-2xl border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $this->stateLabel($state) }}</div>
<div class="mt-1 text-2xl font-semibold text-gray-950 dark:text-white">{{ (int) ($preflightSummary[$state] ?? 0) }}</div>
</div>
@endforeach
</div>
@if ($blockedReasonCounts !== [])
<div class="rounded-2xl border border-gray-200 bg-gray-50/80 p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/50">
<div class="text-sm font-semibold text-gray-950 dark:text-white">Top blocked reasons</div>
<div class="mt-3 flex flex-wrap gap-2">
@foreach ($blockedReasonCounts as $reasonCode => $count)
<x-filament::badge color="danger" size="sm">{{ $this->reasonLabel((string) $reasonCode) }}: {{ (int) $count }}</x-filament::badge>
@endforeach
</div>
</div>
@endif
<div class="grid gap-4 xl:grid-cols-3">
@foreach (['ready', 'manual_mapping_required', 'blocked'] as $bucket)
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
<div class="flex items-center justify-between gap-3">
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ $this->stateLabel($bucket) }}</div>
<x-filament::badge :color="$this->stateColor($bucket)" size="sm">
{{ count(data_get($preflight, 'buckets.'.$bucket, [])) }}
</x-filament::badge>
</div>
<div class="mt-3 space-y-3">
@forelse (data_get($preflight, 'buckets.'.$bucket, []) as $subject)
<div class="rounded-xl border border-gray-200 bg-gray-50/80 px-3 py-3 dark:border-gray-800 dark:bg-gray-900/50">
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ data_get($subject, 'displayName') }}</div>
@if (data_get($subject, 'preflight.reasonLabels', []) !== [])
<div class="mt-2 flex flex-wrap gap-2">
@foreach (data_get($subject, 'preflight.reasonLabels', []) as $reasonLabel)
<x-filament::badge color="gray" size="sm">{{ $reasonLabel }}</x-filament::badge>
@endforeach
</div>
@endif
</div>
@empty
<div class="text-sm text-gray-500 dark:text-gray-400">No governed subjects in this bucket.</div>
@endforelse
</div>
</div>
@endforeach
</div>
</div>
</x-filament::section>
@endif
<x-filament-actions::modals />
</x-filament-panels::page>