Kurzbeschreibung Filament-native UI-Polish für das Tenant-Dashboard und zugehörige Inventory/Operations-Ansichten; entfernt alte custom Blade‑Panel-Wrapper (die die dicken Rahmen erzeugten) und ersetzt sie durch Filament‑Widgets (StatsOverview / TableWidget). Keine DB-Migrationen. Änderungen (Kurz) Dashboard: KPI‑Kacheln als StatsOverviewWidget (4 Tiles). Needs‑Attention: sinnvolle Leerstaat‑UI (3 Health‑Checks + Links) und begrenzte, badge‑gestützte Issue‑Liste. Recent Drift Findings & Recent Operations: Filament TableWidget (10 Zeilen), badge‑Spalten für Severity/Status/Outcome, kurze copyable IDs, freundliche Subject‑Labels statt roher UUIDs. Entfernen der alten Blade-Wrapper, die ring- / shadow Klassen erzeugten. Tests aktualisiert/ergänzt, um Tenant‑Scope und DB‑only Garantien zu prüfen. Kleinigkeiten / UI‑Polish in Inventory/Operations-Listen und Panel‑Provider. Wichtige Dateien (Auswahl) DashboardKpis.php NeedsAttention.php RecentDriftFindings.php RecentOperations.php needs-attention.blade.php Tests: TenantDashboardTenantScopeTest.php, inventory/operations test updates Testing / Verifikation Lokale Tests (empfohlen, vor Merge ausführen): Formatter: Filament assets (falls panel assets geändert wurden): Review‑Hinweise (Was prüfen) UI: Dashboard sieht visuell wie Filament‑Demo‑Widgets aus (keine dicken ring- Rahmen mehr). Tables: Primary text zeigt freundliche Labels, nicht UUIDs; IDs sind copyable und kurz dargestellt. Needs‑Attention: Leerstaat zeigt die 3 Health‑Checks + korrekte Links; bei Issues sind Badges und Farben korrekt. Tenant‑Scope: Keine Daten von anderen Tenants leakieren (prüfe die aktualisierten TenantScope‑Tests). Polling: Widgets poll nur wenn nötig (z.B. aktive Runs existieren). Keine externen HTTP‑Calls oder ungeprüfte Jobs während Dashboard‑Rendering. Deployment / Migrations Keine Datenbankmigrationen. Empfohlen: nach Merge ./vendor/bin/sail artisan filament:assets in Deployment‑Pipeline prüfen, falls neue panel assets registriert wurden. Zusammenfassung für den Reviewer Zweck: Entfernen der alten, handgebauten Panel‑Wrappers und Vereinheitlichung der Dashboard‑UX mit Filament‑nativen Komponenten; kleinere UI‑Polish in Inventory/Operations. Tests: Unit/Feature tests für Tenant‑Scope und DB‑only Verhalten wurden aktualisiert; bitte laufen lassen. Merge: Branch 058-tenant-ui-polish → dev (protected) via Pull Request in Gitea. Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local> Reviewed-on: #70
163 lines
5.7 KiB
PHP
163 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Widgets\Inventory;
|
|
|
|
use App\Filament\Resources\InventorySyncRunResource;
|
|
use App\Models\InventoryItem;
|
|
use App\Models\InventorySyncRun;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Services\Inventory\CoverageCapabilitiesResolver;
|
|
use App\Support\Inventory\InventoryKpiBadges;
|
|
use App\Support\Inventory\InventoryPolicyTypeMeta;
|
|
use App\Support\Inventory\InventorySyncStatusBadge;
|
|
use Filament\Facades\Filament;
|
|
use Filament\Widgets\StatsOverviewWidget;
|
|
use Filament\Widgets\StatsOverviewWidget\Stat;
|
|
use Illuminate\Support\Facades\Blade;
|
|
use Illuminate\Support\HtmlString;
|
|
|
|
class InventoryKpiHeader extends StatsOverviewWidget
|
|
{
|
|
protected static bool $isLazy = false;
|
|
|
|
protected int|string|array $columnSpan = 'full';
|
|
|
|
/**
|
|
* Inventory KPI aggregation source-of-truth:
|
|
* - `inventory_items.policy_type`
|
|
* - `config('tenantpilot.supported_policy_types')` + `config('tenantpilot.foundation_types')` meta (`restore`, `risk`)
|
|
* - dependency capability via `CoverageCapabilitiesResolver`
|
|
*
|
|
* @return array<Stat>
|
|
*/
|
|
protected function getStats(): array
|
|
{
|
|
$tenant = Filament::getTenant();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return [
|
|
Stat::make('Total items', 0),
|
|
Stat::make('Coverage', '0%')->description('Restorable 0 • Partial 0'),
|
|
Stat::make('Last inventory sync', '—'),
|
|
Stat::make('Active ops', 0),
|
|
Stat::make('Inventory ops', 0)->description('Dependencies 0 • Risk 0'),
|
|
];
|
|
}
|
|
|
|
$tenantId = (int) $tenant->getKey();
|
|
|
|
/** @var array<string, int> $countsByPolicyType */
|
|
$countsByPolicyType = InventoryItem::query()
|
|
->where('tenant_id', $tenantId)
|
|
->selectRaw('policy_type, COUNT(*) as aggregate')
|
|
->groupBy('policy_type')
|
|
->pluck('aggregate', 'policy_type')
|
|
->map(fn ($value): int => (int) $value)
|
|
->all();
|
|
|
|
$totalItems = array_sum($countsByPolicyType);
|
|
|
|
$restorableItems = 0;
|
|
$partialItems = 0;
|
|
$riskItems = 0;
|
|
|
|
foreach ($countsByPolicyType as $policyType => $count) {
|
|
if (InventoryPolicyTypeMeta::isRestorable($policyType)) {
|
|
$restorableItems += $count;
|
|
} elseif (InventoryPolicyTypeMeta::isPartial($policyType)) {
|
|
$partialItems += $count;
|
|
}
|
|
|
|
if (InventoryPolicyTypeMeta::isHighRisk($policyType)) {
|
|
$riskItems += $count;
|
|
}
|
|
}
|
|
|
|
$coveragePercent = $totalItems > 0
|
|
? (int) round(($restorableItems / $totalItems) * 100)
|
|
: 0;
|
|
|
|
$lastRun = InventorySyncRun::query()
|
|
->where('tenant_id', $tenantId)
|
|
->latest('id')
|
|
->first();
|
|
|
|
$lastInventorySyncTimeLabel = '—';
|
|
$lastInventorySyncStatusLabel = '—';
|
|
$lastInventorySyncStatusColor = 'gray';
|
|
$lastInventorySyncStatusIcon = 'heroicon-m-clock';
|
|
$lastInventorySyncViewUrl = null;
|
|
|
|
if ($lastRun instanceof InventorySyncRun) {
|
|
$timestamp = $lastRun->finished_at ?? $lastRun->started_at;
|
|
|
|
if ($timestamp) {
|
|
$lastInventorySyncTimeLabel = $timestamp->diffForHumans(['short' => true]);
|
|
}
|
|
|
|
$status = (string) ($lastRun->status ?? '');
|
|
|
|
$badge = InventorySyncStatusBadge::for($status);
|
|
$lastInventorySyncStatusLabel = $badge['label'];
|
|
$lastInventorySyncStatusColor = $badge['color'];
|
|
$lastInventorySyncStatusIcon = $badge['icon'];
|
|
|
|
$lastInventorySyncViewUrl = InventorySyncRunResource::getUrl('view', ['record' => $lastRun], tenant: $tenant);
|
|
}
|
|
|
|
$badgeColor = $lastInventorySyncStatusColor;
|
|
|
|
$lastInventorySyncDescription = Blade::render(<<<'BLADE'
|
|
<div class="flex items-center gap-2">
|
|
<x-filament::badge :color="$badgeColor" size="sm">
|
|
{{ $statusLabel }}
|
|
</x-filament::badge>
|
|
|
|
@if ($viewUrl)
|
|
<x-filament::link :href="$viewUrl" size="sm">
|
|
View run
|
|
</x-filament::link>
|
|
@endif
|
|
</div>
|
|
BLADE, [
|
|
'badgeColor' => $badgeColor,
|
|
'statusLabel' => $lastInventorySyncStatusLabel,
|
|
'viewUrl' => $lastInventorySyncViewUrl,
|
|
]);
|
|
|
|
$activeOps = (int) OperationRun::query()
|
|
->where('tenant_id', $tenantId)
|
|
->active()
|
|
->count();
|
|
|
|
$inventoryOps = (int) OperationRun::query()
|
|
->where('tenant_id', $tenantId)
|
|
->where('type', 'inventory.sync')
|
|
->active()
|
|
->count();
|
|
|
|
$resolver = app(CoverageCapabilitiesResolver::class);
|
|
|
|
$dependenciesItems = 0;
|
|
foreach ($countsByPolicyType as $policyType => $count) {
|
|
if ($policyType !== '' && $resolver->supportsDependencies($policyType)) {
|
|
$dependenciesItems += $count;
|
|
}
|
|
}
|
|
|
|
return [
|
|
Stat::make('Total items', $totalItems),
|
|
Stat::make('Coverage', $coveragePercent.'%')
|
|
->description(new HtmlString(InventoryKpiBadges::coverage($restorableItems, $partialItems))),
|
|
Stat::make('Last inventory sync', $lastInventorySyncTimeLabel)
|
|
->description(new HtmlString($lastInventorySyncDescription)),
|
|
Stat::make('Active ops', $activeOps),
|
|
Stat::make('Inventory ops', $inventoryOps)
|
|
->description(new HtmlString(InventoryKpiBadges::inventoryOps($dependenciesItems, $riskItems))),
|
|
];
|
|
}
|
|
}
|