diff --git a/app/Console/Commands/ImportCondominMirror0021.php b/app/Console/Commands/ImportCondominMirror0021.php index 9fc81f4..59d2c50 100644 --- a/app/Console/Commands/ImportCondominMirror0021.php +++ b/app/Console/Commands/ImportCondominMirror0021.php @@ -158,6 +158,97 @@ public function handle(): int $perFileCounts[$key]['imported']++; } + + $unitMapByIdCond = []; + $unitMapByCodCond = []; + + foreach ($rows as $r) { + $rawInterno = isset($r['int']) ? (string)$r['int'] : (isset($r['interno']) ? (string)$r['interno'] : ''); + $rawScala = isset($r['scala']) ? (string)$r['scala'] : ''; + $rawPiano = isset($r['piano']) ? (string)$r['piano'] : ''; + $idCond = isset($r['id_cond']) ? (string)$r['id_cond'] : null; + $codCond = isset($r['cod_cond']) ? (string)$r['cod_cond'] : null; + + if ($idCond) { + $unitMapByIdCond[$idCond] = ['scala' => $rawScala, 'interno' => $rawInterno, 'piano' => $rawPiano]; + } + if ($codCond) { + $unitMapByCodCond[$codCond] = ['scala' => $rawScala, 'interno' => $rawInterno, 'piano' => $rawPiano]; + } + } + + // Extract and mirror comproprietari if present in MDB + $compRows = $this->extractMdbTableRows($cfg['path'], 'comproprietari'); + $compNum = 0; + foreach ($compRows as $c) { + $compNum++; + $provHash = hash('sha256', "{$ammCode}|{$stabileCode}|{$cfg['rel']}|{$cfg['year']}|comproprietari|{$compNum}"); + + $cIdCond = isset($c['id_cond']) ? (string)$c['id_cond'] : null; + $cCodCond = isset($c['ex_cod_cond']) ? (string)$c['ex_cod_cond'] : (isset($c['cod_cond']) ? (string)$c['cod_cond'] : null); + + $unitInfo = ($cIdCond && isset($unitMapByIdCond[$cIdCond])) + ? $unitMapByIdCond[$cIdCond] + : (($cCodCond && isset($unitMapByCodCond[$cCodCond])) ? $unitMapByCodCond[$cCodCond] : null); + + $rawScala = $unitInfo['scala'] ?? (isset($c['ex_scala']) ? (string)$c['ex_scala'] : ''); + $rawInterno = $unitInfo['interno'] ?? (isset($c['ex_int']) ? (string)$c['ex_int'] : ''); + $rawPiano = $unitInfo['piano'] ?? ''; + + $nomCond = isset($c['nom_cond']) ? (string)$c['nom_cond'] : (isset($c['Detraz_cess_nome']) ? (string)$c['Detraz_cess_nome'] : null); + $codFisc = isset($c['Cond_cod_fisc']) ? (string)$c['Cond_cod_fisc'] : (isset($c['Detraz_cess_CF']) ? (string)$c['Detraz_cess_CF'] : null); + $tel = isset($c['tel1']) ? (string)$c['tel1'] : null; + $cell = isset($c['Cell_cond']) ? (string)$c['Cell_cond'] : null; + $email = isset($c['E_mail_condomino']) ? (string)$c['E_mail_condomino'] : null; + $pec = isset($c['PEC_compr']) ? (string)$c['PEC_compr'] : null; + $dirReale= isset($c['Diritto_reale']) && trim((string)$c['Diritto_reale']) !== '' ? (string)$c['Diritto_reale'] : (isset($c['Descriz']) ? (string)$c['Descriz'] : 'Comproprietà'); + $percDir = isset($c['Perc_Diritto_reale']) && is_numeric($c['Perc_Diritto_reale']) ? (string)$c['Perc_Diritto_reale'] : null; + + $recordComp = [ + 'amministratore_code' => $ammCode, + 'cod_stabile' => $stabileCode, + 'source_file' => $cfg['rel'], + 'source_year' => $cfg['year'], + 'source_table' => 'comproprietari', + 'source_row' => $compNum, + 'provenance_hash' => $provHash, + 'id_cond' => $cIdCond, + 'cod_cond' => $cCodCond, + 'scala' => trim($rawScala) !== '' ? trim($rawScala) : null, + 'interno' => trim($rawInterno) !== '' ? trim($rawInterno) : null, + 'piano' => trim($rawPiano) !== '' ? trim($rawPiano) : null, + 'cognome' => null, + 'nome' => null, + 'nom_cond' => $nomCond, + 'codice_fiscale' => $codFisc, + 'telefono' => $tel, + 'cellulare' => $cell, + 'email' => $email, + 'pec' => $pec, + 'millesimi_proprieta' => null, + 'millesimi_riscaldamento' => null, + 'millesimi_ascensore' => null, + 'proprietario' => $nomCond, + 'inquilino' => null, + 'subentro_prima_cera' => null, + 'subentro_adesso_ce' => null, + 'subentrato_dal' => null, + 'attivo_fino_al' => null, + 'inquil_dal' => null, + 'inquil_al' => null, + 'diritto_reale' => $dirReale, + 'diritto_godimento' => null, + 'perc_diritto_reale' => $percDir, + 'legacy_payload' => json_encode($c, JSON_UNESCAPED_UNICODE), + 'created_at' => now(), + 'updated_at' => now(), + ]; + + DB::connection('gescon_import')->table('condomin_mirror')->updateOrInsert( + ['provenance_hash' => $provHash], + $recordComp + ); + } } $stagingRows = DB::connection('gescon_import')->table('condomin_mirror')->where('cod_stabile', $stabileCode)->count(); @@ -188,7 +279,12 @@ public function handle(): int private function extractMdbCondominRows(string $mdbPath): array { - $proc = new Process(['mdb-export', $mdbPath, 'condomin']); + return $this->extractMdbTableRows($mdbPath, 'condomin'); + } + + private function extractMdbTableRows(string $mdbPath, string $tableName): array + { + $proc = new Process(['mdb-export', $mdbPath, $tableName]); $proc->run(); if (! $proc->isSuccessful()) { diff --git a/app/Filament/Pages/Condomini/NominativiStabile.php b/app/Filament/Pages/Condomini/NominativiStabile.php index 0fdfa10..999627a 100755 --- a/app/Filament/Pages/Condomini/NominativiStabile.php +++ b/app/Filament/Pages/Condomini/NominativiStabile.php @@ -473,11 +473,9 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder $ownerNameSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') ->whereColumn('pur.unita_id', 'unita_immobiliari.id') - ->where('pur.tipo_relazione', 'proprietario') + ->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario']) ->where('pur.attivo', true) - ->orderByDesc('pur.id') - ->selectRaw("TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, '')))") - ->limit(1); + ->selectRaw("GROUP_CONCAT(DISTINCT TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, ''))) SEPARATOR ' / ')"); $tenantNameSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') @@ -491,11 +489,9 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder $ownerCellSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') ->whereColumn('pur.unita_id', 'unita_immobiliari.id') - ->where('pur.tipo_relazione', 'proprietario') + ->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario']) ->where('pur.attivo', true) - ->orderByDesc('pur.id') - ->selectRaw("COALESCE(p.telefono_principale, '')") - ->limit(1); + ->selectRaw("GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, '') SEPARATOR ' / ')"); $tenantCellSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') @@ -509,11 +505,9 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder $ownerCfSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') ->whereColumn('pur.unita_id', 'unita_immobiliari.id') - ->where('pur.tipo_relazione', 'proprietario') + ->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario']) ->where('pur.attivo', true) - ->orderByDesc('pur.id') - ->selectRaw("COALESCE(p.codice_fiscale, '')") - ->limit(1); + ->selectRaw("GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, '') SEPARATOR ' / ')"); $tenantCfSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') diff --git a/app/Filament/Pages/UnitaImmobiliarePage.php b/app/Filament/Pages/UnitaImmobiliarePage.php index ca95944..90fb060 100755 --- a/app/Filament/Pages/UnitaImmobiliarePage.php +++ b/app/Filament/Pages/UnitaImmobiliarePage.php @@ -1385,24 +1385,34 @@ private function populateNominativiStoriciFallback(): void $owner = trim((string) ($m->nom_cond ?? $m->proprietario ?? '')); if ($owner !== '') { $fallbackList[] = [ - 'ruolo' => 'Proprietario', - 'nominativo' => $owner, - 'periodo' => $yLabel, - 'percentuale' => '100,00%', - 'fonte' => 'Archivio storico legacy', - 'detail' => ! empty($m->codice_fiscale) ? ('CF: ' . $m->codice_fiscale) : null, + 'persona_id' => null, + 'nome' => $owner, + 'codice_fiscale'=> ! empty($m->codice_fiscale) ? $m->codice_fiscale : null, + 'tipo_relazione'=> 'proprietario', + 'ruolo_label' => 'Proprietario (' . $yLabel . ')', + 'quota' => 100.0, + 'quota_label' => '100,00', + 'data_inizio' => $yLabel, + 'data_fine' => null, + 'attivo' => false, + 'fonte' => 'Archivio storico legacy', ]; } $inquil = trim((string) ($m->inquilino ?? $m->inquil_nome ?? '')); if ($inquil !== '') { $fallbackList[] = [ - 'ruolo' => 'Inquilino', - 'nominativo' => $inquil, - 'periodo' => $yLabel, - 'percentuale' => '100,00%', - 'fonte' => 'Archivio storico legacy', - 'detail' => null, + 'persona_id' => null, + 'nome' => $inquil, + 'codice_fiscale'=> ! empty($m->inquil_cod_fisc) ? $m->inquil_cod_fisc : null, + 'tipo_relazione'=> 'inquilino', + 'ruolo_label' => 'Inquilino (' . $yLabel . ')', + 'quota' => 100.0, + 'quota_label' => '100,00', + 'data_inizio' => $yLabel, + 'data_fine' => null, + 'attivo' => false, + 'fonte' => 'Archivio storico legacy', ]; } } @@ -3243,6 +3253,7 @@ protected function hydrateRelazioni(): void if (! $this->mostraStorico) { $relazioniStoriche = $proprietari->filter(fn($r) => ! $isRelazioneCorrenteNelAnno($r, $activeAnno)) ->concat($inquilini->filter(fn($r) => ! $isRelazioneCorrenteNelAnno($r, $activeAnno))) + ->concat($relazioniMapped->filter(fn($r) => ! ($r['attivo'] ?? false))) ->unique(fn(array $r) => $this->relationIdentityKey($r)) ->values() ->all(); @@ -3252,6 +3263,41 @@ protected function hydrateRelazioni(): void } $legacyRow = $this->getLegacyCondominRow(); + $legacyOwnerName = trim((string) ($legacyRow?->nom_cond ?? $legacyRow?->proprietario ?? '')); + if ($legacyOwnerName !== '') { + $ownerNameUpper = strtoupper($legacyOwnerName); + $matchedOwner = $proprietari->filter(function (array $p) use ($ownerNameUpper): bool { + $nome = strtoupper(trim((string) ($p['nome'] ?? ''))); + return $nome !== '' && (str_contains($nome, $ownerNameUpper) || str_contains($ownerNameUpper, $nome)); + })->values(); + + if ($matchedOwner->isNotEmpty()) { + $unmatchedOwners = $proprietari->reject(function (array $p) use ($ownerNameUpper): bool { + $nome = strtoupper(trim((string) ($p['nome'] ?? ''))); + return $nome !== '' && (str_contains($nome, $ownerNameUpper) || str_contains($ownerNameUpper, $nome)); + })->values()->all(); + + $proprietari = $matchedOwner; + $relazioniStoriche = array_values(array_unique(array_merge($relazioniStoriche, $unmatchedOwners), SORT_REGULAR)); + } else { + $legacyOwnerRecord = [ + 'persona_id' => null, + 'nome' => $legacyOwnerName, + 'codice_fiscale'=> trim((string) ($legacyRow?->cond_cod_fisc ?? $legacyRow?->codice_fiscale ?? '')), + 'tipo_relazione'=> 'proprietario', + 'ruolo_label' => 'Proprietario 100%', + 'quota' => 100.0, + 'quota_label' => '100,00', + 'data_inizio' => '01/01/' . $activeAnno, + 'data_fine' => null, + 'attivo' => true, + 'fonte' => 'legacy_condomin_mirror', + ]; + $relazioniStoriche = array_values(array_unique(array_merge($relazioniStoriche, $proprietari->all()), SORT_REGULAR)); + $proprietari = collect([$legacyOwnerRecord]); + } + } + $legacyInqName = trim((string) ($legacyRow?->inquil_nome ?? $legacyRow?->inquilino ?? '')); if ($legacyInqName !== '') { $inqNameUpper = strtoupper($legacyInqName); @@ -3291,11 +3337,19 @@ protected function hydrateRelazioni(): void } } + $activeOwnerNames = $proprietari->pluck('nome')->map(fn($n) => strtoupper(trim((string) $n)))->all(); + $fallbackHist = (array) ($this->populateNominativiStoriciFallback() ?? []); + $relazioniStoriche = collect(array_merge($relazioniStoriche, $fallbackHist)) + ->reject(fn($r) => in_array(strtoupper(trim((string) ($r['nome'] ?? ''))), $activeOwnerNames, true)) + ->unique(fn(array $r) => $this->relationIdentityKey($r)) + ->values() + ->all(); + $this->relazioniPerTipo = [ 'proprietari' => $proprietari->values()->all(), 'inquilini' => $inqArr, 'altri' => $altri->values()->all(), - 'storico' => $relazioniStoriche, + 'storico' => ! empty($relazioniStoriche) ? $relazioniStoriche : (array) ($this->nominativiStorici ?? []), ]; } diff --git a/docs/ui-wireframes/nominativi-stabile.md b/docs/ui-wireframes/nominativi-stabile.md index 891ab99..3fbbaef 100644 --- a/docs/ui-wireframes/nominativi-stabile.md +++ b/docs/ui-wireframes/nominativi-stabile.md @@ -8,7 +8,7 @@ # ASCII Wireframe - Nominativi e Anagrafica Unica per Stabile +-----------------------------------------------------------------------------------------------------------------------+ | CONDOMINI > NOMINATIVI E ANAGRAFICA UNICA | | | -| Stabile Attivo: [ 0013 - OTTAVIANO 105 (ID 18) v ] Anno Gestione: [ 2026 (0015 - Attiva) v ] [ Mostra Storico ]| +| Stabile Attivo: [ 0016 - GERMANICO 96 (ID 19) v ] Anno Gestione: [ 0914 (Anno 2026 - Attiva) v ] | | | | +-----------------------------------------------------------------------------------------------------------------+ | | | FILTRI E RICERCA ANAGRAFICA CANONICA | | @@ -20,18 +20,22 @@ # ASCII Wireframe - Nominativi e Anagrafica Unica per Stabile | +-----------------------------------------------------------------------------------------------------------------+ | | | | +-----------------------------------------------------------------------------------------------------------------+ | -| | ANAGRAFICA UNICA E RUOLI CONSOLIDATI [ + Nuovo Nominativo ] | | +| | ANAGRAFICA UNICA E RUOLI CONSOLIDATI (Stabile 0016 - GERMANICO 96) [ + Nuovo Nominativo ] | | | +-----------------------------------------------------------------------------------------------------------------+ | | | NOMINATIVO CANONICO | CF / P.IVA | RUOLO & QUOTA | UNITA IMMOBILIARE | PERIODO / GESTIONE | AZIONI | | | +--------------------------+------------------+-------------------+-------------------+--------------------+--------+ | -| | [NOME NOMINATIVO REAL] | [CODICE FISCALE] | Proprietario 100% | Scala A - Int. 1 | Dal 01/01/2026 | [Apri] | | -| | [Anagrafica 000000XX] | | (Piena proprietà) | (ID XX) | (Gestione 2026) | [Edit] | | +| | FRANCIONI CLAUDIA / | FRNCLD49P51H501B | Comproprietà 50% | Scala B - Int. 4 | Dal 01/01/2000 | [Apri] | | +| | BARONE MICHELE | BRNMHL69M11H501K | (Piena proprieta) | (ID 313) | (Gestione 0914) | [Edit] | | +| | [Email: cecilia.tordini@gmail.com] | | | | | | | +--------------------------+------------------+-------------------+-------------------+--------------------+--------+ | -| | [NOME INQUILINO REAL] | [CODICE FISCALE] | Inquilino 100% | Scala A - Int. 2 | Dal 01/01/2025 | [Apri] | | -| | [Anagrafica 000000YY] | | (Locatario) | (ID YY) | (Gestione 2025-26) | [Edit] | | +| | SAVELLI FABIO | SVLFBA58H27H501P | Comproprietà 50% | Scala A - Int. 25 | Dal 01/01/2000 | [Apri] | | +| | [Anagrafica 000000SF] | | | (ID 334) | (Gestione 0914) | [Edit] | | | +--------------------------+------------------+-------------------+-------------------+--------------------+--------+ | -| | [NOME STORICO REAL] | [CODICE FISCALE] | Inquilino 100% | Scala A - Int. 2 | 01/01/2024-31/12/24| [Apri] | | -| | [Anagrafica 000000ZZ] | | (Locatario stor.) | (ID YY) | (Gestione 2024) | [Edit] | | +| | BINI FLORIANA / | BNIFRN76M50H501V | Nuda proprietà | Scala A - Int. 59 | Dal 01/01/2000 | [Apri] | | +| | BINI VANESSA | BNIVSS79M46H501K | | (ID 368) | (Gestione 0914) | [Edit] | | +| +--------------------------+------------------+-------------------+-------------------+--------------------+--------+ | +| | GRILLO CAMILLO / | GRLCLL41R14H501X | Usufrutto / Nuda | Scala A - Int. 33 | Dal 01/01/2000 | [Apri] | | +| | GRILLO FRANCESCO | GRLFNC77D03H501R | | (ID 342) | (Gestione 0914) | [Edit] | | | +-----------------------------------------------------------------------------------------------------------------+ | +-----------------------------------------------------------------------------------------------------------------------+ ``` diff --git a/tests/Feature/BenedettoBonificaIdempotenteTest.php b/tests/Feature/BenedettoBonificaIdempotenteTest.php index da8e289..2d5f997 100644 --- a/tests/Feature/BenedettoBonificaIdempotenteTest.php +++ b/tests/Feature/BenedettoBonificaIdempotenteTest.php @@ -13,18 +13,14 @@ beforeEach(function () { DB::table('amministratori')->insertOrIgnore(['id' => 1, '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'); - $stabile = Stabile::where('codice_stabile', '0021')->first(); + $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:bonifica-duplicati-benedetto command is 100% idempotent across two runs', function () {