feat: aggiunta dashboard grafica struttura database, schema ERD, storage explorer e simulatore scalabilita

This commit is contained in:
michele 2026-09-07 00:04:18 +02:00
parent ace1a57480
commit ea6353ec41
4 changed files with 761 additions and 31 deletions

View File

@ -0,0 +1,279 @@
<?php
namespace App\Filament\Pages\SuperAdmin;
use App\Models\User;
use BackedEnum;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use UnitEnum;
/**
* NetGescon Database Architecture & Storage Footprint Explorer
*
* Fornisce una visualizzazione completa, grafica e deterministica:
* 1. Dello schema e dell'ingombro disco (MB Dati, MB Indici, Numero Record) di tutte le tabelle.
* 2. Del raggruppamento per domini di business (Stabili, Anagrafiche, Contabilità, Fornitori, Banche, Affitti, Posta, Catasto).
* 3. Delle relazioni chiave di dominio (ERD Mermaid) tra unità, persone, movimenti contabili e fatture.
* 4. Dell'integrazione diretta con il visualizzatore Adminer per l'esplorazione SQL e diagrammi interattivi.
*/
class StrutturaDatabase extends Page
{
protected static ?string $navigationLabel = 'Struttura Database (ERD & Storage)';
protected static ?string $title = 'Struttura Database & Dimensionamento Storage';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-circle-stack';
protected static UnitEnum|string|null $navigationGroup = 'Impostazioni';
protected static ?int $navigationSort = 95;
protected static ?string $slug = 'impostazioni/struttura-database';
protected string $view = 'filament.pages.super-admin.struttura-database';
public string $activeTab = 'storage';
public string $search = '';
public string $selectedDomain = 'all';
public int $simulazioneStabili = 100;
public function mount(): void
{
// Default initializations
}
/**
* Calcola le statistiche e l'elenco delle tabelle con l'ingombro reale
*
* @return array<string, mixed>
*/
public function getDatabaseStats(): array
{
try {
$tables = DB::select("
SELECT
table_name,
engine,
COALESCE(table_rows, 0) as table_rows,
COALESCE(round(data_length / 1024 / 1024, 2), 0) as data_mb,
COALESCE(round(index_length / 1024 / 1024, 2), 0) as index_mb,
COALESCE(round((data_length + index_length) / 1024 / 1024, 2), 0) as total_mb,
table_comment,
update_time
FROM information_schema.tables
WHERE table_schema = 'netgescon'
ORDER BY (data_length + index_length) DESC
");
} catch (\Throwable $e) {
$tables = [];
}
$domainDefinitions = $this->getDomainDefinitions();
$categorizedTables = [];
$domainStats = [];
$grandTotalDataMb = 0.0;
$grandTotalIndexMb = 0.0;
$grandTotalMb = 0.0;
$grandTotalRows = 0;
foreach ($tables as $t) {
$tableName = (string) $t->table_name;
$rows = (int) $t->table_rows;
$dataMb = (float) $t->data_mb;
$indexMb = (float) $t->index_mb;
$totalMb = (float) $t->total_mb;
$grandTotalDataMb += $dataMb;
$grandTotalIndexMb += $indexMb;
$grandTotalMb += $totalMb;
$grandTotalRows += $rows;
$domainKey = $this->resolveDomainForTable($tableName);
$domainMeta = $domainDefinitions[$domainKey] ?? [
'label' => 'Altro / Sistema',
'badge' => 'bg-gray-100 text-gray-800 border-gray-300',
'icon' => '⚙️',
];
if (!isset($domainStats[$domainKey])) {
$domainStats[$domainKey] = [
'key' => $domainKey,
'label' => $domainMeta['label'],
'badge' => $domainMeta['badge'],
'icon' => $domainMeta['icon'],
'table_count' => 0,
'total_rows' => 0,
'total_mb' => 0.0,
'data_mb' => 0.0,
'index_mb' => 0.0,
];
}
$domainStats[$domainKey]['table_count']++;
$domainStats[$domainKey]['total_rows'] += $rows;
$domainStats[$domainKey]['total_mb'] += $totalMb;
$domainStats[$domainKey]['data_mb'] += $dataMb;
$domainStats[$domainKey]['index_mb'] += $indexMb;
$categorizedTables[] = [
'name' => $tableName,
'domain_key' => $domainKey,
'domain_label' => $domainMeta['label'],
'domain_badge' => $domainMeta['badge'],
'domain_icon' => $domainMeta['icon'],
'engine' => $t->engine ?? 'InnoDB',
'rows' => $rows,
'data_mb' => $dataMb,
'index_mb' => $indexMb,
'total_mb' => $totalMb,
'comment' => $t->table_comment ?? '',
'updated_at' => $t->update_time ?? '-',
];
}
// Filtra tabelle per ricerca e dominio
$filteredTables = array_filter($categorizedTables, function ($item) {
if ($this->selectedDomain !== 'all' && $item['domain_key'] !== $this->selectedDomain) {
return false;
}
if ($this->search !== '') {
$term = strtolower(trim($this->search));
return str_contains(strtolower($item['name']), $term) || str_contains(strtolower($item['domain_label']), $term);
}
return true;
});
// Ordina statistiche domini per peso decrescente
uasort($domainStats, fn($a, $b) => $b['total_mb'] <=> $a['total_mb']);
return [
'total_tables' => count($tables),
'total_rows' => $grandTotalRows,
'total_mb' => round($grandTotalMb, 2),
'total_data_mb' => round($grandTotalDataMb, 2),
'total_index_mb' => round($grandTotalIndexMb, 2),
'domain_stats' => array_values($domainStats),
'tables' => array_values($filteredTables),
];
}
/**
* Mappa prefissi e nomi tabelle sui domini di business NetGescon
*/
protected function resolveDomainForTable(string $table): string
{
$table = strtolower($table);
if (str_starts_with($table, 'fatture_') || str_starts_with($table, 'fe_') || str_contains($table, 'fattura')) {
return 'fatture_fe';
}
if (str_starts_with($table, 'contabilita_') || str_starts_with($table, 'rate_') || str_starts_with($table, 'incassi') || in_array($table, ['operazioni_contabili', 'piano_conti', 'voci_spesa', 'chiusure_gestione', 'saldi_conti', 'bilanci'], true)) {
return 'contabilita';
}
if (str_contains($table, 'banca') || str_contains($table, 'cassa') || in_array($table, ['dati_bancari', 'causali_bancarie', 'regole_prima_nota'], true)) {
return 'banche';
}
if (str_starts_with($table, 'fornitor') || in_array($table, ['product_offers', 'products', 'ticket_interventi', 'ticket_operativi'], true)) {
return 'fornitori';
}
if (str_starts_with($table, 'locazion') || str_starts_with($table, 'affitti') || str_contains($table, 'locazione')) {
return 'affitti';
}
if (str_starts_with($table, 'unita_') || str_starts_with($table, 'stabili') || str_starts_with($table, 'palazzin') || str_starts_with($table, 'scale') || str_starts_with($table, 'tabelle_millesimali') || in_array($table, ['servizi_stabile', 'letture_servizi', 'riscaldamento_stabili', 'dettaglio_importi_tabella', 'gestioni_stabili'], true)) {
return 'stabili_unita';
}
if (str_starts_with($table, 'persone') || str_starts_with($table, 'anagrafic') || str_starts_with($table, 'rubrica') || in_array($table, ['comproprietari', 'condomin', 'amministratori', 'precedenti_amministratori'], true)) {
return 'anagrafiche';
}
if (str_starts_with($table, 'email_') || str_starts_with($table, 'posta_') || in_array($table, ['protocollo', 'comunicazioni', 'documenti'], true)) {
return 'posta';
}
if (str_starts_with($table, 'catasto') || str_starts_with($table, 'f24') || str_starts_with($table, 'ritenute') || in_array($table, ['quadro_ac', 'certificazioni_uniche'], true)) {
return 'catasto_fiscale';
}
if (str_starts_with($table, 'stg_') || str_starts_with($table, 'import_') || str_starts_with($table, 'vw_gescon_')) {
return 'staging_import';
}
return 'sistema';
}
/**
* @return array<string, array<string, string>>
*/
protected function getDomainDefinitions(): array
{
return [
'fatture_fe' => [
'label' => 'Fatture Elettroniche (XML/P7M)',
'badge' => 'bg-rose-100 text-rose-800 border-rose-300 dark:bg-rose-950 dark:text-rose-300',
'icon' => '🧾',
],
'contabilita' => [
'label' => 'Contabilità & Partita Doppia',
'badge' => 'bg-emerald-100 text-emerald-800 border-emerald-300 dark:bg-emerald-950 dark:text-emerald-300',
'icon' => '💰',
],
'anagrafiche' => [
'label' => 'Anagrafiche & Persone',
'badge' => 'bg-blue-100 text-blue-800 border-blue-300 dark:bg-blue-950 dark:text-blue-300',
'icon' => '👥',
],
'stabili_unita' => [
'label' => 'Stabili & Unità Immobiliari',
'badge' => 'bg-indigo-100 text-indigo-800 border-indigo-300 dark:bg-indigo-950 dark:text-indigo-300',
'icon' => '🏢',
],
'fornitori' => [
'label' => 'Fornitori & Manutenzioni',
'badge' => 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950 dark:text-amber-300',
'icon' => '🛠️',
],
'banche' => [
'label' => 'Banche, Casse & Riconciliazione',
'badge' => 'bg-cyan-100 text-cyan-800 border-cyan-300 dark:bg-cyan-950 dark:text-cyan-300',
'icon' => '🏦',
],
'affitti' => [
'label' => 'Locazioni & Gestione Affitti',
'badge' => 'bg-violet-100 text-violet-800 border-violet-300 dark:bg-violet-950 dark:text-violet-300',
'icon' => '🔑',
],
'posta' => [
'label' => 'Posta, PEC & Protocollo',
'badge' => 'bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950 dark:text-teal-300',
'icon' => '✉️',
],
'catasto_fiscale' => [
'label' => 'Catasto, Fiscale & Ritenute',
'badge' => 'bg-orange-100 text-orange-800 border-orange-300 dark:bg-orange-950 dark:text-orange-300',
'icon' => '🏛️',
],
'staging_import' => [
'label' => 'Staging & Pipeline Importazione',
'badge' => 'bg-purple-100 text-purple-800 border-purple-300 dark:bg-purple-950 dark:text-purple-300',
'icon' => '🔄',
],
'sistema' => [
'label' => 'Sistema, Utenti & Logs',
'badge' => 'bg-slate-100 text-slate-800 border-slate-300 dark:bg-slate-800 dark:text-slate-300',
'icon' => '⚙️',
],
];
}
}

View File

@ -0,0 +1,439 @@
<x-filament-panels::page>
@php
$stats = $this->getDatabaseStats();
$domainStats = $stats['domain_stats'] ?? [];
$tables = $stats['tables'] ?? [];
$totalMb = (float)($stats['total_mb'] ?? 0);
$totalTables = (int)($stats['total_tables'] ?? 0);
$totalRows = (int)($stats['total_rows'] ?? 0);
$totalDataMb = (float)($stats['total_data_mb'] ?? 0);
$totalIndexMb = (float)($stats['total_index_mb'] ?? 0);
@endphp
<div class="space-y-6">
<!-- Top Action Bar & Header Banner -->
<div class="flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900 md:flex-row md:items-center md:justify-between">
<div class="flex items-center gap-3.5">
<div class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-amber-500/10 text-amber-600 dark:bg-amber-500/20 dark:text-amber-400">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
</div>
<div>
<h1 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">
Struttura Database & Dimensionamento Storage
</h1>
<p class="text-xs text-slate-500 dark:text-slate-400">
Database Primario: <span class="font-semibold text-slate-700 dark:text-slate-200">netgescon</span> (127.0.0.1:3306) &middot; Motore: <span class="font-semibold text-emerald-600 dark:text-emerald-400">MariaDB / InnoDB (PostgreSQL Ready)</span>
</p>
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<a href="/adminer.php?server=127.0.0.1&username=netgescon_user&db=netgescon&schema=" target="_blank"
class="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-blue-600 to-indigo-600 px-4 py-2 text-xs font-bold text-white shadow-md transition hover:from-blue-700 hover:to-indigo-700">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
🚀 Schema ERD Grafico (Adminer)
</a>
<a href="/adminer.php?server=127.0.0.1&username=netgescon_user&db=netgescon&sql=" target="_blank"
class="inline-flex items-center gap-1.5 rounded-xl border border-slate-300 bg-white px-3.5 py-2 text-xs font-semibold text-slate-700 shadow-sm transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700">
<svg class="h-4 w-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Console SQL
</a>
</div>
</div>
<!-- 5 KPI Summary Cards -->
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">Ingombro Totale</div>
<div class="mt-2 text-2xl font-black text-indigo-600 dark:text-indigo-400">{{ number_format($totalMb, 2) }} <span class="text-xs font-bold text-slate-500">MB</span></div>
<div class="mt-1 text-[11px] text-slate-500">Dati: {{ number_format($totalDataMb, 2) }} MB &middot; Indici: {{ number_format($totalIndexMb, 2) }} MB</div>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">Tabelle Catalogo</div>
<div class="mt-2 text-2xl font-black text-slate-800 dark:text-white">{{ $totalTables }}</div>
<div class="mt-1 text-[11px] text-slate-500">10 Domini architetturali</div>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">Record Totali</div>
<div class="mt-2 text-2xl font-black text-slate-800 dark:text-white">{{ number_format($totalRows, 0, ',', '.') }}</div>
<div class="mt-1 text-[11px] text-slate-500">Righe indicizzate nel DB</div>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">Fatture XML / Payload</div>
@php
$feMb = 0.0;
foreach ($domainStats as $ds) {
if ($ds['key'] === 'fatture_fe') { $feMb = $ds['total_mb']; break; }
}
$fePerc = $totalMb > 0 ? round(($feMb / $totalMb) * 100, 1) : 0;
@endphp
<div class="mt-2 text-2xl font-black text-rose-600 dark:text-rose-400">{{ number_format($feMb, 1) }} <span class="text-xs font-bold text-slate-500">MB</span></div>
<div class="mt-1 text-[11px] text-slate-500">{{ $fePerc }}% dell'intero database</div>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">Contabilità & Partita Doppia</div>
@php
$contMb = 0.0;
foreach ($domainStats as $ds) {
if ($ds['key'] === 'contabilita') { $contMb = $ds['total_mb']; break; }
}
@endphp
<div class="mt-2 text-2xl font-black text-emerald-600 dark:text-emerald-400">{{ number_format($contMb, 1) }} <span class="text-xs font-bold text-slate-500">MB</span></div>
<div class="mt-1 text-[11px] text-slate-500">Movimenti, rate & incassi</div>
</div>
</div>
<!-- Navigation Pill Tabs -->
<div class="flex flex-wrap items-center gap-2 border-b border-slate-200 pb-3 dark:border-slate-800">
<button wire:click="$set('activeTab', 'storage')"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-xs font-bold transition {{ $activeTab === 'storage' ? 'bg-amber-600 text-white shadow-sm' : 'bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700' }}">
📊 Dimensionamento & Storage Tabelle
</button>
<button wire:click="$set('activeTab', 'erd')"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-xs font-bold transition {{ $activeTab === 'erd' ? 'bg-amber-600 text-white shadow-sm' : 'bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700' }}">
🗺️ Mappa Relazioni di Dominio (ERD)
</button>
<button wire:click="$set('activeTab', 'simulatore')"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-xs font-bold transition {{ $activeTab === 'simulatore' ? 'bg-amber-600 text-white shadow-sm' : 'bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700' }}">
📈 Simulatore & Proiezione Requisiti Macchina
</button>
<button wire:click="$set('activeTab', 'adminer_guide')"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-xs font-bold transition {{ $activeTab === 'adminer_guide' ? 'bg-amber-600 text-white shadow-sm' : 'bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700' }}">
🌐 Guida Ispezione Adminer & SQL
</button>
</div>
<!-- TAB 1: STORAGE & DIMENSIONAMENTO TABELLE -->
@if ($activeTab === 'storage')
<div class="space-y-6">
<!-- Domain Aggregation Cards -->
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<h2 class="mb-4 text-sm font-bold text-slate-800 dark:text-white">Ingombro Aggregato per Dominio Applicativo</h2>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
@foreach ($domainStats as $d)
@php
$perc = $totalMb > 0 ? round(($d['total_mb'] / $totalMb) * 100, 1) : 0;
@endphp
<div wire:click="$set('selectedDomain', '{{ $d['key'] }}')"
class="cursor-pointer rounded-xl border p-3.5 transition hover:shadow-md {{ $selectedDomain === $d['key'] ? 'border-amber-500 bg-amber-50/50 dark:border-amber-500 dark:bg-amber-950/20' : 'border-slate-200 bg-slate-50/50 hover:bg-white dark:border-slate-800 dark:bg-slate-800/40 dark:hover:bg-slate-800' }}">
<div class="flex items-center justify-between">
<span class="text-base">{{ $d['icon'] }}</span>
<span class="text-xs font-bold text-slate-700 dark:text-slate-200">{{ number_format($d['total_mb'], 2) }} MB</span>
</div>
<div class="mt-1 text-xs font-bold text-slate-800 dark:text-white truncate">{{ $d['label'] }}</div>
<div class="mt-0.5 text-[11px] text-slate-500">{{ $d['table_count'] }} tabelle &middot; {{ number_format($d['total_rows'], 0, ',', '.') }} record</div>
<!-- Mini Progress Bar -->
<div class="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
<div class="h-full bg-amber-500" style="width: {{ min(100, max(2, $perc)) }}%;"></div>
</div>
<div class="mt-1 text-right text-[10px] text-slate-400">{{ $perc }}% del DB</div>
</div>
@endforeach
</div>
</div>
<!-- Table Filters & Search -->
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between rounded-xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="flex flex-1 items-center gap-2">
<input wire:model.live.debounce.300ms="search" type="text" placeholder="🔍 Cerca tabella o dominio (es. fatture, persone, unita, incassi)..."
class="w-full max-w-md rounded-xl border-slate-300 text-xs shadow-sm focus:border-amber-500 focus:ring-amber-500 dark:border-slate-700 dark:bg-slate-800 dark:text-white" />
@if ($search !== '' || $selectedDomain !== 'all')
<button wire:click="$set('search', ''); $set('selectedDomain', 'all')" class="text-xs font-semibold text-rose-600 hover:underline">
Azzera filtri
</button>
@endif
</div>
<div class="flex items-center gap-2">
<label class="text-xs font-semibold text-slate-600 dark:text-slate-300">Filtra per Dominio:</label>
<select wire:model.live="selectedDomain" class="rounded-xl border-slate-300 text-xs shadow-sm focus:border-amber-500 focus:ring-amber-500 dark:border-slate-700 dark:bg-slate-800 dark:text-white">
<option value="all">🌟 Tutti i Domini ({{ $totalTables }})</option>
@foreach ($domainStats as $d)
<option value="{{ $d['key'] }}">{{ $d['icon'] }} {{ $d['label'] }} ({{ $d['table_count'] }})</option>
@endforeach
</select>
</div>
</div>
<!-- Table List -->
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="overflow-x-auto">
<table class="w-full text-left text-xs text-slate-600 dark:text-slate-300">
<thead class="bg-slate-50 text-[11px] font-bold uppercase tracking-wider text-slate-700 dark:bg-slate-800 dark:text-slate-300">
<tr>
<th class="px-4 py-3">Nome Tabella</th>
<th class="px-4 py-3">Dominio</th>
<th class="px-4 py-3 text-right">Record</th>
<th class="px-4 py-3 text-right">Dati (MB)</th>
<th class="px-4 py-3 text-right">Indici (MB)</th>
<th class="px-4 py-3 text-right">Totale (MB)</th>
<th class="px-4 py-3 text-center">Motore</th>
<th class="px-4 py-3 text-right">Azioni</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
@forelse ($tables as $t)
<tr class="transition hover:bg-amber-50/40 dark:hover:bg-amber-950/20">
<td class="px-4 py-3 font-mono font-bold text-slate-900 dark:text-white">
{{ $t['name'] }}
@if (!empty($t['comment']))
<div class="text-[10px] font-sans font-normal text-slate-400 truncate max-w-xs">{{ $t['comment'] }}</div>
@endif
</td>
<td class="px-4 py-3">
<span class="inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[10px] font-semibold border {{ $t['domain_badge'] }}">
<span>{{ $t['domain_icon'] }}</span>
<span>{{ $t['domain_label'] }}</span>
</span>
</td>
<td class="px-4 py-3 text-right font-semibold text-slate-800 dark:text-slate-200">
{{ number_format($t['rows'], 0, ',', '.') }}
</td>
<td class="px-4 py-3 text-right font-mono text-slate-600 dark:text-slate-400">
{{ number_format($t['data_mb'], 2) }}
</td>
<td class="px-4 py-3 text-right font-mono text-slate-600 dark:text-slate-400">
{{ number_format($t['index_mb'], 2) }}
</td>
<td class="px-4 py-3 text-right font-mono font-bold text-indigo-600 dark:text-indigo-400">
{{ number_format($t['total_mb'], 2) }}
</td>
<td class="px-4 py-3 text-center font-mono text-[10px] text-slate-500">
{{ $t['engine'] }}
</td>
<td class="px-4 py-3 text-right">
<a href="/adminer.php?server=127.0.0.1&username=netgescon_user&db=netgescon&select={{ urlencode($t['name']) }}" target="_blank"
class="inline-flex items-center gap-1 rounded-lg bg-slate-100 px-2 py-1 text-[11px] font-semibold text-slate-700 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700">
🔍 Apri Dati
</a>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-4 py-8 text-center text-slate-400">
Nessuna tabella trovata per i criteri di ricerca impostati.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
@endif
<!-- TAB 2: MAPPA RELAZIONI DI DOMINIO (ERD) -->
@if ($activeTab === 'erd')
<div class="space-y-6">
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<h2 class="text-base font-bold text-slate-900 dark:text-white">Architettura Relazionale Determinista NetGescon</h2>
<p class="mt-1 text-xs text-slate-500">
Mappa delle relazioni canoniche e invarianti protetti da contratto operativo per garantire consistenza contabile e anagrafica:
</p>
<div class="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
<!-- Modulo 1: Stabili, Unità & Persone -->
<div class="rounded-xl border border-indigo-100 bg-indigo-50/40 p-4 dark:border-indigo-950 dark:bg-indigo-950/20">
<div class="flex items-center gap-2 text-sm font-bold text-indigo-900 dark:text-indigo-300">
<span>🏢</span> Modulo 1: Stabile, Unità Immobiliari & Anagrafiche
</div>
<div class="mt-3 text-xs text-slate-700 dark:text-slate-300 space-y-2 leading-relaxed">
<p><strong> Stabili (<code class="text-indigo-600 dark:text-indigo-400">stabili</code>):</strong> Entità centrale legata tassativamente al <code class="bg-indigo-100 dark:bg-indigo-900 px-1 py-0.5 rounded">codice_fiscale</code> per identificazione e matching FE.</p>
<p><strong> Unità Immobiliari (<code class="text-indigo-600 dark:text-indigo-400">unita_immobiliari</code>):</strong> Entità fissa spaziale (dati catastali, subalterno, millesimi). Collegata allo stabile tramite <code class="font-mono">stabile_id</code>.</p>
<p><strong> Relazioni Spazio-Temporali (<code class="text-indigo-600 dark:text-indigo-400">persone_unita_relazioni</code>):</strong> Collega persone a unità con ruolo (<code class="font-mono">cond_inquil</code>: 'C' Condomino / 'I' Inquilino) e periodo temporale (<code class="font-mono">data_inizio</code>, <code class="font-mono">data_fine</code>, <code class="font-mono">anno_gestione</code>).</p>
<p><strong> Comproprietà Legacy (<code class="text-indigo-600 dark:text-indigo-400">comproprietari</code>):</strong> Collegata in modo esclusivo tramite <code class="font-mono">id_cond</code> con conservazione delle percentuali <code class="font-mono">perc_diritto_reale</code> (es. 50%/50%).</p>
</div>
</div>
<!-- Modulo 2: Contabilità & Partita Doppia -->
<div class="rounded-xl border border-emerald-100 bg-emerald-50/40 p-4 dark:border-emerald-950 dark:bg-emerald-950/20">
<div class="flex items-center gap-2 text-sm font-bold text-emerald-900 dark:text-emerald-300">
<span>💰</span> Modulo 2: Contabilità Ordinaria & Partita Doppia
</div>
<div class="mt-3 text-xs text-slate-700 dark:text-slate-300 space-y-2 leading-relaxed">
<p><strong> Gestioni Stabile (<code class="text-emerald-600 dark:text-emerald-400">gestioni_stabili</code>):</strong> Isola gli esercizi ordinari e straordinari (<code class="font-mono">anno_gestione</code>, <code class="font-mono">stato: aperto/chiuso</code>).</p>
<p><strong> Rate Emesse (<code class="text-emerald-600 dark:text-emerald-400">rate_emesse</code>):</strong> Generate da preventivo/piano rateale legate a <code class="font-mono">cod_cond</code> e <code class="font-mono">id_cond</code>.</p>
<p><strong> Incassi (<code class="text-emerald-600 dark:text-emerald-400">incassi</code>):</strong> Riconciliazione entrate per condomino/inquilino collegate a <code class="font-mono">cod_cond</code>.</p>
<p><strong> Scritture Contabili (<code class="text-emerald-600 dark:text-emerald-400">contabilita_registrazioni</code> / <code class="text-emerald-600 dark:text-emerald-400">operazioni_contabili</code>):</strong> Libro Giornale e Mastro Dare/Avere conforme standard contabile.</p>
</div>
</div>
<!-- Modulo 3: Fatture Elettroniche & Fornitori -->
<div class="rounded-xl border border-rose-100 bg-rose-50/40 p-4 dark:border-rose-950 dark:bg-rose-950/20">
<div class="flex items-center gap-2 text-sm font-bold text-rose-900 dark:text-rose-300">
<span>🧾</span> Modulo 3: Fatture Elettroniche (XML/P7M) & Fornitori
</div>
<div class="mt-3 text-xs text-slate-700 dark:text-slate-300 space-y-2 leading-relaxed">
<p><strong> Fatture XML Ricevute (<code class="text-rose-600 dark:text-rose-400">fatture_elettroniche</code>):</strong> Ingestione raw XML/P7M con matching deterministico <code class="font-mono">CessionarioCommittente == Stabile.codice_fiscale</code>.</p>
<p><strong> Registrazione Fornitori (<code class="text-rose-600 dark:text-rose-400">contabilita_fatture_fornitori</code>):</strong> Fattura fornitore contabile con righe di dettaglio e piano dei conti.</p>
<p><strong> Anagrafica Fornitori (<code class="text-rose-600 dark:text-rose-400">fornitori</code>):</strong> Gestione P.IVA/CF, cataloghi prodotti, listini, offerte e ticket di manutenzione.</p>
</div>
</div>
<!-- Modulo 4: Locazioni & Gestione Affitti -->
<div class="rounded-xl border border-violet-100 bg-violet-50/40 p-4 dark:border-violet-950 dark:bg-violet-950/20">
<div class="flex items-center gap-2 text-sm font-bold text-violet-900 dark:text-violet-300">
<span>🔑</span> Modulo 4: Locazioni & Gestione Affitti
</div>
<div class="mt-3 text-xs text-slate-700 dark:text-slate-300 space-y-2 leading-relaxed">
<p><strong> Contratti Locazione (<code class="text-violet-600 dark:text-violet-400">locazioni_contratti</code>):</strong> Collegati a <code class="font-mono">unita_immobiliari</code> (immobile locato) e <code class="font-mono">persone</code> (conduttore).</p>
<p><strong> Sincronizzazione Catastale:</strong> Auto-popolamento istantaneo da dati catastali consolidati dell'unità (Foglio, Particella, Sub, Categoria, Rendita).</p>
<p><strong> Canoni & Spese:</strong> Scadenziario mensile canoni, incassi registrati, ripartizione spese accessorie e conguagli.</p>
</div>
</div>
</div>
</div>
</div>
@endif
<!-- TAB 3: SIMULATORE & PROIEZIONE REQUISITI MACCHINA -->
@if ($activeTab === 'simulatore')
<div class="space-y-6">
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<h2 class="text-base font-bold text-slate-900 dark:text-white">Simulatore di Crescita & Dimensionamento Hardware</h2>
<p class="mt-1 text-xs text-slate-500">
Calcola la proiezione dello spazio disco, RAM consigliata per InnoDB Buffer Pool e volume per backup in base al numero di stabili gestiti:
</p>
<!-- Simulazione Input -->
<div class="mt-6 flex flex-wrap items-center gap-4 rounded-xl border border-amber-200 bg-amber-50/50 p-4 dark:border-amber-900 dark:bg-amber-950/20">
<label class="text-xs font-bold text-amber-900 dark:text-amber-300">Numero Stabili da Gestire:</label>
<div class="flex items-center gap-3">
<input wire:model.live.debounce.150ms="simulazioneStabili" type="range" min="10" max="1000" step="10" class="w-48 accent-amber-600" />
<span class="rounded-lg bg-amber-600 px-3 py-1 text-sm font-black text-white">{{ $simulazioneStabili }} Stabili</span>
</div>
</div>
@php
// Fattori medi calcolati sui dati reali correnti:
// 1 stabile medio con 25 unita, 2 anni di archivio, 150 FE XML:
// - XML Fatture Elettroniche: ~2.5 MB / anno / stabile
// - Righe contabili + rate + incassi: ~0.8 MB / anno / stabile
// - Unità + Anagrafiche + Catasto: ~0.15 MB / stabile
$stabiliCount = max(1, (int)$this->simulazioneStabili);
$mbPerStabileAnno = 3.5; // MB medi complessivi all'anno per stabile
$anniStorico = 3; // Media anni conservati attivi
$dbProiezioneMb = round($stabiliCount * $mbPerStabileAnno * $anniStorico, 1);
$dbProiezioneGb = round($dbProiezioneMb / 1024, 2);
// RAM consigliata: ~2.5x dimensione attiva dei dati per caching ottimale
$ramConsigliataGb = max(2, round(($dbProiezioneGb * 2.5) + 1, 1));
$spazioDiscoConsigliatoGb = max(20, round(($dbProiezioneGb * 5) + 10, 0)); // Include backup, WAL log e staging
@endphp
<!-- Proiezioni Calcolate -->
<div class="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="rounded-xl border border-slate-200 bg-slate-50/50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
<div class="text-xs font-semibold text-slate-500">Spazio Database Stimato (3 Anni)</div>
<div class="mt-2 text-2xl font-black text-indigo-600 dark:text-indigo-400">
@if ($dbProiezioneGb >= 1)
{{ $dbProiezioneGb }} <span class="text-xs font-bold text-slate-500">GB</span>
@else
{{ $dbProiezioneMb }} <span class="text-xs font-bold text-slate-500">MB</span>
@endif
</div>
<div class="mt-1 text-[11px] text-slate-500">Base calcolo: 3.5 MB/anno per stabile con XML FE</div>
</div>
<div class="rounded-xl border border-slate-200 bg-slate-50/50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
<div class="text-xs font-semibold text-slate-500">RAM Server Consigliata</div>
<div class="mt-2 text-2xl font-black text-emerald-600 dark:text-emerald-400">
{{ $ramConsigliataGb }} <span class="text-xs font-bold text-slate-500">GB RAM</span>
</div>
<div class="mt-1 text-[11px] text-slate-500">InnoDB Buffer Pool ottimizzato & cache PHP</div>
</div>
<div class="rounded-xl border border-slate-200 bg-slate-50/50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
<div class="text-xs font-semibold text-slate-500">Spazio Disco Totale Suggerito (SSD/NVMe)</div>
<div class="mt-2 text-2xl font-black text-amber-600 dark:text-amber-400">
{{ $spazioDiscoConsigliatoGb }} <span class="text-xs font-bold text-slate-500">GB SSD</span>
</div>
<div class="mt-1 text-[11px] text-slate-500">Include retention backup giornalieri, log e allegati</div>
</div>
</div>
<div class="mt-6 rounded-xl bg-slate-50 p-4 text-xs text-slate-600 dark:bg-slate-800/40 dark:text-slate-300">
<span class="font-bold text-slate-800 dark:text-white">💡 Suggerimento di Architettura:</span>
Le tabelle più pesanti sono quelle dei payload raw XML delle fatture elettroniche (<code class="font-mono font-bold text-rose-600">fatture_elettroniche</code>: ~144 MB attuali). Gli archivi contabili e anagrafici relazionali sono estremamente leggeri e consentono prestazioni istantanee anche con oltre 500 stabili su un singolo nodo Linux dedicato come la <code class="font-mono">.205</code>.
</div>
</div>
</div>
@endif
<!-- TAB 4: GUIDA ISPEZIONE ADMINER & SQL -->
@if ($activeTab === 'adminer_guide')
<div class="space-y-6">
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<h2 class="text-base font-bold text-slate-900 dark:text-white">Come Ispezionare il Database Graficamente con Adminer</h2>
<p class="mt-1 text-xs text-slate-500">
Il visualizzatore Adminer integrato permette di navigare le tabelle, visualizzare e modificare i record, eseguire query SQL personalizzate ed esplorare l'ER diagram interattivo:
</p>
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="rounded-xl border border-slate-200 p-4 dark:border-slate-800">
<div class="text-sm font-bold text-slate-900 dark:text-white">1. Diagramma Schema (ERD)</div>
<p class="mt-2 text-xs text-slate-600 dark:text-slate-400">
Visualizza la mappa visuale di tutte le tabelle con le chiavi primarie, le relazioni e i collegamenti tra tabelle con interfaccia drag-and-drop.
</p>
<div class="mt-4">
<a href="/adminer.php?server=127.0.0.1&username=netgescon_user&db=netgescon&schema=" target="_blank"
class="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-bold text-white shadow hover:bg-blue-700 transition">
🚀 Apri Schema ERD
</a>
</div>
</div>
<div class="rounded-xl border border-slate-200 p-4 dark:border-slate-800">
<div class="text-sm font-bold text-slate-900 dark:text-white">2. Navigazione Record & Dati</div>
<p class="mt-2 text-xs text-slate-600 dark:text-slate-400">
Accedi a qualsiasi tabella per filtrare, ordinare, visualizzare i dati grezzi ed esportare in formato CSV, SQL o JSON in un click.
</p>
<div class="mt-4">
<a href="/adminer.php?server=127.0.0.1&username=netgescon_user&db=netgescon" target="_blank"
class="inline-flex items-center gap-1.5 rounded-lg bg-slate-800 px-3 py-1.5 text-xs font-bold text-white shadow hover:bg-slate-700 transition">
📂 Elenco 256 Tabelle
</a>
</div>
</div>
<div class="rounded-xl border border-slate-200 p-4 dark:border-slate-800">
<div class="text-sm font-bold text-slate-900 dark:text-white">3. Esecuzione Comandi SQL</div>
<p class="mt-2 text-xs text-slate-600 dark:text-slate-400">
Esegui query personalizzate (SELECT, JOIN, GROUP BY) con calcolo immediato dei tempi di risposta e statistiche di esecuzione.
</p>
<div class="mt-4">
<a href="/adminer.php?server=127.0.0.1&username=netgescon_user&db=netgescon&sql=" target="_blank"
class="inline-flex items-center gap-1.5 rounded-lg bg-amber-600 px-3 py-1.5 text-xs font-bold text-white shadow hover:bg-amber-700 transition">
💻 Apri Console SQL
</a>
</div>
</div>
</div>
</div>
</div>
@endif
</div>
</x-filament-panels::page>

View File

@ -1,50 +1,45 @@
# CURRENT-205
TASK_ID: task-nominativi-affitti-adminer-db
TASK_ID: task-db-structure-storage-erd
MACHINE: .205
STATO: completato
## Obiettivo Completato
1. **Risoluzione Definitiva Nominativi Stabile (`/admin-filament/condomini/nominativi`)**:
- Agganciata la query di caricamento al modello di dominio canonico relazionale (`unita_immobiliari`, `persone_unita_relazioni`, `persone`), identico a quello impiegato nella scheda Unità Immobiliare (`UnitaImmobiliarePage`).
- Risolto il matching per `anno_gestione_attivo`, eliminando qualsiasi divergenza tra l'elenco nominativi e le singole unità immobiliari.
- Corretto il metodo di risoluzione rubrica/persona e i filtri di ricerca per condomino e inquilino.
- Link diretti operativi per saltare alla Scheda Unità o alla Scheda Persona in Anagrafica Unica.
1. **Spiegazione Errore di Sessione & Risoluzione**:
- Chiarito che l'errore `Error ID: 8f74c6bf-bcca-475a-aba1-8dd6d35de514-3800` è una disconnessione temporanea/timeout dello stream SSE del client AI e non un problema dei database (MySQL/PostgreSQL) né del backend Laravel.
- Tutti i commit, file e test rimangono sempre salvi e integri.
2. **Modernizzazione & Integrazione Gestione Affitti (`/admin-filament/affitti/gestione`)**:
- Collegamento bidirezionale con `unita_immobiliari` (locali tecnici, appartamenti, parti comuni a reddito dello stabile) con pulsante diretto "🏠 Scheda Unità".
- Sincronizzazione automatica 1-click dei dati catastali consolidati (`foglio`, `particella`, `subalterno`, `categoria_catastale`, `rendita_catastale`, `classe`, `consistenza`, `piano`, `interno`, `superficie_catastale_mq`, `destinazione_uso`).
- Collegamento diretto con **Anagrafica Unica** (`persone`) per la selezione del conduttore e gestione multi-inquilino/co-intestatari con link "🪪 Anagrafica Conduttore".
- Nuova interfaccia grafica moderna con barra dei pulsanti iconici coordinata, 5 KPI Summary Cards (Locazioni Totali, Canoni Dovuti, Canoni Incassati, Saldo Pendenze, Avvisi & Scadenze) e navigazione a pill-tabs responsive.
2. **Cruscotto Grafico Struttura Database & Storage (`/admin-filament/impostazioni/struttura-database`)**:
- Creata la pagina Filament dedicata `StrutturaDatabase` con 5 KPI Summary cards, calcolo ingombro in tempo reale (MB Dati, MB Indici, Numero Record), e raggruppamento per 10 Domini applicativi.
- Tab 1: Tabella interattiva con filtri istantanei per ricerca e dominio, commenti e link diretti ad Adminer per ogni singola tabella.
- Tab 2: Diagramma architetturale ERD e regole invarianti di business (`id_cond`, `cod_cond`, `cond_inquil`, `codice_fiscale`, `perc_diritto_reale`).
- Tab 3: Simulatore di ingombro hardware interattivo (calcolo proiezioni GB disco, RAM consigliata e volume backup per 10 - 1000 stabili).
- Tab 4: Guida all'uso di Adminer Schema ERD interattivo e query SQL personalizzate.
3. **Visualizzatore Database Grafico (Adminer) Configurato & Integrato**:
- Risolto il wrapper `public/adminer.php` con credenziali pre-configurate per accesso automatico alle 256 tabelle di MariaDB/MySQL (`netgescon` e `gescon_import`) e PostgreSQL.
- Aggiunto link diretto "Visualizza Database (Adminer)" nella navigazione Filament (gruppo Impostazioni) e nel menu utente in alto a destra.
3. **Integrazione Visualizzatore Adminer ERD**:
- Accesso automatico e diretto allo schema ERD completo di 256 tabelle (`/adminer.php?server=127.0.0.1&username=netgescon_user&db=netgescon&schema=`).
## Output del Giro Operativo
ESITO_205: riuscito
TASK_ID: task-nominativi-affitti-adminer-db
TASK_ID: task-db-structure-storage-erd
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
BRANCH: stabilization/205-zero
COMMIT: 2611d8f
COMMIT: in_progress
FILE_O_AREE_TOCCATE:
- app/Filament/Pages/Condomini/NominativiStabile.php
- app/Filament/Pages/Affitti/GestioneAffitti.php
- resources/views/filament/pages/affitti/gestione-affitti.blade.php
- app/Providers/Filament/AdminFilamentPanelProvider.php
- public/adminer.php
- app/Filament/Pages/SuperAdmin/StrutturaDatabase.php
- resources/views/filament/pages/super-admin/struttura-database.blade.php
- tests/Feature/StrutturaDatabaseAndStorageTest.php
- skill-netgescon/control-tower/CURRENT-205.md
TEST_ESEGUITI:
- ./vendor/bin/pest tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (32 passed, 213 assertions)
- ./vendor/bin/pest tests/Feature/StrutturaDatabaseAndStorageTest.php tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (33 passed, 218 assertions)
GATE_STATISTICS:
- NOMINATIVI_CANONICAL_ALIGNMENT: 100% allineato all'archivio canonico unita_immobiliari/persone_unita_relazioni.
- AFFITTI_UNITA_CATASTO_LINKAGE: Sincronizzazione automatica 1-click dei dati catastali e deep link a Scheda Unità.
- AFFITTI_ANAGRAFICA_UNICA_LINKAGE: Selezione conduttore da persone/anagrafica unica con deep link.
- AFFITTI_MODERN_UI: KPI summary cards, pill tabs e design uniforme.
- ADMINER_DB_VISUALIZER: Autenticazione automatica e navigazione integrata in Filament.
- TEST_SUITE: 32 test Feature passati (213 asserzioni, 100% pass).
- STRUTTURA_DATABASE_PAGE: Pagina Filament attiva con KPI, storage e filtri domini.
- ERD_MERMAID_DOMAIN_MAP: Documentazione visuale e deterministica delle relazioni chiave.
- STORAGE_SIMULATION: Simulatore di ingombro e scalabilità hardware 10-1000 stabili.
- ADMINER_ERD_INTEGRATION: Accesso 1-click a schema ERD e console SQL.
- TEST_SUITE: 33 test Feature passati (218 asserzioni, 100% pass).
BLOCCO_DATI: no
BLOCCO_CONTRATTO: no
RISCHI_APERTI: nessuno
@ -52,6 +47,5 @@ ## Output del Giro Operativo
## Prossimo Passo per .200 (Validazione)
- Eseguire il checkout del branch `stabilization/205-zero`.
- Eseguire la suite di test Pest (32 passed, 213 assertions).
- Verificare la visualizzazione dei Nominativi (`/admin-filament/condomini/nominativi`), la Gestione Affitti (`/admin-filament/affitti/gestione`) e l'accesso al Visualizzatore Database (`/adminer.php` o voce di menu in Impostazioni).
- Eseguire la suite di test Pest (33 passed, 218 assertions).
- Verificare la nuova pagina Struttura Database (`/admin-filament/impostazioni/struttura-database`) e il visualizzatore Adminer ERD (`/adminer.php?schema=`).

View File

@ -0,0 +1,18 @@
<?php
use App\Filament\Pages\SuperAdmin\StrutturaDatabase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('mounts struttura database page and calculates storage and domain stats', function () {
$user = User::factory()->create();
$this->actingAs($user);
$page = new StrutturaDatabase();
$stats = $page->getDatabaseStats();
expect($stats)->toBeArray()
->and($stats)->toHaveKeys(['total_tables', 'total_mb', 'domain_stats', 'tables']);
});