Compare commits
15 Commits
dc08affa82
...
df09bc67b2
| Author | SHA1 | Date | |
|---|---|---|---|
| df09bc67b2 | |||
| ae8f54dc36 | |||
| 4774ef5751 | |||
| ccc0c1ec39 | |||
| d7a4cd420d | |||
| cf0eaae8af | |||
| 6fb8d20af3 | |||
| 4da5922357 | |||
| fb1ebdf505 | |||
| 0984f27721 | |||
| d3658d32e5 | |||
| 872b3a6b58 | |||
| 45de2eaad2 | |||
| b54ea07f54 | |||
| f60898aae6 |
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GesconBonificaDuplicatiBenedettoCommand extends Command
|
||||
{
|
||||
protected $signature = 'gescon:bonifica-duplicati-benedetto';
|
||||
|
||||
protected $description = 'Bonifica idempotente delle rubriche duplicate di Daniela Benedetto verso la Persona 323 / Rubrica 697 (CF BNDDNL86M58H224O)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Starting bonifica idempotente Benedetto Daniela...');
|
||||
|
||||
$canonicalPersona = DB::table('persone')->where('codice_fiscale', 'BNDDNL86M58H224O')->first()
|
||||
?? DB::table('persone')->find(323);
|
||||
$canonicalPersonaId = $canonicalPersona?->id ?? 323;
|
||||
|
||||
$canonicalRubrica = DB::table('rubrica_universale')->where('codice_fiscale', 'BNDDNL86M58H224O')->first()
|
||||
?? DB::table('rubrica_universale')->where('codice_univoco', '000000JC')->first()
|
||||
?? DB::table('rubrica_universale')->find(697);
|
||||
$canonicalRubricaId = $canonicalRubrica?->id ?? 697;
|
||||
|
||||
$duplicatePersonaIds = DB::table('persone')
|
||||
->where('cognome', 'BENEDETTO')
|
||||
->where('id', '!=', $canonicalPersonaId)
|
||||
->pluck('id')
|
||||
->all();
|
||||
|
||||
$duplicateRubricaIds = DB::table('rubrica_universale')
|
||||
->where('cognome', 'BENEDETTO')
|
||||
->where('id', '!=', $canonicalRubricaId)
|
||||
->pluck('id')
|
||||
->all();
|
||||
|
||||
// 1. Audit before counts
|
||||
$purBefore = DB::table('persone_unita_relazioni')->whereIn('persona_id', $duplicatePersonaIds)->count();
|
||||
$rrBefore = DB::table('rubrica_ruoli')->whereIn('rubrica_id', $duplicateRubricaIds)->count();
|
||||
|
||||
$this->info(sprintf('BEFORE: Duplicate PUR=%d, Duplicate RR=%d', $purBefore, $rrBefore));
|
||||
|
||||
// 2. Remap relations in persone_unita_relazioni
|
||||
if (! empty($duplicatePersonaIds)) {
|
||||
DB::table('persone_unita_relazioni')
|
||||
->whereIn('persona_id', $duplicatePersonaIds)
|
||||
->update([
|
||||
'persona_id' => $canonicalPersonaId,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('persone')
|
||||
->whereIn('id', $duplicatePersonaIds)
|
||||
->update([
|
||||
'attivo' => 0,
|
||||
'note' => '[ARCHIVIATO_DUPLICATO] Riconfigurato verso Persona ' . $canonicalPersonaId . ' CF BNDDNL86M58H224O',
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. Remap roles in rubrica_ruoli & soft archive rubrica
|
||||
if (! empty($duplicateRubricaIds)) {
|
||||
DB::table('rubrica_ruoli')
|
||||
->whereIn('rubrica_id', $duplicateRubricaIds)
|
||||
->update([
|
||||
'rubrica_id' => $canonicalRubricaId,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('rubrica_universale')
|
||||
->whereIn('id', $duplicateRubricaIds)
|
||||
->update([
|
||||
'stato' => 'inattivo',
|
||||
'note' => '[ARCHIVIATO_DUPLICATO] Soft archived verso Rubrica ' . $canonicalRubricaId . ' (CF BNDDNL86M58H224O)',
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 4. Ensure canonical persona & rubrica remain active
|
||||
if ($canonicalPersonaId) {
|
||||
DB::table('persone')->where('id', $canonicalPersonaId)->update(['attivo' => 1]);
|
||||
}
|
||||
if ($canonicalRubricaId) {
|
||||
DB::table('rubrica_universale')->where('id', $canonicalRubricaId)->update(['stato' => 'attivo']);
|
||||
}
|
||||
|
||||
// 5. Audit after counts
|
||||
$purAfter = DB::table('persone_unita_relazioni')->whereIn('persona_id', $duplicatePersonaIds)->count();
|
||||
$rrAfter = DB::table('rubrica_ruoli')->whereIn('rubrica_id', $duplicateRubricaIds)->count();
|
||||
|
||||
$this->info(sprintf('AUDIT: Remaining Duplicate PUR=%d, Remaining Duplicate RR=%d, Canonical Persona Active=1, Canonical Rubrica Status=attivo', $purAfter, $rrAfter));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
507
app/Console/Commands/GesconReconstructMirror0021Command.php
Normal file
507
app/Console/Commands/GesconReconstructMirror0021Command.php
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Persona;
|
||||
use App\Models\PersonaUnitaRelazione;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class GesconReconstructMirror0021Command extends Command
|
||||
{
|
||||
protected $signature = 'gescon:reconstruct-mirror-0021 {--stabile=0021 : Codice dello stabile pilot}';
|
||||
|
||||
protected $description = 'Ricostruisce Anagrafica Unica, Unità Immobiliari e Relazioni Temporali dal condomin_mirror 0021 con gestione rigorosa CF e subentri';
|
||||
|
||||
private array $resolvedPersonasByStableId = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$stabileCode = (string) $this->option('stabile');
|
||||
$this->info("=== Ricostruzione Anagrafica e Unità dal condomin_mirror Stabile {$stabileCode} ===");
|
||||
|
||||
// 1. Estrazione CF Stabile raw da Stabili.mdb (Nessun fallback 80000000021)
|
||||
$rawStabileCf = $this->getRawStabileCodiceFiscale($stabileCode);
|
||||
if (! $rawStabileCf) {
|
||||
$this->error("BLOCCO_DATI: Impossibile recuperare il codice fiscale raw dello stabile {$stabileCode} da dbc/Stabili.mdb.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Verifica o creazione dello Stabile con CF raw estratto
|
||||
$stabile = Stabile::where('codice_stabile', $stabileCode)->first();
|
||||
if (! $stabile) {
|
||||
$adminId = DB::table('amministratori')->value('id') ?? 1;
|
||||
$stabile = Stabile::create([
|
||||
'codice_stabile' => $stabileCode,
|
||||
'denominazione' => 'SUPERCONDOMINIO MILIZIE 3',
|
||||
'indirizzo' => 'Viale delle Milizie 3',
|
||||
'cap' => '00192',
|
||||
'citta' => 'Roma',
|
||||
'provincia' => 'RM',
|
||||
'codice_fiscale' => $rawStabileCf,
|
||||
'amministratore_id' => $adminId,
|
||||
'attivo' => true,
|
||||
]);
|
||||
} else {
|
||||
if ($stabile->codice_fiscale !== $rawStabileCf) {
|
||||
$stabile->update(['codice_fiscale' => $rawStabileCf]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("Stabile di riferimento: ID {$stabile->id} | CF Raw Stabile: {$stabile->codice_fiscale}");
|
||||
|
||||
// 2. Lettura Staging Lossless condomin_mirror
|
||||
if (! Schema::connection('gescon_import')->hasTable('condomin_mirror')) {
|
||||
$this->error("BLOCCO_DATI: Tabella gescon_import.condomin_mirror non esistente.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$mirrorRows = DB::connection('gescon_import')
|
||||
->table('condomin_mirror')
|
||||
->where('cod_stabile', $stabileCode)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$totalMirrorRows = $mirrorRows->count();
|
||||
if ($totalMirrorRows === 0) {
|
||||
$this->error("BLOCCO_DATI: Nessuna riga presente in condomin_mirror per lo stabile {$stabileCode}.");
|
||||
return 1;
|
||||
}
|
||||
$this->info("Righe totali da condomin_mirror: {$totalMirrorRows}");
|
||||
|
||||
// 3. Elaborazione e Ricostruzione
|
||||
$unitaCreatedOrFound = 0;
|
||||
$personeCreatedOrFound = 0;
|
||||
$relazioniProprietariCount = 0;
|
||||
$relazioniInquiliniCount = 0;
|
||||
$processedMirrorRows = 0;
|
||||
|
||||
$unitMap = [];
|
||||
|
||||
foreach ($mirrorRows as $row) {
|
||||
$scalaRaw = trim((string) ($row->scala ?? ''));
|
||||
$intRaw = trim((string) ($row->interno ?? ''));
|
||||
$unitKey = "{$scalaRaw}___{$intRaw}";
|
||||
|
||||
// A) Ricostruzione Unità Fisica: stabile_id + scala + int raw
|
||||
if (! isset($unitMap[$unitKey])) {
|
||||
$unita = UnitaImmobiliare::where('stabile_id', $stabile->id)
|
||||
->where('scala', $scalaRaw)
|
||||
->where('interno', $intRaw)
|
||||
->first();
|
||||
|
||||
if (! $unita) {
|
||||
$unitaCode = '0021-' . ($scalaRaw !== '' ? "{$scalaRaw}-" : '') . $intRaw;
|
||||
$unita = UnitaImmobiliare::create([
|
||||
'stabile_id' => $stabile->id,
|
||||
'scala' => $scalaRaw,
|
||||
'interno' => $intRaw,
|
||||
'codice_unita' => $unitaCode,
|
||||
'denominazione' => "Stabile 0021 Scala {$scalaRaw} Int {$intRaw}",
|
||||
'piano' => $row->piano ? trim((string) $row->piano) : null,
|
||||
'millesimi_generali' => $row->millesimi_proprieta ?: null,
|
||||
'millesimi_proprieta' => $row->millesimi_proprieta ?: null,
|
||||
'millesimi_riscaldamento' => $row->millesimi_riscaldamento ?: null,
|
||||
'millesimi_ascensore' => $row->millesimi_ascensore ?: null,
|
||||
'stato' => 'attiva',
|
||||
]);
|
||||
$unitaCreatedOrFound++;
|
||||
} else {
|
||||
$updates = [];
|
||||
if (empty($unita->piano) && ! empty($row->piano)) {
|
||||
$updates['piano'] = trim((string) $row->piano);
|
||||
}
|
||||
if (is_null($unita->millesimi_generali) && ! is_null($row->millesimi_proprieta)) {
|
||||
$updates['millesimi_generali'] = $row->millesimi_proprieta;
|
||||
}
|
||||
if (is_null($unita->millesimi_riscaldamento) && ! is_null($row->millesimi_riscaldamento)) {
|
||||
$updates['millesimi_riscaldamento'] = $row->millesimi_riscaldamento;
|
||||
}
|
||||
if (is_null($unita->millesimi_ascensore) && ! is_null($row->millesimi_ascensore)) {
|
||||
$updates['millesimi_ascensore'] = $row->millesimi_ascensore;
|
||||
}
|
||||
if (! empty($updates)) {
|
||||
$unita->update($updates);
|
||||
}
|
||||
$unitaCreatedOrFound++;
|
||||
}
|
||||
$unitMap[$unitKey] = $unita;
|
||||
} else {
|
||||
$unita = $unitMap[$unitKey];
|
||||
}
|
||||
|
||||
// B) Ricostruzione Persona Proprietario / Intitolato
|
||||
$personaProprietario = $this->resolveOrCreatePersona($row, false, $rawStabileCf);
|
||||
if ($personaProprietario->wasRecentlyCreated) {
|
||||
$personeCreatedOrFound++;
|
||||
}
|
||||
|
||||
// C) Relazione Temporale Proprietario
|
||||
$idCondStr = trim((string) $row->id_cond);
|
||||
$codCondStr = trim((string) $row->cod_cond);
|
||||
$sourceFile = (string) ($row->source_file ?? '');
|
||||
$sourceYear = (string) ($row->source_year ?? '');
|
||||
$sourceRow = (int) ($row->source_row ?? 0);
|
||||
$provenanceHash = (string) ($row->provenance_hash ?? "mirror_{$row->id}");
|
||||
|
||||
$provenanceFull = "{$sourceFile}#row:{$sourceRow}|{$provenanceHash}";
|
||||
|
||||
$relProp = PersonaUnitaRelazione::where('unita_id', $unita->id)
|
||||
->where('id_cond', $idCondStr)
|
||||
->where('tipo_relazione', 'proprietario')
|
||||
->first();
|
||||
|
||||
$subDalRaw = trim((string) ($row->subentrato_dal ?? ''));
|
||||
$attFinoRaw = trim((string) ($row->attivo_fino_al ?? ''));
|
||||
$subPrimaRaw = trim((string) ($row->subentro_prima_cera ?? ''));
|
||||
$subAdessoRaw = trim((string) ($row->subentro_adesso_ce ?? ''));
|
||||
|
||||
$dataInizioParsed = $this->parseMirrorDate($subDalRaw) ?: '2000-01-01';
|
||||
$dataFineParsed = $this->parseMirrorDate($attFinoRaw);
|
||||
|
||||
$isAttivo = true;
|
||||
if ($attFinoRaw !== '' && $attFinoRaw !== '0' && $attFinoRaw !== '00/00/00' && $attFinoRaw !== '00/00/0000') {
|
||||
$isAttivo = false;
|
||||
}
|
||||
|
||||
if (! $relProp) {
|
||||
$relProp = PersonaUnitaRelazione::create([
|
||||
'persona_id' => $personaProprietario->id,
|
||||
'unita_id' => $unita->id,
|
||||
'tipo_relazione' => 'proprietario',
|
||||
'ruolo_rate' => 'C',
|
||||
'quota_relazione' => $row->millesimi_proprieta ?: 100.00,
|
||||
'data_inizio' => $dataInizioParsed,
|
||||
'data_fine' => $dataFineParsed,
|
||||
'attivo' => $isAttivo,
|
||||
'riceve_comunicazioni' => true,
|
||||
'riceve_convocazioni' => true,
|
||||
'vota_assemblea' => true,
|
||||
'id_cond' => $idCondStr,
|
||||
'cod_cond' => $codCondStr,
|
||||
'provenance' => $provenanceFull,
|
||||
'subentrato_dal' => $subDalRaw !== '' ? $subDalRaw : null,
|
||||
'attivo_fino_al' => $attFinoRaw !== '' ? $attFinoRaw : null,
|
||||
'subentro_prima_cera' => $subPrimaRaw !== '' ? $subPrimaRaw : null,
|
||||
'subentro_adesso_ce' => $subAdessoRaw !== '' ? $subAdessoRaw : null,
|
||||
'note_relazione' => "Gestione: {$sourceYear} | File: {$sourceFile} | Row: {$sourceRow}",
|
||||
]);
|
||||
$relazioniProprietariCount++;
|
||||
} else {
|
||||
$updatesRel = [];
|
||||
if ($relProp->persona_id !== $personaProprietario->id) {
|
||||
$updatesRel['persona_id'] = $personaProprietario->id;
|
||||
}
|
||||
if (empty($relProp->subentrato_dal) && $subDalRaw !== '') {
|
||||
$updatesRel['subentrato_dal'] = $subDalRaw;
|
||||
if ($dataInizioParsed) {
|
||||
$updatesRel['data_inizio'] = $dataInizioParsed;
|
||||
}
|
||||
}
|
||||
if (empty($relProp->attivo_fino_al) && $attFinoRaw !== '') {
|
||||
$updatesRel['attivo_fino_al'] = $attFinoRaw;
|
||||
$updatesRel['attivo'] = false;
|
||||
if ($dataFineParsed) {
|
||||
$updatesRel['data_fine'] = $dataFineParsed;
|
||||
}
|
||||
}
|
||||
if (empty($relProp->subentro_prima_cera) && $subPrimaRaw !== '') {
|
||||
$updatesRel['subentro_prima_cera'] = $subPrimaRaw;
|
||||
}
|
||||
if (empty($relProp->subentro_adesso_ce) && $subAdessoRaw !== '') {
|
||||
$updatesRel['subentro_adesso_ce'] = $subAdessoRaw;
|
||||
}
|
||||
if (! empty($codCondStr) && empty($relProp->cod_cond)) {
|
||||
$updatesRel['cod_cond'] = $codCondStr;
|
||||
}
|
||||
if (! empty($updatesRel)) {
|
||||
$relProp->update($updatesRel);
|
||||
}
|
||||
}
|
||||
|
||||
// D) Inquilino (se presente nel mirror row o nel payload)
|
||||
$payload = json_decode($row->legacy_payload ?? '{}', true);
|
||||
$inquilinoRaw = trim((string) ($row->inquilino ?: ($payload['inquil_nome'] ?? '')));
|
||||
if ($inquilinoRaw !== '' && $inquilinoRaw !== '0') {
|
||||
$personaInquilino = $this->resolveOrCreatePersona($row, true, $rawStabileCf);
|
||||
if ($personaInquilino->wasRecentlyCreated) {
|
||||
$personeCreatedOrFound++;
|
||||
}
|
||||
|
||||
$inqContrattoDal = trim((string) ($payload['Inquil_contratto_dal'] ?? $payload['inquil_dal'] ?? $row->inquil_dal ?? ''));
|
||||
$inqDataInizioParsed = $this->parseMirrorDate($inqContrattoDal) ?: $dataInizioParsed;
|
||||
|
||||
$inqIdCond = "{$idCondStr}_inq";
|
||||
|
||||
// Ogni diversa persona inquilina o diversa occupazione mantenuta come relazione temporale
|
||||
$relInq = PersonaUnitaRelazione::where('unita_id', $unita->id)
|
||||
->where('persona_id', $personaInquilino->id)
|
||||
->where('tipo_relazione', 'inquilino')
|
||||
->first();
|
||||
|
||||
if (! $relInq) {
|
||||
$relInq = PersonaUnitaRelazione::create([
|
||||
'persona_id' => $personaInquilino->id,
|
||||
'unita_id' => $unita->id,
|
||||
'tipo_relazione' => 'inquilino',
|
||||
'ruolo_rate' => 'I',
|
||||
'quota_relazione' => null,
|
||||
'data_inizio' => $inqDataInizioParsed,
|
||||
'data_fine' => $dataFineParsed,
|
||||
'attivo' => $isAttivo,
|
||||
'riceve_comunicazioni' => true,
|
||||
'riceve_convocazioni' => false,
|
||||
'vota_assemblea' => false,
|
||||
'id_cond' => $inqIdCond,
|
||||
'cod_cond' => $codCondStr,
|
||||
'provenance' => $provenanceFull,
|
||||
'subentrato_dal' => $inqContrattoDal !== '' ? $inqContrattoDal : ($subDalRaw !== '' ? $subDalRaw : null),
|
||||
'attivo_fino_al' => $attFinoRaw !== '' ? $attFinoRaw : null,
|
||||
'subentro_prima_cera' => $subPrimaRaw !== '' ? $subPrimaRaw : null,
|
||||
'subentro_adesso_ce' => $subAdessoRaw !== '' ? $subAdessoRaw : null,
|
||||
'note_relazione' => "Inquilino Gestione: {$sourceYear} | Contratto Dal Raw: {$inqContrattoDal} | File: {$sourceFile}",
|
||||
]);
|
||||
$relazioniInquiliniCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$processedMirrorRows++;
|
||||
}
|
||||
|
||||
// 4. Stampa Quadratura Completa
|
||||
$distinctPhysicalUnits = UnitaImmobiliare::where('stabile_id', $stabile->id)->count();
|
||||
|
||||
$int11Relations = PersonaUnitaRelazione::whereHas('unitaImmobiliare', function ($q) use ($stabile) {
|
||||
$q->where('stabile_id', $stabile->id)
|
||||
->where('scala', 'A')
|
||||
->where('interno', '11');
|
||||
})->get();
|
||||
|
||||
$this->info('=== ESITO QUADRATURA 0021 ===');
|
||||
$this->info("RIGHE_MIRROR_TOTALI: {$totalMirrorRows}");
|
||||
$this->info("RIGHE_ELABORATE: {$processedMirrorRows}");
|
||||
$this->info("UNITA_FISICHE_DISTINTE: {$distinctPhysicalUnits}");
|
||||
$this->info("RELAZIONI_PROPRIETARI_TOTALI: {$relazioniProprietariCount}");
|
||||
$this->info("RELAZIONI_INQUILINI_TOTALI: {$relazioniInquiliniCount}");
|
||||
$this->info('VERIFICA_SCALA_A_INT_11: ' . $int11Relations->count() . ' relazioni trovate');
|
||||
|
||||
foreach ($int11Relations as $r11) {
|
||||
$p = $r11->persona;
|
||||
$statusStr = $r11->attivo ? 'ATTIVA' : 'INATTIVA (Uscente)';
|
||||
$cfStr = $p->codice_fiscale ? "CF: {$p->codice_fiscale}" : 'CF: NULL (Candidato)';
|
||||
$this->line(" -> Persona: {$p->nome_completo} | {$cfStr} | Relazione: {$r11->tipo_relazione} | Status: {$statusStr} | sub_dal: {$r11->subentrato_dal} | att_fino: {$r11->attivo_fino_al}");
|
||||
}
|
||||
|
||||
if ($processedMirrorRows === $totalMirrorRows) {
|
||||
$this->info("QUADRATURA_ESITO: PERFETTA (100% delle {$totalMirrorRows} righe specchiate)");
|
||||
} else {
|
||||
$this->error("QUADRATURA_ESITO: DISCREPANZA ({$processedMirrorRows}/{$totalMirrorRows} righe)");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Risolve o crea una Persona seguendo rigorosamente le regole CF:
|
||||
* - CF proprietario = Cond_cod_fisc; CF inquilino = Inquil_cod_fisc;
|
||||
* - Valida ogni CF raw. Se valido e non appartenente allo stabile, assegna il CF a Persona;
|
||||
* - Se privo di CF valido, codice_fiscale = NULL e candidato auditabile;
|
||||
* - Mai usare il CF dello stabile per una persona.
|
||||
*/
|
||||
private function resolveOrCreatePersona(object $row, bool $isTenant, string $stabileCf): Persona
|
||||
{
|
||||
$payload = json_decode($row->legacy_payload ?? '{}', true);
|
||||
|
||||
if ($isTenant) {
|
||||
$rawName = trim((string) ($row->inquilino ?: ($payload['inquil_nome'] ?? '')));
|
||||
$rawCf = trim((string) ($payload['Inquil_cod_fisc'] ?? ''));
|
||||
} else {
|
||||
$rawName = trim((string) ($row->nom_cond ?? ''));
|
||||
$rawCf = trim((string) ($payload['Cond_cod_fisc'] ?? $row->codice_fiscale ?? ''));
|
||||
}
|
||||
|
||||
$cleanCf = strtoupper($rawCf);
|
||||
|
||||
// 1. Validazione Codice Fiscale (deve differire dal CF dello stabile)
|
||||
$isValidCf = false;
|
||||
if ($cleanCf !== '' && $cleanCf !== '0' && $cleanCf !== strtoupper($stabileCf)) {
|
||||
if (preg_match('/^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$/', $cleanCf) || preg_match('/^\d{11}$/', $cleanCf)) {
|
||||
$isValidCf = true;
|
||||
}
|
||||
}
|
||||
|
||||
$idCondStr = trim((string) ($row->id_cond ?? ''));
|
||||
$stableKey = $isTenant ? "0021_ID_{$idCondStr}_INQ_" . md5($rawName) : "0021_ID_{$idCondStr}";
|
||||
|
||||
if ($isValidCf) {
|
||||
$existingByCf = Persona::where('codice_fiscale', $cleanCf)->first();
|
||||
if ($existingByCf) {
|
||||
return $existingByCf;
|
||||
}
|
||||
|
||||
$existingByStable = Persona::where('note', 'LIKE', "%[MDB_STABLE_ID: {$stableKey}]%")->first();
|
||||
if ($existingByStable) {
|
||||
if (empty($existingByStable->codice_fiscale)) {
|
||||
$existingByStable->update(['codice_fiscale' => $cleanCf]);
|
||||
}
|
||||
$this->resolvedPersonasByStableId[$stableKey] = $existingByStable;
|
||||
return $existingByStable;
|
||||
}
|
||||
} else {
|
||||
if (isset($this->resolvedPersonasByStableId[$stableKey])) {
|
||||
return $this->resolvedPersonasByStableId[$stableKey];
|
||||
}
|
||||
|
||||
$existingByStable = Persona::where('note', 'LIKE', "%[MDB_STABLE_ID: {$stableKey}]%")->first();
|
||||
if ($existingByStable) {
|
||||
$this->resolvedPersonasByStableId[$stableKey] = $existingByStable;
|
||||
return $existingByStable;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Creazione Persona
|
||||
$parsed = $this->parseNomCond($rawName);
|
||||
$phoneRaw = $row->cellulare ? trim((string) $row->cellulare) : ($row->telefono ? trim((string) $row->telefono) : null);
|
||||
$phoneToSave = null;
|
||||
if ($phoneRaw) {
|
||||
$cleanPhone = preg_replace('/[^\d+]/', '', $phoneRaw);
|
||||
if ($cleanPhone !== '' && ! Persona::where('telefono_principale', $cleanPhone)->exists()) {
|
||||
$phoneToSave = $cleanPhone;
|
||||
}
|
||||
}
|
||||
|
||||
$emailCandidate = $row->email ? trim((string) $row->email) : null;
|
||||
$pecCandidate = $row->pec ? trim((string) $row->pec) : null;
|
||||
|
||||
$note = $isValidCf
|
||||
? "[MDB_0021] CF Validato da condomin_mirror [MDB_STABLE_ID: {$stableKey}]"
|
||||
: "[CANDIDATO_AUDITABILE] Stabile 0021 [MDB_STABLE_ID: {$stableKey}]";
|
||||
|
||||
$persona = Persona::create([
|
||||
'codice_interno' => Persona::generaCodiceUnivoco(),
|
||||
'tipologia' => $parsed['tipologia'],
|
||||
'cognome' => $parsed['cognome'],
|
||||
'nome' => $parsed['nome'],
|
||||
'ragione_sociale' => $parsed['ragione_sociale'],
|
||||
'codice_fiscale' => $isValidCf ? $cleanCf : null,
|
||||
'telefono_principale' => $phoneToSave,
|
||||
'email_principale' => $emailCandidate,
|
||||
'email_pec' => $pecCandidate,
|
||||
'residenza_via' => $row->indirizzo_corrispondenza ? trim((string) $row->indirizzo_corrispondenza) : null,
|
||||
'note' => $note,
|
||||
'attivo' => true,
|
||||
]);
|
||||
|
||||
$this->resolvedPersonasByStableId[$stableKey] = $persona;
|
||||
|
||||
return $persona;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estrazione CF dello stabile direttamente da dbc/Stabili.mdb
|
||||
*/
|
||||
private function getRawStabileCodiceFiscale(string $stabileCode): ?string
|
||||
{
|
||||
// 1. Check main DB stabili table if pre-populated with valid raw CF
|
||||
$stabileDb = DB::table('stabili')->where('codice_stabile', $stabileCode)->first();
|
||||
if ($stabileDb && ! empty($stabileDb->codice_fiscale) && $stabileDb->codice_fiscale !== '80000000021') {
|
||||
return trim((string) $stabileDb->codice_fiscale);
|
||||
}
|
||||
|
||||
// 2. Read from /mnt/gescon-archives/gescon/dbc/Stabili.mdb
|
||||
$mdbPath = '/mnt/gescon-archives/gescon/dbc/Stabili.mdb';
|
||||
if (file_exists($mdbPath)) {
|
||||
$output = shell_exec('mdb-export ' . escapeshellarg($mdbPath) . ' Stabili 2>/dev/null');
|
||||
if ($output) {
|
||||
$lines = explode("\n", $output);
|
||||
$header = str_getcsv($lines[0] ?? '');
|
||||
$cfIndex = array_search('codice_fisc', $header);
|
||||
$dirIndex = array_search('nome_directory', $header);
|
||||
$codIndex = array_search('cod_stabile', $header);
|
||||
|
||||
if ($cfIndex !== false) {
|
||||
for ($i = 1; $i < count($lines); $i++) {
|
||||
$row = str_getcsv($lines[$i]);
|
||||
$dir = $row[$dirIndex] ?? '';
|
||||
$cod = $row[$codIndex] ?? '';
|
||||
if ($dir === $stabileCode || $cod === $stabileCode) {
|
||||
$cf = trim((string) ($row[$cfIndex] ?? ''));
|
||||
if (! empty($cf)) {
|
||||
return $cf;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function parseNomCond(string $nomCond): array
|
||||
{
|
||||
$nomCond = trim($nomCond);
|
||||
$upper = mb_strtoupper($nomCond);
|
||||
$companyKeywords = ['ATER', 'SRL', 'S.R.L.', 'SPA', 'S.P.A.', 'SAS', 'S.A.S.', 'SNC', 'S.N.C.', 'CONDOMINI', 'EREDI', 'C/O', 'SOC.', 'SOCIETA', 'SOCIETÀ', 'DOMUS'];
|
||||
|
||||
foreach ($companyKeywords as $kw) {
|
||||
if (str_contains($upper, $kw)) {
|
||||
return [
|
||||
'tipologia' => 'giuridica',
|
||||
'cognome' => $nomCond,
|
||||
'nome' => '',
|
||||
'ragione_sociale' => $nomCond,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$parts = preg_split('/\s+/', $nomCond, 2);
|
||||
|
||||
return [
|
||||
'tipologia' => 'fisica',
|
||||
'cognome' => $parts[0] ?? $nomCond,
|
||||
'nome' => $parts[1] ?? '',
|
||||
'ragione_sociale' => null,
|
||||
];
|
||||
}
|
||||
|
||||
private function parseMirrorDate(?string $dateStr): ?string
|
||||
{
|
||||
if (empty($dateStr)) {
|
||||
return null;
|
||||
}
|
||||
$dateStr = trim($dateStr);
|
||||
if ($dateStr === '0' || $dateStr === '00/00/00' || $dateStr === '00/00/0000') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$parts = explode(' ', $dateStr);
|
||||
$datePart = $parts[0];
|
||||
if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})$/', $datePart, $m)) {
|
||||
$month = (int) $m[1];
|
||||
$day = (int) $m[2];
|
||||
$year = (int) $m[3];
|
||||
if ($year < 100) {
|
||||
$year += ($year > 50 ? 1900 : 2000);
|
||||
}
|
||||
if ($month > 12 && $day <= 12) {
|
||||
$tmp = $month;
|
||||
$month = $day;
|
||||
$day = $tmp;
|
||||
}
|
||||
|
||||
return sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
}
|
||||
$dt = new \DateTime($dateStr);
|
||||
|
||||
return $dt->format('Y-m-d');
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
146
app/Console/Commands/GesconSyncAnagraficaConsolidata.php
Normal file
146
app/Console/Commands/GesconSyncAnagraficaConsolidata.php
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\RubricaUniversale;
|
||||
use App\Support\StabileContext;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class GesconSyncAnagraficaConsolidata extends Command
|
||||
{
|
||||
protected $signature = 'gescon:sync-anagrafica-consolidata
|
||||
{amministratore_id=12 : ID dell\'amministratore a cui associare i contatti}
|
||||
{--mdb=/mnt/gescon-archives/gescon/dbc/Fornitori.mdb : Percorso file Fornitori.mdb}';
|
||||
|
||||
protected $description = 'Sincronizza in modo idempotente Anagrafica Unica (persone DB) e promuove Fornitori legacy (Fornitori.mdb)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$adminId = (int) $this->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;
|
||||
}
|
||||
}
|
||||
|
|
@ -148,6 +148,7 @@ public function handle(): int
|
|||
'cognome' => 'Seed',
|
||||
'user_id' => $userId,
|
||||
'codice_univoco' => 'ASEED001',
|
||||
'codice_amministratore' => 'ASEED001',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -458,49 +458,104 @@ protected function getTableQuery(): Builder
|
|||
return LegacyCondominNominativo::query()->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
$this->ensureLegacyViewExists();
|
||||
|
||||
if (! $this->hasLegacyNominativiForStabile($codStabile)) {
|
||||
return $this->buildDomainFallbackQuery((int) $activeStabileId);
|
||||
if (UnitaImmobiliare::query()->where('stabile_id', (int) $activeStabileId)->exists()) {
|
||||
return $this->buildDomainConsolidatedQuery((int) $activeStabileId);
|
||||
}
|
||||
|
||||
$legacyTable = 'vw_legacy_condomin_nominativi';
|
||||
// Se lo stabile non ha unità consolidate nel modello di dominio,
|
||||
// restituisce query vuota per mostrare NESSUN_DATO_CONSOLIDATO
|
||||
// senza alcun fallback runtime alla vista staging legacy.
|
||||
return UnitaImmobiliare::query()->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
$base = LegacyCondominNominativo::query()
|
||||
->where($legacyTable . '.cod_stabile', $codStabile)
|
||||
->where(function (Builder $q): void {
|
||||
$q->whereRaw("TRIM(COALESCE(vw_legacy_condomin_nominativi.nom_cond, '')) <> ''")
|
||||
->orWhereRaw("TRIM(COALESCE(vw_legacy_condomin_nominativi.inquil_nome, '')) <> ''");
|
||||
});
|
||||
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')
|
||||
->where('pur.attivo', true)
|
||||
->orderByDesc('pur.id')
|
||||
->selectRaw("TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, '')))")
|
||||
->limit(1);
|
||||
|
||||
$latestPerUnit = DB::connection('gescon_import')
|
||||
->table('vw_legacy_condomin_nominativi')
|
||||
->select('cod_stabile', 'scala', 'interno')
|
||||
->selectRaw('MAX(legacy_year) as legacy_year')
|
||||
->where('cod_stabile', $codStabile)
|
||||
->groupBy('cod_stabile', 'scala', 'interno');
|
||||
$tenantNameSub = 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', 'inquilino')
|
||||
->where('pur.attivo', true)
|
||||
->orderByDesc('pur.id')
|
||||
->selectRaw("TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, '')))")
|
||||
->limit(1);
|
||||
|
||||
return $base
|
||||
->joinSub($latestPerUnit, 'mx', function ($join): void {
|
||||
$join->on('mx.cod_stabile', '=', 'vw_legacy_condomin_nominativi.cod_stabile')
|
||||
->on('mx.scala', '=', 'vw_legacy_condomin_nominativi.scala')
|
||||
->on('mx.interno', '=', 'vw_legacy_condomin_nominativi.interno')
|
||||
->on('mx.legacy_year', '=', 'vw_legacy_condomin_nominativi.legacy_year');
|
||||
})
|
||||
->when($this->cumulato, function (Builder $q) use ($legacyTable): void {
|
||||
$q->where(function (Builder $sub) use ($legacyTable) {
|
||||
$sub->whereNull($legacyTable . '.cumulo_cond')
|
||||
->orWhere($legacyTable . '.cumulo_cond', '')
|
||||
->orWhereColumn($legacyTable . '.cumulo_cond', $legacyTable . '.cod_cond');
|
||||
$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')
|
||||
->where('pur.attivo', true)
|
||||
->orderByDesc('pur.id')
|
||||
->selectRaw("COALESCE(p.telefono_principale, '')")
|
||||
->limit(1);
|
||||
|
||||
$tenantCellSub = 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', 'inquilino')
|
||||
->where('pur.attivo', true)
|
||||
->orderByDesc('pur.id')
|
||||
->selectRaw("COALESCE(p.telefono_principale, '')")
|
||||
->limit(1);
|
||||
|
||||
$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')
|
||||
->where('pur.attivo', true)
|
||||
->orderByDesc('pur.id')
|
||||
->selectRaw("COALESCE(p.codice_fiscale, '')")
|
||||
->limit(1);
|
||||
|
||||
$tenantCfSub = 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', 'inquilino')
|
||||
->where('pur.attivo', true)
|
||||
->orderByDesc('pur.id')
|
||||
->selectRaw("COALESCE(p.codice_fiscale, '')")
|
||||
->limit(1);
|
||||
|
||||
return UnitaImmobiliare::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereNull('deleted_at')
|
||||
->when($this->cumulato, function (Builder $query) use ($stabileId): void {
|
||||
$query->whereNotExists(function ($sub) use ($stabileId) {
|
||||
$sub->selectRaw('1')
|
||||
->from('unita_pertinenze')
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereColumn('unita_accessoria_id', 'unita_immobiliari.id')
|
||||
->where('cumulo', true);
|
||||
});
|
||||
})
|
||||
->orderBy($legacyTable . '.scala')
|
||||
// Ordine richiesto: per interno (con fallback robusto)
|
||||
->orderByRaw("CASE WHEN vw_legacy_condomin_nominativi.interno IS NULL OR vw_legacy_condomin_nominativi.interno = '' THEN 1 ELSE 0 END")
|
||||
->orderByRaw("LENGTH(vw_legacy_condomin_nominativi.interno)")
|
||||
->orderBy($legacyTable . '.interno')
|
||||
->orderBy($legacyTable . '.cod_cond')
|
||||
->orderBy($legacyTable . '.id');
|
||||
->select([
|
||||
'unita_immobiliari.id',
|
||||
'unita_immobiliari.stabile_id',
|
||||
'unita_immobiliari.scala',
|
||||
'unita_immobiliari.piano',
|
||||
'unita_immobiliari.interno',
|
||||
'unita_immobiliari.codice_unita',
|
||||
])
|
||||
->selectRaw('unita_immobiliari.codice_unita as cod_cond')
|
||||
->selectSub($ownerNameSub, 'nom_cond')
|
||||
->selectSub($tenantNameSub, 'inquil_nome')
|
||||
->selectSub($ownerCellSub, 'cell_cond')
|
||||
->selectSub($tenantCellSub, 'cell_inq')
|
||||
->selectSub($ownerCfSub, 'cond_cod_fisc')
|
||||
->selectSub($tenantCfSub, 'inquil_cod_fisc')
|
||||
->orderBy('unita_immobiliari.scala')
|
||||
->orderByRaw("CASE WHEN unita_immobiliari.interno IS NULL OR unita_immobiliari.interno = '' THEN 1 ELSE 0 END")
|
||||
->orderByRaw("CASE WHEN unita_immobiliari.interno REGEXP '^[0-9]+' THEN CAST(unita_immobiliari.interno AS UNSIGNED) ELSE 999999 END")
|
||||
->orderBy('unita_immobiliari.interno')
|
||||
->orderBy('unita_immobiliari.id');
|
||||
}
|
||||
|
||||
protected function hasLegacyNominativiForStabile(string $codStabile): bool
|
||||
|
|
@ -773,14 +828,14 @@ public function table(Table $table): Table
|
|||
? $this->resolveLegacyStabileCode((int) $activeStabileId)
|
||||
: '';
|
||||
|
||||
$useLegacy = $codStabile !== '' && $this->hasLegacyNominativiForStabile($codStabile);
|
||||
$useLegacy = false;
|
||||
|
||||
$legacyYear = $useLegacy
|
||||
? (string) (DB::connection('gescon_import')->table('condomin')->where('cod_stabile', $codStabile)->max('legacy_year') ?: '')
|
||||
: '';
|
||||
$legacyYear = '';
|
||||
|
||||
return $table
|
||||
->striped()
|
||||
->emptyStateHeading('NESSUN_DATO_CONSOLIDATO')
|
||||
->emptyStateDescription('Lo stabile corrente non dispone di unità immobiliari o persone consolidate nel modello di dominio. Lo staging legacy è disabilitato come sorgente runtime operativa per evitare disallineamenti.')
|
||||
->filters([
|
||||
SelectFilter::make('legacy_year')
|
||||
->label('Anno legacy')
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ public static function canAccess(): bool
|
|||
|
||||
public array $relazioniPerTipo = [];
|
||||
|
||||
public bool $mostraStorico = false;
|
||||
|
||||
public array $canaliComunicazione = [];
|
||||
|
||||
public array $nominativiStorici = [];
|
||||
|
|
@ -2297,9 +2299,16 @@ private function formatLegacyDate(mixed $value): ?string
|
|||
|
||||
private function relationIdentityKey(array $rel): string
|
||||
{
|
||||
$legacyPersonaId = is_numeric($rel['legacy_persona_id'] ?? null) ? (int) $rel['legacy_persona_id'] : 0;
|
||||
if ($legacyPersonaId > 0) {
|
||||
return 'legacy:' . $legacyPersonaId;
|
||||
$cf = strtoupper(trim((string) ($rel['codice_fiscale'] ?? '')));
|
||||
if ($cf !== '') {
|
||||
return 'cf:' . $cf;
|
||||
}
|
||||
|
||||
$rawName = trim((string) ($rel['nome'] ?? ''));
|
||||
if ($rawName !== '') {
|
||||
$parts = preg_split('/\s+/', mb_strtolower($rawName));
|
||||
sort($parts);
|
||||
return 'name:' . implode(' ', $parts);
|
||||
}
|
||||
|
||||
$personaId = is_numeric($rel['persona_id'] ?? null) ? (int) $rel['persona_id'] : 0;
|
||||
|
|
@ -2307,12 +2316,13 @@ private function relationIdentityKey(array $rel): string
|
|||
return 'persona:' . $personaId;
|
||||
}
|
||||
|
||||
$cf = strtoupper(trim((string) ($rel['codice_fiscale'] ?? '')));
|
||||
if ($cf !== '') {
|
||||
return 'cf:' . $cf;
|
||||
}
|
||||
return 'id:0';
|
||||
}
|
||||
|
||||
return 'name:' . strtolower(trim((string) ($rel['nome'] ?? '')));
|
||||
public function toggleMostraStorico(): void
|
||||
{
|
||||
$this->mostraStorico = ! $this->mostraStorico;
|
||||
$this->hydrateRelazioni();
|
||||
}
|
||||
|
||||
private function loadCurrentArchiveRelazioni(string $ruolo): array
|
||||
|
|
@ -2601,6 +2611,8 @@ protected function hydrateDiritti(): void
|
|||
|
||||
protected function hydrateRelazioni(): void
|
||||
{
|
||||
$this->legacyCondominRow = null;
|
||||
|
||||
$formatDate = static function ($value) {
|
||||
if (empty($value)) {
|
||||
return null;
|
||||
|
|
@ -2676,17 +2688,33 @@ protected function hydrateRelazioni(): void
|
|||
->unique(fn(array $d) => (int) ($d['soggetto_id'] ?? 0))
|
||||
->values();
|
||||
|
||||
$today = now()->toDateString();
|
||||
$activeAnno = \App\Support\AnnoGestioneContext::resolveActiveAnno(\Illuminate\Support\Facades\Auth::user());
|
||||
$today = sprintf('%04d-12-31', $activeAnno);
|
||||
$startOfYear = sprintf('%04d-01-01', $activeAnno);
|
||||
|
||||
$purList = PersonaUnitaRelazione::with('persona')
|
||||
->where('unita_id', $this->unita->id)
|
||||
->get();
|
||||
$cfDatesMap = [];
|
||||
foreach ($purList as $pur) {
|
||||
$cf = strtoupper(trim((string) ($pur->persona?->codice_fiscale ?? '')));
|
||||
if ($cf !== '') {
|
||||
$cfDatesMap[$cf] = [
|
||||
'data_inizio' => $pur->data_inizio ?: $pur->subentrato_dal,
|
||||
'data_fine' => $pur->data_fine ?: $pur->attivo_fino_al,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$relazioniMapped = RubricaRuolo::query()
|
||||
->with(['contatto'])
|
||||
->where('stabile_id', $this->stabileId)
|
||||
->where('unita_immobiliare_id', $this->unita->id)
|
||||
->where('is_attivo', true)
|
||||
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($today) {
|
||||
$q->whereNull('data_inizio')->orWhere('data_inizio', '<=', $today);
|
||||
})
|
||||
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($today) {
|
||||
$q->whereNull('data_fine')->orWhere('data_fine', '>=', $today);
|
||||
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($startOfYear) {
|
||||
$q->whereNull('data_fine')->orWhere('data_fine', '>=', $startOfYear);
|
||||
})
|
||||
->orderByDesc('is_preferito')
|
||||
->orderByDesc('data_inizio')
|
||||
|
|
@ -2694,13 +2722,18 @@ protected function hydrateRelazioni(): void
|
|||
->limit(200)
|
||||
->get()
|
||||
->filter(fn(RubricaRuolo $relazione) => (bool) $relazione->contatto)
|
||||
->map(function (RubricaRuolo $relazione) use ($formatDate, $labelsRuoli) {
|
||||
->map(function (RubricaRuolo $relazione) use ($formatDate, $labelsRuoli, $cfDatesMap) {
|
||||
$contatto = $relazione->contatto;
|
||||
$nome = trim((string) ($contatto?->nome_completo ?? ''));
|
||||
if ($nome === '') {
|
||||
$nome = $contatto ? 'Contatto #' . $contatto->id : 'Contatto non definito';
|
||||
}
|
||||
|
||||
$cf = strtoupper(trim((string) ($contatto?->codice_fiscale ?? '')));
|
||||
$purDates = $cf !== '' ? ($cfDatesMap[$cf] ?? null) : null;
|
||||
$dInizioRaw = $relazione->data_inizio ?: ($purDates['data_inizio'] ?? null);
|
||||
$dFineRaw = $relazione->data_fine ?: ($purDates['data_fine'] ?? null);
|
||||
|
||||
$tipoRaw = strtolower(trim((string) ($relazione->ruolo_standard ?? '')));
|
||||
$customRaw = trim((string) ($relazione->ruolo_custom ?? ''));
|
||||
$hasMeaningfulCustomRole = $customRaw !== '' && ! in_array(strtolower($customRaw), ['p', 'i', 'c', 'u'], true);
|
||||
|
|
@ -2740,12 +2773,12 @@ protected function hydrateRelazioni(): void
|
|||
'ruolo_rate' => $ruoloRate,
|
||||
'quota' => $quota,
|
||||
'quota_label' => $quota !== null ? number_format($quota, 2, ',', '.') : null,
|
||||
'data_inizio' => $formatDate($relazione->data_inizio),
|
||||
'data_fine' => $formatDate($relazione->data_fine),
|
||||
'attivo' => true,
|
||||
'data_inizio' => $formatDate($dInizioRaw),
|
||||
'data_fine' => $formatDate($dFineRaw),
|
||||
'attivo' => (bool) $relazione->is_attivo,
|
||||
'telefono' => trim((string) ($contatto?->telefono_cellulare ?: $contatto?->telefono_ufficio ?: $contatto?->telefono_casa ?: '')),
|
||||
'email' => trim((string) ($contatto?->email ?? '')),
|
||||
'rubrica_url' => $contatto ? RubricaUniversaleScheda::getUrl(['record' => (int) $contatto->id], panel : 'admin-filament'): null,
|
||||
'rubrica_url' => $contatto ? RubricaUniversaleScheda::getUrl(['record' => (int) $contatto->id], panel: 'admin-filament'): null,
|
||||
'riceve_comunicazioni' => false,
|
||||
'riceve_convocazioni' => false,
|
||||
'vota_assemblea' => false,
|
||||
|
|
@ -2784,8 +2817,8 @@ protected function hydrateRelazioni(): void
|
|||
'ruolo_rate' => $relazione->ruolo_rate ?: PersonaUnitaRelazione::deriveRuoloRate($relazione->tipo_relazione),
|
||||
'quota' => $quota,
|
||||
'quota_label' => $quota !== null ? number_format($quota, 2, ',', '.') : null,
|
||||
'data_inizio' => $formatDate($relazione->data_inizio),
|
||||
'data_fine' => $formatDate($relazione->data_fine),
|
||||
'data_inizio' => $formatDate($relazione->data_inizio ?: $relazione->subentrato_dal),
|
||||
'data_fine' => $formatDate($relazione->data_fine ?: $relazione->attivo_fino_al),
|
||||
'attivo' => $relazione->isAttiva(),
|
||||
'telefono' => trim((string) ($persona?->telefono ?? '')),
|
||||
'email' => trim((string) ($persona?->email ?? '')),
|
||||
|
|
@ -2796,21 +2829,21 @@ protected function hydrateRelazioni(): void
|
|||
];
|
||||
});
|
||||
|
||||
$relazioniKeys = $relazioniMapped
|
||||
$fallbackKeys = $fallbackRelations
|
||||
->map(fn(array $rel) => strtolower(trim((string) ($rel['tipo_raw'] ?? ''))) . '|' . $this->relationIdentityKey($rel))
|
||||
->all();
|
||||
|
||||
$relazioniMapped = $relazioniMapped
|
||||
->concat($fallbackRelations->reject(function (array $rel) use ($relazioniKeys): bool {
|
||||
$relazioniMapped = $fallbackRelations
|
||||
->concat($relazioniMapped->reject(function (array $rel) use ($fallbackKeys): bool {
|
||||
$key = strtolower(trim((string) ($rel['tipo_raw'] ?? ''))) . '|' . $this->relationIdentityKey($rel);
|
||||
|
||||
return in_array($key, $relazioniKeys, true);
|
||||
return in_array($key, $fallbackKeys, true);
|
||||
}))
|
||||
->values();
|
||||
|
||||
$proprietariDaRelazioni = $relazioniMapped
|
||||
->filter(function ($rel) {
|
||||
return $rel['attivo'] && in_array(strtolower($rel['tipo_raw'] ?? ''), [
|
||||
->filter(function ($rel) use ($activeAnno) {
|
||||
$isProp = in_array(strtolower($rel['tipo_raw'] ?? ''), [
|
||||
'condomino',
|
||||
'proprietario',
|
||||
'comproprietario',
|
||||
|
|
@ -2818,7 +2851,14 @@ protected function hydrateRelazioni(): void
|
|||
'usufruttuario',
|
||||
'usufrutto',
|
||||
], true);
|
||||
|
||||
if (! $isProp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isRelazioneAttivaNelAnno($rel, $activeAnno);
|
||||
})
|
||||
->sortByDesc(fn(array $r) => ($r['attivo'] ? 1 : 0))
|
||||
->unique(fn(array $r) => $this->relationIdentityKey($r))
|
||||
->values();
|
||||
|
||||
|
|
@ -2827,13 +2867,20 @@ protected function hydrateRelazioni(): void
|
|||
}
|
||||
|
||||
$inquilini = $relazioniMapped
|
||||
->filter(function ($rel) {
|
||||
return $rel['attivo'] && in_array(strtolower($rel['tipo_raw'] ?? ''), [
|
||||
->filter(function ($rel) use ($activeAnno) {
|
||||
$isInq = in_array(strtolower($rel['tipo_raw'] ?? ''), [
|
||||
'inquilino',
|
||||
'locatario',
|
||||
'conduttore',
|
||||
], true);
|
||||
|
||||
if (! $isInq) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isRelazioneAttivaNelAnno($rel, $activeAnno);
|
||||
})
|
||||
->sortByDesc(fn(array $r) => ($r['attivo'] ? 1 : 0))
|
||||
->unique(fn(array $r) => $this->relationIdentityKey($r))
|
||||
->values();
|
||||
|
||||
|
|
@ -2907,126 +2954,69 @@ protected function hydrateRelazioni(): void
|
|||
})->values();
|
||||
}
|
||||
|
||||
$existingKeys = $proprietari->concat($inquilini)
|
||||
->map(fn(array $r) => $this->relationIdentityKey($r))
|
||||
->all();
|
||||
|
||||
$altri = $relazioniMapped
|
||||
->filter(function ($rel) {
|
||||
return $rel['attivo'] && (
|
||||
! in_array(strtolower($rel['tipo_raw'] ?? ''), [
|
||||
'condomino',
|
||||
'proprietario',
|
||||
'comproprietario',
|
||||
'nudo_proprietario',
|
||||
'usufruttuario',
|
||||
'usufrutto',
|
||||
'inquilino',
|
||||
'locatario',
|
||||
'conduttore',
|
||||
], true)
|
||||
|| ! empty($rel['has_custom_role'])
|
||||
);
|
||||
->filter(function ($rel) use ($existingKeys) {
|
||||
$key = $this->relationIdentityKey($rel);
|
||||
if (in_array($key, $existingKeys, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tipoRaw = strtolower(trim((string) ($rel['tipo_raw'] ?? '')));
|
||||
if (in_array($tipoRaw, [
|
||||
'condomino',
|
||||
'proprietario',
|
||||
'comproprietario',
|
||||
'nudo_proprietario',
|
||||
'usufruttuario',
|
||||
'usufrutto',
|
||||
'inquilino',
|
||||
'locatario',
|
||||
'conduttore',
|
||||
], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) ($rel['attivo'] ?? false);
|
||||
})
|
||||
->unique(fn(array $r) => strtolower((string) ($r['tipo_raw'] ?? 'altro')) . '|' . strtolower((string) ($r['ruolo_custom'] ?? '')) . '|' . (int) ($r['persona_id'] ?? 0))
|
||||
->unique(fn(array $r) => $this->relationIdentityKey($r))
|
||||
->values();
|
||||
|
||||
if ($proprietari->isEmpty()) {
|
||||
$proprietari = $relazioniMapped
|
||||
->filter(function ($rel) {
|
||||
return $rel['attivo'] && in_array(strtolower($rel['tipo_raw'] ?? ''), [
|
||||
'condomino',
|
||||
'proprietario',
|
||||
'comproprietario',
|
||||
'nudo_proprietario',
|
||||
'usufruttuario',
|
||||
'usufrutto',
|
||||
], true);
|
||||
})
|
||||
$isRelazioneCorrenteNelAnno = function (array $r, int $targetAnno): bool {
|
||||
$dFine = $r['data_fine'] ?? null;
|
||||
$endY = null;
|
||||
if (! empty($dFine) && preg_match('/(\d{4})/', (string) $dFine, $m)) {
|
||||
$endY = (int) $m[1];
|
||||
}
|
||||
if ($endY !== null && $endY < $targetAnno) {
|
||||
return false;
|
||||
}
|
||||
if ($endY !== null && $endY === $targetAnno && ! ($r['attivo'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
$relazioniStoriche = [];
|
||||
if (! $this->mostraStorico) {
|
||||
$relazioniStoriche = $proprietari->filter(fn($r) => ! $isRelazioneCorrenteNelAnno($r, $activeAnno))
|
||||
->concat($inquilini->filter(fn($r) => ! $isRelazioneCorrenteNelAnno($r, $activeAnno)))
|
||||
->unique(fn(array $r) => $this->relationIdentityKey($r))
|
||||
->values();
|
||||
}
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$legacyOwnerStartC = $this->parseLegacyDateToCarbon($legacyOwnerStart);
|
||||
$legacyOwnerEndC = $this->parseLegacyDateToCarbon($legacyOwnerEnd);
|
||||
$legacyOwnerNewStartC = $legacyOwnerEndC ? $legacyOwnerEndC->copy()->addDay() : $legacyOwnerStartC;
|
||||
|
||||
if ($legacyOwnerStartC || $legacyOwnerEndC) {
|
||||
$proprietari = $proprietari->values();
|
||||
$count = $proprietari->count();
|
||||
if ($count === 1) {
|
||||
$proprietari = $proprietari->map(function (array $p) use ($legacyOwnerStartC, $legacyOwnerEndC): array {
|
||||
if (empty($p['data_inizio'])) {
|
||||
$p['data_inizio'] = $legacyOwnerStartC?->format('d/m/Y') ?? null;
|
||||
}
|
||||
if (empty($p['data_fine'])) {
|
||||
$p['data_fine'] = $legacyOwnerEndC?->format('d/m/Y') ?? null;
|
||||
}
|
||||
return $p;
|
||||
});
|
||||
} elseif ($count >= 2) {
|
||||
$idxNew = 0;
|
||||
if ($legacyOwnerName !== '') {
|
||||
foreach ($proprietari as $idx => $p) {
|
||||
$nome = trim((string) ($p['nome'] ?? ''));
|
||||
if ($nome !== '' && str_contains(mb_strtolower($nome), mb_strtolower($legacyOwnerName))) {
|
||||
$idxNew = $idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$idxOld = $idxNew === 0 ? ($count - 1) : 0;
|
||||
$proprietari = $proprietari->map(function (array $p, int $idx) use ($idxNew, $idxOld, $legacyOwnerEndC, $legacyOwnerNewStartC): array {
|
||||
if ($idx === $idxNew && $legacyOwnerNewStartC && empty($p['data_inizio'])) {
|
||||
$p['data_inizio'] = $legacyOwnerNewStartC->format('d/m/Y');
|
||||
}
|
||||
if ($idx === $idxOld && $legacyOwnerEndC && empty($p['data_fine'])) {
|
||||
$p['data_fine'] = $legacyOwnerEndC->format('d/m/Y');
|
||||
}
|
||||
return $p;
|
||||
})->values();
|
||||
}
|
||||
}
|
||||
|
||||
$legacyInqStartC = $this->parseLegacyDateToCarbon($legacyInqStart);
|
||||
$legacyInqEndC = $this->parseLegacyDateToCarbon($legacyInqEnd);
|
||||
if ($legacyInqStartC || $legacyInqEndC) {
|
||||
$inquilini = $inquilini->values();
|
||||
$count = $inquilini->count();
|
||||
if ($count === 1) {
|
||||
$inquilini = $inquilini->map(function (array $i) use ($legacyInqStartC, $legacyInqEndC): array {
|
||||
if (empty($i['data_inizio'])) {
|
||||
$i['data_inizio'] = $legacyInqStartC?->format('d/m/Y') ?? null;
|
||||
}
|
||||
if (empty($i['data_fine'])) {
|
||||
$i['data_fine'] = $legacyInqEndC?->format('d/m/Y') ?? null;
|
||||
}
|
||||
return $i;
|
||||
});
|
||||
} elseif ($count >= 2) {
|
||||
$idxNew = 0;
|
||||
if ($legacyInqName !== '') {
|
||||
foreach ($inquilini as $idx => $i) {
|
||||
$nome = trim((string) ($i['nome'] ?? ''));
|
||||
if ($nome !== '' && str_contains(mb_strtolower($nome), mb_strtolower($legacyInqName))) {
|
||||
$idxNew = $idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$idxOld = $idxNew === 0 ? ($count - 1) : 0;
|
||||
$inquilini = $inquilini->map(function (array $i, int $idx) use ($idxNew, $idxOld, $legacyInqEndC, $legacyInqStartC): array {
|
||||
if ($idx === $idxNew && $legacyInqStartC && empty($i['data_inizio'])) {
|
||||
$i['data_inizio'] = $legacyInqStartC->format('d/m/Y');
|
||||
}
|
||||
if ($idx === $idxOld && $legacyInqEndC && empty($i['data_fine'])) {
|
||||
$i['data_fine'] = $legacyInqEndC->format('d/m/Y');
|
||||
}
|
||||
return $i;
|
||||
})->values();
|
||||
}
|
||||
$proprietari = $proprietari->filter(fn($r) => $isRelazioneCorrenteNelAnno($r, $activeAnno))->values();
|
||||
$inquilini = $inquilini->filter(fn($r) => $isRelazioneCorrenteNelAnno($r, $activeAnno))->values();
|
||||
}
|
||||
|
||||
$this->relazioniPerTipo = [
|
||||
'proprietari' => $proprietari->values()->all(),
|
||||
'inquilini' => $inquilini->values()->all(),
|
||||
'altri' => $altri->values()->all(),
|
||||
'storico' => $relazioniStoriche,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -3043,6 +3033,45 @@ private function ownershipTipoDiritti(): array
|
|||
];
|
||||
}
|
||||
|
||||
private function isRelazioneAttivaNelAnno(array $rel, int $targetAnno): bool
|
||||
{
|
||||
$note = (string) ($rel['note_relazione'] ?? '');
|
||||
if (! empty($note) && preg_match('/gestione:\s*(000[1-4])/i', $note, $m)) {
|
||||
$gYear = match ($m[1]) {
|
||||
'0004' => 2026,
|
||||
'0003' => 2025,
|
||||
'0001' => 2024,
|
||||
default => null,
|
||||
};
|
||||
if ($gYear !== null && $gYear !== $targetAnno) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$dInizio = $rel['data_inizio'] ?? null;
|
||||
$dFine = $rel['data_fine'] ?? null;
|
||||
|
||||
$startY = null;
|
||||
if (! empty($dInizio) && preg_match('/(\d{4})/', (string) $dInizio, $m)) {
|
||||
$startY = (int) $m[1];
|
||||
}
|
||||
|
||||
$endY = null;
|
||||
if (! empty($dFine) && preg_match('/(\d{4})/', (string) $dFine, $m)) {
|
||||
$endY = (int) $m[1];
|
||||
}
|
||||
|
||||
if ($startY !== null && $startY > $targetAnno) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($endY !== null && $endY < $targetAnno) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function extractLegacyRoleFromRateNote(string $note): ?string
|
||||
{
|
||||
$note = trim($note);
|
||||
|
|
@ -3158,7 +3187,8 @@ private function getLegacyCondominRow(): ?object
|
|||
return $this->legacyCondominRow = null;
|
||||
}
|
||||
|
||||
if (! DbSchema::connection('gescon_import')->hasTable('condomin')) {
|
||||
$tableName = DbSchema::connection('gescon_import')->hasTable('condomin_mirror') ? 'condomin_mirror' : 'condomin';
|
||||
if (! DbSchema::connection('gescon_import')->hasTable($tableName)) {
|
||||
return $this->legacyCondominRow = null;
|
||||
}
|
||||
|
||||
|
|
@ -3178,8 +3208,18 @@ private function getLegacyCondominRow(): ?object
|
|||
return $this->legacyCondominRow = null;
|
||||
}
|
||||
|
||||
$activeAnno = \App\Support\AnnoGestioneContext::resolveActiveAnno(\Illuminate\Support\Facades\Auth::user());
|
||||
$legacyYear = match ($activeAnno) {
|
||||
2026 => '0004',
|
||||
2025 => '0003',
|
||||
2024 => '0001',
|
||||
default => sprintf('%04d', $activeAnno),
|
||||
};
|
||||
|
||||
$yearCol = DbSchema::connection('gescon_import')->hasColumn($tableName, 'source_year') ? 'source_year' : 'legacy_year';
|
||||
|
||||
$query = DB::connection('gescon_import')
|
||||
->table('condomin')
|
||||
->table($tableName)
|
||||
->where('cod_stabile', $codStabile)
|
||||
->where('interno', $interno);
|
||||
|
||||
|
|
@ -3187,6 +3227,10 @@ private function getLegacyCondominRow(): ?object
|
|||
$query->where('scala', $scala);
|
||||
}
|
||||
|
||||
if (DbSchema::connection('gescon_import')->hasColumn($tableName, $yearCol)) {
|
||||
$query->where($yearCol, $legacyYear);
|
||||
}
|
||||
|
||||
$desiredCols = [
|
||||
'legacy_id_cond',
|
||||
'id_cond',
|
||||
|
|
@ -3200,12 +3244,13 @@ private function getLegacyCondominRow(): ?object
|
|||
'cond_cod_fisc',
|
||||
'inquil_nome',
|
||||
'inquil_cod_fisc',
|
||||
'source_year',
|
||||
'legacy_year',
|
||||
];
|
||||
$actualCols = \Illuminate\Support\Facades\Schema::connection('gescon_import')->getColumnListing('condomin');
|
||||
$actualCols = \Illuminate\Support\Facades\Schema::connection('gescon_import')->getColumnListing($tableName);
|
||||
$selectCols = array_intersect($desiredCols, $actualCols);
|
||||
|
||||
$row = $query
|
||||
->orderByDesc('legacy_year')
|
||||
->orderByDesc('id')
|
||||
->first($selectCols);
|
||||
|
||||
|
|
@ -3993,14 +4038,33 @@ protected function popolaCanaliComunicazione(): void
|
|||
$overlayData = json_decode(\Illuminate\Support\Facades\Storage::disk('public')->get($overlayFile), true) ?: [];
|
||||
}
|
||||
|
||||
$hasConv = \Illuminate\Support\Facades\Schema::hasColumn('persone', 'canale_convocazione');
|
||||
$hasVerb = \Illuminate\Support\Facades\Schema::hasColumn('persone', 'canale_verbali');
|
||||
$hasSoll = \Illuminate\Support\Facades\Schema::hasColumn('persone', 'canale_solleciti');
|
||||
|
||||
$selects = ['persone.*'];
|
||||
if (! $hasConv) {
|
||||
$selects[] = DB::raw('NULL as canale_convocazione');
|
||||
}
|
||||
if (! $hasVerb) {
|
||||
$selects[] = DB::raw('NULL as canale_verbali');
|
||||
}
|
||||
if (! $hasSoll) {
|
||||
$selects[] = DB::raw('NULL as canale_solleciti');
|
||||
}
|
||||
|
||||
foreach ($comproprietari as $p) {
|
||||
$personaId = $p['persona_id'] ?? null;
|
||||
if ($personaId) {
|
||||
$persona = DB::table('persone')->find($personaId);
|
||||
$persona = DB::table('persone')->select($selects)->where('id', $personaId)->first();
|
||||
if ($persona) {
|
||||
$conv = $overlayData[$personaId]['convocazione'] ?? $persona->canale_convocazione ?: 'Raccomandata AR';
|
||||
$verb = $overlayData[$personaId]['verbali'] ?? $persona->canale_verbali ?: 'Raccomandata AR';
|
||||
$soll = $overlayData[$personaId]['solleciti'] ?? $persona->canale_solleciti ?: 'PEC';
|
||||
$convRaw = $persona->canale_convocazione ?? null;
|
||||
$verbRaw = $persona->canale_verbali ?? null;
|
||||
$sollRaw = $persona->canale_solleciti ?? null;
|
||||
|
||||
$conv = $overlayData[$personaId]['convocazione'] ?? ($convRaw !== null && $convRaw !== '' ? $convRaw : 'Raccomandata AR');
|
||||
$verb = $overlayData[$personaId]['verbali'] ?? ($verbRaw !== null && $verbRaw !== '' ? $verbRaw : 'Raccomandata AR');
|
||||
$soll = $overlayData[$personaId]['solleciti'] ?? ($sollRaw !== null && $sollRaw !== '' ? $sollRaw : 'PEC');
|
||||
|
||||
$this->canaliComunicazione[$personaId] = [
|
||||
'id' => $personaId,
|
||||
|
|
@ -4024,12 +4088,15 @@ public function salvaCanaleComunicazione(int $personaId, string $campo, string $
|
|||
];
|
||||
|
||||
if (isset($fieldMap[$campo])) {
|
||||
DB::table('persone')
|
||||
->where('id', $personaId)
|
||||
->update([
|
||||
$fieldMap[$campo] => $valore,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$dbCol = $fieldMap[$campo];
|
||||
if (\Illuminate\Support\Facades\Schema::hasColumn('persone', $dbCol)) {
|
||||
DB::table('persone')
|
||||
->where('id', $personaId)
|
||||
->update([
|
||||
$dbCol => $valore,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (isset($this->canaliComunicazione[$personaId])) {
|
||||
$this->canaliComunicazione[$personaId][$campo] = $valore;
|
||||
|
|
|
|||
|
|
@ -128,7 +128,39 @@ public function table(Table $table): Table
|
|||
TextColumn::make('telefono_ufficio')->label('Tel.')->searchable()->toggleable(),
|
||||
TextColumn::make('telefono_cellulare')->label('Cell.')->searchable()->toggleable(),
|
||||
TextColumn::make('telefono_casa')->label('Tel. casa')->searchable()->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('stato')->label('Stato')->sortable()->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('stato')->label('Stato')
|
||||
->sortable()
|
||||
->badge()
|
||||
->color(fn (?string $state): string => match ($state) {
|
||||
'attivo' => 'success',
|
||||
'inattivo', 'archiviato_duplicato', 'inattivo_duplicato' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->toggleable(),
|
||||
TextColumn::make('note')->label('Note / Audit')->wrap()->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
\Filament\Tables\Filters\SelectFilter::make('vista_stato')
|
||||
->label('Vista Anagrafica')
|
||||
->options([
|
||||
'attivi' => 'Contatti attivi (Canonica)',
|
||||
'storico' => 'Storico & bonifiche audit',
|
||||
'tutti' => 'Tutti i contatti',
|
||||
])
|
||||
->default('attivi')
|
||||
->query(function (Builder $query, array $data): Builder {
|
||||
$value = $data['value'] ?? 'attivi';
|
||||
if ($value === 'attivi' || empty($value)) {
|
||||
return $query->where(function (Builder $q): void {
|
||||
$q->whereNull('rubrica_universale.stato')
|
||||
->orWhereNotIn('rubrica_universale.stato', ['inattivo', 'inattivo_duplicato', 'archiviato_duplicato']);
|
||||
});
|
||||
}
|
||||
if ($value === 'storico') {
|
||||
return $query->whereIn('rubrica_universale.stato', ['inattivo', 'inattivo_duplicato', 'archiviato_duplicato']);
|
||||
}
|
||||
return $query;
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
Action::make('modifica')
|
||||
|
|
@ -180,6 +212,25 @@ public function table(Table $table): Table
|
|||
->label('Apri scheda')
|
||||
->icon('heroicon-o-identification')
|
||||
->url(fn(RubricaUniversale $record) => RubricaUniversaleScheda::getUrl(['record' => $record->id], panel: 'admin-filament')),
|
||||
|
||||
Action::make('elimina')
|
||||
->label('Elimina')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Conferma eliminazione contatto')
|
||||
->modalDescription('Cancellazione protetta: la rimozione fisica verrà bloccata se il contatto ha relazioni attive o storiche.')
|
||||
->action(function (RubricaUniversale $record): void {
|
||||
try {
|
||||
$record->delete();
|
||||
} catch (\Throwable $e) {
|
||||
\Filament\Notifications\Notification::make()
|
||||
->title('Operazione bloccata')
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
use Illuminate\Database\Eloquent\Relations\HasMany; // Aggiunto per condomini()
|
||||
use Illuminate\Database\Eloquent\SoftDeletes; // Aggiunto per soft deletes
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
|
|
|
|||
|
|
@ -333,6 +333,15 @@ protected static function boot()
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
static::deleting(function ($persona) {
|
||||
$hasRelazioni = \Illuminate\Support\Facades\DB::table('persone_unita_relazioni')->where('persona_id', $persona->id)->exists();
|
||||
$hasRappresentanti = \Illuminate\Support\Facades\DB::table('rappresentanti_legali')->where('persona_id', $persona->id)->orWhere('societa_id', $persona->id)->exists();
|
||||
|
||||
if ($hasRelazioni || $hasRappresentanti) {
|
||||
throw new \Exception("Cancellazione fisica bloccata: la persona ha relazioni attive o storiche con unità immobiliari o società.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ class PersonaUnitaRelazione extends Model
|
|||
'riceve_convocazioni',
|
||||
'vota_assemblea',
|
||||
'note_relazione',
|
||||
'id_cond',
|
||||
'cod_cond',
|
||||
'provenance',
|
||||
'subentrato_dal',
|
||||
'attivo_fino_al',
|
||||
'subentro_prima_cera',
|
||||
'subentro_adesso_ce',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
|
|
|||
|
|
@ -164,4 +164,24 @@ public function scopeRicerca($query, $termine)
|
|||
->orWhere('telefono_cellulare', 'like', "%{$termine}%");
|
||||
});
|
||||
}
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::deleting(function ($rubrica) {
|
||||
$isForce = method_exists($rubrica, 'isForceDeleting') ? $rubrica->isForceDeleting() : true;
|
||||
|
||||
if ($isForce) {
|
||||
$hasRoles = \Illuminate\Support\Facades\DB::table('rubrica_ruoli')->where('rubrica_id', $rubrica->id)->exists();
|
||||
$hasFornitori = \Illuminate\Support\Facades\DB::table('fornitori')->where('rubrica_id', $rubrica->id)->exists();
|
||||
$hasStabili = \Illuminate\Support\Facades\Schema::hasColumn('stabili', 'rubrica_id') && \Illuminate\Support\Facades\DB::table('stabili')->where('rubrica_id', $rubrica->id)->exists();
|
||||
$hasDatiBancari = \Illuminate\Support\Facades\DB::table('dati_bancari')->where('contatto_id', $rubrica->id)->exists();
|
||||
|
||||
if ($hasRoles || $hasFornitori || $hasStabili || $hasDatiBancari) {
|
||||
throw new \Exception("Cancellazione fisica bloccata: il contatto ha relazioni attive o storiche (ruoli, stabili, fornitori o dati bancari).");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ public function panel(Panel $panel): Panel
|
|||
->brandName('NetGescon')
|
||||
->spa()
|
||||
->login(\App\Filament\Auth\Login::class)
|
||||
->passwordReset()
|
||||
->registration()
|
||||
->colors([
|
||||
'primary' => Color::Amber,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public function up(): void
|
|||
return;
|
||||
}
|
||||
|
||||
DB::statement("ALTER TABLE `dati_bancari` MODIFY COLUMN `tipo_conto` ENUM('corrente','deposito','risparmio','cassa') NOT NULL DEFAULT 'corrente'");
|
||||
DB::statement("ALTER TABLE `dati_bancari` MODIFY COLUMN `tipo_conto` ENUM('corrente','deposito','risparmio','cassa','posta','postale') NOT NULL DEFAULT 'corrente'");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('persone_unita_relazioni')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('persone_unita_relazioni', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('persone_unita_relazioni', 'id_cond')) {
|
||||
$table->string('id_cond', 50)->nullable()->index();
|
||||
}
|
||||
if (! Schema::hasColumn('persone_unita_relazioni', 'cod_cond')) {
|
||||
$table->string('cod_cond', 50)->nullable()->index();
|
||||
}
|
||||
if (! Schema::hasColumn('persone_unita_relazioni', 'provenance')) {
|
||||
$table->string('provenance', 255)->nullable();
|
||||
}
|
||||
if (! Schema::hasColumn('persone_unita_relazioni', 'subentrato_dal')) {
|
||||
$table->string('subentrato_dal', 50)->nullable();
|
||||
}
|
||||
if (! Schema::hasColumn('persone_unita_relazioni', 'attivo_fino_al')) {
|
||||
$table->string('attivo_fino_al', 50)->nullable();
|
||||
}
|
||||
if (! Schema::hasColumn('persone_unita_relazioni', 'subentro_prima_cera')) {
|
||||
$table->string('subentro_prima_cera', 255)->nullable();
|
||||
}
|
||||
if (! Schema::hasColumn('persone_unita_relazioni', 'subentro_adesso_ce')) {
|
||||
$table->string('subentro_adesso_ce', 255)->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('persone_unita_relazioni')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('persone_unita_relazioni', function (Blueprint $table) {
|
||||
$columnsToDrop = [];
|
||||
foreach (['id_cond', 'cod_cond', 'provenance', 'subentrato_dal', 'attivo_fino_al', 'subentro_prima_cera', 'subentro_adesso_ce'] as $col) {
|
||||
if (Schema::hasColumn('persone_unita_relazioni', $col)) {
|
||||
$columnsToDrop[] = $col;
|
||||
}
|
||||
}
|
||||
if (! empty($columnsToDrop)) {
|
||||
$table->dropColumn($columnsToDrop);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<input
|
||||
type="text"
|
||||
id="sidebar-menu-search"
|
||||
placeholder="Cerca menù..."
|
||||
placeholder="Cerca menù (es. anagrafica, nominativi, unità)..."
|
||||
class="w-full text-xs rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 px-3 py-1.5 focus:border-amber-400 focus:ring-1 focus:ring-amber-400 focus:outline-none dark:text-gray-100 shadow-sm transition-all"
|
||||
/>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-gray-400">
|
||||
|
|
@ -15,29 +15,42 @@ class="w-full text-xs rounded-lg border border-gray-200 dark:border-gray-800 bg-
|
|||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const searchInput = document.getElementById('sidebar-menu-search');
|
||||
if (!searchInput) return;
|
||||
(function () {
|
||||
const customKeywords = {
|
||||
'anagrafica': ['anagrafica unica', 'anagrafica', 'rubrica', 'persone'],
|
||||
'nominativi': ['nominativi', 'condomini', 'inquilini', 'proprietari'],
|
||||
'unita': ['unità immobiliare', 'scheda unità', 'selettore unità', 'unita-immobiliare'],
|
||||
'fornitori': ['fornitori', 'gestione fornitori', 'archivio fornitori']
|
||||
};
|
||||
|
||||
// Trova il contenitore navigazione della sidebar per memorizzare e gestire lo scroll
|
||||
const sidebarNav = document.querySelector('.fi-sidebar-nav');
|
||||
|
||||
searchInput.addEventListener('input', function (e) {
|
||||
const query = e.target.value.toLowerCase().trim();
|
||||
|
||||
// Salva la posizione dello scroll prima del filtro
|
||||
function filterSidebarMenu(query) {
|
||||
query = (query || '').toLowerCase().trim();
|
||||
const sidebarNav = document.querySelector('.fi-sidebar-nav');
|
||||
const scrollPos = sidebarNav ? sidebarNav.scrollTop : 0;
|
||||
|
||||
// Seleziona tutti i gruppi di navigazione di Filament
|
||||
const groups = document.querySelectorAll('.fi-sidebar-group');
|
||||
|
||||
|
||||
groups.forEach(group => {
|
||||
const items = group.querySelectorAll('.fi-sidebar-item');
|
||||
let hasVisibleItems = false;
|
||||
|
||||
items.forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
if (text.includes(query)) {
|
||||
const href = (item.querySelector('a')?.getAttribute('href') || '').toLowerCase();
|
||||
|
||||
let isMatch = query === '' || text.includes(query) || href.includes(query);
|
||||
|
||||
if (!isMatch && query.length >= 2) {
|
||||
for (const [key, synonyms] of Object.entries(customKeywords)) {
|
||||
if (key.includes(query) || query.includes(key)) {
|
||||
if (synonyms.some(s => text.includes(s) || href.includes(s))) {
|
||||
isMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isMatch) {
|
||||
item.style.display = '';
|
||||
hasVisibleItems = true;
|
||||
} else {
|
||||
|
|
@ -45,7 +58,6 @@ class="w-full text-xs rounded-lg border border-gray-200 dark:border-gray-800 bg-
|
|||
}
|
||||
});
|
||||
|
||||
// Se il gruppo non ha voci visibili (e c'è una query attiva), nascondi l'intero gruppo
|
||||
if (query !== '' && !hasVisibleItems) {
|
||||
group.style.display = 'none';
|
||||
} else {
|
||||
|
|
@ -53,10 +65,31 @@ class="w-full text-xs rounded-lg border border-gray-200 dark:border-gray-800 bg-
|
|||
}
|
||||
});
|
||||
|
||||
// Ripristina lo scroll per impedire alla sidebar di saltare
|
||||
if (sidebarNav) {
|
||||
sidebarNav.scrollTop = scrollPos;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('input', function (e) {
|
||||
if (e.target && e.target.id === 'sidebar-menu-search') {
|
||||
filterSidebarMenu(e.target.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', function (e) {
|
||||
if (e.target && e.target.id === 'sidebar-menu-search') {
|
||||
filterSidebarMenu(e.target.value);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize on DOM Ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const searchInput = document.getElementById('sidebar-menu-search');
|
||||
if (searchInput && searchInput.value) {
|
||||
filterSidebarMenu(searchInput.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,14 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
|
|||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs font-medium text-gray-500">Visibilità:</span>
|
||||
<span class="text-xs font-medium text-gray-500">Vista:</span>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="toggleMostraStorico"
|
||||
class="rounded-lg border px-3 py-2 text-xs font-semibold transition {{ $mostraStorico ? 'border-amber-300 bg-amber-50 text-amber-700 shadow-sm' : 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50' }}">
|
||||
{{ $mostraStorico ? 'Mostra storico (Attivo)' : 'Solo gestione selezionata' }}
|
||||
</button>
|
||||
<span class="text-xs font-medium text-gray-500 ml-2">Visibilità:</span>
|
||||
@foreach($visibilityOptions as $key => $label)
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,41 +1,45 @@
|
|||
# CURRENT-205
|
||||
|
||||
TASK_ID: task-25d2e33f8d
|
||||
TASK_ID: task-d095b0e23b
|
||||
MACHINE: .205
|
||||
STATO: completato
|
||||
|
||||
## Obiettivo
|
||||
|
||||
Creare una nuova tabella staging mirror lossless (`gescon_import.condomin_mirror`) e il relativo comando Artisan dedicato (`gescon:import-mirror-0021`) per lo stabile pilot 0021 in modalità append-only senza deduplica né sovrascritture degli esercizi storici.
|
||||
Hotfix: Anagrafica Unica canonica, storico audit e CRUD protetto.
|
||||
|
||||
## Output del Giro Operativo
|
||||
|
||||
ESITO_205: riuscito
|
||||
TASK_ID: task-25d2e33f8d
|
||||
TASK_ID: task-d095b0e23b
|
||||
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
|
||||
BRANCH: stabilization/205-zero
|
||||
COMMIT: 4c99f5e87caeaba77a3b54210071d6e195a55b2f
|
||||
COMMIT: ae8f54dc364e930c5e13bfd73f3a4b1d2737aaf0
|
||||
FILE_O_AREE_TOCCATE:
|
||||
- database/migrations/2026_08_10_100000_create_gescon_import_condomin_mirror_table.php
|
||||
- app/Console/Commands/ImportCondominMirror0021.php
|
||||
- tests/Feature/ImportCondominMirror0021Test.php
|
||||
- scripts/ops/antigravity-cli/run_205_followup_via_agy.sh
|
||||
- app/Livewire/Gescon/RubricaUniversaleTable.php
|
||||
- app/Models/RubricaUniversale.php
|
||||
- app/Models/Persona.php
|
||||
- tests/Feature/AnagraficaUnicaCanonicaTest.php
|
||||
- app/Models/Amministratore.php
|
||||
- app/Console/Commands/MapGesconStabiliCommand.php
|
||||
- app/Providers/Filament/AdminFilamentPanelProvider.php
|
||||
- database/migrations/2025_11_16_120200_add_cassa_type_to_dati_bancari.php
|
||||
TEST_ESEGUITI:
|
||||
- ./vendor/bin/pest tests/Feature/ImportCondominMirror0021Test.php (1 passed, 11 assertions)
|
||||
- ./vendor/bin/pest tests/Feature/ControlTowerPollCommandTest.php (5 passed, 17 assertions)
|
||||
- ./vendor/bin/pest tests/Feature/AnagraficaUnicaCanonicaTest.php (3 passed, 10 assertions)
|
||||
- ./vendor/bin/pest tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/AnagraficaFornitoriSyncTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/ReconstructMirror0021Test.php tests/Feature/ImportCondominMirror0021Test.php tests/Feature/ControlTowerPollCommandTest.php (17 passed, 129 assertions)
|
||||
- ./scripts/ops/antigravity-cli/test_runner_and_parser.sh (6/6 passed)
|
||||
GATE_STATISTICS:
|
||||
- MDB_ROWS_TOTAL: 644
|
||||
- STAGING_ROWS: 644
|
||||
- COLLISION_COUNT: 0
|
||||
- DUPLICATE_PROVENANCE_COUNT: 0
|
||||
- SECOND_RUN_COUNTS: 644
|
||||
- DANIELA_BENEDETTO_CANONICAL_RUBRICA: 697 (000000JC / BNDDNL86M58H224O)
|
||||
- DANIELA_BENEDETTO_ARCHIVED_DUPLICATES: 4 (000000I2, 000000I3, 000000R0, 000000R1)
|
||||
- VISTA_STATO_FILTER_WORKING: true
|
||||
- PHYSICAL_DELETE_GUARDED: true
|
||||
BLOCCO_DATI: no
|
||||
BLOCCO_CONTRATTO: no
|
||||
RISCHI_APERTI: nessuno
|
||||
|
||||
## Prossimo Passo per .200 (Validazione)
|
||||
|
||||
- Eseguire il checkout del branch `stabilization/205-zero` al commit `4c99f5e87caeaba77a3b54210071d6e195a55b2f`.
|
||||
- Eseguire `php artisan migrate` ed il comando `php artisan gescon:import-mirror-0021`.
|
||||
- Verificare la corretta popolazione di `gescon_import.condomin_mirror` (644 righe totali da 0001, 0003, 0004) e la conservazione del campo `interno` originario.
|
||||
- Eseguire il checkout del branch `stabilization/205-zero` al commit `ae8f54dc364e930c5e13bfd73f3a4b1d2737aaf0`.
|
||||
- Verificare in Anagrafica Unica la vista predefinita "Contatti attivi (Canonica)" (1 riga per Daniela Benedetto).
|
||||
- Verificare il filtro "Storico & bonifiche audit" che mostra le schede duplicate soft-archiviate.
|
||||
- Verificare che il blocco cancellazione impedisca la rimozione fisica se esistono relazioni attive o storiche.
|
||||
|
|
|
|||
65
tests/Feature/AnagraficaFornitoriSyncTest.php
Normal file
65
tests/Feature/AnagraficaFornitoriSyncTest.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use App\Models\Fornitore;
|
||||
use App\Models\RubricaUniversale;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
beforeEach(function () {
|
||||
DB::table('amministratori')->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();
|
||||
});
|
||||
145
tests/Feature/AnagraficaUnicaCanonicaTest.php
Normal file
145
tests/Feature/AnagraficaUnicaCanonicaTest.php
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Gescon\RubricaUniversaleTable;
|
||||
use App\Models\Persona;
|
||||
use App\Models\PersonaUnitaRelazione;
|
||||
use App\Models\RubricaUniversale;
|
||||
use App\Models\User;
|
||||
use App\Models\Stabile;
|
||||
use App\Support\StabileContext;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Livewire;
|
||||
|
||||
beforeEach(function () {
|
||||
$user = User::first();
|
||||
if (! $user) {
|
||||
$user = User::factory()->create(['email' => 'cecilia.tordini@gmail.com']);
|
||||
}
|
||||
|
||||
try {
|
||||
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']);
|
||||
$user->assignRole('amministratore');
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
$adminId = DB::table('amministratori')->insertGetId([
|
||||
'user_id' => $user->id,
|
||||
'nome' => 'Admin Test',
|
||||
'cognome' => 'Test',
|
||||
'codice_amministratore' => 'ADM00001',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$stabile = Stabile::firstOrCreate(['codice_stabile' => '0021'], [
|
||||
'denominazione' => 'SUPERCONDOMINIO MILIZIE 3',
|
||||
'amministratore_id' => $adminId,
|
||||
'indirizzo' => 'Viale delle Milizie 3',
|
||||
'cap' => '00192',
|
||||
'citta' => 'Roma',
|
||||
'provincia' => 'RM',
|
||||
]);
|
||||
if ($stabile->amministratore_id !== $adminId) {
|
||||
$stabile->amministratore_id = $adminId;
|
||||
$stabile->save();
|
||||
}
|
||||
|
||||
$canonical = RubricaUniversale::where('codice_fiscale', 'BNDDNL86M58H224O')->first();
|
||||
if (! $canonical) {
|
||||
RubricaUniversale::create([
|
||||
'amministratore_id' => $adminId,
|
||||
'codice_univoco' => '000000JC',
|
||||
'nome' => 'DANIELA',
|
||||
'cognome' => 'BENEDETTO',
|
||||
'codice_fiscale' => 'BNDDNL86M58H224O',
|
||||
'stato' => 'attivo',
|
||||
'note' => '[MDB_0021] CF Validato',
|
||||
]);
|
||||
RubricaUniversale::create([
|
||||
'amministratore_id' => $adminId,
|
||||
'codice_univoco' => '000000I2',
|
||||
'nome' => 'DANIELA',
|
||||
'cognome' => 'BENEDETTO',
|
||||
'stato' => 'inattivo',
|
||||
'note' => '[ARCHIVIATO_DUPLICATO] Soft archived verso Rubrica 697',
|
||||
]);
|
||||
RubricaUniversale::create([
|
||||
'amministratore_id' => $adminId,
|
||||
'codice_univoco' => '000000I3',
|
||||
'nome' => 'DANIELA',
|
||||
'cognome' => 'BENEDETTO',
|
||||
'stato' => 'inattivo',
|
||||
'note' => '[ARCHIVIATO_DUPLICATO] Soft archived verso Rubrica 697',
|
||||
]);
|
||||
} else {
|
||||
$canonical->amministratore_id = $adminId;
|
||||
$canonical->save();
|
||||
RubricaUniversale::where('cognome', 'LIKE', '%BENEDETTO%')->update(['amministratore_id' => $adminId]);
|
||||
}
|
||||
});
|
||||
|
||||
test('anagrafica unica default view shows only active canonical contacts', function () {
|
||||
$user = User::first();
|
||||
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
Auth::login($user);
|
||||
StabileContext::setFromStabile($stabile, $user);
|
||||
|
||||
$canonical = RubricaUniversale::where('codice_fiscale', 'BNDDNL86M58H224O')->first();
|
||||
expect($canonical)->not->toBeNull();
|
||||
expect($canonical->stato)->toBe('attivo');
|
||||
|
||||
Livewire::test(RubricaUniversaleTable::class)
|
||||
->assertSee('DANIELA')
|
||||
->assertSee('BENEDETTO')
|
||||
->assertDontSee('000000I2');
|
||||
});
|
||||
|
||||
test('anagrafica unica storico filter shows archived duplicate records', function () {
|
||||
$user = User::first();
|
||||
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
Auth::login($user);
|
||||
StabileContext::setFromStabile($stabile, $user);
|
||||
|
||||
Livewire::test(RubricaUniversaleTable::class)
|
||||
->filterTable('vista_stato', 'storico')
|
||||
->assertSee('BENEDETTO');
|
||||
});
|
||||
|
||||
test('physical delete blocked when relations exist', function () {
|
||||
$persona = Persona::where('codice_fiscale', 'BNDDNL86M58H224O')->first();
|
||||
if (! $persona) {
|
||||
$persona = Persona::create([
|
||||
'cognome' => 'BENEDETTO',
|
||||
'nome' => 'DANIELA',
|
||||
'codice_fiscale' => 'BNDDNL86M58H224O',
|
||||
'attivo' => true,
|
||||
]);
|
||||
PersonaUnitaRelazione::create([
|
||||
'persona_id' => $persona->id,
|
||||
'unita_id' => 1,
|
||||
'tipo_relazione' => 'proprietario',
|
||||
'data_inizio' => now(),
|
||||
'attivo' => true,
|
||||
]);
|
||||
} else {
|
||||
if (! PersonaUnitaRelazione::where('persona_id', $persona->id)->exists()) {
|
||||
PersonaUnitaRelazione::create([
|
||||
'persona_id' => $persona->id,
|
||||
'unita_id' => 1,
|
||||
'tipo_relazione' => 'proprietario',
|
||||
'data_inizio' => now(),
|
||||
'attivo' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$hasRelations = PersonaUnitaRelazione::where('persona_id', $persona->id)->exists();
|
||||
expect($hasRelations)->toBeTrue();
|
||||
|
||||
expect(function () use ($persona) {
|
||||
$persona->delete();
|
||||
})->toThrow(\Exception::class, 'Cancellazione fisica bloccata');
|
||||
});
|
||||
108
tests/Feature/BenedettoBonificaIdempotenteTest.php
Normal file
108
tests/Feature/BenedettoBonificaIdempotenteTest.php
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
use App\Filament\Pages\UnitaImmobiliarePage;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use App\Models\User;
|
||||
use App\Support\AnnoGestioneContext;
|
||||
use App\Support\StabileContext;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
test('gescon:bonifica-duplicati-benedetto command is 100% idempotent across two runs', function () {
|
||||
// 1st Run
|
||||
$exitCode1 = Artisan::call('gescon:bonifica-duplicati-benedetto');
|
||||
expect($exitCode1)->toBe(0);
|
||||
|
||||
// 2nd Run
|
||||
$exitCode2 = Artisan::call('gescon:bonifica-duplicati-benedetto');
|
||||
expect($exitCode2)->toBe(0);
|
||||
|
||||
$duplicatePurCount = DB::table('persone_unita_relazioni')->whereIn('persona_id', [277, 278])->count();
|
||||
$duplicateRrCount = DB::table('rubrica_ruoli')->whereIn('rubrica_id', [651, 652, 973, 974])->count();
|
||||
|
||||
expect($duplicatePurCount)->toBe(0);
|
||||
expect($duplicateRrCount)->toBe(0);
|
||||
});
|
||||
|
||||
test('authenticated UI mounts A/11, CAN/11, and A/10 without duplicate cards', function () {
|
||||
Artisan::call('gescon:bonifica-duplicati-benedetto');
|
||||
|
||||
$user = User::first();
|
||||
if (! $user) {
|
||||
$user = User::factory()->create();
|
||||
}
|
||||
expect($user)->not->toBeNull();
|
||||
|
||||
try {
|
||||
if (method_exists($user, 'assignRole')) {
|
||||
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
|
||||
$user->assignRole('admin');
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
expect($stabile)->not->toBeNull();
|
||||
|
||||
$unitA11 = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'A')->where('interno', '11')->first();
|
||||
expect($unitA11)->not->toBeNull();
|
||||
|
||||
Auth::login($user);
|
||||
StabileContext::setActiveStabileId($user, $stabile->id);
|
||||
|
||||
// 1. Check A/11 in 2026
|
||||
request()->merge(['unita_id' => $unitA11->id]);
|
||||
AnnoGestioneContext::setActiveAnno(2026);
|
||||
$pageA11_2026 = new UnitaImmobiliarePage();
|
||||
$pageA11_2026->mount();
|
||||
|
||||
$propsA11_2026 = $pageA11_2026->relazioniPerTipo['proprietari'] ?? [];
|
||||
expect($propsA11_2026)->toHaveCount(1);
|
||||
expect($propsA11_2026[0]['nome'])->toBe('BENEDETTO DANIELA');
|
||||
|
||||
$histA11_2026 = $pageA11_2026->relazioniPerTipo['storico'] ?? [];
|
||||
expect($histA11_2026)->not->toBeEmpty();
|
||||
expect($histA11_2026[0]['nome'])->toContain('ATER');
|
||||
|
||||
// 2. Check CAN/11 (if present in test DB)
|
||||
$unitCAN11 = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('interno', 'LIKE', '%11%')->where('interno', '!=', '11')->first();
|
||||
if ($unitCAN11) {
|
||||
request()->merge(['unita_id' => $unitCAN11->id]);
|
||||
AnnoGestioneContext::setActiveAnno(2026);
|
||||
$pageCAN11 = new UnitaImmobiliarePage();
|
||||
$pageCAN11->mount();
|
||||
|
||||
$propsCAN11 = $pageCAN11->relazioniPerTipo['proprietari'] ?? [];
|
||||
expect($propsCAN11)->not->toBeNull();
|
||||
}
|
||||
|
||||
// 3. Check A/10 in 2026
|
||||
$unitA10 = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'A')->where('interno', '10')->first();
|
||||
if ($unitA10) {
|
||||
request()->merge(['unita_id' => $unitA10->id]);
|
||||
AnnoGestioneContext::setActiveAnno(2026);
|
||||
$pageA10_2026 = new UnitaImmobiliarePage();
|
||||
$pageA10_2026->mount();
|
||||
|
||||
$altriA10_2026 = $pageA10_2026->relazioniPerTipo['altri'] ?? [];
|
||||
expect($altriA10_2026)->toBeEmpty();
|
||||
}
|
||||
});
|
||||
110
tests/Feature/ReconstructMirror0021Test.php
Normal file
110
tests/Feature/ReconstructMirror0021Test.php
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Persona;
|
||||
use App\Models\PersonaUnitaRelazione;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
test('gescon:reconstruct-mirror-0021 reconstructs units, valid raw CFs and multi-gestione tenant relations with 644 quadratura', function () {
|
||||
// Step 0: Ensure Amministratore exists in test environment
|
||||
DB::table('amministratori')->insertOrIgnore(['id' => 1, 'nome' => 'Admin Test', 'cognome' => 'Test', 'created_at' => now(), 'updated_at' => now()]);
|
||||
|
||||
// Step 1: Ensure condomin_mirror is populated
|
||||
$mirrorCount = DB::connection('gescon_import')
|
||||
->table('condomin_mirror')
|
||||
->where('cod_stabile', '0021')
|
||||
->count();
|
||||
|
||||
if ($mirrorCount === 0) {
|
||||
Artisan::call('gescon:import-mirror-0021');
|
||||
}
|
||||
|
||||
// Step 2: Run reconstruction command
|
||||
$exitCode = Artisan::call('gescon:reconstruct-mirror-0021');
|
||||
expect($exitCode)->toBe(0);
|
||||
|
||||
// Assert 1: CF Stabile 0021 = 97487690584 e placeholder 80000000021 assente
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
expect($stabile)->not->toBeNull()
|
||||
->and($stabile->codice_fiscale)->toBe('97487690584')
|
||||
->and($stabile->codice_fiscale)->not->toBe('80000000021');
|
||||
|
||||
// Assert 2: Physical units count
|
||||
$unitsCount = UnitaImmobiliare::where('stabile_id', $stabile->id)->count();
|
||||
expect($unitsCount)->toBe(230);
|
||||
|
||||
// Assert 3: CF Proprietario e Inquilino derivano dalle rispettive colonne raw
|
||||
$personaScillia = Persona::where('codice_fiscale', 'SCLMHL48C23H501F')->first();
|
||||
expect($personaScillia)->not->toBeNull()
|
||||
->and($personaScillia->cognome)->toContain('SCILLIA');
|
||||
|
||||
$personaAter = Persona::where('codice_fiscale', '00410700587')->first();
|
||||
expect($personaAter)->not->toBeNull()
|
||||
->and($personaAter->ragione_sociale)->toContain('ATER');
|
||||
|
||||
// Assert 4: Righe senza CF raw restano NULL (candidato auditabile, mai CF dello stabile)
|
||||
$personaManto = Persona::where('cognome', 'MANTO')->where('nome', 'ANDREA')->first();
|
||||
expect($personaManto)->not->toBeNull()
|
||||
->and($personaManto->codice_fiscale)->toBeNull()
|
||||
->and($personaManto->note)->toContain('CANDIDATO_AUDITABILE');
|
||||
|
||||
// Assert 5: Scala A Int 11 subentro relations (ATER uscente + Benedetto Daniela subentrante)
|
||||
$unitA11 = UnitaImmobiliare::where('stabile_id', $stabile->id)
|
||||
->where('scala', 'A')
|
||||
->where('interno', '11')
|
||||
->first();
|
||||
|
||||
expect($unitA11)->not->toBeNull();
|
||||
|
||||
$relA11 = PersonaUnitaRelazione::where('unita_id', $unitA11->id)->orderBy('id')->get();
|
||||
expect($relA11->count())->toBe(2);
|
||||
|
||||
$aterRel = $relA11->first(fn ($r) => $r->persona_id === $personaAter->id);
|
||||
expect($aterRel)->not->toBeNull()
|
||||
->and($aterRel->attivo)->toBeFalse()
|
||||
->and($aterRel->attivo_fino_al)->toBe('06/08/26 00:00:00')
|
||||
->and($aterRel->subentro_adesso_ce)->toBe('220');
|
||||
|
||||
// Assert 6: Due gestioni con inquilini diversi (es. PINTO LUIGI in 0001 e MEDOSI ALESSANDRO in 0003/0004 su Scala A int 10) producono due relazioni temporali distinte, non una sostituzione
|
||||
$unitA10 = UnitaImmobiliare::where('stabile_id', $stabile->id)
|
||||
->where('scala', 'A')
|
||||
->where('interno', '10')
|
||||
->first();
|
||||
|
||||
expect($unitA10)->not->toBeNull();
|
||||
|
||||
$inqRelsA10 = PersonaUnitaRelazione::where('unita_id', $unitA10->id)
|
||||
->where('tipo_relazione', 'inquilino')
|
||||
->get();
|
||||
|
||||
expect($inqRelsA10->count())->toBe(2);
|
||||
|
||||
$pintoPersona = Persona::where('cognome', 'PINTO')->where('nome', 'LUIGI')->first();
|
||||
$medosiPersona = Persona::where('cognome', 'MEDOSI')->where('nome', 'ALESSANDRO')->first();
|
||||
|
||||
expect($pintoPersona)->not->toBeNull();
|
||||
expect($medosiPersona)->not->toBeNull();
|
||||
|
||||
$pintoRel = $inqRelsA10->first(fn ($r) => $r->persona_id === $pintoPersona->id);
|
||||
$medosiRel = $inqRelsA10->first(fn ($r) => $r->persona_id === $medosiPersona->id);
|
||||
|
||||
expect($pintoRel)->not->toBeNull();
|
||||
expect($medosiRel)->not->toBeNull();
|
||||
|
||||
// Assert 7: Test Idempotenza (seconda esecuzione non duplica nè sostituisce)
|
||||
$secondExit = Artisan::call('gescon:reconstruct-mirror-0021');
|
||||
expect($secondExit)->toBe(0);
|
||||
|
||||
$unitsCountSecond = UnitaImmobiliare::where('stabile_id', $stabile->id)->count();
|
||||
expect($unitsCountSecond)->toBe(230);
|
||||
|
||||
$relA11Second = PersonaUnitaRelazione::where('unita_id', $unitA11->id)->count();
|
||||
expect($relA11Second)->toBe(2);
|
||||
|
||||
$inqRelsA10Second = PersonaUnitaRelazione::where('unita_id', $unitA10->id)
|
||||
->where('tipo_relazione', 'inquilino')
|
||||
->count();
|
||||
expect($inqRelsA10Second)->toBe(2);
|
||||
});
|
||||
130
tests/Feature/UnitaGestioneTemporaleTest.php
Normal file
130
tests/Feature/UnitaGestioneTemporaleTest.php
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
use App\Filament\Pages\UnitaImmobiliarePage;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use App\Models\User;
|
||||
use App\Support\AnnoGestioneContext;
|
||||
use App\Support\StabileContext;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
test('unita immobiliare page filters active owner by selected anno gestione context (2026, 2025, 2024)', function () {
|
||||
$user = User::first();
|
||||
if (! $user) {
|
||||
$user = User::factory()->create();
|
||||
}
|
||||
expect($user)->not->toBeNull();
|
||||
|
||||
try {
|
||||
if (method_exists($user, 'assignRole')) {
|
||||
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
|
||||
$user->assignRole('admin');
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
expect($stabile)->not->toBeNull();
|
||||
|
||||
$unit = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'A')->where('interno', '11')->first();
|
||||
expect($unit)->not->toBeNull();
|
||||
|
||||
Auth::login($user);
|
||||
StabileContext::setActiveStabileId($user, $stabile->id);
|
||||
request()->merge(['unita_id' => $unit->id]);
|
||||
|
||||
// Test Gestione 0004 / 2026 -> BENEDETTO DANIELA
|
||||
AnnoGestioneContext::setActiveAnno(2026);
|
||||
$page2026 = new UnitaImmobiliarePage();
|
||||
$page2026->mount();
|
||||
|
||||
$props2026 = $page2026->relazioniPerTipo['proprietari'] ?? [];
|
||||
expect($props2026)->not->toBeEmpty();
|
||||
expect($props2026[0]['nome'])->toContain('BENEDETTO');
|
||||
|
||||
// Test Gestione 0003 / 2025 -> ATER
|
||||
AnnoGestioneContext::setActiveAnno(2025);
|
||||
$page2025 = new UnitaImmobiliarePage();
|
||||
$page2025->mount();
|
||||
|
||||
$props2025 = $page2025->relazioniPerTipo['proprietari'] ?? [];
|
||||
expect($props2025)->not->toBeEmpty();
|
||||
expect($props2025[0]['nome'])->toContain('ATER');
|
||||
|
||||
// Test Gestione 0001 / 2024 -> ATER
|
||||
AnnoGestioneContext::setActiveAnno(2024);
|
||||
$page2024 = new UnitaImmobiliarePage();
|
||||
$page2024->mount();
|
||||
|
||||
$props2024 = $page2024->relazioniPerTipo['proprietari'] ?? [];
|
||||
expect($props2024)->not->toBeEmpty();
|
||||
expect($props2024[0]['nome'])->toContain('ATER');
|
||||
});
|
||||
|
||||
test('post to anno-gestione-attivo sets session and updates owner context dynamically for unit 13', function () {
|
||||
$user = User::first();
|
||||
if (! $user) {
|
||||
$user = User::factory()->create();
|
||||
}
|
||||
|
||||
try {
|
||||
if (method_exists($user, 'assignRole')) {
|
||||
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
|
||||
$user->assignRole('admin');
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
expect($stabile)->not->toBeNull();
|
||||
|
||||
$unit = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'A')->where('interno', '11')->first();
|
||||
expect($unit)->not->toBeNull();
|
||||
|
||||
// 1. Post 2026
|
||||
$response2026 = $this->actingAs($user)->post('/admin-filament/anno-gestione-attivo', ['anno' => 2026]);
|
||||
$response2026->assertSessionHas('netgescon.anno_gestione', 2026);
|
||||
|
||||
request()->merge(['unita_id' => $unit->id]);
|
||||
StabileContext::setActiveStabileId($user, $stabile->id);
|
||||
$page2026 = new UnitaImmobiliarePage();
|
||||
$page2026->mount();
|
||||
expect($page2026->relazioniPerTipo['proprietari'][0]['nome'])->toContain('BENEDETTO');
|
||||
|
||||
// 2. Post 2025
|
||||
$response2025 = $this->actingAs($user)->post('/admin-filament/anno-gestione-attivo', ['anno' => 2025]);
|
||||
$response2025->assertSessionHas('netgescon.anno_gestione', 2025);
|
||||
|
||||
request()->merge(['unita_id' => $unit->id]);
|
||||
StabileContext::setActiveStabileId($user, $stabile->id);
|
||||
$page2025 = new UnitaImmobiliarePage();
|
||||
$page2025->mount();
|
||||
expect($page2025->relazioniPerTipo['proprietari'][0]['nome'])->toContain('ATER');
|
||||
|
||||
// 3. Post 2024
|
||||
$response2024 = $this->actingAs($user)->post('/admin-filament/anno-gestione-attivo', ['anno' => 2024]);
|
||||
$response2024->assertSessionHas('netgescon.anno_gestione', 2024);
|
||||
|
||||
request()->merge(['unita_id' => $unit->id]);
|
||||
StabileContext::setActiveStabileId($user, $stabile->id);
|
||||
$page2024 = new UnitaImmobiliarePage();
|
||||
$page2024->mount();
|
||||
expect($page2024->relazioniPerTipo['proprietari'][0]['nome'])->toContain('ATER');
|
||||
});
|
||||
88
tests/Feature/UnitaImmobiliarePageTest.php
Normal file
88
tests/Feature/UnitaImmobiliarePageTest.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use App\Models\Stabile;
|
||||
use App\Filament\Pages\UnitaImmobiliarePage;
|
||||
use App\Support\StabileContext;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
test('unita immobiliare page mounts without 500 or undefined property exception when canale_convocazione is null or missing', function () {
|
||||
$user = User::first();
|
||||
if (! $user) {
|
||||
$user = User::factory()->create();
|
||||
}
|
||||
expect($user)->not->toBeNull();
|
||||
|
||||
// Assign admin role if Spatie permissions are present
|
||||
try {
|
||||
if (method_exists($user, 'assignRole')) {
|
||||
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
|
||||
$user->assignRole('admin');
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
expect($stabile)->not->toBeNull();
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
// Unit 13 is Scala A Int 11 (0021-A-11)
|
||||
$unit = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'A')->where('interno', '11')->first();
|
||||
expect($unit)->not->toBeNull();
|
||||
|
||||
request()->merge(['unita_id' => $unit->id]);
|
||||
|
||||
$page = new UnitaImmobiliarePage();
|
||||
|
||||
// Verify mount executes without any Undefined property exception or error
|
||||
$page->mount();
|
||||
|
||||
expect($page->canaliComunicazione)->toBeArray();
|
||||
expect(count($page->canaliComunicazione))->toBeGreaterThan(0);
|
||||
|
||||
foreach ($page->canaliComunicazione as $item) {
|
||||
expect($item)->toHaveKeys(['id', 'nominativo', 'codice_fiscale', 'convocazione', 'verbali', 'solleciti']);
|
||||
expect($item['convocazione'])->not->toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test('unita 13 (0021-A-11) timeline preserves ATER historical and Benedetto Daniela current', function () {
|
||||
$stabile = Stabile::where('codice_stabile', '0021')->first();
|
||||
expect($stabile)->not->toBeNull();
|
||||
|
||||
$unit = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'A')->where('interno', '11')->first();
|
||||
expect($unit)->not->toBeNull();
|
||||
|
||||
$rels = DB::table('persone_unita_relazioni')->where('unita_id', $unit->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();
|
||||
|
||||
$benedettoPerson = DB::table('persone')->where('id', $benedettoRel->persona_id)->first();
|
||||
expect($benedettoPerson->cognome)->toBe('BENEDETTO');
|
||||
expect($benedettoPerson->nome)->toBe('DANIELA');
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user