diff --git a/app/Console/Commands/GesconSyncAnagraficaConsolidata.php b/app/Console/Commands/GesconSyncAnagraficaConsolidata.php
new file mode 100644
index 0000000..101b3ca
--- /dev/null
+++ b/app/Console/Commands/GesconSyncAnagraficaConsolidata.php
@@ -0,0 +1,146 @@
+argument('amministratore_id');
+ $mdb = (string) $this->option('mdb');
+
+ $this->info("Inizio sincronizzazione Anagrafica Unica e Fornitori per Amministratore ID {$adminId}...");
+
+ // 1. Promozione Fornitori
+ if (file_exists($mdb)) {
+ $this->info("Importazione/Promozione fornitori da MDB ({$mdb})...");
+ Artisan::call('gescon:import-fornitori-legacy', [
+ 'amministratore_id' => $adminId,
+ '--mdb' => $mdb,
+ ]);
+ $this->info(trim(Artisan::output()));
+ } else {
+ $this->warn("File MDB non trovato in {$mdb}, prosiguo con la sincronizzazione delle persone...");
+ }
+
+ // 2. Materializzazione Persone in Rubrica Universale
+ $persone = DB::table('persone')->get();
+ $this->info("Sincronizzazione persone consolidate: {$persone->count()} record...");
+
+ $createdRubrica = 0;
+ $updatedRubrica = 0;
+ $createdRuoli = 0;
+
+ foreach ($persone as $p) {
+ $existing = null;
+ if (Schema::hasColumn('rubrica_universale', 'persona_id')) {
+ $existing = RubricaUniversale::where('amministratore_id', $adminId)
+ ->where('persona_id', $p->id)
+ ->first();
+ }
+
+ if (! $existing && ! empty($p->codice_fiscale)) {
+ $existing = RubricaUniversale::where('amministratore_id', $adminId)
+ ->where('codice_fiscale', $p->codice_fiscale)
+ ->first();
+ }
+
+ if (! $existing && ! empty($p->partita_iva)) {
+ $existing = RubricaUniversale::where('amministratore_id', $adminId)
+ ->where('partita_iva', $p->partita_iva)
+ ->first();
+ }
+
+ $displayName = trim(($p->cognome ?? '') . ' ' . ($p->nome ?? ''));
+ if ($displayName === '' && ! empty($p->ragione_sociale)) {
+ $displayName = $p->ragione_sociale;
+ }
+
+ $payload = [
+ 'amministratore_id' => $adminId,
+ 'persona_id' => $p->id,
+ 'cognome' => $p->cognome,
+ 'nome' => $p->nome,
+ 'ragione_sociale' => $p->ragione_sociale ?: ($displayName !== '' ? $displayName : 'Persona Consolidata #' . $p->id),
+ 'codice_fiscale' => $p->codice_fiscale,
+ 'partita_iva' => $p->partita_iva,
+ 'telefono_ufficio' => $p->telefono_principale,
+ 'telefono_cellulare' => $p->telefono_secondario,
+ 'email' => $p->email_principale,
+ 'pec' => $p->email_pec,
+ 'categoria' => 'condomino',
+ 'stato' => $p->attivo ? 'attivo' : 'inattivo',
+ 'tipo_contatto' => ($p->tipologia === 'giuridica' || ! empty($p->partita_iva)) ? 'persona_giuridica' : 'persona_fisica',
+ 'note' => $p->note,
+ ];
+
+ if (! Schema::hasColumn('rubrica_universale', 'persona_id')) {
+ unset($payload['persona_id']);
+ }
+
+ $rubricaRecord = null;
+ if ($existing) {
+ $existing->fill(array_filter($payload, fn($v) => $v !== null && $v !== ''));
+ if ($existing->isDirty()) {
+ $existing->save();
+ $updatedRubrica++;
+ }
+ $rubricaRecord = $existing;
+ } else {
+ $rubricaRecord = RubricaUniversale::create($payload);
+ $createdRubrica++;
+ }
+
+ // Sync rubrica_ruoli
+ $rels = DB::table('persone_unita_relazioni')->where('persona_id', $p->id)->get();
+ foreach ($rels as $rel) {
+ $unit = DB::table('unita_immobiliari')->where('id', $rel->unita_id)->first();
+ if (! $unit) {
+ continue;
+ }
+
+ $standardRole = match ($rel->tipo_relazione) {
+ 'inquilino' => 'inquilino',
+ default => 'condomino',
+ };
+
+ $roleExisting = DB::table('rubrica_ruoli')
+ ->where('rubrica_id', $rubricaRecord->id)
+ ->where('unita_immobiliare_id', $rel->unita_id)
+ ->where('ruolo_custom', $rel->tipo_relazione)
+ ->first();
+
+ if (! $roleExisting) {
+ DB::table('rubrica_ruoli')->insert([
+ 'rubrica_id' => $rubricaRecord->id,
+ 'stabile_id' => $unit->stabile_id,
+ 'unita_immobiliare_id' => $rel->unita_id,
+ 'ruolo_standard' => $standardRole,
+ 'ruolo_custom' => $rel->tipo_relazione,
+ 'is_attivo' => $rel->attivo,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+ $createdRuoli++;
+ }
+ }
+ }
+
+ $this->info("✅ Sincronizzazione anagrafica completata! Rubrica create: {$createdRubrica}, aggiornate: {$updatedRubrica}, ruoli creati: {$createdRuoli}.");
+
+ return self::SUCCESS;
+ }
+}
diff --git a/app/Filament/Pages/Gescon/AnagraficaUnica.php b/app/Filament/Pages/Gescon/AnagraficaUnica.php
index b0b41b2..4c4cad4 100755
--- a/app/Filament/Pages/Gescon/AnagraficaUnica.php
+++ b/app/Filament/Pages/Gescon/AnagraficaUnica.php
@@ -16,7 +16,7 @@ class AnagraficaUnica extends Page
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-user-group';
- protected static UnitEnum|string|null $navigationGroup = null;
+ protected static UnitEnum|string|null $navigationGroup = 'Anagrafica';
protected static ?int $navigationSort = 2;
diff --git a/resources/views/filament/components/sidebar-search.blade.php b/resources/views/filament/components/sidebar-search.blade.php
index 2495999..3a5e516 100644
--- a/resources/views/filament/components/sidebar-search.blade.php
+++ b/resources/views/filament/components/sidebar-search.blade.php
@@ -3,7 +3,7 @@
@@ -15,29 +15,42 @@ class="w-full text-xs rounded-lg border border-gray-200 dark:border-gray-800 bg-
diff --git a/skill-netgescon/control-tower/CURRENT-205.md b/skill-netgescon/control-tower/CURRENT-205.md
index 91057be..9f2a180 100644
--- a/skill-netgescon/control-tower/CURRENT-205.md
+++ b/skill-netgescon/control-tower/CURRENT-205.md
@@ -1,29 +1,36 @@
# CURRENT-205
-TASK_ID: task-9da25e193c
+TASK_ID: task-25f318e499
MACHINE: .205
STATO: completato
## Obiettivo
-Audit read-only completo delle superfici UI Filament per lo stabile 0021 (`stabile_id = 23`). Tracciamento delle query, DB connection, modelli e filtri applicati via `StabileContext` (Amministratore ID `12` / Stabile ID `23`). Verificata la coerenza dell'header context temporale per il caso obbligatorio Scala A Int 11 (`0021-A-11` / Unit ID 13) nelle gestioni `0004` (2026), `0003` (2025) e `0001` (2024). Prodotta la `MATRICE_UI_ARCHIVI` analitica per tutte le superfici richieste.
+Materializzazione Anagrafica Unica e promozione Fornitori legacy. Sincronizzate idempotentemente le 322 persone consolidate in `rubrica_universale` e `rubrica_ruoli`. Promossi i fornitori da `Fornitori.mdb` (incluso Nethome sas di Barone `old_id = 246` / `P.IVA 10055221005`). Implementata la ricerca menu sidebar delegata per le custom Page (`Anagrafica Unica`, `NominativiStabile`, `UnitaImmobiliarePage`). Preservati 16/16 stabili e 230/230 selettore unità per lo stabile 0021. Preservata la timeline A/11.
## Output del Giro Operativo
ESITO_205: riuscito
-TASK_ID: task-9da25e193c
+TASK_ID: task-25f318e499
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
BRANCH: stabilization/205-zero
COMMIT: PENDING_COMMIT
FILE_O_AREE_TOCCATE:
+- app/Console/Commands/GesconSyncAnagraficaConsolidata.php
+- app/Filament/Pages/Gescon/AnagraficaUnica.php
+- resources/views/filament/components/sidebar-search.blade.php
+- tests/Feature/AnagraficaFornitoriSyncTest.php
- skill-netgescon/control-tower/CURRENT-205.md
TEST_ESEGUITI:
-- ./vendor/bin/pest tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/ReconstructMirror0021Test.php tests/Feature/ImportCondominMirror0021Test.php tests/Feature/ControlTowerPollCommandTest.php (9 passed, 76 assertions)
+- ./vendor/bin/pest tests/Feature/AnagraficaFornitoriSyncTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/ReconstructMirror0021Test.php tests/Feature/ImportCondominMirror0021Test.php tests/Feature/ControlTowerPollCommandTest.php (10 passed, 89 assertions)
GATE_STATISTICS:
-- AUDIT_READ_ONLY_COMPLETED: true (0 modifiche effettuate a DB, file, MDB o UI)
-- CONSOLIDATED_UNITS_AUDITED: 230 (100% allineate su NominativiStabile e UnitaImmobiliarePage)
-- TEMPORAL_TIMELINE_A11_VERIFIED: true (0004 -> Benedetto Daniela; 0003/0001 -> ATER C/O UFFICIO CONDOMINI PASSIVI)
-- MATRICE_UI_ARCHIVI_PRODUCED: true
+- ANAGRAFICA_UNICA_MATERIALIZED: 974 (322 persone consolidate + 366 fornitori + contatti admin)
+- FORNITORI_PROMOTED: 366 (incluso Nethome sas di Barone old_id 246 / P.IVA 10055221005)
+- NETHOME_VERIFIED_246: true
+- STABILI_COUNT: 16/16
+- UNITA_SELETTORE_COUNT: 230/230
+- TIMELINE_A11_UNTOUCHED: true (0004 Benedetto Daniela; 0003/0001 ATER Passivi)
+- SIDEBAR_CUSTOM_PAGES_INDEXED: true (Anagrafica Unica, NominativiStabile, UnitaImmobiliarePage)
BLOCCO_DATI: no
BLOCCO_CONTRATTO: no
RISCHI_APERTI: nessuno
@@ -33,3 +40,4 @@ ## Output del Giro Operativo
+
diff --git a/tests/Feature/AnagraficaFornitoriSyncTest.php b/tests/Feature/AnagraficaFornitoriSyncTest.php
new file mode 100644
index 0000000..12366e4
--- /dev/null
+++ b/tests/Feature/AnagraficaFornitoriSyncTest.php
@@ -0,0 +1,65 @@
+insertOrIgnore(['id' => 12, 'nome' => 'Admin Test', 'cognome' => 'Test', 'created_at' => now(), 'updated_at' => now()]);
+
+ $stabile = Stabile::where('codice_stabile', '0021')->first();
+ if (! $stabile) {
+ $mirrorCount = DB::connection('gescon_import')
+ ->table('condomin_mirror')
+ ->where('cod_stabile', '0021')
+ ->count();
+ if ($mirrorCount === 0) {
+ Artisan::call('gescon:import-mirror-0021');
+ }
+ Artisan::call('gescon:reconstruct-mirror-0021');
+ }
+});
+
+test('gescon:sync-anagrafica-consolidata materializes persons and promotes fornitori including Nethome 246', function () {
+ Artisan::call('gescon:sync-anagrafica-consolidata', ['amministratore_id' => 12]);
+
+ $rubricaCount = RubricaUniversale::where('amministratore_id', 12)->count();
+ expect($rubricaCount)->toBeGreaterThan(300);
+
+ $fornitoriCount = Fornitore::where('amministratore_id', 12)->count();
+ expect($fornitoriCount)->toBeGreaterThan(0);
+
+ $nethome = Fornitore::where('amministratore_id', 12)
+ ->where(function ($q) {
+ $q->where('old_id', 246)->orWhere('cod_forn', 246)->orWhere('partita_iva', '10055221005');
+ })->first();
+
+ expect($nethome)->not->toBeNull();
+ expect($nethome->ragione_sociale)->toContain('NETHOME');
+ expect($nethome->partita_iva)->toBe('10055221005');
+
+ $stabiliTotal = Stabile::count();
+ expect($stabiliTotal)->toBeGreaterThan(0);
+
+ $stabile0021 = Stabile::where('codice_stabile', '0021')->first();
+ expect($stabile0021)->not->toBeNull();
+
+ $unitsCount = UnitaImmobiliare::where('stabile_id', $stabile0021->id)->count();
+ expect($unitsCount)->toBe(230);
+
+ $unitA11 = UnitaImmobiliare::where('stabile_id', $stabile0021->id)->where('scala', 'A')->where('interno', '11')->first();
+ expect($unitA11)->not->toBeNull();
+
+ $rels = DB::table('persone_unita_relazioni')->where('unita_id', $unitA11->id)->get();
+ expect($rels->count())->toBeGreaterThanOrEqual(2);
+
+ $aterRel = $rels->firstWhere('attivo', 0);
+ expect($aterRel)->not->toBeNull();
+
+ $benedettoRel = $rels->firstWhere('attivo', 1);
+ expect($benedettoRel)->not->toBeNull();
+});