diff --git a/app/Console/Commands/GesconAutoSyncAnagrafiche.php b/app/Console/Commands/GesconAutoSyncAnagrafiche.php index 431e4cc..db058f4 100644 --- a/app/Console/Commands/GesconAutoSyncAnagrafiche.php +++ b/app/Console/Commands/GesconAutoSyncAnagrafiche.php @@ -114,7 +114,7 @@ public function handle(): int $codCondRaw = trim((string) ($row->cod_cond ?? '')); // Dedup: se E_lostesso_Di è valorizzato, usa il record master come riferimento per chiavi/identità. - $masterId = (int) ($row->E_lostesso_Di ?? 0); + $masterId = (int) ($row->E_lostesso_Di ?? $row->e_lostesso_di ?? 0); $masterRow = null; if ($masterId > 0) { if (isset($condominById[$masterId])) { diff --git a/app/Console/Commands/ImportGesconFullPipeline.php b/app/Console/Commands/ImportGesconFullPipeline.php index 39a4a48..63e0e5b 100644 --- a/app/Console/Commands/ImportGesconFullPipeline.php +++ b/app/Console/Commands/ImportGesconFullPipeline.php @@ -1,12 +1,11 @@ option('anno'); + if ($anno === null) { + return null; + } + + $value = trim((string) $anno); + if ($value === '') { + return null; + } + + return sprintf('%04d', (int) $value); + } + + private function scopeCondominQuery($query) + { + $legacyYear = $this->requestedLegacyYear(); + if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_year')) { + $query->where('legacy_year', $legacyYear); + } + + return $query; + } + // Mapping campi configurato via UI (per-utente/per-stabile). Caricato on-demand. private const BOOLEAN_TARGET_KEYS = [ 'stabile.ascensore', @@ -70,11 +94,11 @@ class ImportGesconFullPipeline extends Command 'stabile.piano_sottotetto', ]; - private array $fieldMapping = []; - private array $banksMapping = []; + private array $fieldMapping = []; + private array $banksMapping = []; private array $manualFieldValues = []; - private bool $forceMapping = false; - private string $splitMode = 'palazzine'; + private bool $forceMapping = false; + private string $splitMode = 'palazzine'; // Cache per risoluzione conto bancario di default per stabile private array $defaultContoCache = []; // Legacy row cache per stabili (usata da mapField migliorata) @@ -82,17 +106,17 @@ class ImportGesconFullPipeline extends Command public function handle(): int { - if (!$this->option('stabile')) { + if (! $this->option('stabile')) { $this->error('Devi specificare --stabile= (es. --stabile=0001).'); return 1; } - $solo = $this->option('solo') ?: 'tutto'; - $dry = (bool)$this->option('dry-run'); - $limit = $this->option('limit') ? (int)$this->option('limit') : null; - $this->isDryRun = $dry; - $this->forceMapping = (bool)$this->option('force-mapping'); - $this->splitMode = in_array($this->option('split-mode') ?? 'palazzine', ['none', 'palazzine', 'stabili-per-scala'], true) - ? (string)$this->option('split-mode') + $solo = $this->option('solo') ?: 'tutto'; + $dry = (bool) $this->option('dry-run'); + $limit = $this->option('limit') ? (int) $this->option('limit') : null; + $this->isDryRun = $dry; + $this->forceMapping = (bool) $this->option('force-mapping'); + $this->splitMode = in_array($this->option('split-mode') ?? 'palazzine', ['none', 'palazzine', 'stabili-per-scala'], true) + ? (string) $this->option('split-mode') : 'palazzine'; $this->stabileId = $this->resolveStabileId(); @@ -113,7 +137,7 @@ public function handle(): int foreach ($steps as $step) { $method = 'step' . ucfirst($step); - if (!method_exists($this, $method)) { + if (! method_exists($this, $method)) { $this->warn("Skip step sconosciuto: {$step}"); continue; } @@ -137,7 +161,7 @@ public function handle(): int } } - if (!$this->option('no-views')) { + if (! $this->option('no-views')) { $this->callSilently('migrate', ['--path' => 'database/migrations/2025_08_14_120000_create_gescon_domain_tables_and_views.php']); $this->info('🔄 Viste QA aggiornate.'); } @@ -149,16 +173,16 @@ public function handle(): int private function stepStabili(?int $limit): int { // Crea stabili di dominio partendo da staging.stabili (se presente) o da condomin.cod_stabile - $created = 0; - $updated = 0; + $created = 0; + $updated = 0; $stagingHasStabili = Schema::connection('gescon_import')->hasTable('stabili'); - $src = $stagingHasStabili + $src = $stagingHasStabili ? DB::connection('gescon_import')->table('stabili') : (Schema::connection('gescon_import')->hasTable('condomin') ? DB::connection('gescon_import')->table('condomin')->select('cod_stabile as cod_stabile')->distinct() : null); - if (!$src) { + if (! $src) { // Fallback: se non c'è staging e abbiamo --mdb e --stabile, prova a creare lo stabile dai metadati di Stabili.mdb $codRichiesto = $this->option('stabile'); - $mdbPath = $this->option('mdb') + $mdbPath = $this->option('mdb') ?: ($this->option('path') ? rtrim($this->option('path'), '/') . '/Stabili.mdb' : null); if ($codRichiesto && $mdbPath && is_file($mdbPath) && Schema::hasTable('stabili')) { $created += $this->createStabileFromMdbIfNeeded($mdbPath, $codRichiesto); @@ -169,39 +193,51 @@ private function stepStabili(?int $limit): int if ($this->option('stabile')) { $src->where('cod_stabile', $this->option('stabile')); } - if ($limit) $src->limit($limit); + if ($limit) { + $src->limit($limit); + } + foreach ($src->get() as $row) { $cod = $row->cod_stabile ?? null; - if (!$cod) continue; - if (!Schema::hasTable('stabili')) continue; - $col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; - $exists = DB::table('stabili')->where($col, $cod)->first(); + if (! $cod) { + continue; + } + + if (! Schema::hasTable('stabili')) { + continue; + } + + $col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; + $exists = DB::table('stabili')->where($col, $cod)->first(); $amminId = null; if (Schema::hasColumn('stabili', 'amministratore_id') && Schema::hasTable('amministratori')) { $optAdm = $this->option('amministratore-id'); if ($optAdm) { - $existsAdm = DB::table('amministratori')->where('id', (int)$optAdm)->exists(); - $amminId = $existsAdm ? (int)$optAdm : null; + $existsAdm = DB::table('amministratori')->where('id', (int) $optAdm)->exists(); + $amminId = $existsAdm ? (int) $optAdm : null; } - if (!$amminId) { + if (! $amminId && $exists && isset($exists->amministratore_id)) { + $amminId = (int) $exists->amministratore_id ?: null; + } + if (! $amminId && ! $exists) { $amminId = DB::table('amministratori')->value('id') ?? null; } } $payload = [ - $col => $cod, - 'denominazione' => $this->mapField('stabile.denominazione', $cod), - 'indirizzo' => $this->mapField('stabile.indirizzo', 'ND'), - 'citta' => 'ND', - 'cap' => $this->mapField('stabile.cap', '00000'), - 'provincia' => $this->mapField('stabile.provincia', 'XX'), + $col => $cod, + 'denominazione' => $this->mapField('stabile.denominazione', $cod), + 'indirizzo' => $this->mapField('stabile.indirizzo', 'ND'), + 'citta' => 'ND', + 'cap' => $this->mapField('stabile.cap', '00000'), + 'provincia' => $this->mapField('stabile.provincia', 'XX'), 'amministratore_id' => $amminId, - 'attivo' => true, - 'updated_at' => now() + 'attivo' => true, + 'updated_at' => now(), ]; - if (!$exists) { + if (! $exists) { $payload['created_at'] = now(); // Tag provenienza per visibilità in UI - $payload['note'] = trim((string)($payload['note'] ?? '')); + $payload['note'] = trim((string) ($payload['note'] ?? '')); $payload['note'] = ($payload['note'] ? ($payload['note'] . ' ') : '') . '[GESCON]'; $this->dynamicInsert('stabili', $payload); $created++; @@ -209,13 +245,16 @@ private function stepStabili(?int $limit): int // Upsert non distruttivo: aggiorna solo se vuoti e se mapping prevede un valore $updates = []; foreach (['denominazione', 'indirizzo', 'cap', 'provincia'] as $k) { - if (!empty($payload[$k])) { - if (empty($exists->$k)) $updates[$k] = $payload[$k]; + if (! empty($payload[$k])) { + if (empty($exists->$k)) { + $updates[$k] = $payload[$k]; + } + } } // Se richiesto esplicitamente, riassegna amministratore dello stabile esistente if ($amminId && Schema::hasColumn('stabili', 'amministratore_id')) { - $cur = (int)($exists->amministratore_id ?? 0); + $cur = (int) ($exists->amministratore_id ?? 0); if ($cur !== $amminId) { if ($this->isDryRun) { if ($this->output->isVerbose()) { @@ -224,18 +263,18 @@ private function stepStabili(?int $limit): int } else { DB::table('stabili')->where('id', $exists->id)->update([ 'amministratore_id' => $amminId, - 'updated_at' => now(), + 'updated_at' => now(), ]); $updated++; $this->line(" -> riassegnato stabile {$cod} all'amministratore #{$amminId}"); } } } - if (!empty($updates)) { + if (! empty($updates)) { $updates['updated_at'] = now(); // Aggiungi tag GESCON se manca if (Schema::hasColumn('stabili', 'note')) { - $note = (string)($exists->note ?? ''); + $note = (string) ($exists->note ?? ''); if (stripos($note, 'GESCON') === false) { $updates['note'] = trim($note . ' [GESCON]'); } @@ -252,8 +291,7 @@ private function stepStabili(?int $limit): int } } // Aggiorna metadati da Stabili.mdb se disponibile (nome_directory corrisponde a --stabile, es. 0021) - $mdbPath = $this->option('mdb') - ?: ($this->option('path') ? rtrim($this->option('path'), '/') . '/Stabili.mdb' : '/mnt/gescon-archives/gescon/dbc/Stabili.mdb'); + $mdbPath = $this->resolveStabiliMdbPath((string) ($this->option('path') ?: '')); $codRichiesto = $this->option('stabile'); if ($codRichiesto && is_file($mdbPath)) { $row = $this->mdbStabiliFindByDirectory($mdbPath, $codRichiesto); @@ -261,7 +299,7 @@ private function stepStabili(?int $limit): int $col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; // Match robusto: gestisci eventuali zeri iniziali nel codice $stab = DB::table('stabili')->where($col, $codRichiesto)->first(); - if (!$stab && $col === 'codice_stabile') { + if (! $stab && $col === 'codice_stabile') { $alt = ltrim($codRichiesto, '0'); if ($alt !== '' && $alt !== $codRichiesto) { $stab = DB::table('stabili')->where($col, $alt)->first(); @@ -269,27 +307,26 @@ private function stepStabili(?int $limit): int } if ($stab) { // Separa CF condominio (11 cifre) e CF amministratore (16 alfanumerico tipico persona fisica) - $cfCondo = $row['cf_condominio'] - ?? (isset($row['codice_fiscale']) && preg_match('/^\d{11}$/', (string)$row['codice_fiscale']) ? $row['codice_fiscale'] : null); - $cfAmm = $row['cf_amministratore'] - ?? (isset($row['cin']) && preg_match('/^(?=.*[A-Z])[A-Z0-9]{16}$/i', (string)$row['cin']) ? $row['cin'] : null); + $cfCondo = $row['cf_condominio'] ?? (isset($row['codice_fiscale']) && preg_match('/^\d{11}$/', (string) $row['codice_fiscale']) ? $row['codice_fiscale'] : null); + $cfAmm = $row['cf_amministratore'] ?? (isset($row['cin']) && preg_match('/^(?=.*[A-Z])[A-Z0-9]{16}$/i', (string) $row['cin']) ? $row['cin'] : null); // Base update dai valori MDB $update = [ - 'denominazione' => $row['denominazione'] ?? $codRichiesto, - 'indirizzo' => $row['indirizzo'] ?? null, - 'cap' => $row['cap'] ?? null, - 'citta' => $row['citta'] ?? null, - 'provincia' => $row['provincia'] ?? null, + 'denominazione' => $row['denominazione'] ?? $codRichiesto, + 'indirizzo' => $row['indirizzo'] ?? null, + 'cap' => $row['cap'] ?? null, + 'citta' => $row['citta'] ?? null, + 'provincia' => $row['provincia'] ?? null, 'codice_fiscale' => $cfCondo ?? null, - 'updated_at' => now(), + 'updated_at' => now(), ]; if ($cfAmm && Schema::hasColumn('stabili', 'cod_fisc_amministratore')) { $update['cod_fisc_amministratore'] = $cfAmm; } // Applica mapping se configurato (rispetta --force-mapping per overwrite) $update = $this->applyStabileMapping($update, $row, $stab); - $cols = Schema::getColumnListing('stabili'); + $update = $this->applyLegacyStabileDerivedFields($update, $row, $stab); + $cols = Schema::getColumnListing('stabili'); $update = array_intersect_key($update, array_flip($cols)); // Non inserire chiavi con valore NULL per evitare violazioni NOT NULL $update = array_filter($update, function ($v) { @@ -324,15 +361,15 @@ private function stepStabili(?int $limit): int } } } - if (!empty($update)) { + if (! empty($update)) { if (Schema::hasColumn('stabili', 'note')) { $note = DB::table('stabili')->where('id', $stab->id)->value('note'); - if (stripos((string)$note, 'GESCON') === false) { - $update['note'] = trim((string)$note . ' [GESCON]'); + if (stripos((string) $note, 'GESCON') === false) { + $update['note'] = trim((string) $note . ' [GESCON]'); } } $this->safeUpdateStabile($stab->id, $update); - if (!$this->isDryRun) { + if (! $this->isDryRun) { $updated++; } } @@ -345,8 +382,8 @@ private function stepStabili(?int $limit): int $stage = DB::connection('gescon_import')->table('stabili')->where('cod_stabile', $codRichiesto)->first(); if ($stage && Schema::hasTable('stabili')) { $stabCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; - $stab = DB::table('stabili')->where($stabCol, $codRichiesto)->first(); - if (!$stab && $stabCol === 'codice_stabile') { + $stab = DB::table('stabili')->where($stabCol, $codRichiesto)->first(); + if (! $stab && $stabCol === 'codice_stabile') { $alt = ltrim($codRichiesto, '0'); if ($alt !== '' && $alt !== $codRichiesto) { $stab = DB::table('stabili')->where($stabCol, $alt)->first(); @@ -354,10 +391,9 @@ private function stepStabili(?int $limit): int } if ($stab) { $cols = Schema::getColumnListing('stabili'); - $map = []; + $map = []; // CF condominio - $map['codice_fiscale'] = $stage->codice_fiscale - ?? ($stage->cf ?? ($stage->cf_condominio ?? null)); + $map['codice_fiscale'] = $stage->codice_fiscale ?? ($stage->codice_fiscale_cond ?? ($stage->cf ?? ($stage->cf_condominio ?? null))); // CF amministratore (se presente in staging) $map['cod_fisc_amministratore'] = $stage->cod_fisc_amministratore ?? ($stage->cf_amministratore ?? null); // Dati catasto (normalizza verso colonne effettive) @@ -398,8 +434,8 @@ private function stepStabili(?int $limit): int } } // Applica mapping UI anche sui dati staging (consente override esplicito CF quando forzato) - $update = $this->applyStabileMapping($update, (array)$stage, $stab); - if (!empty($update)) { + $update = $this->applyStabileMapping($update, (array) $stage, $stab); + if (! empty($update)) { // Verbose log: evidenzia cambio CF da staging if ($this->output->isVerbose()) { $oldCf = $stab->codice_fiscale ?? null; @@ -417,7 +453,7 @@ private function stepStabili(?int $limit): int } $update['updated_at'] = now(); $this->safeUpdateStabile($stab->id, $update); - if (!$this->isDryRun) { + if (! $this->isDryRun) { $updated++; } } @@ -433,15 +469,18 @@ private function stepStabili(?int $limit): int private function stepUnita(?int $limit): int { // Fonte: gescon_import.condomin (elenco soggetti/unità) - if (!Schema::connection('gescon_import')->hasTable('condomin')) return 0; - $count = 0; - $updated = 0; + if (! Schema::connection('gescon_import')->hasTable('condomin')) { + return 0; + } + + $count = 0; + $updated = 0; $hasDetailedSchema = Schema::hasColumn('unita_immobiliari', 'codice_unita'); - $parentStabile = null; + $parentStabile = null; if (Schema::hasTable('stabili') && $this->option('stabile')) { - $stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; + $stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; $parentStabile = DB::table('stabili')->where($stabileCol, $this->option('stabile'))->first(); - if (!$parentStabile && $stabileCol === 'codice_stabile') { + if (! $parentStabile && $stabileCol === 'codice_stabile') { $alt = ltrim($this->option('stabile'), '0'); if ($alt !== '' && $alt !== $this->option('stabile')) { $parentStabile = DB::table('stabili')->where($stabileCol, $alt)->first(); @@ -449,12 +488,12 @@ private function stepUnita(?int $limit): int } } $pertinenzeToLink = []; - $seenCondIds = []; - $processed = 0; - $batch = $limit ? min(max(1, $limit), 1000) : 1000; - $offset = 0; + $seenCondIds = []; + $processed = 0; + $batch = $limit ? min(max(1, $limit), 1000) : 1000; + $offset = 0; while (true) { - $q = DB::connection('gescon_import')->table('condomin'); + $q = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin')); if ($this->option('stabile')) { $q->where('cod_stabile', $this->option('stabile')); } @@ -463,7 +502,16 @@ private function stepUnita(?int $limit): int } $q->limit($batch)->offset($offset); $rows = $q->get(); - if ($rows->isEmpty()) break; + if ($rows->isEmpty()) { + break; + } + + $normalizePartyName = static function (?string $value): string { + $value = Str::upper(trim((string) $value)); + $value = preg_replace('/\s+/u', ' ', $value ?? ''); + + return $value ?? ''; + }; foreach ($rows as $r) { $legacyCondId = null; if (isset($r->cod_cond)) { @@ -481,11 +529,11 @@ private function stepUnita(?int $limit): int // risolvi stabile_id per riga (multi-stabili) $rowStabileId = null; if (Schema::hasTable('stabili')) { - $stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; - $stRow = DB::table('stabili')->where($stabileCol, $r->cod_stabile ?? $this->option('stabile'))->first(); + $stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; + $stRow = DB::table('stabili')->where($stabileCol, $r->cod_stabile ?? $this->option('stabile'))->first(); $rowStabileId = $stRow->id ?? $this->stabileId; // Split per scala: crea/usa stabile per scala - $scalaCur = trim((string)($r->scala ?? 'A')) ?: 'A'; + $scalaCur = trim((string) ($r->scala ?? 'A')) ?: 'A'; if ($this->splitMode === 'stabili-per-scala' && $parentStabile) { [$childId, $childCode] = $this->ensureStabilePerScala($parentStabile, $scalaCur); if ($childId) { @@ -497,14 +545,11 @@ private function stepUnita(?int $limit): int $rowStabileId = $this->stabileId; } $interno = $r->interno ?? null; - $sub = $r->sub ?? $interno; + $sub = $r->sub ?? $interno; // Riconosci pertinenze nel campo INT o note: cantina, box/garage, posto auto, soffitta/solaio, locale tecnico - $internoStr = is_string($interno) ? trim($interno) : (string)$interno; - if ($internoStr !== '' && preg_match('/APP\.?\s*COND\.?/i', $internoStr)) { - continue; - } - $ptype = null; - $pcode = null; // tipo pertinenza e codice interno normalizzato + $internoStr = is_string($interno) ? trim($interno) : (string) $interno; + $ptype = null; + $pcode = null; // tipo pertinenza e codice interno normalizzato if ($internoStr !== '') { if (preg_match('/^CAN\s*(\d+)$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) { $ptype = 'cantina'; @@ -523,36 +568,36 @@ private function stepUnita(?int $limit): int $pcode = 'LT' . ($m[2] ?? ''); } } - $scalaVal = trim((string)($r->scala ?? 'A')) ?: 'A'; + $scalaVal = trim((string) ($r->scala ?? 'A')) ?: 'A'; // Piano: normalizza (R=0, T=0, 1=1, -1 o S= -1) $pianoVal = 0; if (isset($r->piano)) { - $pv = is_string($r->piano) ? strtoupper(trim($r->piano)) : (string)$r->piano; + $pv = is_string($r->piano) ? strtoupper(trim($r->piano)) : (string) $r->piano; if ($pv === 'T' || $pv === 'PT' || $pv === 'R' || $pv === 'PR' || $pv === '') { $pianoVal = 0; } elseif ($pv === 'S' || $pv === 'PS' || $pv === '-1') { $pianoVal = -1; } elseif (is_numeric($pv)) { - $pianoVal = (int)$pv; + $pianoVal = (int) $pv; } } if ($hasDetailedSchema) { // Codice unita coerente: STAB-SCALA-INT (per cantine: STAB-SCALA-CAN) - $intPart = $ptype && $pcode ? $pcode : (($interno ?? null) ?: '0'); + $intPart = $ptype && $pcode ? $pcode : (($interno ?? null) ?: '0'); $codiceBaseStab = ($r->cod_stabile ?? $this->option('stabile')) ?: 'S0'; // Se stabili-per-scala, includi comunque la scala nel codice per leggibilità $codice = $codiceBaseStab . '-' . ($scalaVal ?: 'A') . '-' . $intPart; // Filtri post-normalizzazione: pertinenze e piano min/max if ($this->option('only-pertinenze')) { - $opt = strtolower((string)$this->option('only-pertinenze')); - $isPert = (bool)$ptype; - $ok = $opt === 'any' ? $isPert : ($isPert && $ptype === $opt); - if (!$ok) { + $opt = strtolower((string) $this->option('only-pertinenze')); + $isPert = (bool) $ptype; + $ok = $opt === 'any' ? $isPert : ($isPert && $ptype === $opt); + if (! $ok) { continue; } } - $pMin = $this->option('piano-min') !== null ? (int)$this->option('piano-min') : null; - $pMax = $this->option('piano-max') !== null ? (int)$this->option('piano-max') : null; + $pMin = $this->option('piano-min') !== null ? (int) $this->option('piano-min') : null; + $pMax = $this->option('piano-max') !== null ? (int) $this->option('piano-max') : null; if ($pMin !== null && $pianoVal < $pMin) { continue; } @@ -566,10 +611,10 @@ private function stepUnita(?int $limit): int ->where('legacy_cond_id', $legacyCondId) ->first(); } - if (!$exists) { + if (! $exists) { $exists = DB::table('unita_immobiliari')->where('codice_unita', $codice)->first(); if ($exists && $legacyCondId !== null && $legacyCondId !== '' && Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) { - if (!empty($exists->legacy_cond_id) && (string) $exists->legacy_cond_id !== (string) $legacyCondId) { + if (! empty($exists->legacy_cond_id) && (string) $exists->legacy_cond_id !== (string) $legacyCondId) { $codice = $codice . '-C' . $legacyCondId; $exists = DB::table('unita_immobiliari')->where('codice_unita', $codice)->first(); } @@ -593,42 +638,42 @@ private function stepUnita(?int $limit): int $patch['scala'] = $scalaVal; } } - if (!empty($patch)) { + if (! empty($patch)) { $patch['updated_at'] = now(); - if (!$this->isDryRun) { + if (! $this->isDryRun) { DB::table('unita_immobiliari')->where('id', $exists->id)->update($patch); } $updated++; } - if (!$this->isDryRun && $exists->id) { + if (! $this->isDryRun && $exists->id) { $this->upsertUnitaNominativiFromLegacy((int) $exists->id, (int) $rowStabileId, $legacyCondId, $r); } continue; } $data = [ - 'stabile_id' => $rowStabileId, - 'palazzina_id' => null, - 'codice_unita' => $codice, - 'denominazione' => trim(($r->cognome ?? '') . ' ' . ($r->nome ?? '')) ?: null, - 'descrizione' => null, - 'palazzina' => $this->splitMode === 'palazzine' ? $scalaVal : null, - 'scala' => $scalaVal, - 'piano' => $pianoVal, - 'interno' => (string)($ptype && $pcode ? $pcode : $interno), - 'subalterno' => (string)$sub, + 'stabile_id' => $rowStabileId, + 'palazzina_id' => null, + 'codice_unita' => $codice, + 'denominazione' => trim(($r->cognome ?? '') . ' ' . ($r->nome ?? '')) ?: null, + 'descrizione' => null, + 'palazzina' => $this->splitMode === 'palazzine' ? $scalaVal : null, + 'scala' => $scalaVal, + 'piano' => $pianoVal, + 'interno' => (string) ($ptype && $pcode ? $pcode : $interno), + 'subalterno' => (string) $sub, 'categoria_catastale' => null, - 'classe' => null, - 'consistenza' => null, - 'rendita_catastale' => null, - 'millesimi_generali' => 0, - 'legacy_cond_id' => $legacyCondId, - 'tipo_unita' => $ptype ?: 'abitazione', + 'classe' => null, + 'consistenza' => null, + 'rendita_catastale' => null, + 'millesimi_generali' => 0, + 'legacy_cond_id' => $legacyCondId, + 'tipo_unita' => $ptype ?: 'abitazione', 'stato_conservazione' => 'buono', - 'stato_occupazione' => 'occupata_proprietario', - 'attiva' => true, - 'unita_demo' => false, - 'created_at' => now(), - 'updated_at' => now() + 'stato_occupazione' => 'occupata_proprietario', + 'attiva' => true, + 'unita_demo' => false, + 'created_at' => now(), + 'updated_at' => now(), ]; $this->dynamicInsert('unita_immobiliari', $data); $unitaInserted = DB::table('unita_immobiliari') @@ -641,23 +686,26 @@ private function stepUnita(?int $limit): int }) ->orderByDesc('id') ->first(); - if (!$this->isDryRun && $unitaInserted?->id) { + if (! $this->isDryRun && $unitaInserted?->id) { $this->upsertUnitaNominativiFromLegacy((int) $unitaInserted->id, (int) $rowStabileId, $legacyCondId, $r); } // Se è pertinenza, memorizza relazione da collegare in un secondo pass if ($ptype) { $pertinenzeToLink[] = [ - 'stabile_id' => $rowStabileId, - 'scala' => $r->scala ?? 'A', - 'interno' => (string)($pcode ?: $interno), - 'tipo' => $ptype, + 'stabile_id' => $rowStabileId, + 'scala' => $r->scala ?? 'A', + 'interno' => (string) ($pcode ?: $interno), + 'tipo' => $ptype, 'proprietario_cognome' => $r->cognome ?? null, - 'proprietario_nome' => $r->nome ?? null, + 'proprietario_nome' => $r->nome ?? null, ]; } } else { // schema semplice (anagrafiche base) $criteria = ['stabile_id' => $this->stabileId]; - if ($interno !== null) $criteria['interno'] = (string)$interno; + if ($interno !== null) { + $criteria['interno'] = (string) $interno; + } + $exists = DB::table('unita_immobiliari')->where($criteria)->first(); if ($exists) { // Aggiorna palazzina/scala se in split palazzine @@ -669,27 +717,30 @@ private function stepUnita(?int $limit): int if (Schema::hasColumn('unita_immobiliari', 'scala') && (empty($exists->scala) || $exists->scala !== $scalaVal)) { $patch['scala'] = $scalaVal; } - if (!empty($patch)) { + if (! empty($patch)) { $patch['updated_at'] = now(); - if (!$this->isDryRun) DB::table('unita_immobiliari')->where('id', $exists->id)->update($patch); + if (! $this->isDryRun) { + DB::table('unita_immobiliari')->where('id', $exists->id)->update($patch); + } + $updated++; } } continue; } $data = [ - 'stabile_id' => $rowStabileId, - 'interno' => (string)$interno, - 'scala' => $scalaVal, - 'piano' => $pianoVal, - 'subalterno' => (string)$sub, + 'stabile_id' => $rowStabileId, + 'interno' => (string) $interno, + 'scala' => $scalaVal, + 'piano' => $pianoVal, + 'subalterno' => (string) $sub, 'categoria_catastale' => null, - 'superficie' => null, - 'vani' => null, - 'indirizzo' => $r->indirizzo_corrispondenza ?? null, - 'note' => null, - 'created_at' => now(), - 'updated_at' => now() + 'superficie' => null, + 'vani' => null, + 'indirizzo' => $r->indirizzo_corrispondenza ?? null, + 'note' => null, + 'created_at' => now(), + 'updated_at' => now(), ]; $this->dynamicInsert('unita_immobiliari', $data); } @@ -703,8 +754,8 @@ private function stepUnita(?int $limit): int } // Cleanup: rimuovi unità non presenti in condomin (evita "alieni") - if (!$this->isDryRun && $this->option('stabile')) { - $condRows = DB::connection('gescon_import')->table('condomin') + if (! $this->isDryRun && $this->option('stabile')) { + $condRows = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin')) ->where('cod_stabile', $this->option('stabile')) ->select('scala', 'interno', 'cod_cond') ->get(); @@ -717,11 +768,11 @@ private function stepUnita(?int $limit): int if (isset($c->cod_cond)) { $condSet[(string) $c->cod_cond] = true; } - $key = strtoupper(trim((string) $c->scala)) . '|' . strtoupper(trim((string) $c->interno)); + $key = strtoupper(trim((string) $c->scala)) . '|' . strtoupper(trim((string) $c->interno)); $condSet[$key] = true; } - if (!empty($condSet)) { + if (! empty($condSet)) { $unitRows = DB::table('unita_immobiliari') ->where('stabile_id', $this->stabileId) ->whereNull('deleted_at') @@ -729,7 +780,7 @@ private function stepUnita(?int $limit): int ->get(); $alienIds = []; foreach ($unitRows as $u) { - if (Schema::hasColumn('unita_immobiliari', 'legacy_cond_id') && !empty($u->legacy_cond_id)) { + if (Schema::hasColumn('unita_immobiliari', 'legacy_cond_id') && ! empty($u->legacy_cond_id)) { if (array_key_exists((string) $u->legacy_cond_id, $condSet)) { continue; } @@ -742,10 +793,10 @@ private function stepUnita(?int $limit): int $alienIds[] = (int) $u->id; } - if (!empty($alienIds)) { + if (! empty($alienIds)) { $now = now(); DB::table('unita_immobiliari')->whereIn('id', $alienIds)->update([ - 'attiva' => 0, + 'attiva' => 0, 'deleted_at' => $now, 'updated_at' => $now, ]); @@ -760,7 +811,7 @@ private function stepUnita(?int $limit): int } } // Collega pertinenze alle unità principali (cumulo) se tabella disponibile - if (!empty($pertinenzeToLink) && Schema::hasTable('unita_pertinenze')) { + if (! empty($pertinenzeToLink) && Schema::hasTable('unita_pertinenze')) { foreach ($pertinenzeToLink as $p) { // Heuristic: cerca unità principale con stesso stabile e scala; usa primo match non pertinenza $main = DB::table('unita_immobiliari') @@ -776,13 +827,13 @@ private function stepUnita(?int $limit): int ->first(); if ($main && $cant) { $this->dynamicInsert('unita_pertinenze', [ - 'stabile_id' => $p['stabile_id'], + 'stabile_id' => $p['stabile_id'], 'unita_principale_id' => $main->id, 'unita_accessoria_id' => $cant->id, - 'tipo' => $p['tipo'] ?? 'pertinenza', - 'cumulo' => true, - 'created_at' => now(), - 'updated_at' => now(), + 'tipo' => $p['tipo'] ?? 'pertinenza', + 'cumulo' => true, + 'created_at' => now(), + 'updated_at' => now(), ]); } } @@ -795,33 +846,36 @@ private function stepUnita(?int $limit): int private function stepSoggetti(?int $limit): int { // Deriva da proprietari/inquilini nei campi condomin - if (!Schema::connection('gescon_import')->hasTable('condomin')) return 0; + if (! Schema::connection('gescon_import')->hasTable('condomin')) { + return 0; + } + $stabileId = $this->resolveStabileId(); - $count = 0; - $updated = 0; + $count = 0; + $updated = 0; // Se nel mapping sono presenti codici cassa target (es. CCB, CDL), assicurali nelle casse try { - if (!empty($this->banksMapping) && Schema::hasTable('casse') && $stabileId) { + if (! empty($this->banksMapping) && Schema::hasTable('casse') && $stabileId) { foreach ($this->banksMapping as $bm) { - if (!is_array($bm)) { + if (! is_array($bm)) { continue; } $code = $this->resolveMappingCodiceCassa($bm); if ($code) { $exists = DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', $code)->first(); - if (!$exists) { + if (! $exists) { DB::table('casse')->insert([ - 'tenant_id' => null, - 'stabile_id' => $stabileId, - 'cod_cassa' => $code, - 'descrizione' => 'Cassa ' . $code, - 'tipo' => ($code === 'CON') ? 'cassa_contanti' : (($code === 'CDL') ? 'altro' : 'conto_bancario_locale'), - 'iban' => null, + 'tenant_id' => null, + 'stabile_id' => $stabileId, + 'cod_cassa' => $code, + 'descrizione' => 'Cassa ' . $code, + 'tipo' => ($code === 'CON') ? 'cassa_contanti' : (($code === 'CDL') ? 'altro' : 'conto_bancario_locale'), + 'iban' => null, 'intestazione_conto' => null, - 'attiva' => true, - 'meta' => json_encode(['from_mapping' => true]), - 'created_at' => now(), - 'updated_at' => now(), + 'attiva' => true, + 'meta' => json_encode(['from_mapping' => true]), + 'created_at' => now(), + 'updated_at' => now(), ]); } } @@ -830,10 +884,10 @@ private function stepSoggetti(?int $limit): int } catch (\Throwable $e) { } $processed = 0; - $batch = $limit ? min(max(1, $limit), 1000) : 1000; - $offset = 0; + $batch = $limit ? min(max(1, $limit), 1000) : 1000; + $offset = 0; while (true) { - $q = DB::connection('gescon_import')->table('condomin'); + $q = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin')); if ($this->option('stabile')) { $q->where('cod_stabile', $this->option('stabile')); } @@ -842,34 +896,40 @@ private function stepSoggetti(?int $limit): int } $q->limit($batch)->offset($offset); $rows = $q->get(); - if ($rows->isEmpty()) break; + if ($rows->isEmpty()) { + break; + } + foreach ($rows as $r) { - $cf = trim((string)($r->codice_fiscale ?? '')) ?: null; - if ($cf) $cf = strtoupper($cf); - $email = trim((string)($r->email ?? '')) ?: null; - $nome = $r->nome ?? null; + $cf = trim((string) ($r->codice_fiscale ?? '')) ?: null; + if ($cf) { + $cf = strtoupper($cf); + } + + $email = trim((string) ($r->email ?? '')) ?: null; + $nome = $r->nome ?? null; $cognome = $r->cognome ?? null; // Unique/dedup: preferisci CF -> email -> nome+cognome normalizzati (no uso old_id per evitare duplicati per UI) - $useDbTrigger = (bool)env('GESCON_USE_DB_TRIGGER_CODES', false); - $exists = $this->findExistingSoggetto($cf, $email, $nome, $cognome); + $useDbTrigger = (bool) env('GESCON_USE_DB_TRIGGER_CODES', false); + $exists = $this->findExistingSoggetto($cf, $email, $nome, $cognome); if ($exists) { // Upsert non distruttivo: completa campi mancanti $patch = []; foreach ( [ - 'nome' => $nome, - 'cognome' => $cognome, + 'nome' => $nome, + 'cognome' => $cognome, 'codice_fiscale' => $cf, - 'email' => $email, - 'telefono' => $r->telefono ?? null, - 'indirizzo' => $r->indirizzo_corrispondenza ?? null, + 'email' => $email, + 'telefono' => $r->telefono ?? null, + 'indirizzo' => $r->indirizzo_corrispondenza ?? null, ] as $k => $v ) { - if (!empty($v) && (empty($exists->$k) || $exists->$k === 'ND')) { + if (! empty($v) && (empty($exists->$k) || $exists->$k === 'ND')) { $patch[$k] = $v; } } - if (!empty($patch)) { + if (! empty($patch)) { $patch['updated_at'] = now(); DB::table('soggetti')->where('id', $exists->id)->update($patch); $updated++; @@ -884,26 +944,26 @@ private function stepSoggetti(?int $limit): int $tipo = 'proprietario'; } $data = [ - 'old_id' => $this->safeOldId($r->cod_cond ?? null), - 'nome' => $nome, - 'cognome' => $cognome, + 'old_id' => $this->safeOldId($r->cod_cond ?? null), + 'nome' => $nome, + 'cognome' => $cognome, 'ragione_sociale' => null, - 'codice_fiscale' => $cf, - 'partita_iva' => null, - 'email' => $email, - 'telefono' => $r->telefono ?? null, - 'indirizzo' => $r->indirizzo_corrispondenza ?? null, - 'cap' => null, - 'citta' => null, - 'provincia' => null, - 'tipo' => $tipo ?? 'proprietario', - 'attivo' => Schema::hasColumn('soggetti', 'attivo') ? true : null, - 'created_at' => now(), - 'updated_at' => now() + 'codice_fiscale' => $cf, + 'partita_iva' => null, + 'email' => $email, + 'telefono' => $r->telefono ?? null, + 'indirizzo' => $r->indirizzo_corrispondenza ?? null, + 'cap' => null, + 'citta' => null, + 'provincia' => null, + 'tipo' => $tipo ?? 'proprietario', + 'attivo' => Schema::hasColumn('soggetti', 'attivo') ? true : null, + 'created_at' => now(), + 'updated_at' => now(), ]; - if (!$useDbTrigger) { + if (! $useDbTrigger) { $baseKey = $this->normalizeNameKey($nome, $cognome) ?: ($email ?: 'ND'); - $code = $this->generateStableCondKey('S', $r->cod_stabile ?? 'S0', $baseKey); + $code = $this->generateStableCondKey('S', $r->cod_stabile ?? 'S0', $baseKey); // Ensure uniqueness by retrying with cond code salt $tries = 0; while (Schema::hasTable('soggetti') && DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 3) { @@ -936,16 +996,28 @@ private function stepSoggetti(?int $limit): int } private function stepDiritti(?int $limit): int { - if (!Schema::connection('gescon_import')->hasTable('condomin')) return 0; - if (!Schema::hasTable('proprieta') || !Schema::hasTable('unita_immobiliari') || !Schema::hasTable('soggetti')) return 0; - $created = 0; - $updated = 0; - $skipped = 0; - $processed = 0; - $batch = $limit ? min(max(1, $limit), 1000) : 1000; - $offset = 0; + if (! Schema::connection('gescon_import')->hasTable('condomin')) { + return 0; + } + + if (! Schema::hasTable('proprieta') || ! Schema::hasTable('unita_immobiliari') || ! Schema::hasTable('soggetti')) { + return 0; + } + + $created = 0; + $updated = 0; + $skipped = 0; + $processed = 0; + $batch = $limit ? min(max(1, $limit), 1000) : 1000; + $offset = 0; + $normalizePartyName = static function (?string $value): string { + $value = Str::upper(trim((string) $value)); + $value = preg_replace('/\s+/u', ' ', $value ?? ''); + + return $value ?? ''; + }; while (true) { - $q = DB::connection('gescon_import')->table('condomin'); + $q = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin')); if ($this->option('stabile')) { $q->where('cod_stabile', $this->option('stabile')); } @@ -954,91 +1026,198 @@ private function stepDiritti(?int $limit): int } $q->limit($batch)->offset($offset); $rows = $q->get(); - if ($rows->isEmpty()) break; + if ($rows->isEmpty()) { + break; + } + foreach ($rows as $r) { // Resolve unità $unita = $this->findUnitaByStagingRow($r); - if (!$unita) { + if (! $unita) { $skipped++; continue; } - // Resolve soggetto (già creato in stepSoggetti) - $sog = $this->findExistingSoggetto(trim($r->codice_fiscale ?? '') ?: null, trim($r->email ?? '') ?: null, $r->nome ?? null, $r->cognome ?? null); - if (!$sog) { - // crea minimale se proprio assente - $payload = [ - 'nome' => $r->nome ?? null, - 'cognome' => $r->cognome ?? null, - 'codice_fiscale' => (trim($r->codice_fiscale ?? '') ?: null), - 'email' => (trim($r->email ?? '') ?: null), - 'tipo' => ($r->inquilino ?? 0) ? 'inquilino' : 'proprietario', - 'created_at' => now(), - 'updated_at' => now() + $tasks = []; + + $ownerCf = trim((string) ($r->codice_fiscale ?? $r->cond_cod_fisc ?? '')) ?: null; + $ownerEmail = trim((string) ($r->email ?? $r->e_mail_condomino ?? '')) ?: null; + $ownerNome = isset($r->nome) ? trim((string) $r->nome) : null; + $ownerCognome = isset($r->cognome) ? trim((string) $r->cognome) : null; + $ownerNome = $ownerNome !== '' ? $ownerNome : null; + $ownerCognome = $ownerCognome !== '' ? $ownerCognome : null; + if (($r->proprietario ?? 0) || $ownerCf || $ownerEmail || $ownerNome || $ownerCognome) { + $tasks[] = [ + 'tipo' => 'proprieta', + 'nome' => $ownerNome, + 'cognome' => $ownerCognome, + 'cf' => $ownerCf, + 'email' => $ownerEmail, + 'telefono' => trim((string) ($r->telefono ?? $r->tel1 ?? '')) ?: null, + 'percentuale_possesso' => isset($r->proprietario_diritto_percentuale) && is_numeric($r->proprietario_diritto_percentuale) + ? (float) $r->proprietario_diritto_percentuale + : (isset($r->perc_diritto_reale) && is_numeric($r->perc_diritto_reale) ? (float) $r->perc_diritto_reale : null), + 'percentuale_detrazione' => isset($r->perc_detrazione) && is_numeric($r->perc_detrazione) + ? (float) $r->perc_detrazione + : null, + 'data_inizio' => $this->normalizeLegacyDate($r->proprietario_subentrato_dal ?? $r->subentrato_dal ?? null), + 'data_fine' => $this->normalizeLegacyDate($r->proprietario_attivo_fino_al ?? $r->attivo_fino_al ?? null), + 'legacy_suffix' => 'owner', + 'soggetto_tipo' => 'proprietario', ]; - if (Schema::hasColumn('soggetti', 'codice_univoco')) { - $baseKey = $this->normalizeNameKey($payload['nome'] ?? '', $payload['cognome'] ?? '') ?: (($payload['email'] ?? null) ?: 'ND'); - $code = $this->generateStableCondKey('S', $r->cod_stabile ?? 'S0', $baseKey); - $tries = 0; - while (DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 5) { - $salt = ($r->cod_cond ?? '') . '-' . ($tries + 1); - $code = $this->generateStableCondKey('S', $r->cod_stabile ?? 'S0', $baseKey . '|' . $salt); - $tries++; - } - $payload['codice_univoco'] = $code; - } - if (isset($payload['codice_univoco'])) { - $existingByCode = DB::table('soggetti')->where('codice_univoco', $payload['codice_univoco'])->first(); - if ($existingByCode) { - $sog = $existingByCode; - } - } - if (!$sog) { - $sid = DB::table('soggetti')->insertGetId($payload); - $sog = DB::table('soggetti')->where('id', $sid)->first(); - } } - $tipo = ($r->proprietario ?? 0) ? 'proprieta' : (($r->inquilino ?? 0) ? 'locazione' : 'proprieta'); - $percPossesso = isset($r->perc_diritto_reale) && is_numeric($r->perc_diritto_reale) - ? (float) $r->perc_diritto_reale - : null; - $percDetrazione = isset($r->perc_detrazione) && is_numeric($r->perc_detrazione) - ? (float) $r->perc_detrazione - : null; - $exists = DB::table('proprieta') - ->where('unita_immobiliare_id', $unita->id) - ->where('soggetto_id', $sog->id) - ->first(); - if ($exists) { - $patch = []; - if (($exists->tipo_diritto ?? '') !== $tipo) { - $patch['tipo_diritto'] = $tipo; + + $tenantName = trim((string) ($r->inquil_nome ?? $r->inquilino_denominazione ?? '')) ?: null; + $tenantCf = trim((string) ($r->inquil_cod_fisc ?? '')) ?: null; + $tenantEmail = trim((string) ($r->e_mail_inquilino ?? '')) ?: null; + $tenantPhone = trim((string) ($r->inquil_tel1 ?? '')) ?: null; + if (($r->inquilino ?? 0) || $tenantName || $tenantCf || $tenantEmail || $tenantPhone) { + $tasks[] = [ + 'tipo' => 'locazione', + 'nome' => $tenantName, + 'cognome' => null, + 'cf' => $tenantCf, + 'email' => $tenantEmail, + 'telefono' => $tenantPhone, + 'percentuale_possesso' => null, + 'percentuale_detrazione' => null, + 'data_inizio' => $this->normalizeLegacyDate($r->inquil_dal ?? $r->inquilino_subentrato_dal ?? null), + 'data_fine' => $this->normalizeLegacyDate($r->inquil_al ?? $r->inquilino_attivo_fino_al ?? null), + 'legacy_suffix' => 'tenant', + 'soggetto_tipo' => 'inquilino', + ]; + } + + if ($tasks === []) { + $skipped++; + continue; + } + + foreach ($tasks as $task) { + $sog = $this->findExistingSoggetto($task['cf'], $task['email'], $task['nome'], $task['cognome']); + if ($sog && $task['tipo'] === 'locazione' && ! empty($task['nome'])) { + $matchedName = trim((string) (($sog->cognome ?? '') . ' ' . ($sog->nome ?? ''))); + if ($matchedName === '' && ! empty($sog->ragione_sociale)) { + $matchedName = trim((string) $sog->ragione_sociale); + } + + $tenantName = trim((string) (($task['cognome'] ?? '') . ' ' . ($task['nome'] ?? ''))); + $sameUnitOwner = DB::table('proprieta') + ->where('unita_immobiliare_id', $unita->id) + ->where('soggetto_id', $sog->id) + ->where('tipo_diritto', 'proprieta') + ->exists(); + + if ($sameUnitOwner && $normalizePartyName($matchedName) !== $normalizePartyName($tenantName)) { + $sog = null; + } } - if ($tipo === 'proprieta' && $percPossesso !== null && (float) ($exists->percentuale_possesso ?? 0) !== $percPossesso) { - $patch['percentuale_possesso'] = $percPossesso; + if (! $sog) { + $payload = [ + 'nome' => $task['nome'], + 'cognome' => $task['cognome'], + 'codice_fiscale' => $task['cf'], + 'email' => $task['email'], + 'telefono' => $task['telefono'], + 'tipo' => $task['soggetto_tipo'], + 'created_at' => now(), + 'updated_at' => now(), + ]; + if (Schema::hasColumn('soggetti', 'codice_univoco')) { + $baseKey = $this->normalizeNameKey($payload['nome'] ?? '', $payload['cognome'] ?? '') + ?: (($payload['codice_fiscale'] ?? null) ?: (($payload['email'] ?? null) ?: 'ND')); + $code = $this->generateStableCondKey('S', $r->cod_stabile ?? 'S0', $baseKey); + $tries = 0; + while (DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 5) { + $salt = ($r->cod_cond ?? '') . '-' . ($task['legacy_suffix'] ?? 'rel') . '-' . ($tries + 1); + $code = $this->generateStableCondKey('S', $r->cod_stabile ?? 'S0', $baseKey . '|' . $salt); + $tries++; + } + $payload['codice_univoco'] = $code; + } + if (isset($payload['codice_univoco'])) { + $existingByCode = DB::table('soggetti')->where('codice_univoco', $payload['codice_univoco'])->first(); + if ($existingByCode) { + $sog = $existingByCode; + } + } + if (! $sog) { + $sid = DB::table('soggetti')->insertGetId($payload); + $sog = DB::table('soggetti')->where('id', $sid)->first(); + } } - if ($tipo === 'proprieta' && Schema::hasColumn('proprieta', 'percentuale_detrazione') && $percDetrazione !== null && (float) ($exists->percentuale_detrazione ?? 0) !== $percDetrazione) { - $patch['percentuale_detrazione'] = $percDetrazione; - } - if ($patch !== []) { - $patch['updated_at'] = now(); - DB::table('proprieta')->where('id', $exists->id)->update($patch); - $updated++; - } else { + + if (! $sog) { $skipped++; + continue; + } + + $tipo = (string) $task['tipo']; + if ($tipo === 'locazione') { + $legacyWrongLocazione = DB::table('proprieta') + ->where('unita_immobiliare_id', $unita->id) + ->where('tipo_diritto', 'locazione') + ->where('soggetto_id', '!=', $sog->id) + ->whereIn('soggetto_id', function ($query) use ($unita) { + $query->select('soggetto_id') + ->from('proprieta') + ->where('unita_immobiliare_id', $unita->id) + ->where('tipo_diritto', 'proprieta'); + }) + ->orderBy('id') + ->first(); + + if ($legacyWrongLocazione) { + $this->moveProprietaRowToSoggetto( + (int) $legacyWrongLocazione->id, + (int) $sog->id, + $task['data_inizio'] ?? null, + $task['data_fine'] ?? null + ); + $updated++; + continue; + } + } + + $exists = DB::table('proprieta') + ->where('unita_immobiliare_id', $unita->id) + ->where('soggetto_id', $sog->id) + ->where('tipo_diritto', $tipo) + ->first(); + if ($exists) { + $patch = []; + if ($tipo === 'proprieta' && $task['percentuale_possesso'] !== null && (float) ($exists->percentuale_possesso ?? 0) !== (float) $task['percentuale_possesso']) { + $patch['percentuale_possesso'] = $task['percentuale_possesso']; + } + if ($tipo === 'proprieta' && Schema::hasColumn('proprieta', 'percentuale_detrazione') && $task['percentuale_detrazione'] !== null && (float) ($exists->percentuale_detrazione ?? 0) !== (float) $task['percentuale_detrazione']) { + $patch['percentuale_detrazione'] = $task['percentuale_detrazione']; + } + if (($task['data_inizio'] ?? null) && (string) ($exists->data_inizio ?? '') !== (string) $task['data_inizio']) { + $patch['data_inizio'] = $task['data_inizio']; + } + if (array_key_exists('data_fine', $task) && (string) ($exists->data_fine ?? '') !== (string) ($task['data_fine'] ?? '')) { + $patch['data_fine'] = $task['data_fine']; + } + if ($patch !== []) { + $patch['updated_at'] = now(); + DB::table('proprieta')->where('id', $exists->id)->update($patch); + $updated++; + } else { + $skipped++; + } + } else { + DB::table('proprieta')->insert([ + 'unita_immobiliare_id' => $unita->id, + 'soggetto_id' => $sog->id, + 'tipo_diritto' => $tipo, + 'percentuale_possesso' => $tipo === 'proprieta' ? $task['percentuale_possesso'] : null, + 'percentuale_detrazione' => ($tipo === 'proprieta' && Schema::hasColumn('proprieta', 'percentuale_detrazione')) ? $task['percentuale_detrazione'] : null, + 'data_inizio' => $task['data_inizio'], + 'data_fine' => $task['data_fine'], + 'created_at' => now(), + 'updated_at' => now(), + ]); + $created++; } - } else { - DB::table('proprieta')->insert([ - 'unita_immobiliare_id' => $unita->id, - 'soggetto_id' => $sog->id, - 'tipo_diritto' => $tipo, - 'percentuale_possesso' => $tipo === 'proprieta' ? $percPossesso : null, - 'percentuale_detrazione' => ($tipo === 'proprieta' && Schema::hasColumn('proprieta', 'percentuale_detrazione')) ? $percDetrazione : null, - 'data_inizio' => null, - 'data_fine' => null, - 'created_at' => now(), - 'updated_at' => now() - ]); - $created++; } $processed++; if ($limit && $processed >= $limit) { @@ -1074,8 +1253,8 @@ private function stepDiritti(?int $limit): int if (! $unita) { $fake = (object) [ - 'scala' => $cr->ex_scala ?? null, - 'interno' => $cr->ex_int ?? null, + 'scala' => $cr->ex_scala ?? null, + 'interno' => $cr->ex_int ?? null, 'cod_cond' => $legacyCond, ]; $unita = $this->findUnitaByStagingRow($fake); @@ -1086,38 +1265,38 @@ private function stepDiritti(?int $limit): int continue; } - $nom = trim((string) ($cr->nom_cond ?? '')); - $nome = null; + $nom = trim((string) ($cr->nom_cond ?? '')); + $nome = null; $cognome = null; if ($nom !== '') { if (str_contains($nom, ',')) { [$cognome, $nome] = array_map('trim', explode(',', $nom, 2)); } else { - $parts = preg_split('/\s+/', $nom); + $parts = preg_split('/\s+/', $nom); $cognome = $parts[0] ?? null; - $nome = isset($parts[1]) ? implode(' ', array_slice($parts, 1)) : null; + $nome = isset($parts[1]) ? implode(' ', array_slice($parts, 1)) : null; } } - $cf = trim((string) ($cr->cond_cod_fisc ?? '')) ?: null; + $cf = trim((string) ($cr->cond_cod_fisc ?? '')) ?: null; $email = trim((string) ($cr->e_mail_condomino ?? '')) ?: null; $sog = $this->findExistingSoggetto($cf, $email, $nome, $cognome); if (! $sog) { $payload = [ - 'nome' => $nome, - 'cognome' => $cognome, + 'nome' => $nome, + 'cognome' => $cognome, 'codice_fiscale' => $cf, - 'email' => $email, - 'tipo' => 'proprietario', - 'created_at' => now(), - 'updated_at' => now(), + 'email' => $email, + 'tipo' => 'proprietario', + 'created_at' => now(), + 'updated_at' => now(), ]; if (Schema::hasColumn('soggetti', 'codice_univoco')) { $baseKey = $this->normalizeNameKey($payload['nome'] ?? '', $payload['cognome'] ?? '') ?: (($payload['email'] ?? null) ?: 'ND'); - $code = $this->generateStableCondKey('S', $cr->cod_stabile ?? 'S0', $baseKey); - $tries = 0; + $code = $this->generateStableCondKey('S', $cr->cod_stabile ?? 'S0', $baseKey); + $tries = 0; while (DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 5) { $salt = ($legacyCond ?? '') . '-' . ($tries + 1); $code = $this->generateStableCondKey('S', $cr->cod_stabile ?? 'S0', $baseKey . '|' . $salt); @@ -1171,13 +1350,13 @@ private function stepDiritti(?int $limit): int } else { DB::table('proprieta')->insert([ 'unita_immobiliare_id' => $unita->id, - 'soggetto_id' => $sog->id, - 'tipo_diritto' => 'proprieta', + 'soggetto_id' => $sog->id, + 'tipo_diritto' => 'proprieta', 'percentuale_possesso' => $percentuale, - 'data_inizio' => null, - 'data_fine' => null, - 'created_at' => now(), - 'updated_at' => now(), + 'data_inizio' => null, + 'data_fine' => null, + 'created_at' => now(), + 'updated_at' => now(), ]); $created++; } @@ -1189,10 +1368,10 @@ private function stepDiritti(?int $limit): int } private function stepMillesimi(?int $limit): int { - $headersCreated = 0; + $headersCreated = 0; $detailsInserted = 0; - $detailsUpdated = 0; - $nordAvailable = Schema::connection('gescon_import')->hasTable('dett_tab') && Schema::connection('gescon_import')->hasColumn('dett_tab', 'nord'); + $detailsUpdated = 0; + $nordAvailable = Schema::connection('gescon_import')->hasTable('dett_tab') && Schema::connection('gescon_import')->hasColumn('dett_tab', 'nord'); // 1) Prepara elenco intestazioni tabella da importare (preferisci tabelle_millesimali, altrimenti deriva da dett_tab) $headerRows = collect(); @@ -1201,7 +1380,10 @@ private function stepMillesimi(?int $limit): int if ($this->option('stabile')) { $q->where('cod_stabile', $this->option('stabile')); } - if ($limit) $q->limit($limit); + if ($limit) { + $q->limit($limit); + } + $headerRows = $q->get(); } if ($headerRows->isEmpty() && Schema::connection('gescon_import')->hasTable('dett_tab')) { @@ -1220,8 +1402,8 @@ private function stepMillesimi(?int $limit): int // 2) Calcola importi preventivo/consuntivo per tabella/anno (se disponibili) $importiMap = []; - $nordMap = []; - $totMmMap = []; + $nordMap = []; + $totMmMap = []; if (Schema::connection('gescon_import')->hasTable('dett_tab')) { $q = DB::connection('gescon_import')->table('dett_tab as d') ->select( @@ -1241,7 +1423,7 @@ private function stepMillesimi(?int $limit): int } $q->groupBy('d.cod_tab', 'd.legacy_year'); foreach ($q->get() as $row) { - $key = strtoupper(trim((string) ($row->cod_tab ?? ''))) . '|' . (string) ($row->legacy_year ?? ''); + $key = strtoupper(trim((string) ($row->cod_tab ?? ''))) . '|' . (string) ($row->legacy_year ?? ''); $importiMap[$key] = $row; } @@ -1252,7 +1434,7 @@ private function stepMillesimi(?int $limit): int ->select('cod_tab', 'legacy_year', DB::raw('SUM(mm) as tot_mm')) ->groupBy('cod_tab', 'legacy_year'); foreach ($mmQuery->get() as $row) { - $key = strtoupper(trim((string) ($row->cod_tab ?? ''))) . '|' . (string) ($row->legacy_year ?? ''); + $key = strtoupper(trim((string) ($row->cod_tab ?? ''))) . '|' . (string) ($row->legacy_year ?? ''); $totMmMap[$key] = is_numeric($row->tot_mm ?? null) ? (float) $row->tot_mm : null; } @@ -1267,7 +1449,7 @@ private function stepMillesimi(?int $limit): int } $nq->groupBy('d.cod_tab', 'd.legacy_year'); foreach ($nq->get() as $row) { - $key = strtoupper(trim((string) ($row->cod_tab ?? ''))) . '|' . (string) ($row->legacy_year ?? ''); + $key = strtoupper(trim((string) ($row->cod_tab ?? ''))) . '|' . (string) ($row->legacy_year ?? ''); $nordMap[$key] = is_numeric($row->nord ?? null) ? (int) $row->nord : null; } } @@ -1280,16 +1462,16 @@ private function stepMillesimi(?int $limit): int if ($code === '') { continue; } - $code = strtoupper($code); - $legacyYear = isset($h->legacy_year) ? trim((string) $h->legacy_year) : null; + $code = strtoupper($code); + $legacyYear = isset($h->legacy_year) ? trim((string) $h->legacy_year) : null; $annoGestione = Schema::hasColumn('tabelle_millesimali', 'anno_gestione') ? $this->resolveAnnoFromLegacyYear($legacyYear) : null; - $rawCalcolo = $h->tipo_calcolo ?? $h->calcolo ?? null; - $calcolo = $this->normalizeLegacyCalcolo($rawCalcolo); - $calcoloDb = in_array($calcolo, ['millesimi', 'parti', 'fisso'], true) ? $calcolo : 'millesimi'; - $tipoLegacy = $this->normalizeLegacyTipo($h->tipologia ?? $h->tipo ?? null); + $rawCalcolo = $h->tipo_calcolo ?? $h->calcolo ?? null; + $calcolo = $this->normalizeLegacyCalcolo($rawCalcolo); + $calcoloDb = in_array($calcolo, ['millesimi', 'parti', 'fisso'], true) ? $calcolo : 'millesimi'; + $tipoLegacy = $this->normalizeLegacyTipo($h->tipologia ?? $h->tipo ?? null); $tipoTabella = $this->resolveTipoTabellaFromLegacy($tipoLegacy, $calcolo, $code); $ordine = null; @@ -1303,17 +1485,23 @@ private function stepMillesimi(?int $limit): int $ordine = $nordMap[$code . '|' . (string) ($legacyYear ?? '')] ?? null; } - $meta = []; - $importKey = $code . '|' . (string) ($legacyYear ?? ''); - $importi = $importiMap[$importKey] ?? null; + $meta = []; + $importKey = $code . '|' . (string) ($legacyYear ?? ''); + $importi = $importiMap[$importKey] ?? null; $meta['legacy_year'] = $legacyYear; - if ($tipoLegacy) $meta['tipo'] = $tipoLegacy; - if ($rawCalcolo) $meta['calcolo'] = $rawCalcolo; + if ($tipoLegacy) { + $meta['tipo'] = $tipoLegacy; + } + + if ($rawCalcolo) { + $meta['calcolo'] = $rawCalcolo; + } + if ($importi) { - $meta['tot_prev'] = is_numeric($importi->tot_prev ?? null) ? (float) $importi->tot_prev : null; - $meta['tot_prev_euro'] = is_numeric($importi->tot_prev_euro ?? null) ? (float) $importi->tot_prev_euro : null; - $meta['tot_cons'] = is_numeric($importi->tot_cons ?? null) ? (float) $importi->tot_cons : null; - $meta['tot_cons_euro'] = is_numeric($importi->tot_cons_euro ?? null) ? (float) $importi->tot_cons_euro : null; + $meta['tot_prev'] = is_numeric($importi->tot_prev ?? null) ? (float) $importi->tot_prev : null; + $meta['tot_prev_euro'] = is_numeric($importi->tot_prev_euro ?? null) ? (float) $importi->tot_prev_euro : null; + $meta['tot_cons'] = is_numeric($importi->tot_cons ?? null) ? (float) $importi->tot_cons : null; + $meta['tot_cons_euro'] = is_numeric($importi->tot_cons_euro ?? null) ? (float) $importi->tot_cons_euro : null; $meta['tot_ex_cons_euro'] = is_numeric($importi->tot_ex_cons_euro ?? null) ? (float) $importi->tot_ex_cons_euro : null; } @@ -1359,7 +1547,7 @@ private function stepMillesimi(?int $limit): int } } if ($totaleMillesimi <= 0) { - $mmKey = $code . '|' . (string) ($legacyYear ?? ''); + $mmKey = $code . '|' . (string) ($legacyYear ?? ''); $mmFallback = $totMmMap[$mmKey] ?? null; if (is_numeric($mmFallback) && $mmFallback > 0 && $mmFallback <= 999999.9999) { $totaleMillesimi = (float) $mmFallback; @@ -1367,22 +1555,22 @@ private function stepMillesimi(?int $limit): int } $payload = [ - 'stabile_id' => $this->stabileId, - 'codice_tabella' => $code, - 'nome_tabella' => $code, - 'denominazione' => trim((string) ($h->denominazione ?? $h->descrizione ?? '')) ?: ('Tab ' . $code), - 'descrizione' => trim((string) ($h->descrizione ?? $h->descr ?? '')) ?: null, - 'tipo_tabella' => $tipoTabella ?: 'custom', - 'tipo_calcolo' => $calcoloDb, + 'stabile_id' => $this->stabileId, + 'codice_tabella' => $code, + 'nome_tabella' => $code, + 'denominazione' => trim((string) ($h->denominazione ?? $h->descrizione ?? '')) ?: ('Tab ' . $code), + 'descrizione' => trim((string) ($h->descrizione ?? $h->descr ?? '')) ?: null, + 'tipo_tabella' => $tipoTabella ?: 'custom', + 'tipo_calcolo' => $calcoloDb, 'totale_millesimi' => $totaleMillesimi, - 'attiva' => true, - 'legacy_codice' => $code, - 'meta_legacy' => $meta, + 'attiva' => true, + 'legacy_codice' => $code, + 'meta_legacy' => $meta, ]; if ($ordine !== null) { $payload['ordine_visualizzazione'] = $ordine; - $payload['nord'] = $ordine; - $payload['ordinamento'] = $ordine; + $payload['nord'] = $ordine; + $payload['ordinamento'] = $ordine; } if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione') && $annoGestione) { $payload['anno_gestione'] = (int) $annoGestione; @@ -1397,13 +1585,13 @@ private function stepMillesimi(?int $limit): int $currentMeta = is_array($exists->meta_legacy) ? $exists->meta_legacy : (is_string($exists->meta_legacy) ? json_decode($exists->meta_legacy, true) : []); - $currentMeta = is_array($currentMeta) ? $currentMeta : []; + $currentMeta = is_array($currentMeta) ? $currentMeta : []; $filtered['meta_legacy'] = json_encode(array_merge($currentMeta, $meta)); } if (Schema::hasColumn('tabelle_millesimali', 'updated_at')) { $filtered['updated_at'] = now(); } - if (!empty($filtered)) { + if (! empty($filtered)) { DB::table('tabelle_millesimali')->where('id', $exists->id)->update($filtered); } } else { @@ -1419,14 +1607,18 @@ private function stepMillesimi(?int $limit): int } // 3) Inserisci le righe (dettagli) se presenti in staging - if (!Schema::connection('gescon_import')->hasTable('dett_tab')) return $headersCreated; + if (! Schema::connection('gescon_import')->hasTable('dett_tab')) { + return $headersCreated; + } $detailsTable = Schema::hasTable('dettaglio_millesimi') ? 'dettaglio_millesimi' : (Schema::hasTable('tabelle_millesimali_righe') ? 'tabelle_millesimali_righe' : null); - if (!$detailsTable) return $headersCreated; + if (! $detailsTable) { + return $headersCreated; + } - $detailsCols = Schema::getColumnListing($detailsTable); + $detailsCols = Schema::getColumnListing($detailsTable); $hasRuoloLegacy = ($detailsTable === 'dettaglio_millesimi') && in_array('ruolo_legacy', $detailsCols, true); $src = DB::connection('gescon_import')->table('dett_tab as d'); @@ -1436,19 +1628,21 @@ private function stepMillesimi(?int $limit): int $src->where('c.cod_stabile', $this->option('stabile')); } } - if ($limit) $src->limit($limit); + if ($limit) { + $src->limit($limit); + } - $canImporti = Schema::hasTable('dettaglio_importi_tabella'); - $importiUpdated = 0; + $canImporti = Schema::hasTable('dettaglio_importi_tabella'); + $importiUpdated = 0; $importiInserted = 0; - $importiSkipped = 0; + $importiSkipped = 0; foreach ($src->get() as $r) { // Trova la tabella dominio per codice - $scalaRow = isset($r->scala) ? (string)$r->scala : null; + $scalaRow = isset($r->scala) ? (string) $r->scala : null; $targetStabileId = $this->resolveStabileIdForScala($scalaRow); - $legacyYear = isset($r->legacy_year) ? trim((string) $r->legacy_year) : null; - $annoGestione = Schema::hasColumn('tabelle_millesimali', 'anno_gestione') + $legacyYear = isset($r->legacy_year) ? trim((string) $r->legacy_year) : null; + $annoGestione = Schema::hasColumn('tabelle_millesimali', 'anno_gestione') ? $this->resolveAnnoFromLegacyYear($legacyYear) : null; if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) { @@ -1488,17 +1682,17 @@ private function stepMillesimi(?int $limit): int $tab = $fallback->first(); } // Crea intestazione on-demand se mancante per questo stabile - if (!$tab) { + if (! $tab) { $payloadH = [ - 'stabile_id' => $targetStabileId, - 'codice_tabella' => $r->cod_tab, - 'nome_tabella' => $r->cod_tab, - 'denominazione' => 'Tab ' . $r->cod_tab, - 'tipo_tabella' => 'custom', + 'stabile_id' => $targetStabileId, + 'codice_tabella' => $r->cod_tab, + 'nome_tabella' => $r->cod_tab, + 'denominazione' => 'Tab ' . $r->cod_tab, + 'tipo_tabella' => 'custom', 'totale_millesimi' => 1000, - 'attiva' => true, - 'created_at' => now(), - 'updated_at' => now(), + 'attiva' => true, + 'created_at' => now(), + 'updated_at' => now(), ]; if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione') && $annoGestione) { $payloadH['anno_gestione'] = (int) $annoGestione; @@ -1507,8 +1701,14 @@ private function stepMillesimi(?int $limit): int $tab = DB::table('tabelle_millesimali') ->where('stabile_id', $targetStabileId) ->where(function ($q) use ($r) { - if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) $q->orWhere('codice_tabella', $r->cod_tab); - if (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) $q->orWhere('nome_tabella', $r->cod_tab); + if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) { + $q->orWhere('codice_tabella', $r->cod_tab); + } + + if (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) { + $q->orWhere('nome_tabella', $r->cod_tab); + } + }) ->when(Schema::hasColumn('tabelle_millesimali', 'anno_gestione'), function ($q) use ($annoGestione) { if ($annoGestione) { @@ -1520,7 +1720,9 @@ private function stepMillesimi(?int $limit): int ->first(); $headersCreated++; } - if (!$tab) continue; + if (! $tab) { + continue; + } // Best-effort: risolvi unita_immobiliare per legacy_cond_id (id_cond), fallback scala + interno $unitaId = null; @@ -1537,7 +1739,7 @@ private function stepMillesimi(?int $limit): int } } - if (!$unitaId) { + if (! $unitaId) { $crit = []; if (Schema::hasColumn('unita_immobiliari', 'stabile_id')) { $crit['stabile_id'] = $targetStabileId; @@ -1547,7 +1749,7 @@ private function stepMillesimi(?int $limit): int } if (Schema::hasColumn('unita_immobiliari', 'interno')) { // Normalizza interno: CANxx per cantine - $int = isset($r->interno) ? (string)$r->interno : null; + $int = isset($r->interno) ? (string) $r->interno : null; if (is_string($int) && preg_match('/^\s*CAN\s*(\d+)\s*$/i', $int, $m)) { $int = 'CAN' . $m[1]; } @@ -1555,7 +1757,7 @@ private function stepMillesimi(?int $limit): int $crit['interno'] = $int; } } - if (!empty($crit)) { + if (! empty($crit)) { $u = DB::table('unita_immobiliari') ->whereNull('deleted_at') ->where($crit) @@ -1565,11 +1767,11 @@ private function stepMillesimi(?int $limit): int } } - if (!$unitaId) { + if (! $unitaId) { continue; } - $mm = (float)($r->mm ?? 0); + $mm = (float) ($r->mm ?? 0); if ($mm < 0) { $mm = 0; } @@ -1584,7 +1786,7 @@ private function stepMillesimi(?int $limit): int $nord = null; if ($nordAvailable && isset($r->nord)) { // Normalizza come intero - $n = is_numeric($r->nord) ? (int)$r->nord : null; + $n = is_numeric($r->nord) ? (int) $r->nord : null; $nord = $n !== null ? $n : null; } if ($detailsTable === 'dettaglio_millesimi') { @@ -1595,8 +1797,8 @@ private function stepMillesimi(?int $limit): int ->first(); if ($exists) { $currentMm = is_numeric($exists->millesimi ?? null) ? (float) $exists->millesimi : 0.0; - $upd = [ - 'millesimi' => max($currentMm, $mm), + $upd = [ + 'millesimi' => max($currentMm, $mm), 'updated_at' => now(), ]; // Se la tabella ha una colonna per ordine (es. posizione, ordinale, nord), prova ad aggiornarla @@ -1615,11 +1817,11 @@ private function stepMillesimi(?int $limit): int } else { $payload = [ 'tabella_millesimale_id' => $tab->id, - 'unita_immobiliare_id' => $unitaId, - 'millesimi' => $mm, - 'note' => null, - 'created_at' => now(), - 'updated_at' => now(), + 'unita_immobiliare_id' => $unitaId, + 'millesimi' => $mm, + 'note' => null, + 'created_at' => now(), + 'updated_at' => now(), ]; if ($nord !== null) { foreach (['nord', 'ordine', 'posizione', 'ordinale'] as $c) { @@ -1639,10 +1841,10 @@ private function stepMillesimi(?int $limit): int ->first(); if ($exists) { $upd = [ - 'millesimi' => (float)($exists->millesimi ?? 0) + $mm, + 'millesimi' => (float) ($exists->millesimi ?? 0) + $mm, 'valore_prev' => $r->prev ?? $exists->valore_prev, 'valore_cons' => $r->cons ?? $exists->valore_cons, - 'updated_at' => now(), + 'updated_at' => now(), ]; if ($nord !== null) { $cols = Schema::getColumnListing('tabelle_millesimali_righe'); @@ -1660,13 +1862,13 @@ private function stepMillesimi(?int $limit): int } else { $payload = [ 'tabella_millesimale_id' => $tab->id, - 'unita_immobiliare_id' => $unitaId, - 'ruolo' => $r->cond_inquil ?? null, - 'millesimi' => $mm, - 'valore_prev' => $r->prev ?? null, - 'valore_cons' => $r->cons ?? null, - 'created_at' => now(), - 'updated_at' => now(), + 'unita_immobiliare_id' => $unitaId, + 'ruolo' => $r->cond_inquil ?? null, + 'millesimi' => $mm, + 'valore_prev' => $r->prev ?? null, + 'valore_cons' => $r->cons ?? null, + 'created_at' => now(), + 'updated_at' => now(), ]; if ($nord !== null) { $cols = Schema::getColumnListing('tabelle_millesimali_righe'); @@ -1696,24 +1898,24 @@ private function stepMillesimi(?int $limit): int } $payloadImporti = [ - 'millesimi' => $mm, - 'prev_euro' => isset($r->prev_euro) && is_numeric($r->prev_euro) ? (float) $r->prev_euro : null, - 'cons_euro' => isset($r->cons_euro) && is_numeric($r->cons_euro) ? (float) $r->cons_euro : null, - 'ex_cons_euro' => isset($r->ex_cons_euro) && is_numeric($r->ex_cons_euro) ? (float) $r->ex_cons_euro : null, - 'n_stra' => isset($r->n_stra) && is_numeric($r->n_stra) ? (int) $r->n_stra : null, - 'unico' => isset($r->unico) ? (int) $r->unico : null, - 'proviene_ors' => isset($r->proviene_ors) ? (string) $r->proviene_ors : null, + 'millesimi' => $mm, + 'prev_euro' => isset($r->prev_euro) && is_numeric($r->prev_euro) ? (float) $r->prev_euro : null, + 'cons_euro' => isset($r->cons_euro) && is_numeric($r->cons_euro) ? (float) $r->cons_euro : null, + 'ex_cons_euro' => isset($r->ex_cons_euro) && is_numeric($r->ex_cons_euro) ? (float) $r->ex_cons_euro : null, + 'n_stra' => isset($r->n_stra) && is_numeric($r->n_stra) ? (int) $r->n_stra : null, + 'unico' => isset($r->unico) ? (int) $r->unico : null, + 'proviene_ors' => isset($r->proviene_ors) ? (string) $r->proviene_ors : null, 'proviene_n_stra' => isset($r->proviene_n_stra) && is_numeric($r->proviene_n_stra) ? (int) $r->proviene_n_stra : null, - 'proviene_eserc' => isset($r->proviene_eserc) && is_numeric($r->proviene_eserc) ? (int) $r->proviene_eserc : null, - 'legacy_payload' => isset($r->payload) ? (array) $r->payload : null, - 'updated_at' => now(), + 'proviene_eserc' => isset($r->proviene_eserc) && is_numeric($r->proviene_eserc) ? (int) $r->proviene_eserc : null, + 'legacy_payload' => isset($r->payload) ? (array) $r->payload : null, + 'updated_at' => now(), ]; $match = [ 'tabella_millesimale_id' => $tab->id, - 'unita_immobiliare_id' => $unitaId, - 'ruolo_legacy' => $role, - 'legacy_year' => $legacyYear, + 'unita_immobiliare_id' => $unitaId, + 'ruolo_legacy' => $role, + 'legacy_year' => $legacyYear, ]; $exists = DB::table('dettaglio_importi_tabella')->where($match)->first(); @@ -1738,7 +1940,7 @@ private function stepMillesimi(?int $limit): int // 4) Completa righe mancanti: ogni unità deve avere una riga per ogni tabella if ($detailsTable === 'dettaglio_millesimi' && Schema::hasTable('unita_immobiliari')) { $detailCols = Schema::getColumnListing('dettaglio_millesimi'); - $unitaIds = DB::table('unita_immobiliari') + $unitaIds = DB::table('unita_immobiliari') ->where('stabile_id', $this->stabileId) ->whereNull('deleted_at') ->pluck('id') @@ -1747,7 +1949,7 @@ private function stepMillesimi(?int $limit): int ->values() ->all(); - if (!empty($unitaIds)) { + if (! empty($unitaIds)) { $tabIds = DB::table('tabelle_millesimali') ->where('stabile_id', $this->stabileId) ->pluck('id') @@ -1766,19 +1968,19 @@ private function stepMillesimi(?int $limit): int continue; } - $now = now(); + $now = now(); $rows = []; foreach ($missing as $uid) { - if (!$uid) { + if (! $uid) { continue; } $row = [ 'tabella_millesimale_id' => $tabId, - 'unita_immobiliare_id' => $uid, - 'millesimi' => 0, - 'note' => null, - 'created_at' => $now, - 'updated_at' => $now, + 'unita_immobiliare_id' => $uid, + 'millesimi' => 0, + 'note' => null, + 'created_at' => $now, + 'updated_at' => $now, ]; if (in_array('partecipa', $detailCols, true)) { $row['partecipa'] = 0; @@ -1800,37 +2002,114 @@ private function stepMillesimi(?int $limit): int } return $affected ?: $headersCreated; } + private function inferStraordinariaSequence(mixed ...$candidates): ?int + { + foreach ($candidates as $candidate) { + $value = strtoupper(trim((string) ($candidate ?? ''))); + if ($value === '') { + continue; + } + + if (preg_match('/([0-9]{4})$/', $value, $match)) { + $sequence = (int) substr($match[1], -2); + if ($sequence > 0) { + return $sequence; + } + } + + if (preg_match('/(?:^|[^0-9])([0-9]{2})$/', $value, $match)) { + $sequence = (int) $match[1]; + if ($sequence > 0) { + return $sequence; + } + } + } + + return null; + } + + private function buildScopedLegacyCode(?string $legacyCode, ?int $annoGestione, ?int $numeroStraordinaria = null, ?string $fallbackPrefix = null): string + { + $base = trim((string) $legacyCode); + if ($base === '') { + $base = trim((string) ($fallbackPrefix ?: 'LEGACY')); + } + + if (! $annoGestione) { + return $base; + } + + if ($numeroStraordinaria !== null) { + return sprintf('%s@%d-%02d', $base, $annoGestione, $numeroStraordinaria); + } + + return sprintf('%s@%d', $base, $annoGestione); + } + + private function isFiscalStraordinaria(object $row): bool + { + return $this->normalizeLegacyPartyName($row->descriz_ccp ?? null) === 'DETRAZ.FISC.'; + } + + private function sumStraordinariaImporto(object $row): ?float + { + $sum = 0.0; + $found = false; + foreach (range(1, 12) as $idx) { + $field = 'rata_' . $idx; + if (isset($row->{$field}) && is_numeric($row->{$field})) { + $sum += (float) $row->{$field}; + $found = true; + } + } + + return $found ? $sum : null; + } + private function stepVoci(?int $limit): int { $stabileId = $this->resolveStabileId(); - $src = DB::connection('gescon_import')->table('voc_spe'); - if ($limit) $src->limit($limit); - $count = 0; + $src = DB::connection('gescon_import')->table('voc_spe'); + if ($limit) { + $src->limit($limit); + } + + $count = 0; $updated = 0; foreach ($src->get() as $v) { $cod = $v->cod ?? null; - if (!$cod) continue; - $legacyYear = isset($v->legacy_year) ? (string) $v->legacy_year : null; + if (! $cod) { + continue; + } + + $legacyYear = isset($v->legacy_year) ? (string) $v->legacy_year : null; + $annoGestione = $this->resolveLegacySnapshotYear($legacyYear, $v); + $tipoLegacy = match ($v->v_ors ?? $v->V_ORS ?? '') { + 'R' => 'riscaldamento', + 'S' => 'straordinaria', + default => 'ordinaria' + }; + $numeroStraordinaria = $tipoLegacy === 'straordinaria' + ? $this->inferStraordinariaSequence($v->tabella ?? null, $v->cod ?? null, $v->descriz ?? null) + : null; + $codiceDominio = $tipoLegacy === 'straordinaria' + ? $this->buildScopedLegacyCode((string) $cod, $annoGestione, $numeroStraordinaria, 'VOCSPE') + : (string) $cod; $exists = null; if (Schema::hasColumn('voci_spesa', 'codice')) { - $q = DB::table('voci_spesa')->where('codice', $cod); + $q = DB::table('voci_spesa')->where('codice', $codiceDominio); if ($stabileId && Schema::hasColumn('voci_spesa', 'stabile_id')) { $q->where('stabile_id', $stabileId); } - $tipoLegacy = match ($v->v_ors ?? $v->V_ORS ?? '') { - 'R' => 'riscaldamento', - 'S' => 'straordinaria', - default => 'ordinaria' - }; - $gestioneId = $this->resolveGestioneContabileIdForStabile($stabileId, $legacyYear, $tipoLegacy, null); + $gestioneId = $this->resolveGestioneContabileIdForStabile($stabileId, $legacyYear, $tipoLegacy, $numeroStraordinaria); if ($gestioneId && Schema::hasColumn('voci_spesa', 'gestione_contabile_id')) { $q->where('gestione_contabile_id', $gestioneId); } $exists = $q->orderBy('id')->first(); - if (!$exists) { + if (! $exists) { $exists = DB::table('voci_spesa') - ->where('codice', $cod) + ->where('codice', $codiceDominio) ->when($stabileId && Schema::hasColumn('voci_spesa', 'stabile_id'), function ($qq) use ($stabileId) { $qq->where('stabile_id', $stabileId); }) @@ -1839,8 +2118,8 @@ private function stepVoci(?int $limit): int } } $tabellaId = null; - if (Schema::hasTable('tabelle_millesimali') && !empty($v->tabella)) { - $tabellaId = DB::table('tabelle_millesimali') + if (Schema::hasTable('tabelle_millesimali') && ! empty($v->tabella)) { + $tabellaQuery = DB::table('tabelle_millesimali') ->where('stabile_id', $stabileId) ->where(function ($q) use ($v) { if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) { @@ -1849,40 +2128,38 @@ private function stepVoci(?int $limit): int if (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) { $q->orWhere('nome_tabella', $v->tabella); } - }) - ->value('id'); + }); + if ($annoGestione && Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) { + $tabellaQuery->where('anno_gestione', $annoGestione); + } + $tabellaId = $tabellaQuery->value('id'); } - $tipoLegacy = match ($v->v_ors ?? $v->V_ORS ?? '') { - 'R' => 'riscaldamento', - 'S' => 'straordinaria', - default => 'ordinaria' - }; - $gestioneId = $this->resolveGestioneContabileIdForStabile($stabileId, $legacyYear, $tipoLegacy, null); - $data = [ - 'stabile_id' => (Schema::hasColumn('voci_spesa', 'stabile_id') ? $stabileId : null), - 'gestione_contabile_id' => Schema::hasColumn('voci_spesa', 'gestione_contabile_id') ? $gestioneId : null, - 'codice' => $cod, - 'legacy_codice' => Schema::hasColumn('voci_spesa', 'legacy_codice') ? $cod : null, - 'descrizione' => $v->descriz ?? '', - 'categoria' => (Schema::hasColumn('voci_spesa', 'categoria') ? $tipoLegacy : null), - 'tipo_gestione' => (Schema::hasColumn('voci_spesa', 'tipo_gestione') ? 'ordinaria' : null), - 'tipo' => $tipoLegacy, + $gestioneId = $this->resolveGestioneContabileIdForStabile($stabileId, $legacyYear, $tipoLegacy, $numeroStraordinaria); + $data = [ + 'stabile_id' => (Schema::hasColumn('voci_spesa', 'stabile_id') ? $stabileId : null), + 'gestione_contabile_id' => Schema::hasColumn('voci_spesa', 'gestione_contabile_id') ? $gestioneId : null, + 'codice' => $codiceDominio, + 'legacy_codice' => Schema::hasColumn('voci_spesa', 'legacy_codice') ? $cod : null, + 'descrizione' => $v->descriz ?? '', + 'categoria' => (Schema::hasColumn('voci_spesa', 'categoria') ? $tipoLegacy : null), + 'tipo_gestione' => (Schema::hasColumn('voci_spesa', 'tipo_gestione') ? $tipoLegacy : null), + 'tipo' => $tipoLegacy, 'tabella_millesimale_default_id' => (Schema::hasColumn('voci_spesa', 'tabella_millesimale_default_id') ? $tabellaId : null), - 'importo_default' => Schema::hasColumn('voci_spesa', 'importo_default') + 'importo_default' => Schema::hasColumn('voci_spesa', 'importo_default') ? (isset($v->preventivo_euro) && is_numeric($v->preventivo_euro) ? (float) $v->preventivo_euro : (isset($v->importo_euro) && is_numeric($v->importo_euro) ? (float) $v->importo_euro : null)) : null, - 'importo_consuntivo' => Schema::hasColumn('voci_spesa', 'importo_consuntivo') + 'importo_consuntivo' => Schema::hasColumn('voci_spesa', 'importo_consuntivo') ? (isset($v->consuntivo_euro) && is_numeric($v->consuntivo_euro) ? (float) $v->consuntivo_euro : null) : null, - 'percentuale_condomino' => Schema::hasColumn('voci_spesa', 'percentuale_condomino') + 'percentuale_condomino' => Schema::hasColumn('voci_spesa', 'percentuale_condomino') ? (isset($v->perc_proprietario) && is_numeric($v->perc_proprietario) ? (float) $v->perc_proprietario : 100.0) : null, - 'percentuale_inquilino' => Schema::hasColumn('voci_spesa', 'percentuale_inquilino') + 'percentuale_inquilino' => Schema::hasColumn('voci_spesa', 'percentuale_inquilino') ? (isset($v->perc_inquilino) && is_numeric($v->perc_inquilino) ? (float) $v->perc_inquilino : 0.0) : null, - 'note' => $v->note ?? null, - 'created_at' => now(), - 'updated_at' => now() + 'note' => $v->note ?? null, + 'created_at' => now(), + 'updated_at' => now(), ]; if ($exists) { $patch = []; @@ -1891,7 +2168,7 @@ private function stepVoci(?int $limit): int $patch[$k] = $data[$k]; } } - if (!empty($patch)) { + if (! empty($patch)) { $patch['updated_at'] = now(); DB::table('voci_spesa')->where('id', $exists->id)->update($patch); $updated++; @@ -1909,16 +2186,22 @@ private function stepVoci(?int $limit): int private function stepBanche(?int $limit): int { // Importa conti bancari per lo stabile corrente leggendo Anagr_casse da generale_stabile.mdb - if (!Schema::hasTable('dati_bancari')) return 0; - $stabileCode = (string)($this->option('stabile') ?? ''); - if ($stabileCode === '') return 0; + if (! Schema::hasTable('dati_bancari')) { + return 0; + } + + $stabileCode = (string) ($this->option('stabile') ?? ''); + if ($stabileCode === '') { + return 0; + } + // Risolvi stabile_id $stabileId = $this->resolveStabileId(); // Determina root archivi: usa --path come radice (default noto) - $root = rtrim((string)($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/'); + $root = rtrim((string) ($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/'); $stableDir = $root . '/' . $stabileCode; - $generale = $stableDir . '/generale_stabile.mdb'; - $rows = []; + $generale = $stableDir . '/generale_stabile.mdb'; + $rows = []; // 0) Preferisci staging anag_casse se presente $stagingTable = null; @@ -1933,7 +2216,10 @@ private function stepBanche(?int $limit): int if ($stabileCode && Schema::connection('gescon_import')->hasColumn($stagingTable, 'cod_stabile')) { $q->where('cod_stabile', $stabileCode); } - if ($limit) $q->limit($limit); + if ($limit) { + $q->limit($limit); + } + $rows = $q->get()->map(function ($r): array { $out = []; foreach ((array) $r as $k => $v) { @@ -1948,36 +2234,49 @@ private function stepBanche(?int $limit): int } // Fallback: cerca Anagr_casse nei singolo_anno.mdb (può contenere info ridondanti) if (empty($rows) && is_dir($stableDir)) { - foreach ((array)glob($stableDir . '/*/singolo_anno.mdb') as $mdb) { + foreach ((array) glob($stableDir . '/*/singolo_anno.mdb') as $mdb) { $part = $this->mdbExportToRows($mdb, ['Anagr_casse', 'anagr_casse', 'ANAGR_CASSE']); - if (!empty($part)) { + if (! empty($part)) { $rows = array_merge($rows, $part); } - if ($limit && count($rows) >= $limit) break; + if ($limit && count($rows) >= $limit) { + break; + } + } } - if (empty($rows)) return 0; + if ($stabileId) { + $configRaw = DB::table('stabili')->where('id', $stabileId)->value('configurazione_avanzata'); + $configRows = $this->buildBancheRowsFromStabileConfig($configRaw); + if ($configRows !== []) { + $rows = $this->mergeLegacyBancheRows($rows, $configRows); + } + } + if (empty($rows)) { + return 0; + } + // Applica mapping personalizzato (se presente) per individuare le colonne legacy $mapLegacy = [ - 'nome_banca' => ['banca', 'istituto', 'nome_banca', 'denominazione', 'denom_banca'], - 'iban' => ['iban', 'iban_conto', 'iban1'], - 'swift' => ['swift', 'bic', 'bic_swift'], - 'conto' => ['n_conto', 'numero_conto', 'conto'], + 'nome_banca' => ['banca', 'istituto', 'nome_banca', 'denominazione', 'denom_banca'], + 'iban' => ['iban', 'iban_conto', 'iban1'], + 'swift' => ['swift', 'bic', 'bic_swift'], + 'conto' => ['n_conto', 'numero_conto', 'conto'], 'intestatario' => ['intestatario', 'intestazione', 'titolare'], - 'cod_cassa' => ['cod_cassa', 'codcassa', 'codice_cassa', 'modalita_pagamento', 'codice'], - 'descrizione' => ['descrizione', 'descr', 'denominazione', 'note'] + 'cod_cassa' => ['cod_cassa', 'codcassa', 'codice_cassa', 'modalita_pagamento', 'codice'], + 'descrizione' => ['descrizione', 'descr', 'denominazione', 'note'], ]; - if (!empty($this->banksMapping) && is_array($this->banksMapping)) { + if (! empty($this->banksMapping) && is_array($this->banksMapping)) { // banksMapping formato: [{legacy:{...}, target:{...}}, ...] → usa chiavi legacy quando compilate foreach ($this->banksMapping as $bm) { - if (!is_array($bm)) { + if (! is_array($bm)) { continue; } foreach (['nome_banca', 'iban', 'swift', 'conto', 'intestatario', 'cod_cassa'] as $k) { $legacyField = $this->resolveMappingLegacyField($bm, $k); if ($legacyField) { $lk = strtolower(trim($legacyField)); - if (!isset($mapLegacy[$k])) { + if (! isset($mapLegacy[$k])) { $mapLegacy[$k] = []; } array_unshift($mapLegacy[$k], $lk); @@ -1998,64 +2297,79 @@ private function stepBanche(?int $limit): int $nostroAlready = false; } } - $created = 0; - $updated = 0; + $created = 0; + $updated = 0; $seenKeys = []; foreach ($rows as $r) { // Costruisci payload best-effort cercando tra alias $getFirst = function (array $aliases) use ($r) { foreach ($aliases as $a) { - if (array_key_exists($a, $r) && trim((string)$r[$a]) !== '') return trim((string)$r[$a]); + if (array_key_exists($a, $r) && trim((string) $r[$a]) !== '') { + return trim((string) $r[$a]); + } + } return null; }; - $denom = $getFirst($mapLegacy['nome_banca']); - $iban = strtoupper(preg_replace('/\s+/', '', (string)($getFirst($mapLegacy['iban']) ?? ''))); - $swift = strtoupper((string)($getFirst($mapLegacy['swift']) ?? '')); - $numero = $getFirst($mapLegacy['conto']); - $intest = $getFirst($mapLegacy['intestatario']); + $denom = $getFirst($mapLegacy['nome_banca']); + $iban = strtoupper(preg_replace('/\s+/', '', (string) ($getFirst($mapLegacy['iban']) ?? ''))); + $swift = strtoupper((string) ($getFirst($mapLegacy['swift']) ?? '')); + $numero = $getFirst($mapLegacy['conto']); + $intest = $getFirst($mapLegacy['intestatario']); $codCassa = $getFirst($mapLegacy['cod_cassa']); - $descr = $getFirst($mapLegacy['descrizione']); - if (!$denom && !$iban && !$numero && !$codCassa && !$descr) continue; // riga non informativa + $descr = $getFirst($mapLegacy['descrizione']); + if (! $denom && ! $iban && ! $numero && ! $codCassa && ! $descr) { + continue; + } + // riga non informativa // Chiave dedup per stabile: IBAN preferito, altrimenti numero conto $key = $iban ?: ($numero ? ('N:' . $numero) : ($codCassa ? ('C:' . strtoupper(trim($codCassa))) : null)); - if (!$key) continue; - if (isset($seenKeys[$key])) continue; // evita duplicati da diverse sorgenti + if (! $key) { + continue; + } + + if (isset($seenKeys[$key])) { + continue; + } + // evita duplicati da diverse sorgenti $seenKeys[$key] = true; // Caso 1: IBAN presente → tratta come conto bancario (dati_bancari) - if (!$iban) { + if (! $iban) { // Nessun IBAN → registra come cassa generica in tabella 'casse' if (Schema::hasTable('casse')) { - $code = strtoupper(trim((string)($codCassa ?: ''))); + $code = strtoupper(trim((string) ($codCassa ?: ''))); if ($code === '') { // fallback generico evitando collisioni: GEN, GEN1, ... $base = 'GEN'; $code = $base; - $idx = 1; + $idx = 1; while (DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', $code)->exists()) { $code = $base . (++$idx); } } $existsC = DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', $code)->first(); - if (!$existsC) { + if (! $existsC) { DB::table('casse')->insert([ - 'tenant_id' => null, - 'stabile_id' => $stabileId, - 'cod_cassa' => $code, - 'descrizione' => ($descr ?: ($denom ?: 'Cassa generica')), - 'tipo' => ($code === 'CON') ? 'cassa_contanti' : 'altro', - 'iban' => null, + 'tenant_id' => null, + 'stabile_id' => $stabileId, + 'cod_cassa' => $code, + 'descrizione' => ($descr ?: ($denom ?: 'Cassa generica')), + 'tipo' => ($code === 'CON') ? 'cassa_contanti' : 'altro', + 'iban' => null, 'intestazione_conto' => $intest ?: null, - 'attiva' => true, - 'meta' => json_encode(['from' => 'Anagr_casse', 'numero' => $numero]), - 'created_at' => $now, - 'updated_at' => $now, + 'attiva' => true, + 'meta' => json_encode(['from' => 'Anagr_casse', 'numero' => $numero]), + 'created_at' => $now, + 'updated_at' => $now, ]); $created++; } else { $upd = []; - if ($descr && (empty($existsC->descrizione) || $existsC->descrizione === 'ND')) $upd['descrizione'] = $descr; - if (!empty($upd)) { + if ($descr && (empty($existsC->descrizione) || $existsC->descrizione === 'ND')) { + $upd['descrizione'] = $descr; + } + + if (! empty($upd)) { $upd['updated_at'] = $now; DB::table('casse')->where('id', $existsC->id)->update($upd); $updated++; @@ -2063,7 +2377,7 @@ private function stepBanche(?int $limit): int } } // Per la cassa contanti "CON" crea/aggiorna anche un DatiBancari di tipo cassa per renderlo visibile nell'UI - $codeUp = strtoupper(trim((string)$codCassa)); + $codeUp = strtoupper(trim((string) $codCassa)); if ($codeUp === 'CON' && Schema::hasTable('dati_bancari') && $stabileId) { $existingDb = DB::table('dati_bancari') ->where('stabile_id', $stabileId) @@ -2076,25 +2390,25 @@ private function stepBanche(?int $limit): int }) ->first(); $payloadCassa = [ - 'stabile_id' => $stabileId, - 'contatto_id' => null, - 'tipo_conto' => 'cassa', + 'stabile_id' => $stabileId, + 'contatto_id' => null, + 'tipo_conto' => 'cassa', 'denominazione_banca' => $denom ?: ($descr ?: 'Cassa contanti'), - 'numero_conto' => $codeUp, - 'iban' => null, - 'legacy_cod_cassa' => $codeUp, - 'abi' => null, - 'cab' => null, - 'cin' => null, - 'bic_swift' => null, - 'intestazione_conto' => $intest ?: null, + 'numero_conto' => $codeUp, + 'iban' => null, + 'legacy_cod_cassa' => $codeUp, + 'abi' => null, + 'cab' => null, + 'cin' => null, + 'bic_swift' => null, + 'intestazione_conto' => $intest ?: null, 'data_saldo_iniziale' => $now->toDateString(), - 'saldo_iniziale' => 0, - 'valuta' => 'EUR', - 'stato_conto' => 'attivo', - 'is_nostro_conto' => !$nostroAlready, - 'note' => '[GESCON] Cassa contanti', - 'updated_at' => $now, + 'saldo_iniziale' => 0, + 'valuta' => 'EUR', + 'stato_conto' => 'attivo', + 'is_nostro_conto' => ! $nostroAlready, + 'note' => '[GESCON] Cassa contanti', + 'updated_at' => $now, ]; if ($existingDb) { DB::table('dati_bancari')->where('id', $existingDb->id)->update($payloadCassa); @@ -2107,7 +2421,10 @@ private function stepBanche(?int $limit): int } } // Non creare dati_bancari in assenza di IBAN, passa alla prossima riga - if ($limit && ($created + $updated) >= $limit) break; + if ($limit && ($created + $updated) >= $limit) { + break; + } + continue; } // Cerca esistente per stabile_id + IBAN/numero @@ -2118,38 +2435,38 @@ private function stepBanche(?int $limit): int ->whereRaw('REPLACE(UPPER(iban)," ","") = ?', [$iban]) ->first(); } - if (!$exists && $numero) { + if (! $exists && $numero) { $exists = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->where('numero_conto', $numero) ->first(); } $payload = [ - 'stabile_id' => $stabileId, - 'contatto_id' => null, - 'tipo_conto' => 'corrente', + 'stabile_id' => $stabileId, + 'contatto_id' => null, + 'tipo_conto' => 'corrente', 'denominazione_banca' => $denom ?: 'Banca', - 'numero_conto' => $numero, - 'iban' => $iban ?: null, - 'legacy_cod_cassa' => $codCassa ?: null, - 'abi' => null, - 'cab' => null, - 'cin' => null, - 'bic_swift' => $swift ?: null, - 'intestazione_conto' => $intest ?: null, + 'numero_conto' => $numero, + 'iban' => $iban ?: null, + 'legacy_cod_cassa' => $codCassa ?: null, + 'abi' => null, + 'cab' => null, + 'cin' => null, + 'bic_swift' => $swift ?: null, + 'intestazione_conto' => $intest ?: null, 'data_saldo_iniziale' => $now->toDateString(), - 'saldo_iniziale' => 0, - 'valuta' => 'EUR', - 'stato_conto' => 'attivo', - 'is_nostro_conto' => false, - 'note' => '[GESCON]', - 'created_at' => $now, - 'updated_at' => $now, + 'saldo_iniziale' => 0, + 'valuta' => 'EUR', + 'stato_conto' => 'attivo', + 'is_nostro_conto' => false, + 'note' => '[GESCON]', + 'created_at' => $now, + 'updated_at' => $now, ]; - if (!$nostroAlready) { + if (! $nostroAlready) { // Marca il primo che inseriamo come nostro conto $payload['is_nostro_conto'] = true; - $nostroAlready = true; + $nostroAlready = true; } if ($exists) { // Aggiorna campi vuoti @@ -2157,11 +2474,17 @@ private function stepBanche(?int $limit): int foreach (['denominazione_banca', 'numero_conto', 'iban', 'bic_swift', 'intestazione_conto', 'legacy_cod_cassa'] as $c) { $newV = $payload[$c] ?? null; $oldV = $exists->$c ?? null; - if ($newV && (!$oldV || $oldV === 'ND')) $upd[$c] = $newV; + if ($newV && (! $oldV || $oldV === 'ND')) { + $upd[$c] = $newV; + } + } - if (!empty($upd)) { + if (! empty($upd)) { $upd['updated_at'] = $now; - if (!$this->isDryRun) DB::table('dati_bancari')->where('id', $exists->id)->update($upd); + if (! $this->isDryRun) { + DB::table('dati_bancari')->where('id', $exists->id)->update($upd); + } + $updated++; } continue; @@ -2177,38 +2500,38 @@ private function stepBanche(?int $limit): int ->orderByDesc('id') ->first(); } - if (!$inserted && $numero) { + if (! $inserted && $numero) { $inserted = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->where('numero_conto', $numero) ->orderByDesc('id') ->first(); } - if ($inserted && !empty($this->banksMapping)) { + if ($inserted && ! empty($this->banksMapping)) { $assigned = null; foreach ($this->banksMapping as $bm) { - if (!is_array($bm)) { + if (! is_array($bm)) { continue; } $targetCode = $this->resolveMappingCodiceCassa($bm); - if (!$targetCode) { + if (! $targetCode) { continue; } - $legacyCode = $bm['legacy_cod_cassa'] ?? $this->resolveMappingLegacyField($bm, 'cod_cassa'); - $legacyCode = $legacyCode ? strtoupper(trim((string)$legacyCode)) : null; - $currentLegacyCode = isset($inserted->legacy_cod_cassa) ? strtoupper(trim((string)$inserted->legacy_cod_cassa)) : null; + $legacyCode = $bm['legacy_cod_cassa'] ?? $this->resolveMappingLegacyField($bm, 'cod_cassa'); + $legacyCode = $legacyCode ? strtoupper(trim((string) $legacyCode)) : null; + $currentLegacyCode = isset($inserted->legacy_cod_cassa) ? strtoupper(trim((string) $inserted->legacy_cod_cassa)) : null; if ($legacyCode && $currentLegacyCode && $legacyCode === $currentLegacyCode) { $assigned = $targetCode; break; } // Compatibilità formato precedente: confronta IBAN/numero reali se disponibili - $legacyIban = isset($bm['iban']) ? strtoupper(preg_replace('/\s+/', '', (string)$bm['iban'])) : null; - $legacyNum = isset($bm['conto']) ? (string)$bm['conto'] : null; + $legacyIban = isset($bm['iban']) ? strtoupper(preg_replace('/\s+/', '', (string) $bm['iban'])) : null; + $legacyNum = isset($bm['conto']) ? (string) $bm['conto'] : null; if ($legacyIban && $iban && $legacyIban === $iban) { $assigned = $targetCode; break; } - if ($legacyNum && $numero && $legacyNum === (string)$numero) { + if ($legacyNum && $numero && $legacyNum === (string) $numero) { $assigned = $targetCode; break; } @@ -2216,14 +2539,17 @@ private function stepBanche(?int $limit): int if ($assigned) { DB::table('dati_bancari')->where('id', $inserted->id)->update([ 'legacy_cod_cassa' => $assigned, - 'updated_at' => now(), + 'updated_at' => now(), ]); } } } catch (\Throwable $e) { } $created++; - if ($limit && ($created + $updated) >= $limit) break; + if ($limit && ($created + $updated) >= $limit) { + break; + } + } if (($created + $updated) > 0) { $this->line(" → conti bancari creati: {$created}, aggiornati: {$updated}"); @@ -2233,36 +2559,36 @@ private function stepBanche(?int $limit): int if (Schema::hasTable('casse') && $stabileId) { // CON: Cassa contanti, sempre $existsCon = DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', 'CON')->first(); - if (!$existsCon) { + if (! $existsCon) { DB::table('casse')->insert([ - 'tenant_id' => null, - 'stabile_id' => $stabileId, - 'cod_cassa' => 'CON', - 'descrizione' => 'Cassa contanti', - 'tipo' => 'cassa_contanti', - 'iban' => null, + 'tenant_id' => null, + 'stabile_id' => $stabileId, + 'cod_cassa' => 'CON', + 'descrizione' => 'Cassa contanti', + 'tipo' => 'cassa_contanti', + 'iban' => null, 'intestazione_conto' => null, - 'attiva' => true, - 'meta' => json_encode(['seeded' => true]), - 'created_at' => now(), - 'updated_at' => now(), + 'attiva' => true, + 'meta' => json_encode(['seeded' => true]), + 'created_at' => now(), + 'updated_at' => now(), ]); } // CDL: Cassa di lavoro (crea se non esiste; opzionale ma utile) $existsCdl = DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', 'CDL')->first(); - if (!$existsCdl) { + if (! $existsCdl) { DB::table('casse')->insert([ - 'tenant_id' => null, - 'stabile_id' => $stabileId, - 'cod_cassa' => 'CDL', - 'descrizione' => 'Cassa di lavoro', - 'tipo' => 'altro', - 'iban' => null, + 'tenant_id' => null, + 'stabile_id' => $stabileId, + 'cod_cassa' => 'CDL', + 'descrizione' => 'Cassa di lavoro', + 'tipo' => 'altro', + 'iban' => null, 'intestazione_conto' => null, - 'attiva' => true, - 'meta' => json_encode(['seeded' => true]), - 'created_at' => now(), - 'updated_at' => now(), + 'attiva' => true, + 'meta' => json_encode(['seeded' => true]), + 'created_at' => now(), + 'updated_at' => now(), ]); } } @@ -2273,37 +2599,49 @@ private function stepBanche(?int $limit): int } private function stepOperazioni(?int $limit): int { - if (!Schema::connection('gescon_import')->hasTable('operazioni')) return 0; + if (! Schema::connection('gescon_import')->hasTable('operazioni')) { + return 0; + } + $src = DB::connection('gescon_import')->table('operazioni'); - if ($limit) $src->limit($limit); + if ($limit) { + $src->limit($limit); + } + $count = 0; foreach ($src->get() as $o) { $legacy = $o->id_operaz ?? null; - if (!$legacy) continue; + if (! $legacy) { + continue; + } + // Se la tabella dominio non esiste, in dry-run consideriamo solo il conteggio e saltiamo le scritture - if (!Schema::hasTable('operazioni_contabili')) { + if (! Schema::hasTable('operazioni_contabili')) { $count++; continue; } $exists = DB::table('operazioni_contabili')->where('legacy_id', $legacy)->first(); - if ($exists) continue; + if ($exists) { + continue; + } + $data = [ - 'tenant_id' => null, - 'gestione_id' => $this->resolveGestioneId($o->anno ?? null, $o->compet ?? null, $o->gestione ?? null), - 'legacy_id' => $legacy, - 'descrizione' => $o->note ?? ($o->natura ?? 'Operazione'), - 'data_operazione' => $o->dt_spe ?? now()->toDateString(), - 'compet' => in_array($o->compet ?? 'C', ['C', 'P']) ? $o->compet : 'C', - 'natura2' => $o->natura2 ?? null, - 'n_stra' => $o->n_stra ?? 0, - 'protocollo_numero' => $this->nextProtocolNumber(), + 'tenant_id' => null, + 'gestione_id' => $this->resolveGestioneId($o->anno ?? null, $o->compet ?? null, $o->gestione ?? null), + 'legacy_id' => $legacy, + 'descrizione' => $o->note ?? ($o->natura ?? 'Operazione'), + 'data_operazione' => $o->dt_spe ?? now()->toDateString(), + 'compet' => in_array($o->compet ?? 'C', ['C', 'P']) ? $o->compet : 'C', + 'natura2' => $o->natura2 ?? null, + 'n_stra' => $o->n_stra ?? 0, + 'protocollo_numero' => $this->nextProtocolNumber(), 'protocollo_completo' => null, - 'dare' => (float)($o->importo_spese ?? 0), - 'avere' => (float)($o->importo_entrate ?? 0), - 'conto_contabile' => $o->cod_spe ?? null, - 'stato_operazione' => 'confermata', - 'created_at' => now(), - 'updated_at' => now() + 'dare' => (float) ($o->importo_spese ?? 0), + 'avere' => (float) ($o->importo_entrate ?? 0), + 'conto_contabile' => $o->cod_spe ?? null, + 'stato_operazione' => 'confermata', + 'created_at' => now(), + 'updated_at' => now(), ]; // Collega conto bancario se la colonna esiste nello schema if (Schema::hasColumn('operazioni_contabili', 'conto_bancario_id')) { @@ -2311,10 +2649,10 @@ private function stepOperazioni(?int $limit): int // 1) prova via cod_cassa della riga (se presente in staging operazioni) $codCassa = $o->cod_cassa ?? $o->modalita_pagamento ?? null; if ($codCassa) { - $contoId = $this->resolveContoByCodCassa($this->stabileId, (string)$codCassa); + $contoId = $this->resolveContoByCodCassa($this->stabileId, (string) $codCassa); } // 2) fallback al default per stabile - if (!$contoId) { + if (! $contoId) { $contoId = $this->resolveDefaultContoBancarioId($this->stabileId); } if ($contoId) { @@ -2325,7 +2663,7 @@ private function stepOperazioni(?int $limit): int if (Schema::hasColumn('operazioni_contabili', 'cassa_id') && empty($data['conto_bancario_id'] ?? null)) { $codCassa = $o->cod_cassa ?? $o->modalita_pagamento ?? null; if ($codCassa) { - $cassaId = $this->resolveCassaByCodCassa($this->stabileId, (string)$codCassa); + $cassaId = $this->resolveCassaByCodCassa($this->stabileId, (string) $codCassa); if ($cassaId) { $data['cassa_id'] = $cassaId; } @@ -2342,7 +2680,10 @@ private function stepRate(?int $limit): int return $this->importRateEmesseDaEmissioniDettaglio($limit); } - if (!Schema::connection('gescon_import')->hasTable('rate')) return 0; + if (! Schema::connection('gescon_import')->hasTable('rate')) { + return 0; + } + $src = DB::connection('gescon_import')->table('rate as r'); // Filtro per stabile: se rate.cod_stabile non esiste, prova join con condomin usando r.cod_cond o r.id_cond if ($this->option('stabile')) { @@ -2355,43 +2696,49 @@ private function stepRate(?int $limit): int Schema::connection('gescon_import')->hasColumn('condomin', 'cod_stabile') ) { $joinLeft = Schema::connection('gescon_import')->hasColumn('rate', 'cod_cond') ? 'cod_cond' : 'id_cond'; - $src = $src->join('condomin as c', 'c.' . $joinLeft, '=', 'r.' . $joinLeft) + $src = $src->join('condomin as c', 'c.' . $joinLeft, '=', 'r.' . $joinLeft) ->where('c.cod_stabile', $stab) ->select('r.*'); } } - if ($limit) $src->limit($limit); + if ($limit) { + $src->limit($limit); + } + $count = 0; foreach ($src->get() as $r) { - $legacy = $r->id - ?? ($r->n_rata ?? null) - ?? ($r->numero_rata ?? null) - ?? ($r->n_emissione ?? null); - if (!$legacy) { + $legacy = $r->id ?? ($r->n_rata ?? null) ?? ($r->numero_rata ?? null) ?? ($r->n_emissione ?? null); + if (! $legacy) { // Fallback deterministico: hash di campi tipici $key = implode('|', [ - (string)($r->cod_stabile ?? $this->option('stabile') ?? ''), - (string)($r->cod_cond ?? $r->id_cond ?? ''), - (string)($r->data_emissione ?? ''), - (string)($r->importo ?? $r->importo_euro ?? ''), - (string)($r->causale ?? '') + (string) ($r->cod_stabile ?? $this->option('stabile') ?? ''), + (string) ($r->cod_cond ?? $r->id_cond ?? ''), + (string) ($r->data_emissione ?? ''), + (string) ($r->importo ?? $r->importo_euro ?? ''), + (string) ($r->causale ?? ''), ]); $legacy = hexdec(substr(hash('crc32b', $key), 0, 8)); } - if (!$legacy) continue; + if (! $legacy) { + continue; + } + if (Schema::hasColumn('rate_emesse', 'legacy_id')) { // tabella evoluta con legacy $exists = DB::table('rate_emesse')->where('legacy_id', $legacy)->first(); - if ($exists) continue; + if ($exists) { + continue; + } + $data = [ - 'legacy_id' => $legacy, - 'periodo_anno' => $r->data_emissione ? (int)date('Y', strtotime($r->data_emissione)) : null, - 'periodo_mese' => $r->data_emissione ? (int)date('n', strtotime($r->data_emissione)) : null, - 'importo' => $r->importo ?? 0, - 'descrizione' => $r->causale ?? null, + 'legacy_id' => $legacy, + 'periodo_anno' => $r->data_emissione ? (int) date('Y', strtotime($r->data_emissione)) : null, + 'periodo_mese' => $r->data_emissione ? (int) date('n', strtotime($r->data_emissione)) : null, + 'importo' => $r->importo ?? 0, + 'descrizione' => $r->causale ?? null, 'competenza_da' => $r->data_emissione ?? null, - 'competenza_a' => $r->data_scadenza ?? null, - 'created_at' => now(), - 'updated_at' => now() + 'competenza_a' => $r->data_scadenza ?? null, + 'created_at' => now(), + 'updated_at' => now(), ]; $this->dynamicInsert('rate_emesse', $data); } else { @@ -2405,28 +2752,35 @@ private function stepRate(?int $limit): int private function importRateEmesseDaEmissioniDettaglio(?int $limit): int { - if (!Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')) return 0; - if (!Schema::connection('gescon_import')->hasTable('rate_emissioni')) return 0; + if (! Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')) { + return 0; + } + + if (! Schema::connection('gescon_import')->hasTable('rate_emissioni')) { + return 0; + } $stab = $this->option('stabile'); - $src = DB::connection('gescon_import')->table('rate_emissioni_dettaglio as d'); + $src = DB::connection('gescon_import')->table('rate_emissioni_dettaglio as d'); if ($stab) { $src->where('d.cod_stabile', $stab); } - if ($limit) $src->limit($limit); + if ($limit) { + $src->limit($limit); + } $emissioni = DB::connection('gescon_import')->table('rate_emissioni') ->when($stab, fn($q) => $q->where('cod_stabile', $stab)) ->get() ->groupBy(fn($r) => (string) $r->numero_emissione); - $piani = []; - $count = 0; + $piani = []; + $count = 0; $cumuloCache = []; $hasCondomin = Schema::connection('gescon_import')->hasTable('condomin'); foreach ($src->get() as $row) { - $codStabile = (string) ($row->cod_stabile ?? $stab ?? ''); + $codStabile = (string) ($row->cod_stabile ?? $stab ?? ''); $numeroEmissione = (int) ($row->numero_emissione ?? 0); if ($codStabile === '' || $numeroEmissione <= 0) { continue; @@ -2434,20 +2788,20 @@ private function importRateEmesseDaEmissioniDettaglio(?int $limit): int $codCond = (string) ($row->cod_cond ?? ''); $unitaId = $this->resolveUnitaIdFromCodCond($codStabile, $codCond); - if (!$unitaId) { + if (! $unitaId) { continue; } $soggettoId = $this->resolveSoggettoResponsabileId($unitaId, (string) ($row->tipo_soggetto ?? '')); - if (!$soggettoId) { + if (! $soggettoId) { continue; } - if (!isset($piani[$numeroEmissione])) { + if (! isset($piani[$numeroEmissione])) { $piani[$numeroEmissione] = $this->ensurePianoRateizzazionePerEmissione($codStabile, $numeroEmissione, $emissioni); } $pianoId = $piani[$numeroEmissione]; - if (!$pianoId) { + if (! $pianoId) { continue; } @@ -2460,24 +2814,24 @@ private function importRateEmesseDaEmissioniDettaglio(?int $limit): int : (is_numeric($row->numero_mese ?? null) ? (int) $row->numero_mese : 1); $dataEmissione = $row->data_emissione ?? null; - $dataScadenza = $row->data_scadenza ?? null; + $dataScadenza = $row->data_scadenza ?? null; $emissioneRows = $emissioni[(string) $numeroEmissione] ?? null; - $emissioneRow = $emissioneRows ? $emissioneRows->first() : null; - if (!$dataEmissione && $emissioneRow?->data_emissione) { + $emissioneRow = $emissioneRows ? $emissioneRows->first() : null; + if (! $dataEmissione && $emissioneRow?->data_emissione) { $dataEmissione = $emissioneRow->data_emissione; } - if (!$dataScadenza && $emissioneRow?->data_scadenza) { + if (! $dataScadenza && $emissioneRow?->data_scadenza) { $dataScadenza = $emissioneRow->data_scadenza; } $numeroRataEff = $numeroRata; - $condInq = trim((string) ($row->cond_inq ?? $row->tipo_soggetto ?? '')); + $condInq = trim((string) ($row->cond_inq ?? $row->tipo_soggetto ?? '')); $raggruppamento = trim((string) ($row->raggruppamento ?? '')); if ($raggruppamento === '' && $hasCondomin && $codCond !== '') { $cacheKey = $codStabile . '|' . $codCond; - if (!array_key_exists($cacheKey, $cumuloCache)) { + if (! array_key_exists($cacheKey, $cumuloCache)) { $cumuloCache[$cacheKey] = DB::connection('gescon_import') ->table('condomin') ->where('cod_stabile', $codStabile) @@ -2509,7 +2863,7 @@ private function importRateEmesseDaEmissioniDettaglio(?int $limit): int ->where('numero_rata_progressivo', $numeroRataEff) ->first(); if ($existing) { - $descr = (string) ($row->descrizione ?? $emissioneRow?->descrizione ?? ''); + $descr = (string) ($row->descrizione ?? $emissioneRow?->descrizione ?? ''); $descrizione = (string) ($existing->descrizione ?? ''); if ($descr !== '' && $descrizione !== '' && stripos($descrizione, $descr) === false) { $descrizione = trim($descrizione . ' | ' . $descr); @@ -2518,7 +2872,7 @@ private function importRateEmesseDaEmissioniDettaglio(?int $limit): int } $noteAgg = implode('|', array_filter($noteParts, fn($v) => $v !== '')); - $note = (string) ($existing->note ?? ''); + $note = (string) ($existing->note ?? ''); if ($note === '') { $note = $noteAgg; } elseif ($noteAgg !== '' && stripos($note, $noteAgg) === false) { @@ -2528,20 +2882,20 @@ private function importRateEmesseDaEmissioniDettaglio(?int $limit): int DB::table('rate_emesse') ->where('id', $existing->id) ->update([ - 'descrizione' => $descrizione, - 'importo_originario_unita' => (float) ($existing->importo_originario_unita ?? 0) + $importo, + 'descrizione' => $descrizione, + 'importo_originario_unita' => (float) ($existing->importo_originario_unita ?? 0) + $importo, 'importo_addebitato_soggetto' => (float) ($existing->importo_addebitato_soggetto ?? 0) + $importo, - 'note' => $note, - 'updated_at' => now(), + 'note' => $note, + 'updated_at' => now(), ]); continue; } // Evita duplicati: stessa emissione, unità, soggetto, rata e date $noteNeedle = '%n_emissione=' . $numeroEmissione . '%' - . 'cod_cond_src=' . $codCond . '%' - . 'o_r_s=' . (string) ($row->tipo_quota ?? $row->tipo_soggetto ?? '') . '%' - . 'n_mese=' . (string) ($row->numero_mese ?? '') . '%'; + . 'cod_cond_src=' . $codCond . '%' + . 'o_r_s=' . (string) ($row->tipo_quota ?? $row->tipo_soggetto ?? '') . '%' + . 'n_mese=' . (string) ($row->numero_mese ?? '') . '%'; $dupExists = DB::table('rate_emesse') ->where('piano_rateizzazione_id', $pianoId) ->where('unita_immobiliare_id', $unitaId) @@ -2556,22 +2910,22 @@ private function importRateEmesseDaEmissioniDettaglio(?int $limit): int } $payload = [ - 'piano_rateizzazione_id' => $pianoId, - 'unita_immobiliare_id' => $unitaId, - 'soggetto_responsabile_id' => $soggettoId, - 'numero_rata_progressivo' => $numeroRataEff, - 'descrizione' => $row->descrizione ?? ($emissioneRow?->descrizione ?? null), - 'importo_originario_unita' => $importo, + 'piano_rateizzazione_id' => $pianoId, + 'unita_immobiliare_id' => $unitaId, + 'soggetto_responsabile_id' => $soggettoId, + 'numero_rata_progressivo' => $numeroRataEff, + 'descrizione' => $row->descrizione ?? ($emissioneRow?->descrizione ?? null), + 'importo_originario_unita' => $importo, 'percentuale_addebito_soggetto' => 100.0, - 'importo_addebitato_soggetto' => $importo, - 'data_emissione' => $dataEmissione ?? now()->toDateString(), - 'data_scadenza' => $dataScadenza ?? ($dataEmissione ?? now()->toDateString()), - 'stato_rata' => 'EMESSA', - 'data_ultimo_pagamento' => null, - 'importo_pagato' => 0, - 'note' => implode('|', array_filter($noteParts, fn($v) => $v !== '')), - 'created_at' => now(), - 'updated_at' => now(), + 'importo_addebitato_soggetto' => $importo, + 'data_emissione' => $dataEmissione ?? now()->toDateString(), + 'data_scadenza' => $dataScadenza ?? ($dataEmissione ?? now()->toDateString()), + 'stato_rata' => 'EMESSA', + 'data_ultimo_pagamento' => null, + 'importo_pagato' => 0, + 'note' => implode('|', array_filter($noteParts, fn($v) => $v !== '')), + 'created_at' => now(), + 'updated_at' => now(), ]; $this->dynamicInsert('rate_emesse', $payload); @@ -2584,29 +2938,31 @@ private function importRateEmesseDaEmissioniDettaglio(?int $limit): int private function resolveUnitaIdFromCodCond(string $codStabile, string $codCond): ?int { $codCond = trim($codCond); - if ($codCond === '') return null; + if ($codCond === '') { + return null; + } $stabileId = $this->stabileId; if (Schema::hasTable('stabili')) { $stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; - $stRow = DB::table('stabili')->where($stabileCol, $codStabile)->first(); + $stRow = DB::table('stabili')->where($stabileCol, $codStabile)->first(); if ($stRow) { $stabileId = $stRow->id; } } - if (!$stabileId) { + if (! $stabileId) { return null; } $cond = null; if (Schema::connection('gescon_import')->hasTable('condomin')) { - $cond = DB::connection('gescon_import')->table('condomin') + $cond = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin')) ->where('cod_stabile', $codStabile) ->where('cod_cond', $codCond) ->first(['scala', 'interno', 'codice_fiscale', 'email', 'nome', 'cognome']); } - $scala = $cond?->scala ? trim((string) $cond->scala) : 'A'; + $scala = $cond?->scala ? trim((string) $cond->scala) : 'A'; $interno = $cond?->interno ? trim((string) $cond->interno) : ''; if ($interno === '') { $interno = $codCond; @@ -2631,11 +2987,11 @@ private function resolveUnitaIdFromCodCond(string $codStabile, string $codCond): } if ($cond) { - $cf = trim((string) ($cond->codice_fiscale ?? '')) ?: null; - $email = trim((string) ($cond->email ?? '')) ?: null; - $nome = $cond->nome ?? null; + $cf = trim((string) ($cond->codice_fiscale ?? '')) ?: null; + $email = trim((string) ($cond->email ?? '')) ?: null; + $nome = $cond->nome ?? null; $cognome = $cond->cognome ?? null; - $sog = $this->findExistingSoggetto($cf, $email, $nome, $cognome); + $sog = $this->findExistingSoggetto($cf, $email, $nome, $cognome); if ($sog && Schema::hasTable('proprieta')) { $row = DB::table('proprieta') ->where('soggetto_id', $sog->id) @@ -2650,43 +3006,43 @@ private function resolveUnitaIdFromCodCond(string $codStabile, string $codCond): } if (Schema::hasTable('unita_immobiliari')) { - $codice = $codStabile . '-' . $scala . '-' . $interno; + $codice = $codStabile . '-' . $scala . '-' . $interno; $existing = DB::table('unita_immobiliari') ->where('stabile_id', $stabileId) ->where('codice_unita', $codice) ->first(['id']); - if (!$existing) { + if (! $existing) { $uid = DB::table('unita_immobiliari')->insertGetId([ - 'stabile_id' => $stabileId, - 'codice_unita' => $codice, - 'scala' => $scala, - 'interno' => $interno, - 'denominazione' => trim((string)($nome ?? '') . ' ' . (string)($cognome ?? '')) ?: null, - 'tipo_unita' => 'abitazione', + 'stabile_id' => $stabileId, + 'codice_unita' => $codice, + 'scala' => $scala, + 'interno' => $interno, + 'denominazione' => trim((string) ($nome ?? '') . ' ' . (string) ($cognome ?? '')) ?: null, + 'tipo_unita' => 'abitazione', 'stato_occupazione' => 'occupata_proprietario', - 'attiva' => true, - 'created_at' => now(), - 'updated_at' => now(), + 'attiva' => true, + 'created_at' => now(), + 'updated_at' => now(), ]); $existing = $uid ? (object) ['id' => $uid] : null; } if ($existing && Schema::hasTable('soggetti')) { - if (!$sog) { + if (! $sog) { $payload = [ - 'old_id' => $this->safeOldId($codCond), - 'nome' => $nome, - 'cognome' => $cognome, + 'old_id' => $this->safeOldId($codCond), + 'nome' => $nome, + 'cognome' => $cognome, 'codice_fiscale' => $cf, - 'email' => $email, - 'tipo' => 'proprietario', - 'created_at' => now(), - 'updated_at' => now(), + 'email' => $email, + 'tipo' => 'proprietario', + 'created_at' => now(), + 'updated_at' => now(), ]; if (Schema::hasColumn('soggetti', 'codice_univoco')) { $baseKey = $this->normalizeNameKey($nome ?? '', $cognome ?? '') ?: ($email ?: 'ND'); - $code = $this->generateStableCondKey('S', $codStabile ?: 'S0', $baseKey); - $tries = 0; + $code = $this->generateStableCondKey('S', $codStabile ?: 'S0', $baseKey); + $tries = 0; while (DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 5) { $salt = ($codCond ?? '') . '-' . ($tries + 1); $code = $this->generateStableCondKey('S', $codStabile ?: 'S0', $baseKey . '|' . $salt); @@ -2703,13 +3059,13 @@ private function resolveUnitaIdFromCodCond(string $codStabile, string $codCond): ->where('unita_immobiliare_id', $existing->id) ->where('soggetto_id', $sog->id) ->first(['id']); - if (!$existsProp) { + if (! $existsProp) { DB::table('proprieta')->insert([ 'unita_immobiliare_id' => $existing->id, - 'soggetto_id' => $sog->id, - 'tipo_diritto' => 'proprieta', - 'created_at' => now(), - 'updated_at' => now(), + 'soggetto_id' => $sog->id, + 'tipo_diritto' => 'proprieta', + 'created_at' => now(), + 'updated_at' => now(), ]); } } @@ -2722,7 +3078,7 @@ private function resolveUnitaIdFromCodCond(string $codStabile, string $codCond): } $codice = $codStabile . '-' . $scala . '-' . $interno; - $unita = DB::table('unita_immobiliari') + $unita = DB::table('unita_immobiliari') ->where('stabile_id', $stabileId) ->where('codice_unita', $codice) ->first(['id']); @@ -2731,37 +3087,37 @@ private function resolveUnitaIdFromCodCond(string $codStabile, string $codCond): return (int) $unita->id; } - if (!$cond && Schema::hasTable('unita_immobiliari')) { - $scala = 'A'; + if (! $cond && Schema::hasTable('unita_immobiliari')) { + $scala = 'A'; $interno = $codCond; - $codice = $codStabile . '-' . $scala . '-' . $interno; - $uid = DB::table('unita_immobiliari')->insertGetId([ - 'stabile_id' => $stabileId, - 'codice_unita' => $codice, - 'scala' => $scala, - 'interno' => $interno, - 'denominazione' => 'Unità legacy ' . $codCond, - 'tipo_unita' => 'abitazione', + $codice = $codStabile . '-' . $scala . '-' . $interno; + $uid = DB::table('unita_immobiliari')->insertGetId([ + 'stabile_id' => $stabileId, + 'codice_unita' => $codice, + 'scala' => $scala, + 'interno' => $interno, + 'denominazione' => 'Unità legacy ' . $codCond, + 'tipo_unita' => 'abitazione', 'stato_occupazione' => 'occupata_proprietario', - 'attiva' => true, - 'created_at' => now(), - 'updated_at' => now(), + 'attiva' => true, + 'created_at' => now(), + 'updated_at' => now(), ]); if ($uid) { if (Schema::hasTable('soggetti')) { $payload = [ - 'old_id' => $this->safeOldId($codCond), - 'nome' => 'Condomino', - 'cognome' => $codCond, - 'tipo' => 'proprietario', + 'old_id' => $this->safeOldId($codCond), + 'nome' => 'Condomino', + 'cognome' => $codCond, + 'tipo' => 'proprietario', 'created_at' => now(), 'updated_at' => now(), ]; if (Schema::hasColumn('soggetti', 'codice_univoco')) { $baseKey = 'COND-' . $codCond; - $code = $this->generateStableCondKey('S', $codStabile ?: 'S0', $baseKey); - $tries = 0; + $code = $this->generateStableCondKey('S', $codStabile ?: 'S0', $baseKey); + $tries = 0; while (DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 5) { $code = $this->generateStableCondKey('S', $codStabile ?: 'S0', $baseKey . '|' . ($tries + 1)); $tries++; @@ -2772,10 +3128,10 @@ private function resolveUnitaIdFromCodCond(string $codStabile, string $codCond): if ($sid && Schema::hasTable('proprieta')) { DB::table('proprieta')->insert([ 'unita_immobiliare_id' => $uid, - 'soggetto_id' => $sid, - 'tipo_diritto' => 'proprieta', - 'created_at' => now(), - 'updated_at' => now(), + 'soggetto_id' => $sid, + 'tipo_diritto' => 'proprieta', + 'created_at' => now(), + 'updated_at' => now(), ]); } } @@ -2788,10 +3144,12 @@ private function resolveUnitaIdFromCodCond(string $codStabile, string $codCond): private function resolveSoggettoResponsabileId(int $unitaId, string $tipoSoggetto = ''): ?int { - if (!Schema::hasTable('proprieta')) return null; + if (! Schema::hasTable('proprieta')) { + return null; + } $tipoSoggetto = strtoupper(trim($tipoSoggetto)); - $prefer = $tipoSoggetto === 'I' ? 'locazione' : 'proprieta'; + $prefer = $tipoSoggetto === 'I' ? 'locazione' : 'proprieta'; $row = DB::table('proprieta') ->where('unita_immobiliare_id', $unitaId) @@ -2805,20 +3163,24 @@ private function resolveSoggettoResponsabileId(int $unitaId, string $tipoSoggett private function ensurePianoRateizzazionePerEmissione(string $codStabile, int $numeroEmissione, $emissioni): ?int { - if (!Schema::hasTable('piano_rateizzazione')) return null; + if (! Schema::hasTable('piano_rateizzazione')) { + return null; + } $stabileId = $this->stabileId; if (Schema::hasTable('stabili')) { $stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; - $stRow = DB::table('stabili')->where($stabileCol, $codStabile)->first(); + $stRow = DB::table('stabili')->where($stabileCol, $codStabile)->first(); if ($stRow) { $stabileId = $stRow->id; } } - if (!$stabileId) return null; + if (! $stabileId) { + return null; + } $noteKey = 'emissione=' . $numeroEmissione; - $exists = DB::table('piano_rateizzazione') + $exists = DB::table('piano_rateizzazione') ->where('stabile_id', $stabileId) ->where('note', 'like', '%' . $noteKey . '%') ->first(['id']); @@ -2827,10 +3189,10 @@ private function ensurePianoRateizzazionePerEmissione(string $codStabile, int $n } $emissioneRows = $emissioni[(string) $numeroEmissione] ?? null; - $emissioneRow = $emissioneRows ? $emissioneRows->first() : null; - $descr = $emissioneRow?->descrizione ?: ('Emissione ' . $numeroEmissione); - $dataInizio = $emissioneRow?->data_emissione ?? now()->toDateString(); - $dataFine = $emissioneRow?->data_scadenza ?? $dataInizio; + $emissioneRow = $emissioneRows ? $emissioneRows->first() : null; + $descr = $emissioneRow?->descrizione ?: ('Emissione ' . $numeroEmissione); + $dataInizio = $emissioneRow?->data_emissione ?? now()->toDateString(); + $dataFine = $emissioneRow?->data_scadenza ?? $dataInizio; $totale = 0.0; if (Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')) { @@ -2841,24 +3203,24 @@ private function ensurePianoRateizzazionePerEmissione(string $codStabile, int $n } $payload = [ - 'stabile_id' => $stabileId, + 'stabile_id' => $stabileId, 'unita_immobiliare_id' => null, - 'descrizione' => $descr, - 'importo_totale' => $totale, - 'numero_rate' => 1, - 'importo_rata' => $totale, - 'data_inizio' => $dataInizio, - 'data_fine' => $dataFine, - 'frequenza' => 'MENSILE', - 'stato' => 'ATTIVO', - 'importo_pagato' => 0, - 'importo_residuo' => $totale, - 'note' => 'emissione=' . $numeroEmissione . '|cod_stabile=' . $codStabile, - 'created_at' => now(), - 'updated_at' => now(), + 'descrizione' => $descr, + 'importo_totale' => $totale, + 'numero_rate' => 1, + 'importo_rata' => $totale, + 'data_inizio' => $dataInizio, + 'data_fine' => $dataFine, + 'frequenza' => 'MENSILE', + 'stato' => 'ATTIVO', + 'importo_pagato' => 0, + 'importo_residuo' => $totale, + 'note' => 'emissione=' . $numeroEmissione . '|cod_stabile=' . $codStabile, + 'created_at' => now(), + 'updated_at' => now(), ]; - if (!$this->isDryRun) { + if (! $this->isDryRun) { $id = DB::table('piano_rateizzazione')->insertGetId($payload); return is_numeric($id) ? (int) $id : null; } @@ -2867,38 +3229,56 @@ private function ensurePianoRateizzazionePerEmissione(string $codStabile, int $n } private function stepIncassi(?int $limit): int { - if (!Schema::connection('gescon_import')->hasTable('incassi')) return 0; + if (! Schema::connection('gescon_import')->hasTable('incassi')) { + return 0; + } + $src = DB::connection('gescon_import')->table('incassi'); if ($this->option('stabile')) { $src->where('cod_stabile', $this->option('stabile')); } - if ($limit) $src->limit($limit); + if ($limit) { + $src->limit($limit); + } + $count = 0; foreach ($src->get() as $i) { $legacy = $i->id ?? null; - if (!$legacy) continue; - if (!Schema::hasTable('incassi')) continue; + if (! $legacy) { + continue; + } + + if (! Schema::hasTable('incassi')) { + continue; + } + $legacyCol = Schema::hasColumn('incassi', 'legacy_id') ? 'legacy_id' : (Schema::hasColumn('incassi', 'id_incasso_gescon') ? 'id_incasso_gescon' : null); - if (!$legacyCol) continue; + if (! $legacyCol) { + continue; + } + $existsQuery = DB::table('incassi')->where($legacyCol, $legacy); if ($this->stabileId && Schema::hasColumn('incassi', 'condominio_id')) { $existsQuery->where('condominio_id', $this->stabileId); } $exists = $existsQuery->first(); - if ($exists) continue; - $data = []; + if ($exists) { + continue; + } + + $data = []; $data[$legacyCol] = $legacy; - $codStabile = (string) ($i->cod_stabile ?? ($this->option('stabile') ?? '')); - $codCond = (string) ($i->cod_cond ?? ''); - $condInq = (string) ($i->cond_inq ?? $i->cond_inquil ?? ''); - $unitaId = $codCond !== '' ? $this->resolveUnitaIdFromCodCond($codStabile, $codCond) : null; - $soggettoId = $unitaId ? $this->resolveSoggettoResponsabileId($unitaId, $condInq) : null; + $codStabile = (string) ($i->cod_stabile ?? ($this->option('stabile') ?? '')); + $codCond = (string) ($i->cod_cond ?? ''); + $condInq = (string) ($i->cond_inq ?? $i->cond_inquil ?? ''); + $unitaId = $codCond !== '' ? $this->resolveUnitaIdFromCodCond($codStabile, $codCond) : null; + $soggettoId = $unitaId ? $this->resolveSoggettoResponsabileId($unitaId, $condInq) : null; if (Schema::hasColumn('incassi', 'data_incasso')) { - $data['data_incasso'] = $i->data_incasso ?? $i->dt_empag ?? now()->toDateString(); - $data['importo'] = $i->importo_pagato_euro ?? $i->importo_euro ?? $i->importo ?? 0; - $data['descrizione'] = $i->descrizione ?? $i->note ?? null; + $data['data_incasso'] = $i->data_incasso ?? $i->dt_empag ?? now()->toDateString(); + $data['importo'] = $i->importo_pagato_euro ?? $i->importo_euro ?? $i->importo ?? 0; + $data['descrizione'] = $i->descrizione ?? $i->note ?? null; $data['riferimento_orig'] = $i->riferimento_pagamento ?? null; - $data['ruolo_quota'] = $condInq !== '' ? $condInq : null; + $data['ruolo_quota'] = $condInq !== '' ? $condInq : null; if (Schema::hasColumn('incassi', 'cod_cond_gescon')) { $data['cod_cond_gescon'] = $codCond !== '' ? $codCond : null; } @@ -2929,7 +3309,7 @@ private function stepIncassi(?int $limit): int if (Schema::hasColumn('incassi', 'cod_cassa_gescon')) { $data['cod_cassa_gescon'] = $i->cod_cassa ?? $i->modalita_pagamento ?? null; } - if (Schema::hasColumn('incassi', 'proviene_n_stra') && !empty($i->n_stra)) { + if (Schema::hasColumn('incassi', 'proviene_n_stra') && ! empty($i->n_stra)) { $data['proviene_n_stra'] = $i->n_stra; } if (Schema::hasColumn('incassi', 'condomino_id') && $soggettoId) { @@ -2937,52 +3317,55 @@ private function stepIncassi(?int $limit): int } // collega eventuale conto per incasso se colonna esiste if (Schema::hasColumn('incassi', 'conto_bancario_id')) { - $contoId = null; + $contoId = null; $codCassa = $i->cod_cassa ?? $i->modalita_pagamento ?? null; if ($codCassa) { - $contoId = $this->resolveContoByCodCassa($this->stabileId, (string)$codCassa); + $contoId = $this->resolveContoByCodCassa($this->stabileId, (string) $codCassa); } - if (!$contoId) { + if (! $contoId) { $contoId = $this->resolveDefaultContoBancarioId($this->stabileId); } - if ($contoId) $data['conto_bancario_id'] = $contoId; + if ($contoId) { + $data['conto_bancario_id'] = $contoId; + } + } // collega cassa per incasso se disponibile e nessun conto bancario è stato risolto if (Schema::hasColumn('incassi', 'cassa_id') && empty($data['conto_bancario_id'] ?? null)) { $codCassa = $i->cod_cassa ?? $i->modalita_pagamento ?? null; if ($codCassa) { - $cassaId = $this->resolveCassaByCodCassa($this->stabileId, (string)$codCassa); + $cassaId = $this->resolveCassaByCodCassa($this->stabileId, (string) $codCassa); if ($cassaId) { $data['cassa_id'] = $cassaId; if (Schema::hasColumn('incassi', 'cod_cassa_legacy')) { - $data['cod_cassa_legacy'] = (string)$codCassa; + $data['cod_cassa_legacy'] = (string) $codCassa; } } } } } else { // schema complesso (nuova tabella incassi) $data = array_merge($data, [ - 'tenant_id' => 'T1', - 'gestione_id' => null, - 'condominio_id' => $this->stabileId ?? 1, - 'conto_bancario_id' => (function () use ($i) { + 'tenant_id' => 'T1', + 'gestione_id' => null, + 'condominio_id' => $this->stabileId ?? 1, + 'conto_bancario_id' => (function () use ($i) { $cod = $i->cod_cassa ?? $i->modalita_pagamento ?? null; - $id = $cod ? $this->resolveContoByCodCassa($this->stabileId, (string)$cod) : null; + $id = $cod ? $this->resolveContoByCodCassa($this->stabileId, (string) $cod) : null; return $id ?: ($this->resolveDefaultContoBancarioId($this->stabileId) ?? null); })(), - 'cod_cond_gescon' => $i->cod_cond ?? $i->cod_cond ?? null, - 'cond_inquil' => $i->cond_inq ?? null, - 'n_riferimento' => $i->riferimento_pagamento ?? null, - 'anno_rif' => $i->data_incasso ? (int)date('Y', strtotime($i->data_incasso)) : 0, - 'n_mese' => $i->n_mese ?? null, - 'o_r_s' => null, - 'importo_pagato' => $i->importo ?? 0, + 'cod_cond_gescon' => $i->cod_cond ?? $i->cod_cond ?? null, + 'cond_inquil' => $i->cond_inq ?? null, + 'n_riferimento' => $i->riferimento_pagamento ?? null, + 'anno_rif' => $i->data_incasso ? (int) date('Y', strtotime($i->data_incasso)) : 0, + 'n_mese' => $i->n_mese ?? null, + 'o_r_s' => null, + 'importo_pagato' => $i->importo ?? 0, 'importo_pagato_euro' => $i->importo_euro ?? 0, - 'dt_empag' => $i->data_incasso ?? null, - 'descrizione' => $i->note ?? null, - 'cod_cassa_gescon' => null, - 'created_at' => now(), - 'updated_at' => now() + 'dt_empag' => $i->data_incasso ?? null, + 'descrizione' => $i->note ?? null, + 'cod_cassa_gescon' => null, + 'created_at' => now(), + 'updated_at' => now(), ]); // Se lo schema esteso prevedesse cassa_id, potremmo valorizzarlo qui se conto bancario non risolto } @@ -2999,7 +3382,10 @@ private function stepIncassi(?int $limit): int */ private function resolveContoByCodCassa(?int $stabileId, string $codCassa): ?int { - if (!$stabileId || !Schema::hasTable('dati_bancari')) return null; + if (! $stabileId || ! Schema::hasTable('dati_bancari')) { + return null; + } + $cod = strtoupper(trim($codCassa)); // Normalizza numeri conto da formati esterni (es. "CC N.000104951005" → "000104951005") $normNumero = null; @@ -3008,7 +3394,10 @@ private function resolveContoByCodCassa(?int $stabileId, string $codCassa): ?int $normNumero = ltrim($m[1], '0'); } } - if ($cod === '') return null; + if ($cod === '') { + return null; + } + $row = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->where(function ($q) use ($cod) { @@ -3019,7 +3408,7 @@ private function resolveContoByCodCassa(?int $stabileId, string $codCassa): ?int ->orderByDesc('is_nostro_conto') ->orderByDesc('updated_at') ->first(); - if (!$row && $normNumero) { + if (! $row && $normNumero) { $row = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->where(function ($q) use ($normNumero) { @@ -3030,7 +3419,7 @@ private function resolveContoByCodCassa(?int $stabileId, string $codCassa): ?int ->orderByDesc('updated_at') ->first(); } - return $row?->id ? (int)$row->id : null; + return $row?->id ? (int) $row->id : null; } /** @@ -3038,16 +3427,25 @@ private function resolveContoByCodCassa(?int $stabileId, string $codCassa): ?int */ private function resolveCassaByCodCassa(?int $stabileId, string $codCassa): ?int { - if (!$stabileId || !Schema::hasTable('casse')) return null; + if (! $stabileId || ! Schema::hasTable('casse')) { + return null; + } + $cod = strtoupper(trim($codCassa)); - if ($cod === '') return null; + if ($cod === '') { + return null; + } + $row = DB::table('casse') ->where('stabile_id', $stabileId) ->where('cod_cassa', $cod) ->first(); // Mappature comuni: "CCB" potrebbe indicare il conto corrente bancario associato all'IBAN; in tal caso priorità al conto_bancario, ma lasciamo anche una cassa con stesso codice se creata - if (!$row) return null; - return (int)$row->id; + if (! $row) { + return null; + } + + return (int) $row->id; } private function stepGiroconti(?int $limit): int { @@ -3058,24 +3456,33 @@ private function stepGiroconti(?int $limit): int } else { return 0; } - if ($limit) $src->limit($limit); + if ($limit) { + $src->limit($limit); + } + $count = 0; foreach ($src->get() as $g) { $rif = $g->riferimento ?? null; - if (!$rif) continue; + if (! $rif) { + continue; + } + $exists = DB::table('movimenti_interni')->where('riferimento', $rif)->first(); - if ($exists) continue; + if ($exists) { + continue; + } + $data = [ - 'legacy_id' => null, + 'legacy_id' => null, 'riferimento' => $rif, - 'data' => $g->Data_giroconto ?? now()->toDateString(), + 'data' => $g->Data_giroconto ?? now()->toDateString(), 'descrizione' => $g->Descrizione ?? '', - 'importo' => $g->Importo ?? 0, - 'conto_from' => $g->Cod_uscita ?? '', - 'conto_to' => $g->Cod_entrata ?? '', - 'tipo' => $g->tipo_riga ?? null, - 'created_at' => now(), - 'updated_at' => now() + 'importo' => $g->Importo ?? 0, + 'conto_from' => $g->Cod_uscita ?? '', + 'conto_to' => $g->Cod_entrata ?? '', + 'tipo' => $g->tipo_riga ?? null, + 'created_at' => now(), + 'updated_at' => now(), ]; $this->dynamicInsert('movimenti_interni', $data); $count++; @@ -3084,35 +3491,90 @@ private function stepGiroconti(?int $limit): int } private function stepStraord(?int $limit): int { - if (!Schema::connection('gescon_import')->hasTable('straordinarie')) return 0; + if (! Schema::connection('gescon_import')->hasTable('straordinarie')) { + return 0; + } + $src = DB::connection('gescon_import')->table('straordinarie'); - if ($limit) $src->limit($limit); + if ($this->option('stabile') && Schema::connection('gescon_import')->hasColumn('straordinarie', 'cod_stabile')) { + $src->where('cod_stabile', $this->option('stabile')); + } + if ($limit) { + $src->limit($limit); + } + $count = 0; foreach ($src->get() as $s) { $legacy = $s->id_stra ?? null; - if (!$legacy) continue; - $exists = DB::table('lavori_straordinari')->where('legacy_id', $legacy)->first(); - if ($exists) continue; + if (! $legacy) { + continue; + } + + $legacyYear = isset($s->legacy_year) ? (string) $s->legacy_year : null; + $annoGestione = $this->resolveLegacySnapshotYear($legacyYear, $s); + $numeroStraordinaria = $this->inferStraordinariaSequence($s->codice ?? null, $s->descriz_prev_cons ?? null, $legacy); + $gestioneId = $this->resolveGestioneContabileIdForStabile($this->resolveStabileId(), $legacyYear, 'S', $numeroStraordinaria); + $codiceDominio = $this->buildScopedLegacyCode((string) ($s->codice ?? ''), $annoGestione, $numeroStraordinaria, 'STRA'); + $exists = DB::table('lavori_straordinari') + ->when($gestioneId, fn($q) => $q->where('gestione_id', $gestioneId)) + ->where('codice', $codiceDominio) + ->first(); + if ($exists) { + continue; + } + + $isFiscal = $this->isFiscalStraordinaria($s); + $descrizione = trim((string) ($s->descriz_prev_cons ?? '')); + if ($descrizione === '') { + $descrizione = trim((string) ($s->descriz_ccp ?? '')) ?: ('Straordinaria ' . $legacy); + } $data = [ - 'legacy_id' => $legacy, - 'codice' => $s->codice ?? ('STRA-' . $legacy), - 'descrizione' => $s->descriz_prev_cons ?? '', - 'numero_rate' => $s->num_rate ?? 0, - 'data_inizio' => isset($s->inizio_anno, $s->inizio_mese) ? sprintf('%04d-%02d-01', $s->inizio_anno, $s->inizio_mese) : null, - 'natura' => $s->natura ?? 'spesa', - 'gruppo' => $s->aggregazione ?? null, - 'periodicita' => $s->periodicita ?? null, - 'created_at' => now(), - 'updated_at' => now() + 'legacy_id' => $legacy, + 'gestione_id' => $gestioneId, + 'codice' => $codiceDominio, + 'descrizione' => $descrizione, + 'numero_rate' => $s->num_rate ?? 0, + 'data_inizio' => isset($s->inizio_anno, $s->inizio_mese) ? sprintf('%04d-%02d-01', $s->inizio_anno, $s->inizio_mese) : null, + 'natura' => $this->normalizeStraordinariaNatura($s->natura ?? null), + 'gruppo' => $s->aggregazione ?? null, + 'periodicita' => $s->periodicita ?? null, + 'note_json' => json_encode([ + 'legacy_code' => $s->codice ?? null, + 'legacy_year' => $legacyYear, + 'anno_gestione' => $annoGestione, + 'numero_straordinaria' => $numeroStraordinaria, + 'descriz_ccp' => $s->descriz_ccp ?? null, + 'is_fiscal_declaration' => $isFiscal, + 'fiscal_declaration_key' => $isFiscal && $annoGestione ? sprintf('%d-%02d', $annoGestione, (int) ($numeroStraordinaria ?? 1)) : null, + ]), + 'importo_previsto' => $this->sumStraordinariaImporto($s), + 'chiuso' => false, + 'created_at' => now(), + 'updated_at' => now(), ]; $this->dynamicInsert('lavori_straordinari', $data); $count++; } return $count; } + + private function normalizeStraordinariaNatura($value): string + { + $normalized = strtoupper(trim((string) $value)); + + if (in_array($normalized, ['E', 'ENTRATA', 'ENTRATE'], true)) { + return 'entrata'; + } + + return 'spesa'; + } + private function stepAddebiti(?int $limit): int { - if (!Schema::connection('gescon_import')->hasTable('dett_pers')) return 0; + if (! Schema::connection('gescon_import')->hasTable('dett_pers')) { + return 0; + } + $src = DB::connection('gescon_import')->table('dett_pers as d'); if (Schema::connection('gescon_import')->hasTable('condomin')) { $src = $src->leftJoin('condomin as c', 'c.cod_cond', '=', 'd.id_cond') @@ -3121,7 +3583,10 @@ private function stepAddebiti(?int $limit): int $src->where('c.cod_stabile', $this->option('stabile')); } } - if ($limit) $src->limit($limit); + if ($limit) { + $src->limit($limit); + } + $count = 0; foreach ($src->get() as $d) { $legacySeed = implode('|', [ @@ -3133,19 +3598,22 @@ private function stepAddebiti(?int $limit): int (string) ($d->tipo_gestione ?? ''), ]); $legacyUnsigned = sprintf('%u', crc32($legacySeed)); - $legacy = (int) ($legacyUnsigned % 2147483647); + $legacy = (int) ($legacyUnsigned % 2147483647); if ($legacy <= 0) { $legacy = (int) $legacyUnsigned; } $exists = DB::table('addebiti_personalizzati')->where('legacy_id', $legacy)->first(); - if ($exists) continue; + if ($exists) { + continue; + } + $unitaId = null; if (Schema::hasTable('unita_immobiliari')) { - $unita = $this->findUnitaByStagingRow($d); + $unita = $this->findUnitaByStagingRow($d); $unitaId = $unita->id ?? null; } $tabellaId = null; - if (Schema::hasTable('tabelle_millesimali') && !empty($d->tabella)) { + if (Schema::hasTable('tabelle_millesimali') && ! empty($d->tabella)) { $tabellaId = DB::table('tabelle_millesimali') ->where('stabile_id', $this->resolveStabileIdForScala($d->scala ?? null)) ->where(function ($q) use ($d) { @@ -3173,27 +3641,27 @@ private function stepAddebiti(?int $limit): int ); } $data = [ - 'legacy_id' => $legacy, - 'gestione_id' => $gestioneId, - 'unita_immobiliare_id' => $unitaId, - 'tipo_gestione' => $d->tipo_gestione ?? null, - 'numero_straordinaria' => $d->n_stra ?? 0, - 'codice_spesa' => $d->n_spe ?? null, - 'natura2' => $d->natura2 ?? null, - 'importo' => $d->importo ?? 0, - 'ruolo' => $ruolo, + 'legacy_id' => $legacy, + 'gestione_id' => $gestioneId, + 'unita_immobiliare_id' => $unitaId, + 'tipo_gestione' => $d->tipo_gestione ?? null, + 'numero_straordinaria' => $d->n_stra ?? 0, + 'codice_spesa' => $d->n_spe ?? null, + 'natura2' => $d->natura2 ?? null, + 'importo' => $d->importo ?? 0, + 'ruolo' => $ruolo, 'tabella_millesimale_id' => $tabellaId, - 'is_unico' => ($d->unico ?? 0) ? true : false, - 'meta' => json_encode([ - 'legacy_year' => $d->legacy_year ?? null, - 'legacy_file' => $d->legacy_file ?? null, + 'is_unico' => ($d->unico ?? 0) ? true : false, + 'meta' => json_encode([ + 'legacy_year' => $d->legacy_year ?? null, + 'legacy_file' => $d->legacy_file ?? null, 'legacy_source_id' => $d->id ?? null, - 'id_cond' => $d->id_cond ?? null, - 'tabella' => $d->tabella ?? null, - 'payload' => $d->payload ?? null, + 'id_cond' => $d->id_cond ?? null, + 'tabella' => $d->tabella ?? null, + 'payload' => $d->payload ?? null, ]), - 'created_at' => now(), - 'updated_at' => now() + 'created_at' => now(), + 'updated_at' => now(), ]; $this->dynamicInsert('addebiti_personalizzati', $data); $count++; @@ -3202,28 +3670,43 @@ private function stepAddebiti(?int $limit): int } private function stepDetrazioni(?int $limit): int { - if (!Schema::connection('gescon_import')->hasTable('detrazioni_fiscali')) return 0; + if (! Schema::connection('gescon_import')->hasTable('detrazioni_fiscali')) { + return 0; + } + $src = DB::connection('gescon_import')->table('detrazioni_fiscali'); - if ($limit) $src->limit($limit); + if ($this->option('stabile') && Schema::connection('gescon_import')->hasColumn('detrazioni_fiscali', 'cod_stabile')) { + $src->where('cod_stabile', $this->option('stabile')); + } + if ($limit) { + $src->limit($limit); + } + $count = 0; foreach ($src->get() as $d) { $legacy = $d->id ?? null; - if (!$legacy) continue; + if (! $legacy) { + continue; + } + $exists = DB::table('detrazioni_fiscali_dom')->where('legacy_rif', $legacy)->first(); - if ($exists) continue; + if ($exists) { + continue; + } + $data = [ - 'legacy_rif' => $legacy, - 'descrizione' => $d->tipo_detrazione ?? '', - 'tipologia' => $d->tipo_detrazione ?? null, - 'importo' => $d->importo_euro ?? 0, + 'legacy_rif' => $legacy, + 'descrizione' => $d->tipo_detrazione ?? '', + 'tipologia' => $d->tipo_detrazione ?? null, + 'importo' => $d->importo_euro ?? 0, 'cessione_flag' => false, - 'importi_json' => json_encode([ - 'tot' => $d->importo_detraibile ?? null, - 'bon' => null, - 'non_bon' => null + 'importi_json' => json_encode([ + 'tot' => $d->importo_detraibile ?? null, + 'bon' => null, + 'non_bon' => null, ]), - 'created_at' => now(), - 'updated_at' => now() + 'created_at' => now(), + 'updated_at' => now(), ]; $this->dynamicInsert('detrazioni_fiscali_dom', $data); $count++; @@ -3242,46 +3725,65 @@ private function preflightStaging(): void { try { $stab = $this->option('stabile'); - if (!$stab) return; - // Autoderiva --mdb-condomin se non fornito e abbiamo --path/--anno - if (!$this->option('mdb-condomin')) { - $root = rtrim((string)($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/'); - $anno = $this->option('anno') ? sprintf('%04d', (int)$this->option('anno')) : null; - if ($anno) { - $cand = $root . '/' . $stab . '/' . $anno . '/singolo_anno.mdb'; - if (is_file($cand)) { - // Hack: set option at runtime using Symfony input if available - try { - $this->input->setOption('mdb-condomin', $cand); - } catch (\Throwable $e) { + if (! $stab) { + return; + } + + $legacyYearOpt = $this->option('anno') ? sprintf('%04d', (int) $this->option('anno')) : null; + + // Autoderiva --mdb-condomin se non fornito, preferendo l'anno richiesto e poi l'ultima annualità disponibile. + if (! $this->option('mdb-condomin')) { + $resolved = $this->resolveCondominMdbForStabile($stab, $legacyYearOpt); + if ($resolved !== null) { + try { + $this->input->setOption('mdb-condomin', $resolved['path']); + } catch (\Throwable $e) { + } + + if ($this->output->isVerbose()) { + $this->line(sprintf( + ' ↪︎ MDB condomin risolto da anni: %s (dir=%s, anno_o=%s, anno_r=%s)', + $resolved['path'], + (string) ($resolved['legacy_year'] ?? '-'), + (string) ($resolved['anno_o'] ?? '-'), + (string) ($resolved['anno_r'] ?? '-') + )); + } + + if ($legacyYearOpt === null) { + if (! empty($resolved['anno_o'])) { + $legacyYearOpt = (string) $resolved['anno_o']; + } elseif (! empty($resolved['legacy_year'])) { + $legacyYearOpt = (string) $resolved['legacy_year']; } } } } // Se manca la tabella o non ha righe per lo stabile, e abbiamo un MDB esplicito per condomin, caricalo $hasCondTable = Schema::connection('gescon_import')->hasTable('condomin'); - if (!$hasCondTable) return; - $hasCodCol = Schema::connection('gescon_import')->hasColumn('condomin', 'cod_stabile'); - $needsLoad = false; - if ($hasCodCol) { - $q = DB::connection('gescon_import')->table('condomin')->where('cod_stabile', $stab); - $needsLoad = !$q->exists(); - } else { - $needsLoad = DB::connection('gescon_import')->table('condomin')->count() === 0; + if (! $hasCondTable) { + return; } - $legacyYearOpt = $this->option('anno') ? sprintf('%04d', (int)$this->option('anno')) : null; + + $needsLoad = $this->stagingTableNeedsLoadForStabile('condomin', $stab); if ($needsLoad) { $mdbCondomin = $this->option('mdb-condomin'); if ($mdbCondomin && is_file($mdbCondomin)) { $this->line(" ↪︎ Precarico staging condomin da MDB per stabile {$stab}..."); // Propaga limit se presente $args = [ - '--mdb' => $mdbCondomin, - '--table' => 'condomin', + '--mdb' => $mdbCondomin, + '--table' => 'condomin', '--stabile' => $stab, ]; - if ($legacyYearOpt) $args['--legacy-year'] = (string) $legacyYearOpt; - if ($this->option('limit')) $args['--limit'] = (string)$this->option('limit'); + if ($legacyYearOpt) { + $args['--legacy-year'] = (string) $legacyYearOpt; + } + + if ($this->option('limit')) { + $args['--limit'] = (string) $this->option('limit'); + } + $this->callSilently('gescon:load-mdb', $args); } } @@ -3293,52 +3795,60 @@ private function preflightStaging(): void if ($mdbCondomin && is_file($mdbCondomin)) { // dett_tab if (Schema::connection('gescon_import')->hasTable('dett_tab')) { - $need = DB::connection('gescon_import')->table('dett_tab')->count() === 0; + $need = $this->stagingTableNeedsLoadForStabile('dett_tab', $stab); if ($need) { $this->callSilently('gescon:load-mdb', [ - '--mdb' => $mdbCondomin, - '--table' => 'dett_tab', - '--stabile' => $stab, + '--mdb' => $mdbCondomin, + '--table' => 'dett_tab', + '--stabile' => $stab, '--legacy-year' => $legacyYearOpt, ]); } } // tabelle_millesimali if (Schema::connection('gescon_import')->hasTable('tabelle_millesimali')) { - $need = DB::connection('gescon_import')->table('tabelle_millesimali')->count() === 0; + $need = $this->stagingTableNeedsLoadForStabile('tabelle_millesimali', $stab); if ($need) { $this->callSilently('gescon:load-mdb', [ - '--mdb' => $mdbCondomin, - '--table' => 'tabelle_millesimali', - '--stabile' => $stab, + '--mdb' => $mdbCondomin, + '--table' => 'tabelle_millesimali', + '--stabile' => $stab, '--legacy-year' => $legacyYearOpt, ]); } } // voc_spe (voci spesa) if (Schema::connection('gescon_import')->hasTable('voc_spe')) { - $need = DB::connection('gescon_import')->table('voc_spe')->count() === 0; + $need = $this->stagingTableNeedsLoadForStabile('voc_spe', $stab); if ($need) { $this->callSilently('gescon:load-mdb', [ - '--mdb' => $mdbCondomin, - '--table' => 'voc_spe', - '--stabile' => $stab, + '--mdb' => $mdbCondomin, + '--table' => 'voc_spe', + '--stabile' => $stab, + '--legacy-year' => $legacyYearOpt, + ]); + } + } + // comproprietari + if (Schema::connection('gescon_import')->hasTable('comproprietari')) { + $need = $this->stagingTableNeedsLoadForStabile('comproprietari', $stab); + if ($need) { + $this->callSilently('gescon:load-mdb', [ + '--mdb' => $mdbCondomin, + '--table' => 'comproprietari', + '--stabile' => $stab, '--legacy-year' => $legacyYearOpt, ]); } } // Cre_Deb_preced (crediti/debiti pregressi) if (Schema::connection('gescon_import')->hasTable('cre_deb_preced')) { - if (Schema::connection('gescon_import')->hasColumn('cre_deb_preced', 'cod_stabile')) { - $need = !DB::connection('gescon_import')->table('cre_deb_preced')->where('cod_stabile', $stab)->exists(); - } else { - $need = DB::connection('gescon_import')->table('cre_deb_preced')->count() === 0; - } + $need = $this->stagingTableNeedsLoadForStabile('cre_deb_preced', $stab); if ($need) { $this->callSilently('gescon:load-mdb', [ - '--mdb' => $mdbCondomin, - '--table' => 'cre_deb_preced', - '--stabile' => $stab, + '--mdb' => $mdbCondomin, + '--table' => 'cre_deb_preced', + '--stabile' => $stab, '--legacy-year' => $legacyYearOpt, ]); } @@ -3355,12 +3865,12 @@ private function preflightStaging(): void ]; foreach ($more as $tb) { if (Schema::connection('gescon_import')->hasTable($tb)) { - $need = DB::connection('gescon_import')->table($tb)->count() === 0; + $need = $this->stagingTableNeedsLoadForStabile($tb, $stab); if ($need) { $this->callSilently('gescon:load-mdb', [ - '--mdb' => $mdbCondomin, - '--table' => $tb, - '--stabile' => $stab, + '--mdb' => $mdbCondomin, + '--table' => $tb, + '--stabile' => $stab, '--legacy-year' => $legacyYearOpt, ]); } @@ -3375,30 +3885,156 @@ private function preflightStaging(): void } } + private function resolveCondominMdbForStabile(string $stab, ?string $legacyYear = null): ?array + { + $root = rtrim((string) ($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/'); + $stableDir = $root . '/' . $stab; + if (! is_dir($stableDir)) { + return null; + } + + if ($legacyYear) { + $preferred = $stableDir . '/' . $legacyYear . '/singolo_anno.mdb'; + if (is_file($preferred)) { + return [ + 'path' => $preferred, + 'legacy_year' => $legacyYear, + 'anno_o' => ctype_digit($legacyYear) && strlen($legacyYear) === 4 ? $legacyYear : null, + 'anno_r' => ctype_digit($legacyYear) && strlen($legacyYear) === 4 ? $legacyYear : null, + ]; + } + } + + $anniRows = []; + $generale = $stableDir . '/generale_stabile.mdb'; + foreach ($this->mdbExportToRows($generale, ['anni', 'Anni', 'ANNI']) as $row) { + $dir = trim((string) ($row['nome_dir'] ?? $row['nome_dir '] ?? '')); + if ($dir === '') { + continue; + } + + $candidatePath = $stableDir . '/' . $dir . '/singolo_anno.mdb'; + if (! is_file($candidatePath)) { + continue; + } + + $annoO = trim((string) ($row['anno_o'] ?? $row['anno'] ?? $row['anno_gestione'] ?? '')); + $annoR = trim((string) ($row['anno_r'] ?? '')); + $anniRows[] = [ + 'path' => $candidatePath, + 'legacy_year' => $dir, + 'anno_o' => $annoO !== '' ? $annoO : null, + 'anno_r' => $annoR !== '' ? $annoR : null, + ]; + } + + if ($legacyYear) { + foreach ($anniRows as $candidate) { + if ($legacyYear === ($candidate['anno_o'] ?? null) || $legacyYear === ($candidate['anno_r'] ?? null)) { + return $candidate; + } + } + } + + if ($anniRows !== []) { + usort($anniRows, static function (array $left, array $right): int { + $leftAnnoO = is_numeric($left['anno_o'] ?? null) ? (int) $left['anno_o'] : -1; + $rightAnnoO = is_numeric($right['anno_o'] ?? null) ? (int) $right['anno_o'] : -1; + if ($leftAnnoO !== $rightAnnoO) { + return $rightAnnoO <=> $leftAnnoO; + } + + $leftAnnoR = is_numeric($left['anno_r'] ?? null) ? (int) $left['anno_r'] : -1; + $rightAnnoR = is_numeric($right['anno_r'] ?? null) ? (int) $right['anno_r'] : -1; + if ($leftAnnoR !== $rightAnnoR) { + return $rightAnnoR <=> $leftAnnoR; + } + + return strcmp((string) ($right['legacy_year'] ?? ''), (string) ($left['legacy_year'] ?? '')); + }); + + return $anniRows[0]; + } + + $candidates = []; + foreach ((array) glob($stableDir . '/*/singolo_anno.mdb') as $candidate) { + if (! is_file($candidate)) { + continue; + } + + $dirName = basename(dirname($candidate)); + $sortKey = ctype_digit($dirName) ? (int) $dirName : -1; + $candidates[] = [ + 'path' => $candidate, + 'legacy_year' => $dirName !== '' ? $dirName : null, + 'anno_o' => null, + 'anno_r' => null, + 'sort_key' => $sortKey, + ]; + } + + if ($candidates === []) { + return null; + } + + usort($candidates, static function (array $left, array $right): int { + if ($left['sort_key'] === $right['sort_key']) { + return strcmp((string) $right['legacy_year'], (string) $left['legacy_year']); + } + + return $right['sort_key'] <=> $left['sort_key']; + }); + + return Arr::except($candidates[0], ['sort_key']); + } + + private function stagingTableNeedsLoadForStabile(string $table, string $stab): bool + { + if (! Schema::connection('gescon_import')->hasTable($table)) { + return false; + } + + $conn = DB::connection('gescon_import'); + if (Schema::connection('gescon_import')->hasColumn($table, 'cod_stabile')) { + return ! $conn->table($table)->where('cod_stabile', $stab)->exists(); + } + + return $conn->table($table)->count() === 0; + } + private function createStabileFromMdbIfNeeded(string $mdbPath, string $codRichiesto): int { $col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : (Schema::hasColumn('stabili', 'denominazione') ? 'denominazione' : null); - if (!$col) return 0; + if (! $col) { + return 0; + } + $exists = DB::table('stabili')->where($col, $codRichiesto)->first(); - if (!$exists && $col === 'codice_stabile') { + if (! $exists && $col === 'codice_stabile') { $alt = ltrim($codRichiesto, '0'); if ($alt !== '' && $alt !== $codRichiesto) { $exists = DB::table('stabili')->where($col, $alt)->first(); } } - if ($exists) return 0; + if ($exists) { + return 0; + } + $row = $this->mdbStabiliFindByDirectory($mdbPath, $codRichiesto); - if (!$row) return 0; + if (! $row) { + return 0; + } + $payload = [ - $col => $codRichiesto, - 'denominazione' => $row['denominazione'] ?? $codRichiesto, - 'indirizzo' => $row['indirizzo'] ?? 'ND', - 'citta' => $row['citta'] ?? 'ND', - 'cap' => $row['cap'] ?? '00000', - 'provincia' => $row['provincia'] ?? 'XX', + $col => $codRichiesto, + 'denominazione' => $row['denominazione'] ?? $codRichiesto, + 'indirizzo' => $row['indirizzo'] ?? 'ND', + 'citta' => $row['citta'] ?? 'ND', + 'cap' => $row['cap'] ?? '00000', + 'provincia' => $row['provincia'] ?? 'XX', 'codice_fiscale' => $row['cf_condominio'] ?? null, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => now(), + 'updated_at' => now(), ]; // Tag provenienza if (Schema::hasColumn('stabili', 'note')) { @@ -3414,14 +4050,14 @@ private function createStabileFromMdbIfNeeded(string $mdbPath, string $codRichie } // === Helper contestuali === - private ?int $stabileId = null; - private int $protocolCounter = 0; - private bool $isDryRun = false; + private ?int $stabileId = null; + private int $protocolCounter = 0; + private bool $isDryRun = false; private array $scalaStabileCache = []; private function normalizeNameKey(?string $nome, ?string $cognome): string { - $full = trim(strtoupper(trim((string)($cognome ?? '')) . ' ' . trim((string)($nome ?? '')))); + $full = trim(strtoupper(trim((string) ($cognome ?? '')) . ' ' . trim((string) ($nome ?? '')))); $full = preg_replace('/\s+/', ' ', $full); $full = preg_replace('/[^A-Z0-9 ]/', '', $full); return trim($full); @@ -3432,23 +4068,37 @@ private function findExistingSoggetto(?string $cf, ?string $email, ?string $nome $q = DB::table('soggetti'); if ($cf) { $found = (clone $q)->where('codice_fiscale', $cf)->first(); - if ($found) return $found; + if ($found) { + return $found; + } + } if ($email) { $found = DB::table('soggetti')->where('email', $email)->first(); - if ($found) return $found; + if ($found) { + return $found; + } + } $key = $this->normalizeNameKey($nome, $cognome); if ($key) { - $found = DB::table('soggetti')->whereRaw("UPPER(CONCAT(TRIM(COALESCE(cognome,'')),' ',TRIM(COALESCE(nome,'')))) = ?", [$key])->first(); - if ($found) return $found; + $found = DB::table('soggetti') + ->whereRaw("TRIM(UPPER(CONCAT(TRIM(COALESCE(cognome,'')),' ',TRIM(COALESCE(nome,''))))) = ?", [$key]) + ->first(); + if ($found) { + return $found; + } + } return null; } private function dedupSoggettiBy(string $field): void { - if (!Schema::hasTable('soggetti') || !in_array($field, Schema::getColumnListing('soggetti'))) return; + if (! Schema::hasTable('soggetti') || ! in_array($field, Schema::getColumnListing('soggetti'))) { + return; + } + $dups = DB::table('soggetti') ->select($field, DB::raw('COUNT(*) as c')) ->whereNotNull($field) @@ -3462,16 +4112,16 @@ private function dedupSoggettiBy(string $field): void foreach ($rows->skip(1) as $rm) { // Riassegna proprieta if (Schema::hasTable('proprieta')) { - DB::table('proprieta')->where('soggetto_id', $rm->id)->update(['soggetto_id' => $keep->id]); + $this->mergeProprietaForDuplicateSoggetto((int) $rm->id, (int) $keep->id); } // Unisci eventuali campi mancanti $update = []; foreach (['nome', 'cognome', 'email', 'telefono', 'indirizzo', 'codice_fiscale', 'tipo'] as $f) { - if (empty($keep->$f) && !empty($rm->$f)) { + if (empty($keep->$f) && ! empty($rm->$f)) { $update[$f] = $rm->$f; } } - if (!empty($update)) { + if (! empty($update)) { DB::table('soggetti')->where('id', $keep->id)->update($update); } // Elimina duplicato @@ -3480,9 +4130,86 @@ private function dedupSoggettiBy(string $field): void } } + private function mergeProprietaForDuplicateSoggetto(int $removeId, int $keepId): void + { + $rows = DB::table('proprieta') + ->where('soggetto_id', $removeId) + ->orderBy('id') + ->get(); + + foreach ($rows as $row) { + $this->moveProprietaRowToSoggetto((int) $row->id, $keepId); + } + } + + private function moveProprietaRowToSoggetto(int $rowId, int $targetSoggettoId, ?string $forcedDataInizio = null, ?string $forcedDataFine = null): void + { + $row = DB::table('proprieta')->where('id', $rowId)->first(); + if (! $row) { + return; + } + + $existing = DB::table('proprieta') + ->where('soggetto_id', $targetSoggettoId) + ->where('unita_immobiliare_id', $row->unita_immobiliare_id) + ->where('tipo_diritto', $row->tipo_diritto) + ->where('id', '!=', $rowId) + ->first(); + + if ($existing) { + $patch = []; + + if (empty($existing->percentuale_possesso) && ! empty($row->percentuale_possesso)) { + $patch['percentuale_possesso'] = $row->percentuale_possesso; + } + + if (empty($existing->percentuale_detrazione) && ! empty($row->percentuale_detrazione)) { + $patch['percentuale_detrazione'] = $row->percentuale_detrazione; + } + + $existingStart = $this->normalizeLegacyDate($existing->data_inizio ?? null); + $rowStart = $forcedDataInizio ?? $this->normalizeLegacyDate($row->data_inizio ?? null); + if ($rowStart && (! $existingStart || $rowStart < $existingStart)) { + $patch['data_inizio'] = $rowStart; + } + + $existingEnd = $this->normalizeLegacyDate($existing->data_fine ?? null); + $rowEnd = $forcedDataFine ?? $this->normalizeLegacyDate($row->data_fine ?? null); + if ($rowEnd && (! $existingEnd || $rowEnd > $existingEnd)) { + $patch['data_fine'] = $rowEnd; + } + + if ($patch !== []) { + $patch['updated_at'] = now(); + DB::table('proprieta')->where('id', $existing->id)->update($patch); + } + + DB::table('proprieta')->where('id', $rowId)->delete(); + return; + } + + $patch = [ + 'soggetto_id' => $targetSoggettoId, + 'updated_at' => now(), + ]; + + if ($forcedDataInizio !== null) { + $patch['data_inizio'] = $forcedDataInizio; + } + + if ($forcedDataFine !== null) { + $patch['data_fine'] = $forcedDataFine; + } + + DB::table('proprieta')->where('id', $rowId)->update($patch); + } + private function findUnitaByStagingRow($r) { - if (!Schema::hasTable('unita_immobiliari')) return null; + if (! Schema::hasTable('unita_immobiliari')) { + return null; + } + $crit = []; if ($this->stabileId !== null && Schema::hasColumn('unita_immobiliari', 'stabile_id')) { $crit['stabile_id'] = $this->stabileId; @@ -3491,7 +4218,7 @@ private function findUnitaByStagingRow($r) $crit['scala'] = $r->scala; } if (Schema::hasColumn('unita_immobiliari', 'interno')) { - $int = isset($r->interno) ? (string)$r->interno : null; + $int = isset($r->interno) ? (string) $r->interno : null; if (is_string($int) && preg_match('/^\s*CAN\s*(\d+)\s*$/i', $int, $m)) { $int = 'CAN' . $m[1]; } @@ -3502,7 +4229,7 @@ private function findUnitaByStagingRow($r) $crit['interno'] = $int; } } - if (!empty($crit)) { + if (! empty($crit)) { return DB::table('unita_immobiliari')->where($crit)->first(); } return null; @@ -3512,12 +4239,18 @@ private function queryMdbSingle(string $mdbPath, string $sql): ?array { $cmd = 'mdb-sql ' . escapeshellarg($mdbPath) . ' <<< ' . escapeshellarg($sql); $out = shell_exec($cmd); - if (!$out) return null; + if (! $out) { + return null; + } + $lines = array_values(array_filter(array_map('trim', explode("\n", $out)))); - if (count($lines) < 3) return null; + if (count($lines) < 3) { + return null; + } + // Try to detect header and data rows (very naive parser) $headers = []; - $data = []; + $data = []; foreach ($lines as $i => $line) { if ($i === 0 || stripos($line, '\t') !== false) { $headers = array_map('trim', preg_split('/\s+/', $line)); @@ -3527,13 +4260,16 @@ private function queryMdbSingle(string $mdbPath, string $sql): ?array // Fallback: use space-split headers might be unreliable; try tab-split on last 2 lines $last = end($lines); $prev = prev($lines); - if (!$headers && $prev) { + if (! $headers && $prev) { $headers = array_map('trim', preg_split('/\t+/', $prev)); - $data = array_map('trim', preg_split('/\t+/', $last)); + $data = array_map('trim', preg_split('/\t+/', $last)); } else { $data = array_map('trim', preg_split('/\s+/', $last)); } - if (!$headers || !$data || count($headers) !== count($data)) return null; + if (! $headers || ! $data || count($headers) !== count($data)) { + return null; + } + return array_combine($headers, $data); } @@ -3541,8 +4277,8 @@ private function mdbStabiliFindByDirectory(string $mdbPath, string $directoryCod { // Robust export: pipe delimiter and quoted fields to preserve newlines within fields $table = 'Stabili'; - $tmp = tempnam(sys_get_temp_dir(), 'mdbcsv_'); - $cmd = sprintf( + $tmp = tempnam(sys_get_temp_dir(), 'mdbcsv_'); + $cmd = sprintf( 'mdb-export -D %s -d %s -q %s -R "\\n" %s %s > %s 2>/dev/null', escapeshellarg('%Y-%m-%d'), escapeshellarg('|'), @@ -3552,87 +4288,306 @@ private function mdbStabiliFindByDirectory(string $mdbPath, string $directoryCod escapeshellarg($tmp) ); @shell_exec($cmd); - if (!is_file($tmp) || filesize($tmp) === 0) { + if (! is_file($tmp) || filesize($tmp) === 0) { @unlink($tmp); return null; } $fh = fopen($tmp, 'r'); - if (!$fh) { + if (! $fh) { @unlink($tmp); return null; } $headers = fgetcsv($fh, 0, '|'); - if (!$headers) { + if (! $headers) { fclose($fh); @unlink($tmp); return null; } - $headers = array_map(fn($h) => strtolower(trim((string)$h)), $headers); - $code = trim($directoryCode); + $headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers); + $code = trim($directoryCode); while (($cols = fgetcsv($fh, 0, '|')) !== false) { - if ($cols === [null] || $cols === false) continue; + if ($cols === [null] || $cols === false) { + continue; + } + $countH = count($headers); - if (count($cols) < $countH) $cols = array_pad($cols, $countH, null); - if (count($cols) > $countH) $cols = array_slice($cols, 0, $countH); + if (count($cols) < $countH) { + $cols = array_pad($cols, $countH, null); + } + + if (count($cols) > $countH) { + $cols = array_slice($cols, 0, $countH); + } + $row = @array_combine($headers, $cols); - if (!$row) continue; - $nomeDir = $row['nome_directory'] ?? ($row['internet_cod_stab'] ?? ($row['cod_stabile'] ?? null)); - if (!$nomeDir || trim((string)$nomeDir) !== $code) continue; + if (! $row) { + continue; + } + + $nomeDir = $row['nome_directory'] ?? ($row['codice_directory'] ?? ($row['internet_cod_stab'] ?? ($row['cod_stabile'] ?? null))); + if (! $nomeDir || trim((string) $nomeDir) !== $code) { + continue; + } // Prefer direct columns if exist $cfCondo = $row['cf_condominio'] ?? $row['cod_fisc'] ?? null; - $cfAmm = $row['cin'] ?? $row['cod_fisc_amministratore'] ?? null; + $cfAmm = $row['cin'] ?? $row['cod_fisc_amministratore'] ?? null; // Heuristics over all values to fill missing - $allText = strtoupper(implode(' ', array_map(fn($v) => (string)$v, $row))); - if (!$cfCondo && preg_match('/(\d\D*){11}/', preg_replace('/\D+/', '', $allText))) { + $allText = strtoupper(implode(' ', array_map(fn($v) => (string) $v, $row))); + if (! $cfCondo && preg_match('/(\d\D*){11}/', preg_replace('/\D+/', '', $allText))) { // Extract contiguous 11 digits if (preg_match('/\d{11}/', $allText, $m)) { $cfCondo = $m[0]; } } - if (!$cfAmm) { + if (! $cfAmm) { if (preg_match('/(?=.*[A-Z])[A-Z0-9]{16}/', $allText, $m)) { $cfAmm = $m[0]; } } - $den = $row['denominazione'] ?? ($row['fe_denominazione'] ?? null); + $den = $row['denominazione'] ?? ($row['fe_denominazione'] ?? null); $indir = $row['indirizzo'] ?? ($row['indir_autorizzazione'] ?? ($row['indir_autorizzazione_2'] ?? null)); - $cap = $row['cap'] ?? null; + $cap = $row['cap'] ?? null; $citta = $row['citta'] ?? null; - $pr = $row['pr'] ?? ($row['catasto_pr'] ?? null); + $pr = $row['pr'] ?? ($row['catasto_pr'] ?? null); fclose($fh); @unlink($tmp); - return [ - 'denominazione' => $den, - 'indirizzo' => $indir, - 'cap' => $cap, - 'citta' => $citta, - 'provincia' => $pr, + return array_merge($row, [ + 'denominazione' => $den, + 'indirizzo' => $indir, + 'cap' => $cap, + 'citta' => $citta, + 'provincia' => $pr, // Provide both when possible - 'cf_condominio' => $cfCondo, + 'cf_condominio' => $cfCondo, 'cf_amministratore' => $cfAmm, // Keep a generic for mapping - 'codice_fiscale' => $cfCondo ?? $cfAmm, - 'cin' => $cfAmm, - ]; + 'codice_fiscale' => $cfCondo ?? $cfAmm, + 'cin' => $cfAmm, + ]); } fclose($fh); @unlink($tmp); return null; } + private function resolveStabiliMdbPath(string $basePath = ''): ?string + { + $candidates = []; + $explicit = trim((string) ($this->option('mdb') ?? '')); + if ($explicit !== '') { + $candidates[] = $explicit; + } + + $basePath = trim($basePath); + if ($basePath !== '') { + $normalizedBase = rtrim($basePath, '/'); + $candidates[] = $normalizedBase . '/dbc/Stabili.mdb'; + $candidates[] = $normalizedBase . '/dbc/stabili.mdb'; + $candidates[] = $normalizedBase . '/Stabili.mdb'; + $candidates[] = $normalizedBase . '/stabili.mdb'; + } + + $candidates[] = '/mnt/gescon-archives/gescon/dbc/Stabili.mdb'; + $candidates[] = '/mnt/gescon-archives/gescon/Stabili.mdb'; + + foreach (array_values(array_unique(array_filter($candidates))) as $candidate) { + if (is_file($candidate) && is_readable($candidate)) { + return $candidate; + } + } + + return $explicit !== '' ? $explicit : null; + } + + private function applyLegacyStabileDerivedFields(array $update, array $legacyRow, $existing): array + { + $ordMonths = $this->extractLegacyRateMonths($legacyRow, 'ord_rata_'); + $risMonths = $this->extractLegacyRateMonths($legacyRow, 'ris_rata_'); + + if ($ordMonths !== [] && Schema::hasColumn('stabili', 'rate_ordinarie_mesi')) { + $current = $this->decodeJsonArray($existing->rate_ordinarie_mesi ?? null); + if ($this->forceMapping || $current === []) { + $update['rate_ordinarie_mesi'] = json_encode($ordMonths); + } + } + + if ($risMonths !== [] && Schema::hasColumn('stabili', 'rate_riscaldamento_mesi')) { + $current = $this->decodeJsonArray($existing->rate_riscaldamento_mesi ?? null); + if ($this->forceMapping || $current === []) { + $update['rate_riscaldamento_mesi'] = json_encode($risMonths); + } + } + + $ibanBanca = $this->normalizeIbanValue($legacyRow['iban_banca'] ?? ($legacyRow['iban_principale'] ?? null)); + $ibanPosta = $this->normalizeIbanValue($legacyRow['iban_posta'] ?? null); + $bancaPrincipale = trim((string) ($legacyRow['banca'] ?? ($legacyRow['denominazione_banca'] ?? ''))); + $bancaSecondaria = trim((string) ($legacyRow['banca2'] ?? '')); + if ($bancaSecondaria === '' && ($ibanPosta || ! empty($legacyRow['num_ccp'] ?? null))) { + $bancaSecondaria = 'Conto postale'; + } + + if ($ibanBanca && Schema::hasColumn('stabili', 'iban_principale') && ($this->forceMapping || empty($existing->iban_principale))) { + $update['iban_principale'] = $ibanBanca; + } + if ($bancaPrincipale !== '' && Schema::hasColumn('stabili', 'banca_principale') && ($this->forceMapping || empty($existing->banca_principale))) { + $update['banca_principale'] = $bancaPrincipale; + } + if ($ibanPosta && Schema::hasColumn('stabili', 'iban_secondario') && ($this->forceMapping || empty($existing->iban_secondario))) { + $update['iban_secondario'] = $ibanPosta; + } + if ($bancaSecondaria !== '' && Schema::hasColumn('stabili', 'banca_secondaria') && ($this->forceMapping || empty($existing->banca_secondaria))) { + $update['banca_secondaria'] = $bancaSecondaria; + } + if (Schema::hasColumn('stabili', 'iban_condominio') && ($this->forceMapping || empty($existing->iban_condominio))) { + $ibanCondominio = $ibanBanca ?: $ibanPosta; + if ($ibanCondominio) { + $update['iban_condominio'] = $ibanCondominio; + } + } + + return $update; + } + + private function extractLegacyRateMonths(array $legacyRow, string $prefix): array + { + $months = []; + foreach (range(1, 12) as $month) { + $key = $prefix . $month; + if (! array_key_exists($key, $legacyRow)) { + continue; + } + $value = strtolower(trim((string) $legacyRow[$key])); + if ($value !== '' && ! in_array($value, ['0', 'false', 'no', 'off', 'n'], true)) { + $months[] = $month; + } + } + + return $months; + } + + private function decodeJsonArray(mixed $value): array + { + if (is_array($value)) { + return array_values(array_map('intval', $value)); + } + if (! is_string($value) || trim($value) === '') { + return []; + } + $decoded = json_decode($value, true); + if (! is_array($decoded)) { + return []; + } + + return array_values(array_map('intval', $decoded)); + } + + private function normalizeIbanValue(mixed $value): ?string + { + if (! is_string($value)) { + return null; + } + $normalized = strtoupper(preg_replace('/\s+/', '', $value)); + + return $normalized !== '' ? $normalized : null; + } + + private function buildBancheRowsFromStabileConfig(mixed $configRaw): array + { + if (is_string($configRaw) && trim($configRaw) !== '') { + $configRaw = json_decode($configRaw, true); + } + if (! is_array($configRaw)) { + return []; + } + + $banca = is_array($configRaw['banca'] ?? null) ? $configRaw['banca'] : []; + if ($banca === []) { + return []; + } + + $rows = []; + $ibanBanca = $this->normalizeIbanValue($banca['iban_banca'] ?? null); + if ($ibanBanca) { + $rows[] = [ + 'banca' => trim((string) ($banca['banca'] ?? 'Banca condominio')), + 'iban' => $ibanBanca, + 'cod_cassa' => 'CCB', + 'intestatario' => trim((string) ($banca['intestaz_ccp'] ?? '')), + ]; + } + + $ibanPosta = $this->normalizeIbanValue($banca['iban_posta'] ?? null); + $numeroPosta = trim((string) ($banca['num_ccp'] ?? '')); + if ($ibanPosta || $numeroPosta !== '') { + $rows[] = [ + 'banca' => trim((string) ($banca['banca2'] ?? 'Conto postale')), + 'iban' => $ibanPosta, + 'n_conto' => $numeroPosta, + 'cod_cassa' => 'CCP', + 'intestatario' => trim((string) ($banca['intestaz_ccp'] ?? '')), + ]; + } + + return $rows; + } + + private function mergeLegacyBancheRows(array $rows, array $configRows): array + { + $seen = []; + foreach ($rows as $row) { + if (! is_array($row)) { + continue; + } + $iban = $this->normalizeIbanValue($row['iban'] ?? ($row['iban_banca'] ?? ($row['iban_posta'] ?? null))); + $numero = trim((string) ($row['n_conto'] ?? ($row['numero_conto'] ?? ''))); + $codCassa = strtoupper(trim((string) ($row['cod_cassa'] ?? ''))); + $key = $iban ?: ($numero !== '' ? 'N:' . $numero : ($codCassa !== '' ? 'C:' . $codCassa : null)); + if ($key) { + $seen[$key] = true; + } + } + + foreach ($configRows as $row) { + if (! is_array($row)) { + continue; + } + $iban = $this->normalizeIbanValue($row['iban'] ?? null); + $numero = trim((string) ($row['n_conto'] ?? ($row['numero_conto'] ?? ''))); + $codCassa = strtoupper(trim((string) ($row['cod_cassa'] ?? ''))); + $key = $iban ?: ($numero !== '' ? 'N:' . $numero : ($codCassa !== '' ? 'C:' . $codCassa : null)); + if (! $key || isset($seen[$key])) { + continue; + } + + $rows[] = $row; + $seen[$key] = true; + } + + return $rows; + } + private function resolveStabileId(): ?int { $cod = $this->option('stabile'); - if (!$cod) return null; - if (!Schema::hasTable('stabili')) return null; + if (! $cod) { + return null; + } + + if (! Schema::hasTable('stabili')) { + return null; + } + $col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : (Schema::hasColumn('stabili', 'denominazione') ? 'denominazione' : null); - if (!$col) return null; + if (! $col) { + return null; + } + $row = DB::table('stabili')->where($col, $cod)->first(); - if (!$row && $col === 'codice_stabile') { + if (! $row && $col === 'codice_stabile') { $alt = ltrim($cod, '0'); if ($alt !== '' && $alt !== $cod) { $row = DB::table('stabili')->where($col, $alt)->first(); @@ -3642,11 +4597,11 @@ private function resolveStabileId(): ?int if ($row) { $optAdm = $this->option('amministratore-id'); if ($optAdm && Schema::hasColumn('stabili', 'amministratore_id')) { - $target = (int)$optAdm; - if ((int)($row->amministratore_id ?? 0) !== $target && !$this->option('dry-run')) { + $target = (int) $optAdm; + if ((int) ($row->amministratore_id ?? 0) !== $target && ! $this->option('dry-run')) { DB::table('stabili')->where('id', $row->id)->update([ 'amministratore_id' => $target, - 'updated_at' => now(), + 'updated_at' => now(), ]); } } @@ -3661,18 +4616,18 @@ private function resolveStabileId(): ?int if (Schema::hasColumn('stabili', 'amministratore_id')) { // Preferisci un amministratore passato via opzione $optAdm = $this->option('amministratore-id'); - if ($optAdm && DB::table('amministratori')->where('id', (int)$optAdm)->exists()) { - $amminId = (int)$optAdm; + if ($optAdm && DB::table('amministratori')->where('id', (int) $optAdm)->exists()) { + $amminId = (int) $optAdm; } else { $ammin = DB::table('amministratori')->first(); - if (!$ammin) { + if (! $ammin) { // Ensure there is at least one user $user = DB::table('users')->first(); - if (!$user) { + if (! $user) { $userId = DB::table('users')->insertGetId([ - 'name' => 'Admin Seed', - 'email' => 'admin-seed@example.local', - 'password' => bcrypt('password'), + 'name' => 'Admin Seed', + 'email' => 'admin-seed@example.local', + 'password' => bcrypt('password'), 'created_at' => now(), 'updated_at' => now(), ]); @@ -3680,13 +4635,13 @@ private function resolveStabileId(): ?int $userId = $user->id; } $amminId = DB::table('amministratori')->insertGetId([ - 'nome' => 'Admin', - 'cognome' => 'Seed', - 'user_id' => $userId, + 'nome' => 'Admin', + 'cognome' => 'Seed', + 'user_id' => $userId, 'denominazione_studio' => 'Seed Studio', - 'codice_univoco' => $this->generateCode('A', 8), - 'created_at' => now(), - 'updated_at' => now(), + 'codice_univoco' => $this->generateCode('A', 8), + 'created_at' => now(), + 'updated_at' => now(), ]); } else { $amminId = $ammin->id; @@ -3695,16 +4650,16 @@ private function resolveStabileId(): ?int } // Create minimal stabile row compatible with either schema $data = [ - $col => $cod, - 'denominazione' => $cod, - 'indirizzo' => 'ND', - 'citta' => 'ND', - 'cap' => '00000', - 'provincia' => 'XX', - 'attivo' => true, + $col => $cod, + 'denominazione' => $cod, + 'indirizzo' => 'ND', + 'citta' => 'ND', + 'cap' => '00000', + 'provincia' => 'XX', + 'attivo' => true, 'amministratore_id' => $amminId, - 'created_at' => now(), - 'updated_at' => now() + 'created_at' => now(), + 'updated_at' => now(), ]; $this->dynamicInsert('stabili', $data); $row = DB::table('stabili')->where($col, $cod)->first(); @@ -3713,15 +4668,15 @@ private function resolveStabileId(): ?int private function generateCode(string $prefix, int $len = 6): string { - return $prefix . str_pad((string)random_int(0, 10 ** $len - 1), $len, '0', STR_PAD_LEFT); + return $prefix . str_pad((string) random_int(0, 10 ** $len - 1), $len, '0', STR_PAD_LEFT); } private function generateStableCondKey(string $prefix, $stabile, $cond, int $len = 8): string { - $len = max(1, $len); - $prefix = (string)$prefix; + $len = max(1, $len); + $prefix = (string) $prefix; $usable = max(1, $len - strlen($prefix)); - $raw = (string)$stabile . '|' . (string)$cond; + $raw = (string) $stabile . '|' . (string) $cond; // Use CRC32B for a short, deterministic hex digest; uppercase for consistency $digest = strtoupper(substr(hash('crc32b', $raw), 0, $usable)); // Ensure exact length @@ -3738,7 +4693,10 @@ private function nextProtocolNumber(): int private function safeOldId($old) { - if (!$old) return null; + if (! $old) { + return null; + } + // If already used by another soggetto, skip to avoid unique violation $exists = Schema::hasTable('soggetti') ? DB::table('soggetti')->where('old_id', $old)->exists() : false; return $exists ? null : $old; @@ -3754,19 +4712,26 @@ private function resolveAnnoFromLegacyYear(?string $legacyYear): ?string return $legacyYear; } - if (!$this->legacyYearMapLoaded) { + if (preg_match('/(19|20)\d{2}/', $legacyYear, $match)) { + return $match[0]; + } + + if (! $this->legacyYearMapLoaded) { $this->legacyYearMapLoaded = true; - $this->legacyYearMap = []; + $this->legacyYearMap = []; $stabileCode = (string) ($this->option('stabile') ?? ''); - $root = rtrim((string) ($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/'); + $root = rtrim((string) ($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/'); if ($stabileCode !== '') { $generale = $root . '/' . $stabileCode . '/generale_stabile.mdb'; - $rows = $this->mdbExportToRows($generale, ['anni', 'Anni', 'ANNI']); + $rows = $this->mdbExportToRows($generale, ['anni', 'Anni', 'ANNI']); foreach ($rows as $row) { - $dir = trim((string) ($row['nome_dir'] ?? $row['nome_dir '] ?? '')); + $dir = trim((string) ($row['nome_dir'] ?? $row['nome_dir '] ?? '')); $anno = trim((string) ($row['anno_o'] ?? $row['anno'] ?? $row['anno_gestione'] ?? '')); if ($dir !== '' && $anno !== '') { + if (preg_match('/(19|20)\d{2}/', $anno, $match)) { + $anno = $match[0]; + } $this->legacyYearMap[$dir] = $anno; } } @@ -3778,7 +4743,7 @@ private function resolveAnnoFromLegacyYear(?string $legacyYear): ?string private function normalizeLegacyDate(?string $value): ?string { - if (!is_string($value)) { + if (! is_string($value)) { return null; } $value = trim($value); @@ -3792,146 +4757,252 @@ private function normalizeLegacyDate(?string $value): ?string return date('Y-m-d', $ts); } - private function upsertUnitaNominativiFromLegacy(int $unitaId, int $stabileId, ?string $legacyCondId, object $row): void + private function resolveLegacySnapshotYear(?string $legacyYear, ?object $row = null): ?int { - if (!Schema::hasTable('unita_immobiliare_nominativi')) { - return; + $resolved = $this->resolveAnnoFromLegacyYear($legacyYear); + if ($resolved !== null && is_numeric($resolved)) { + return (int) $resolved; } - $payloads = []; + if ($row) { + foreach (['inizio_anno', 'anno_gestione', 'anno'] as $field) { + $value = trim((string) ($row->{$field} ?? '')); + if ($value !== '' && preg_match('/\d{4}/', $value, $m)) { + return (int) $m[0]; + } + } - $ownerStart = $this->normalizeLegacyDate($row->subentrato_dal ?? $row->proprietario_subentrato_dal ?? null); - $ownerEnd = $this->normalizeLegacyDate($row->attivo_fino_al ?? $row->proprietario_attivo_fino_al ?? null); + foreach (['subentrato_dal', 'attivo_fino_al', 'inquil_dal', 'inquil_al'] as $field) { + $normalized = $this->normalizeLegacyDate($row->{$field} ?? null); + if ($normalized) { + return (int) substr($normalized, 0, 4); + } + } + } - $subentroPrima = trim((string) ($row->subentro_prima_cera ?? '')); - $subentroAdesso = trim((string) ($row->subentro_adesso_ce ?? '')); + $optAnno = trim((string) ($this->option('anno') ?? '')); + if ($optAnno !== '' && is_numeric($optAnno)) { + return (int) $optAnno; + } - $resolveOwnerName = function (?object $source) use ($row): ?string { - $src = $source ?: $row; - $name = trim((string) ($src->proprietario_denominazione ?? $src->nom_cond ?? '')); + return null; + } + + private function startOfYear(?int $year): ?string + { + return $year ? sprintf('%04d-01-01', $year) : null; + } + + private function previousDay(?string $date): ?string + { + if (! $date) { + return null; + } + + $ts = strtotime($date . ' -1 day'); + + return $ts !== false ? date('Y-m-d', $ts) : null; + } + + private function normalizeLegacyPartyName(?string $value): string + { + $value = Str::upper(trim((string) $value)); + $value = preg_replace('/\s+/u', ' ', $value ?? ''); + + return trim((string) ($value ?? '')); + } + + private function extractLegacySnapshotParty(object $row, string $role, ?string $defaultLegacyCondId = null): array + { + $legacyYear = isset($row->legacy_year) ? trim((string) $row->legacy_year) : null; + $snapshotYear = $this->resolveLegacySnapshotYear($legacyYear, $row); + + if ($role === 'C') { + $name = trim((string) ($row->proprietario_denominazione ?? $row->nom_cond ?? '')); if ($name === '') { - $name = trim((string) (($src->cognome ?? '') . ' ' . ($src->nome ?? ''))); - } - return $name !== '' ? $name : null; - }; - - $resolveLegacyRow = function (string $idCond) use ($row): ?object { - if ($idCond === '') { - return null; - } - $query = DB::connection('gescon_import') - ->table('condomin') - ->where('id_cond', $idCond); - - if (!empty($row->cod_stabile)) { - $query->where('cod_stabile', $row->cod_stabile); + $name = trim((string) (($row->cognome ?? '') . ' ' . ($row->nome ?? ''))); } - if (property_exists($row, 'legacy_year') && !empty($row->legacy_year)) { - $query->where('legacy_year', $row->legacy_year); - } - - return $query->first(); - }; - - $ownerPerc = isset($row->proprietario_diritto_percentuale) && is_numeric($row->proprietario_diritto_percentuale) - ? (float) $row->proprietario_diritto_percentuale - : null; - - if ($subentroPrima !== '') { - $oldRow = $resolveLegacyRow($subentroPrima); - $oldName = $resolveOwnerName($oldRow); - $oldStart = $this->normalizeLegacyDate($oldRow?->subentrato_dal ?? null); - $oldEnd = $ownerStart ?: $this->normalizeLegacyDate($oldRow?->attivo_fino_al ?? null); - - if ($oldName || $oldStart || $oldEnd) { - $payloads[] = [ - 'ruolo' => 'C', - 'nominativo' => $oldName, - 'data_inizio' => $oldStart, - 'data_fine' => $oldEnd, - 'percentuale' => $ownerPerc, - 'legacy_cond_id' => $subentroPrima, - ]; - } - } - - $newOwnerRow = $subentroAdesso !== '' ? $resolveLegacyRow($subentroAdesso) : null; - $newOwnerName = $resolveOwnerName($newOwnerRow); - $newOwnerStart = $ownerStart ?: $this->normalizeLegacyDate($newOwnerRow?->subentrato_dal ?? null); - $newOwnerEnd = $ownerEnd ?: $this->normalizeLegacyDate($newOwnerRow?->attivo_fino_al ?? null); - - if (!empty($newOwnerName) || $newOwnerStart || $newOwnerEnd) { - $payloads[] = [ - 'ruolo' => 'C', - 'nominativo' => $newOwnerName, - 'data_inizio' => $newOwnerStart, - 'data_fine' => $newOwnerEnd, - 'percentuale' => $ownerPerc, - 'legacy_cond_id' => $subentroAdesso !== '' ? $subentroAdesso : $legacyCondId, - ]; - } - - $fallbackOwnerName = $resolveOwnerName(null); - if (empty($payloads) && ($fallbackOwnerName || $ownerStart || $ownerEnd)) { - $payloads[] = [ - 'ruolo' => 'C', - 'nominativo' => $fallbackOwnerName, - 'data_inizio' => $ownerStart, - 'data_fine' => $ownerEnd, - 'percentuale' => $ownerPerc, - 'legacy_cond_id' => $legacyCondId, + return [ + 'ruolo' => 'C', + 'legacy_year' => $legacyYear, + 'snapshot_year' => $snapshotYear, + 'legacy_cond_id' => trim((string) ($row->cod_cond ?? $defaultLegacyCondId ?? '')) ?: $defaultLegacyCondId, + 'nominativo' => $name !== '' ? $name : null, + 'data_inizio' => $this->normalizeLegacyDate($row->proprietario_subentrato_dal ?? $row->subentrato_dal ?? null), + 'data_fine' => $this->normalizeLegacyDate($row->proprietario_attivo_fino_al ?? $row->attivo_fino_al ?? null), + 'percentuale' => isset($row->proprietario_diritto_percentuale) && is_numeric($row->proprietario_diritto_percentuale) + ? (float) $row->proprietario_diritto_percentuale + : (isset($row->perc_diritto_reale) && is_numeric($row->perc_diritto_reale) ? (float) $row->perc_diritto_reale : null), ]; } $tenantName = trim((string) ($row->inquilino_denominazione ?? $row->inquil_nome ?? $row->inquil ?? '')); - $tenantStart = $this->normalizeLegacyDate($row->inquil_dal ?? $row->inquilino_subentrato_dal ?? null); - $tenantEnd = $this->normalizeLegacyDate($row->inquil_al ?? $row->inquilino_attivo_fino_al ?? null); - $tenantPerc = isset($row->inquilino_diritto_percentuale) && is_numeric($row->inquilino_diritto_percentuale) - ? (float) $row->inquilino_diritto_percentuale - : null; - if (!empty($tenantName) || $tenantStart || $tenantEnd) { - $payloads[] = [ - 'ruolo' => 'I', - 'nominativo' => $tenantName !== '' ? $tenantName : null, - 'data_inizio' => $tenantStart, - 'data_fine' => $tenantEnd, - 'percentuale' => $tenantPerc, - 'legacy_cond_id' => $legacyCondId, + return [ + 'ruolo' => 'I', + 'legacy_year' => $legacyYear, + 'snapshot_year' => $snapshotYear, + 'legacy_cond_id' => trim((string) ($row->cod_cond ?? $defaultLegacyCondId ?? '')) ?: $defaultLegacyCondId, + 'nominativo' => $tenantName !== '' ? $tenantName : null, + 'data_inizio' => $this->normalizeLegacyDate($row->inquil_dal ?? $row->inquilino_subentrato_dal ?? null), + 'data_fine' => $this->normalizeLegacyDate($row->inquil_al ?? $row->inquilino_attivo_fino_al ?? null), + 'percentuale' => isset($row->inquilino_diritto_percentuale) && is_numeric($row->inquilino_diritto_percentuale) + ? (float) $row->inquilino_diritto_percentuale + : null, + ]; + } + + private function loadLegacyCondominHistoryRows(object $row, ?string $legacyCondId): array + { + $codStabile = trim((string) ($row->cod_stabile ?? '')); + $codCond = trim((string) ($row->cod_cond ?? $legacyCondId ?? '')); + + if ($codStabile === '' || $codCond === '') { + return [$row]; + } + + $historyRows = DB::connection('gescon_import') + ->table('condomin') + ->where('cod_stabile', $codStabile) + ->where('cod_cond', $codCond) + ->orderByDesc('legacy_year') + ->orderByDesc('id') + ->get() + ->all(); + + if ($historyRows === []) { + return [$row]; + } + + $byYear = []; + foreach ($historyRows as $historyRow) { + $snapshotYear = $this->resolveLegacySnapshotYear(isset($historyRow->legacy_year) ? (string) $historyRow->legacy_year : null, $historyRow); + $bucket = $snapshotYear ? (string) $snapshotYear : ('legacy:' . (string) ($historyRow->legacy_year ?? '0')); + if (! isset($byYear[$bucket])) { + $byYear[$bucket] = $historyRow; + } + } + + $historyRows = array_values($byYear); + usort($historyRows, function (object $a, object $b): int { + $yearA = $this->resolveLegacySnapshotYear(isset($a->legacy_year) ? (string) $a->legacy_year : null, $a) ?? 0; + $yearB = $this->resolveLegacySnapshotYear(isset($b->legacy_year) ? (string) $b->legacy_year : null, $b) ?? 0; + if ($yearA !== $yearB) { + return $yearB <=> $yearA; + } + + return ((int) ($b->id ?? 0)) <=> ((int) ($a->id ?? 0)); + }); + + return $historyRows; + } + + private function buildRoleHistoryPayloads(array $historyRows, string $role, ?string $defaultLegacyCondId = null): array + { + $snapshots = []; + foreach ($historyRows as $historyRow) { + $snapshot = $this->extractLegacySnapshotParty($historyRow, $role, $defaultLegacyCondId); + if (! $snapshot['nominativo'] && ! $snapshot['data_inizio'] && ! $snapshot['data_fine']) { + continue; + } + $snapshots[] = $snapshot; + } + + if ($snapshots === []) { + return []; + } + + $payloads = []; + $current = null; + foreach ($snapshots as $snapshot) { + $snapshotNameKey = $this->normalizeLegacyPartyName($snapshot['nominativo'] ?? null); + $snapshotStart = $snapshot['data_inizio'] ?? $this->startOfYear($snapshot['snapshot_year']); + $snapshotEnd = $snapshot['data_fine']; + + if ($current === null) { + $current = [ + 'ruolo' => $role, + 'nominativo' => $snapshot['nominativo'], + 'data_inizio' => $snapshotStart, + 'data_fine' => $snapshotEnd, + 'percentuale' => $snapshot['percentuale'], + 'legacy_cond_id' => $snapshot['legacy_cond_id'] ?? $defaultLegacyCondId, + 'legacy_years' => array_values(array_filter([$snapshot['legacy_year']])), + ]; + continue; + } + + $currentNameKey = $this->normalizeLegacyPartyName($current['nominativo'] ?? null); + $sameName = $snapshotNameKey !== '' && $snapshotNameKey === $currentNameKey; + $samePercent = $snapshot['percentuale'] === null || $current['percentuale'] === null + || abs((float) $snapshot['percentuale'] - (float) $current['percentuale']) < 0.0005; + + if ($sameName && $samePercent) { + $current['data_inizio'] = $snapshot['data_inizio'] ?? $this->startOfYear($snapshot['snapshot_year']) ?? $current['data_inizio']; + $current['percentuale'] = $current['percentuale'] ?? $snapshot['percentuale']; + if (! empty($snapshot['legacy_year'])) { + $current['legacy_years'][] = $snapshot['legacy_year']; + } + continue; + } + + $payloads[] = $current; + $current = [ + 'ruolo' => $role, + 'nominativo' => $snapshot['nominativo'], + 'data_inizio' => $snapshotStart, + 'data_fine' => $snapshotEnd ?: $this->previousDay($current['data_inizio']), + 'percentuale' => $snapshot['percentuale'], + 'legacy_cond_id' => $snapshot['legacy_cond_id'] ?? $defaultLegacyCondId, + 'legacy_years' => array_values(array_filter([$snapshot['legacy_year']])), ]; } - foreach ($payloads as $p) { - $match = [ + if ($current !== null) { + $payloads[] = $current; + } + + return $payloads; + } + + private function upsertUnitaNominativiFromLegacy(int $unitaId, int $stabileId, ?string $legacyCondId, object $row): void + { + if (! Schema::hasTable('unita_immobiliare_nominativi')) { + return; + } + + $historyRows = $this->loadLegacyCondominHistoryRows($row, $legacyCondId); + $payloads = array_merge( + $this->buildRoleHistoryPayloads($historyRows, 'C', $legacyCondId), + $this->buildRoleHistoryPayloads($historyRows, 'I', $legacyCondId) + ); + + DB::table('unita_immobiliare_nominativi') + ->where('unita_immobiliare_id', $unitaId) + ->where('stabile_id', $stabileId) + ->where('fonte', 'legacy_condomin') + ->delete(); + + foreach ($payloads as $payload) { + DB::table('unita_immobiliare_nominativi')->insert([ 'unita_immobiliare_id' => $unitaId, - 'stabile_id' => $stabileId, - 'legacy_cond_id' => $p['legacy_cond_id'] ?? $legacyCondId, - 'ruolo' => $p['ruolo'], - 'data_inizio' => $p['data_inizio'], - 'data_fine' => $p['data_fine'], - ]; - - $exists = DB::table('unita_immobiliare_nominativi')->where($match)->first(); - $values = array_merge($match, [ - 'nominativo' => $p['nominativo'], - 'percentuale' => $p['percentuale'], - 'fonte' => 'legacy_condomin', - 'legacy_payload' => json_encode([ - 'legacy_year' => $row->legacy_year ?? null, - 'cod_cond' => $match['legacy_cond_id'], - 'subentro_prima_cera' => $subentroPrima !== '' ? $subentroPrima : null, - 'subentro_adesso_ce' => $subentroAdesso !== '' ? $subentroAdesso : null, + 'stabile_id' => $stabileId, + 'legacy_cond_id' => $payload['legacy_cond_id'] ?? $legacyCondId, + 'ruolo' => $payload['ruolo'], + 'nominativo' => $payload['nominativo'], + 'data_inizio' => $payload['data_inizio'], + 'data_fine' => $payload['data_fine'], + 'percentuale' => $payload['percentuale'], + 'fonte' => 'legacy_condomin', + 'legacy_payload' => json_encode([ + 'cod_cond' => $payload['legacy_cond_id'] ?? $legacyCondId, + 'legacy_years' => array_values(array_unique($payload['legacy_years'] ?? [])), + 'history_mode' => 'latest-year-backfill', ]), - 'updated_at' => now(), + 'created_at' => now(), + 'updated_at' => now(), ]); - - if ($exists) { - DB::table('unita_immobiliare_nominativi')->where('id', $exists->id)->update($values); - } else { - $values['created_at'] = now(); - DB::table('unita_immobiliare_nominativi')->insert($values); - } } } @@ -3943,10 +5014,10 @@ private function normalizeLegacyCalcolo(?string $raw): ?string } return match ($v) { - 'm' => 'millesimi', + 'm' => 'millesimi', 'a', 'c' => 'a_consumo', - 'x' => 'conguagli', - 'p' => 'personali', + 'x' => 'conguagli', + 'p' => 'personali', default => $v, }; } @@ -3959,7 +5030,7 @@ private function normalizeLegacyTipo(?string $raw): ?string } return match ($v) { - 'O', 'ORD', 'ORDINARIA' => 'O', + 'O', 'ORD', 'ORDINARIA' => 'O', 'R', 'RISC', 'RISCALDAMENTO' => 'R', 'S', 'STRA', 'STRAORDINARIA' => 'S', default => $v, @@ -3968,31 +5039,31 @@ private function normalizeLegacyTipo(?string $raw): ?string private function resolveTipoTabellaFromLegacy(?string $tipoLegacy, ?string $calcolo, ?string $codice): ?string { - $c = strtolower(trim((string) ($calcolo ?? ''))); + $c = strtolower(trim((string) ($calcolo ?? ''))); $code = strtoupper(trim((string) ($codice ?? ''))); if ($code === 'ACQUA' || in_array($c, ['a_consumo', 'acqua', 'consumo', 'c', 'a'], true)) { return 'custom'; } return match ($tipoLegacy) { - 'R' => 'riscaldamento', + 'R' => 'riscaldamento', default => 'custom', }; } private function resolveGestioneContabileIdForStabile(?int $stabileId, ?string $legacyYear, ?string $tipoLegacy, ?int $numeroStraordinaria = null): ?int { - if (!Schema::hasTable('gestioni_contabili')) { + if (! Schema::hasTable('gestioni_contabili')) { return null; } $anno = $this->resolveAnnoFromLegacyYear($legacyYear) ?: (is_string($this->option('anno')) ? (string) $this->option('anno') : null); - if (!$anno || !is_numeric($anno)) { + if (! $anno || ! is_numeric($anno)) { return null; } $tipoLegacy = is_string($tipoLegacy) ? strtoupper(trim($tipoLegacy)) : null; - $tipo = match ($tipoLegacy) { + $tipo = match ($tipoLegacy) { 'R', 'RISCALDAMENTO' => 'riscaldamento', 'S', 'STRAORDINARIA' => 'straordinaria', default => 'ordinaria', @@ -4019,22 +5090,22 @@ private function resolveGestioneContabileIdForStabile(?int $stabileId, ?string $ } $payload = [ - 'tenant_id' => 'T1', - 'stabile_id' => $stabileId, - 'anno_gestione' => (int) $anno, - 'tipo_gestione' => $tipo, + 'tenant_id' => 'T1', + 'stabile_id' => $stabileId, + 'anno_gestione' => (int) $anno, + 'tipo_gestione' => $tipo, 'numero_straordinaria' => $tipo === 'straordinaria' ? ($numeroStraordinaria ?? 1) : null, - 'denominazione' => strtoupper(substr($tipo, 0, 1)) . $anno, - 'data_inizio' => date('Y-01-01', strtotime($anno . '-01-01')), - 'data_fine' => date('Y-12-31', strtotime($anno . '-12-31')), - 'stato' => 'aperta', - 'protocollo_prefix' => strtoupper(substr($tipo, 0, 1)) . $anno . ($tipo === 'straordinaria' ? '-' . str_pad((string) ($numeroStraordinaria ?? 1), 2, '0', STR_PAD_LEFT) : ''), - 'ultimo_protocollo' => 0, - 'created_at' => now(), - 'updated_at' => now(), + 'denominazione' => strtoupper(substr($tipo, 0, 1)) . $anno, + 'data_inizio' => date('Y-01-01', strtotime($anno . '-01-01')), + 'data_fine' => date('Y-12-31', strtotime($anno . '-12-31')), + 'stato' => 'aperta', + 'protocollo_prefix' => strtoupper(substr($tipo, 0, 1)) . $anno . ($tipo === 'straordinaria' ? '-' . str_pad((string) ($numeroStraordinaria ?? 1), 2, '0', STR_PAD_LEFT) : ''), + 'ultimo_protocollo' => 0, + 'created_at' => now(), + 'updated_at' => now(), ]; - $cols = Schema::getColumnListing('gestioni_contabili'); + $cols = Schema::getColumnListing('gestioni_contabili'); $insert = array_intersect_key($payload, array_flip($cols)); DB::table('gestioni_contabili')->insert($insert); @@ -4050,46 +5121,70 @@ private function resolveGestioneContabileIdForStabile(?int $stabileId, ?string $ private function resolveGestioneId($anno, $compet, $tipo): ?int { - if (!Schema::hasTable('gestioni_contabili')) return null; + if (! Schema::hasTable('gestioni_contabili')) { + return null; + } + // Normalize tipo from O/R/S to descriptive if needed - $map = ['O' => 'ordinaria', 'R' => 'riscaldamento', 'S' => 'straordinaria']; + $map = ['O' => 'ordinaria', 'R' => 'riscaldamento', 'S' => 'straordinaria']; $tipo = $map[$tipo ?? 'O'] ?? ($tipo ?: 'ordinaria'); $anno = $anno ?: date('Y'); - $row = DB::table('gestioni_contabili') - ->where(['anno_gestione' => (int)$anno, 'tipo_gestione' => $tipo]) + $row = DB::table('gestioni_contabili') + ->where(['anno_gestione' => (int) $anno, 'tipo_gestione' => $tipo]) ->first(); - if ($row) return $row->id; + if ($row) { + return $row->id; + } + // In dry-run, avoid creating missing gestione - if ($this->option('dry-run')) return null; + if ($this->option('dry-run')) { + return null; + } + // Create minimal compatible record satisfying not-null constraints $data = [ - 'tenant_id' => 'T1', - 'anno_gestione' => (int)$anno, - 'tipo_gestione' => $tipo, + 'tenant_id' => 'T1', + 'anno_gestione' => (int) $anno, + 'tipo_gestione' => $tipo, 'numero_straordinaria' => null, - 'denominazione' => strtoupper(substr($tipo, 0, 1)) . $anno, - 'data_inizio' => date('Y-01-01', strtotime($anno . '-01-01')), - 'data_fine' => date('Y-12-31', strtotime($anno . '-12-31')), - 'stato' => 'aperta', - 'protocollo_prefix' => strtoupper(substr($tipo, 0, 1)) . $anno, - 'ultimo_protocollo' => 0, - 'created_at' => now(), - 'updated_at' => now() + 'denominazione' => strtoupper(substr($tipo, 0, 1)) . $anno, + 'data_inizio' => date('Y-01-01', strtotime($anno . '-01-01')), + 'data_fine' => date('Y-12-31', strtotime($anno . '-12-31')), + 'stato' => 'aperta', + 'protocollo_prefix' => strtoupper(substr($tipo, 0, 1)) . $anno, + 'ultimo_protocollo' => 0, + 'created_at' => now(), + 'updated_at' => now(), ]; DB::table('gestioni_contabili')->insert($data); - $row = DB::table('gestioni_contabili')->where(['anno_gestione' => (int)$anno, 'tipo_gestione' => $tipo])->first(); + $row = DB::table('gestioni_contabili')->where(['anno_gestione' => (int) $anno, 'tipo_gestione' => $tipo])->first(); return $row?->id; } private function dynamicInsert(string $table, array $data): void { - if (!Schema::hasTable($table)) return; // silently skip - if ($this->isDryRun) return; // skip writes in dry-run - $cols = Schema::getColumnListing($table); + if (! Schema::hasTable($table)) { + return; + } + // silently skip + if ($this->isDryRun) { + return; + } + // skip writes in dry-run + $cols = Schema::getColumnListing($table); $filtered = array_intersect_key($data, array_flip($cols)); - if (!isset($filtered['created_at']) && in_array('created_at', $cols)) $filtered['created_at'] = now(); - if (!isset($filtered['updated_at']) && in_array('updated_at', $cols)) $filtered['updated_at'] = now(); - if (!empty($filtered)) DB::table($table)->insert($filtered); + if (! isset($filtered['created_at']) && in_array('created_at', $cols)) { + $filtered['created_at'] = now(); + } + + if (! isset($filtered['updated_at']) && in_array('updated_at', $cols)) { + $filtered['updated_at'] = now(); + } + + if (! empty($filtered)) { + DB::table($table)->insert($filtered); + } + } /** @@ -4099,8 +5194,13 @@ private function dynamicInsert(string $table, array $data): void */ private function resolveDefaultContoBancarioId(?int $stabileId): ?int { - if (!$stabileId || !Schema::hasTable('dati_bancari')) return null; - if (isset($this->defaultContoCache[$stabileId])) return $this->defaultContoCache[$stabileId]; + if (! $stabileId || ! Schema::hasTable('dati_bancari')) { + return null; + } + + if (isset($this->defaultContoCache[$stabileId])) { + return $this->defaultContoCache[$stabileId]; + } // 1) Prova match per IBAN tra stabile e dati_bancari $ibanStabile = $this->getIbanForStabile($stabileId); @@ -4111,7 +5211,7 @@ private function resolveDefaultContoBancarioId(?int $stabileId): ?int ->orderByDesc('updated_at') ->first(); if ($row) { - return $this->defaultContoCache[$stabileId] = (int)$row->id; + return $this->defaultContoCache[$stabileId] = (int) $row->id; } } @@ -4124,7 +5224,7 @@ private function resolveDefaultContoBancarioId(?int $stabileId): ?int ->orderByDesc('updated_at') ->first(); if ($row) { - return $this->defaultContoCache[$stabileId] = (int)$row->id; + return $this->defaultContoCache[$stabileId] = (int) $row->id; } // 3) Ultimo fallback: qualsiasi conto dello stabile @@ -4132,7 +5232,7 @@ private function resolveDefaultContoBancarioId(?int $stabileId): ?int ->where('stabile_id', $stabileId) ->orderByDesc('updated_at') ->first(); - return $this->defaultContoCache[$stabileId] = ($row ? (int)$row->id : null); + return $this->defaultContoCache[$stabileId] = ($row ? (int) $row->id : null); } /** @@ -4140,8 +5240,11 @@ private function resolveDefaultContoBancarioId(?int $stabileId): ?int */ private function getIbanForStabile(?int $stabileId): ?string { - if (!$stabileId || !Schema::hasTable('stabili')) return null; - $cols = Schema::getColumnListing('stabili'); + if (! $stabileId || ! Schema::hasTable('stabili')) { + return null; + } + + $cols = Schema::getColumnListing('stabili'); $candidate = null; foreach (['iban_condominio', 'iban_principale', 'iban'] as $col) { if (in_array($col, $cols, true)) { @@ -4152,63 +5255,90 @@ private function getIbanForStabile(?int $stabileId): ?string } } } - if (!$candidate) return null; - $iban = strtoupper(preg_replace('/\s+/', '', (string)$candidate)); + if (! $candidate) { + return null; + } + + $iban = strtoupper(preg_replace('/\s+/', '', (string) $candidate)); return $iban ?: null; } private function ensureStabilePerScala($parentStabile, string $scala): array { // Ritorna [childId, childCode] - if (!Schema::hasTable('stabili')) return [null, null]; - $col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; + if (! Schema::hasTable('stabili')) { + return [null, null]; + } + + $col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; $parentCode = $col === 'codice_stabile' ? ($parentStabile->codice_stabile ?? null) : ($parentStabile->denominazione ?? null); - if (!$parentCode) return [$parentStabile->id ?? null, $parentCode]; + if (! $parentCode) { + return [$parentStabile->id ?? null, $parentCode]; + } + $childCode = rtrim($parentCode, '-') . '-' . strtoupper($scala ?: 'A'); - if (isset($this->scalaStabileCache[$childCode])) return $this->scalaStabileCache[$childCode]; + if (isset($this->scalaStabileCache[$childCode])) { + return $this->scalaStabileCache[$childCode]; + } + $child = DB::table('stabili')->where($col, $childCode)->first(); - if (!$child && !$this->isDryRun) { + if (! $child && ! $this->isDryRun) { // Crea minimale ereditando alcuni campi $payload = [ - $col => $childCode, - 'denominazione' => ($parentStabile->denominazione ?? $childCode) . ' - Scala ' . strtoupper($scala ?: 'A'), - 'indirizzo' => $parentStabile->indirizzo ?? 'ND', - 'citta' => $parentStabile->citta ?? 'ND', - 'cap' => $parentStabile->cap ?? '00000', - 'provincia' => $parentStabile->provincia ?? 'XX', + $col => $childCode, + 'denominazione' => ($parentStabile->denominazione ?? $childCode) . ' - Scala ' . strtoupper($scala ?: 'A'), + 'indirizzo' => $parentStabile->indirizzo ?? 'ND', + 'citta' => $parentStabile->citta ?? 'ND', + 'cap' => $parentStabile->cap ?? '00000', + 'provincia' => $parentStabile->provincia ?? 'XX', 'amministratore_id' => $parentStabile->amministratore_id ?? null, - 'attivo' => true, - 'created_at' => now(), - 'updated_at' => now(), + 'attivo' => true, + 'created_at' => now(), + 'updated_at' => now(), ]; if (Schema::hasColumn('stabili', 'note')) { - $payload['note'] = trim((string)($parentStabile->note ?? '')) . ' [GESCON SCALA]'; + $payload['note'] = trim((string) ($parentStabile->note ?? '')) . ' [GESCON SCALA]'; } $this->dynamicInsert('stabili', $payload); $child = DB::table('stabili')->where($col, $childCode)->first(); } - $res = [$child->id ?? null, $childCode]; + $res = [$child->id ?? null, $childCode]; $this->scalaStabileCache[$childCode] = $res; return $res; } private function resolveStabileIdForScala(?string $scala): ?int { - if ($this->splitMode !== 'stabili-per-scala') return $this->stabileId; - $scala = trim((string)($scala ?? 'A')) ?: 'A'; - if (!Schema::hasTable('stabili')) return $this->stabileId; - $col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; + if ($this->splitMode !== 'stabili-per-scala') { + return $this->stabileId; + } + + $scala = trim((string) ($scala ?? 'A')) ?: 'A'; + if (! Schema::hasTable('stabili')) { + return $this->stabileId; + } + + $col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; $parentCode = $this->option('stabile'); - if (!$parentCode) return $this->stabileId; + if (! $parentCode) { + return $this->stabileId; + } + $childCode = rtrim($parentCode, '-') . '-' . strtoupper($scala); - $row = DB::table('stabili')->where($col, $childCode)->first(); - if ($row) return $row->id; + $row = DB::table('stabili')->where($col, $childCode)->first(); + if ($row) { + return $row->id; + } + return $this->stabileId; } private function safeUpdateStabile(int $id, array $update): void { - if (empty($update)) return; + if (empty($update)) { + return; + } + if ($this->isDryRun) { if ($this->output->isVerbose()) { $this->line(" (dry-run) update stabili skip for id {$id}"); @@ -4217,7 +5347,7 @@ private function safeUpdateStabile(int $id, array $update): void } // Gestisci unicità codice_fiscale con riassegnazione se --force-mapping if (isset($update['codice_fiscale']) && Schema::hasColumn('stabili', 'codice_fiscale')) { - $cf = trim((string)$update['codice_fiscale']); + $cf = trim((string) $update['codice_fiscale']); if ($cf !== '') { $other = DB::table('stabili')->where('codice_fiscale', $cf)->where('id', '!=', $id)->first(); if ($other) { @@ -4242,7 +5372,7 @@ private function safeUpdateStabile(int $id, array $update): void } } } - if (!empty($update)) { + if (! empty($update)) { DB::table('stabili')->where('id', $id)->update($update); } } @@ -4251,86 +5381,84 @@ private function applyStabileMapping(array $update, array $legacyRow, $existing) { try { $aliases = [ - 'comune' => 'citta', - 'codice_fiscale' => 'codice_fiscale', + 'comune' => 'citta', + 'codice_fiscale' => 'codice_fiscale', 'cod_fisc_amministratore' => 'cod_fisc_amministratore', ]; $columnMap = [ // Identità e indirizzo - 'stabile.denominazione' => 'denominazione', - 'stabile.indirizzo' => 'indirizzo', - 'stabile.cap' => 'cap', - 'stabile.citta' => 'citta', - 'stabile.provincia' => 'provincia', - 'stabile.comune_cod' => 'citta', // best-effort legacy code - 'stabile.codice_fiscale' => 'codice_fiscale', + 'stabile.denominazione' => 'denominazione', + 'stabile.indirizzo' => 'indirizzo', + 'stabile.cap' => 'cap', + 'stabile.citta' => 'citta', + 'stabile.provincia' => 'provincia', + 'stabile.comune_cod' => 'citta', // best-effort legacy code + 'stabile.codice_fiscale' => 'codice_fiscale', // Catasto (scegli colonne presenti) - 'stabile.foglio' => (Schema::hasColumn('stabili', 'foglio') ? 'foglio' : (Schema::hasColumn('stabili', 'foglio_catasto') ? 'foglio_catasto' : 'foglio')), - 'stabile.foglio_catasto' => (Schema::hasColumn('stabili', 'foglio_catasto') ? 'foglio_catasto' : (Schema::hasColumn('stabili', 'foglio') ? 'foglio' : 'foglio_catasto')), - 'stabile.particella' => (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : 'particella'))), - 'stabile.particella_catasto' => (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : 'particella_catasto'))), - 'stabile.mappale' => (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : 'mappale'))), - 'stabile.subalterno' => 'subalterno', - 'stabile.categoria' => (Schema::hasColumn('stabili', 'categoria') ? 'categoria' : (Schema::hasColumn('stabili', 'categoria_catastale') ? 'categoria_catastale' : 'categoria')), - 'stabile.categoria_catastale' => (Schema::hasColumn('stabili', 'categoria_catastale') ? 'categoria_catastale' : (Schema::hasColumn('stabili', 'categoria') ? 'categoria' : 'categoria_catastale')), - 'stabile.classe' => (Schema::hasColumn('stabili', 'classe_catastale') ? 'classe_catastale' : (Schema::hasColumn('stabili', 'classe') ? 'classe' : 'classe_catastale')), - 'stabile.consistenza' => (Schema::hasColumn('stabili', 'consistenza_catastale') ? 'consistenza_catastale' : (Schema::hasColumn('stabili', 'consistenza') ? 'consistenza' : 'consistenza_catastale')), - 'stabile.rendita_catastale' => 'rendita_catastale', - 'stabile.superficie_catastale' => 'superficie_catastale', - 'stabile.codice_comune_catasto' => 'codice_comune_catasto', + 'stabile.foglio' => (Schema::hasColumn('stabili', 'foglio') ? 'foglio' : (Schema::hasColumn('stabili', 'foglio_catasto') ? 'foglio_catasto' : 'foglio')), + 'stabile.foglio_catasto' => (Schema::hasColumn('stabili', 'foglio_catasto') ? 'foglio_catasto' : (Schema::hasColumn('stabili', 'foglio') ? 'foglio' : 'foglio_catasto')), + 'stabile.particella' => (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : 'particella'))), + 'stabile.particella_catasto' => (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : 'particella_catasto'))), + 'stabile.mappale' => (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : 'mappale'))), + 'stabile.subalterno' => 'subalterno', + 'stabile.categoria' => (Schema::hasColumn('stabili', 'categoria') ? 'categoria' : (Schema::hasColumn('stabili', 'categoria_catastale') ? 'categoria_catastale' : 'categoria')), + 'stabile.categoria_catastale' => (Schema::hasColumn('stabili', 'categoria_catastale') ? 'categoria_catastale' : (Schema::hasColumn('stabili', 'categoria') ? 'categoria' : 'categoria_catastale')), + 'stabile.classe' => (Schema::hasColumn('stabili', 'classe_catastale') ? 'classe_catastale' : (Schema::hasColumn('stabili', 'classe') ? 'classe' : 'classe_catastale')), + 'stabile.consistenza' => (Schema::hasColumn('stabili', 'consistenza_catastale') ? 'consistenza_catastale' : (Schema::hasColumn('stabili', 'consistenza') ? 'consistenza' : 'consistenza_catastale')), + 'stabile.rendita_catastale' => 'rendita_catastale', + 'stabile.superficie_catastale' => 'superficie_catastale', + 'stabile.codice_comune_catasto' => 'codice_comune_catasto', // Caratteristiche edificio - 'stabile.numero_palazzine' => 'numero_palazzine', - 'stabile.numero_scale_per_palazzina' => 'numero_scale_per_palazzina', - 'stabile.numero_piani' => 'numero_piani', - 'stabile.piano_seminterrato' => 'piano_seminterrato', - 'stabile.piano_sottotetto' => 'piano_sottotetto', - 'stabile.ascensore' => 'presenza_ascensore', - 'stabile.presenza_ascensore' => 'presenza_ascensore', - 'stabile.cortile_giardino' => 'cortile_giardino', - 'stabile.superficie_cortile' => 'superficie_cortile', + 'stabile.numero_palazzine' => 'numero_palazzine', + 'stabile.numero_scale_per_palazzina' => 'numero_scale_per_palazzina', + 'stabile.numero_piani' => 'numero_piani', + 'stabile.piano_seminterrato' => 'piano_seminterrato', + 'stabile.piano_sottotetto' => 'piano_sottotetto', + 'stabile.ascensore' => 'presenza_ascensore', + 'stabile.presenza_ascensore' => 'presenza_ascensore', + 'stabile.cortile_giardino' => 'cortile_giardino', + 'stabile.superficie_cortile' => 'superficie_cortile', 'stabile.riscaldamento_centralizzato' => 'riscaldamento_centralizzato', - 'stabile.acqua_centralizzata' => 'acqua_centralizzata', - 'stabile.gas_centralizzato' => 'gas_centralizzato', - 'stabile.servizio_portineria' => 'servizio_portineria', - 'stabile.orari_portineria' => 'orari_portineria', - 'stabile.videocitofono' => 'videocitofono', - 'stabile.antenna_tv_centralizzata' => 'antenna_tv_centralizzata', - 'stabile.internet_condominiale' => 'internet_condominiale', + 'stabile.acqua_centralizzata' => 'acqua_centralizzata', + 'stabile.gas_centralizzato' => 'gas_centralizzato', + 'stabile.servizio_portineria' => 'servizio_portineria', + 'stabile.orari_portineria' => 'orari_portineria', + 'stabile.videocitofono' => 'videocitofono', + 'stabile.antenna_tv_centralizzata' => 'antenna_tv_centralizzata', + 'stabile.internet_condominiale' => 'internet_condominiale', // Avanzate / stato - 'stabile.configurazione_avanzata' => 'configurazione_avanzata', - 'stabile.ultima_generazione_unita' => 'ultima_generazione_unita', - 'stabile.stato' => 'stato', - 'stabile.registro_note' => 'registro_note', + 'stabile.configurazione_avanzata' => 'configurazione_avanzata', + 'stabile.ultima_generazione_unita' => 'ultima_generazione_unita', + 'stabile.stato' => 'stato', + 'stabile.registro_note' => 'registro_note', // Amministratore (1: campi su stabile, storicizzazione arriverà dopo) - 'stabile.cod_fisc_amministratore' => 'cod_fisc_amministratore', - 'stabile.data_nomina_amministratore' => 'data_nomina', - 'stabile.data_scadenza_mandato' => 'scadenza_mandato', + 'stabile.cod_fisc_amministratore' => 'cod_fisc_amministratore', + 'stabile.data_nomina_amministratore' => 'data_nomina', + 'stabile.data_scadenza_mandato' => 'scadenza_mandato', ]; $inverse = []; - if (!empty($this->fieldMapping) && is_array($this->fieldMapping)) { + if (! empty($this->fieldMapping) && is_array($this->fieldMapping)) { foreach ($this->fieldMapping as $legacyKey => $target) { - if (!is_string($legacyKey) || !is_string($target)) { + if (! is_string($legacyKey) || ! is_string($target)) { continue; } $inverse[$target] = $legacyKey; } } - if (!empty($inverse)) { + if (! empty($inverse)) { foreach ($columnMap as $tKey => $colName) { - if (!isset($inverse[$tKey])) { + if (! isset($inverse[$tKey])) { continue; } - $legacyKey = (string)$inverse[$tKey]; - $lk = strtolower($legacyKey); - $lk = $aliases[$lk] ?? $lk; + $legacyKey = (string) $inverse[$tKey]; + $lk = strtolower($legacyKey); + $lk = $aliases[$lk] ?? $lk; if ($lk === 'codice_fiscale') { - $val = $legacyRow['codice_fiscale'] - ?? ($legacyRow['cin'] ?? ($legacyRow['cod_fiscale'] ?? ($legacyRow['cf'] ?? ($legacyRow['cf_condominio'] ?? null)))); + $val = $legacyRow['codice_fiscale'] ?? ($legacyRow['cin'] ?? ($legacyRow['cod_fiscale'] ?? ($legacyRow['cf'] ?? ($legacyRow['cf_condominio'] ?? null)))); } elseif ($lk === 'cod_fisc_amministratore') { - $val = $legacyRow['cf_amministratore'] - ?? ($legacyRow['cod_fisc_amministratore'] ?? ($legacyRow['cin'] ?? null)); + $val = $legacyRow['cf_amministratore'] ?? ($legacyRow['cod_fisc_amministratore'] ?? ($legacyRow['cin'] ?? null)); } elseif ($tKey === 'banca.iban') { $val = $legacyRow['iban'] ?? ($legacyRow['iban_condominio'] ?? null); } else { @@ -4341,7 +5469,7 @@ private function applyStabileMapping(array $update, array $legacyRow, $existing) } if (in_array($tKey, self::BOOLEAN_TARGET_KEYS, true)) { $valStr = is_string($val) ? trim(strtolower($val)) : $val; - $val = in_array($valStr, ['1', 'y', 'yes', 'si', 'sì', 'true', 'vero', 1, true], true) ? 1 : 0; + $val = in_array($valStr, ['1', 'y', 'yes', 'si', 'sì', 'true', 'vero', 1, true], true) ? 1 : 0; } $current = $existing->$colName ?? null; if ($this->forceMapping || empty($current) || $current === 'ND') { @@ -4350,9 +5478,9 @@ private function applyStabileMapping(array $update, array $legacyRow, $existing) } } - if (!empty($this->manualFieldValues)) { + if (! empty($this->manualFieldValues)) { foreach ($this->manualFieldValues as $target => $rawValue) { - if (!is_string($target)) { + if (! is_string($target)) { continue; } $manualValue = $this->getManualValueForTarget($target); @@ -4360,13 +5488,13 @@ private function applyStabileMapping(array $update, array $legacyRow, $existing) continue; } $colName = $columnMap[$target] ?? null; - if (!$colName && strpos($target, 'stabile.') === 0) { + if (! $colName && strpos($target, 'stabile.') === 0) { $candidate = substr($target, strlen('stabile.')); if ($candidate !== '' && Schema::hasColumn('stabili', $candidate)) { $colName = $candidate; } } - if (!$colName) { + if (! $colName) { continue; } $current = $existing->$colName ?? null; @@ -4376,7 +5504,7 @@ private function applyStabileMapping(array $update, array $legacyRow, $existing) } } - if (!empty($update)) { + if (! empty($update)) { $update['updated_at'] = now(); } return $update; @@ -4390,8 +5518,8 @@ private function applyStabileMapping(array $update, array $legacyRow, $existing) private function resolveMappingCodiceCassa(array $mapping): ?string { - $target = isset($mapping['target']) && is_array($mapping['target']) ? $mapping['target'] : []; - $legacy = isset($mapping['legacy']) && is_array($mapping['legacy']) ? $mapping['legacy'] : []; + $target = isset($mapping['target']) && is_array($mapping['target']) ? $mapping['target'] : []; + $legacy = isset($mapping['legacy']) && is_array($mapping['legacy']) ? $mapping['legacy'] : []; $candidates = [ $target['codice_cassa'] ?? null, $mapping['target_cod_cassa'] ?? null, @@ -4411,7 +5539,7 @@ private function resolveMappingCodiceCassa(array $mapping): ?string private function resolveMappingLegacyField(array $mapping, string $field): ?string { - $legacy = isset($mapping['legacy']) && is_array($mapping['legacy']) ? $mapping['legacy'] : []; + $legacy = isset($mapping['legacy']) && is_array($mapping['legacy']) ? $mapping['legacy'] : []; $candidates = [ $legacy[$field] ?? null, $mapping['legacy_' . $field] ?? null, @@ -4427,10 +5555,10 @@ private function resolveMappingLegacyField(array $mapping, string $field): ?stri private function loadMappingIfAny(): void { try { - $this->fieldMapping = []; - $this->banksMapping = []; + $this->fieldMapping = []; + $this->banksMapping = []; $this->manualFieldValues = []; - if (!Schema::hasTable('gescon_import_mappings')) { + if (! Schema::hasTable('gescon_import_mappings')) { return; } @@ -4486,7 +5614,7 @@ private function loadMappingIfAny(): void private function getManualValueForTarget(string $targetKey) { - if (empty($this->manualFieldValues) || !array_key_exists($targetKey, $this->manualFieldValues)) { + if (empty($this->manualFieldValues) || ! array_key_exists($targetKey, $this->manualFieldValues)) { return null; } $value = $this->manualFieldValues[$targetKey]; @@ -4501,10 +5629,10 @@ private function getManualValueForTarget(string $targetKey) return $value ? 1 : 0; } if (is_numeric($value)) { - return (int)$value === 1 ? 1 : 0; + return (int) $value === 1 ? 1 : 0; } - $normalized = strtolower((string)$value); - $truthy = ['1', 'y', 'yes', 'si', 'sì', 'true', 'vero', 'on']; + $normalized = strtolower((string) $value); + $truthy = ['1', 'y', 'yes', 'si', 'sì', 'true', 'vero', 'on']; return in_array($normalized, $truthy, true) ? 1 : 0; } return $value; @@ -4517,12 +5645,18 @@ private function mapField(string $targetKey, $default = null) return $manualValue; } // Migliorato: se mapping presente e targetKey è stabile.*, tenta di leggere dal record legacy cache - if (empty($this->fieldMapping) || strpos($targetKey, 'stabile.') !== 0) return $default; + if (empty($this->fieldMapping) || strpos($targetKey, 'stabile.') !== 0) { + return $default; + } + // Carica record legacy una sola volta if ($this->stabileLegacyRow === null) { $this->stabileLegacyRow = $this->loadStabileLegacyRow(); } - if (!$this->stabileLegacyRow) return $default; + if (! $this->stabileLegacyRow) { + return $default; + } + $legacyKeyMatched = null; foreach ($this->fieldMapping as $legacyKey => $target) { if ($target === $targetKey) { @@ -4530,24 +5664,27 @@ private function mapField(string $targetKey, $default = null) break; } } - if (!$legacyKeyMatched) return $default; - $row = $this->stabileLegacyRow; + if (! $legacyKeyMatched) { + return $default; + } + + $row = $this->stabileLegacyRow; $targetAttr = substr($targetKey, strlen('stabile.')); // Mappa candidate column names per attributi target $candidatesMap = [ - 'denominazione' => ['denominazione', 'nome', 'ragione_sociale', 'descrizione', 'descr', 'nome_condominio'], - 'indirizzo' => ['indirizzo', 'via', 'indir', 'address'], - 'numero_civico' => ['numero_civico', 'civico', 'n_civico', 'nciv'], - 'cap' => ['cap', 'cap_condominio', 'zip'], - 'comune_cod' => ['comune', 'comune_cod', 'citta', 'città', 'localita', 'loc'], - 'provincia' => ['provincia', 'prov', 'pr'], - 'foglio' => ['foglio', 'ac_foglio', 'foglio_catasto'], - 'particella' => ['particella', 'mappale', 'ac_particella', 'part'], - 'subalterno' => ['subalterno', 'sub', 'ac_sub', 'ac_subalterno'], - 'categoria_catastale' => ['categoria_catastale', 'categoria', 'cat_categoria'], - 'classe' => ['classe', 'cat_classe'], - 'consistenza' => ['consistenza', 'cat_consistenza'], - 'rendita_catastale' => ['rendita_catastale', 'rendita', 'cat_rendita'], + 'denominazione' => ['denominazione', 'nome', 'ragione_sociale', 'descrizione', 'descr', 'nome_condominio'], + 'indirizzo' => ['indirizzo', 'via', 'indir', 'address'], + 'numero_civico' => ['numero_civico', 'civico', 'n_civico', 'nciv'], + 'cap' => ['cap', 'cap_condominio', 'zip'], + 'comune_cod' => ['comune', 'comune_cod', 'citta', 'città', 'localita', 'loc'], + 'provincia' => ['provincia', 'prov', 'pr'], + 'foglio' => ['foglio', 'ac_foglio', 'foglio_catasto'], + 'particella' => ['particella', 'mappale', 'ac_particella', 'part'], + 'subalterno' => ['subalterno', 'sub', 'ac_sub', 'ac_subalterno'], + 'categoria_catastale' => ['categoria_catastale', 'categoria', 'cat_categoria'], + 'classe' => ['classe', 'cat_classe'], + 'consistenza' => ['consistenza', 'cat_consistenza'], + 'rendita_catastale' => ['rendita_catastale', 'rendita', 'cat_rendita'], 'cod_fisc_amministratore' => ['cod_fisc_amministratore', 'cf_amministratore', 'amministratore_cf', 'cf_amm', 'cod_fiscale_amm'], ]; $value = $default; @@ -4560,7 +5697,10 @@ private function mapField(string $targetKey, $default = null) } } else { // fallback: prova legacyKey directly - if (isset($row[$legacyKeyMatched]) && $row[$legacyKeyMatched] !== '') $value = $row[$legacyKeyMatched]; + if (isset($row[$legacyKeyMatched]) && $row[$legacyKeyMatched] !== '') { + $value = $row[$legacyKeyMatched]; + } + } return $value ?? $default; } @@ -4569,16 +5709,27 @@ private function loadStabileLegacyRow(): ?array { try { $code = $this->option('stabile'); - if (!$code) return null; + if (! $code) { + return null; + } + // 1) Staging stabili if (Schema::connection('gescon_import')->hasTable('stabili')) { $r = DB::connection('gescon_import')->table('stabili')->where('cod_stabile', $code)->first(); - if ($r) return (array)$r; + if ($r) { + return (array) $r; + } + } // 2) Staging condomin if (Schema::connection('gescon_import')->hasTable('condomin')) { - $r = DB::connection('gescon_import')->table('condomin')->where('cod_stabile', $code)->first(); - if ($r) return (array)$r; + $r = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin')) + ->where('cod_stabile', $code) + ->first(); + if ($r) { + return (array) $r; + } + } // 3) MDB fallback (usa stessa logica di mdbExportToRows ridotta) $base = $this->option('path'); @@ -4588,7 +5739,7 @@ private function loadStabileLegacyRow(): ?array $rows = $this->mdbExportToRows($mdb, ['Stabili', 'stabili', 'Condomin', 'condomin']); foreach ($rows as $row) { foreach (['cod_stabile', 'codice', 'codice_stabile', 'stabile', 'cod'] as $cf) { - if (isset($row[$cf]) && trim((string)$row[$cf]) === $code) { + if (isset($row[$cf]) && trim((string) $row[$cf]) === $code) { return $row; } } @@ -4606,11 +5757,14 @@ private function loadStabileLegacyRow(): ?array */ private function mdbExportToRows(string $mdbPath, array $tableCandidates): array { - $binTables = trim((string)@shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables'; - $binExport = trim((string)@shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export'; - if (!is_file($mdbPath) || !is_readable($mdbPath)) return []; + $binTables = trim((string) @shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables'; + $binExport = trim((string) @shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export'; + if (! is_file($mdbPath) || ! is_readable($mdbPath)) { + return []; + } + $tables = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: ''; - $table = null; + $table = null; if ($tables) { $names = array_filter(preg_split('/\r?\n/', trim($tables))); foreach ($tableCandidates as $cand) { @@ -4621,7 +5775,7 @@ private function mdbExportToRows(string $mdbPath, array $tableCandidates): array } } } - if (!$table) { + if (! $table) { foreach ($tableCandidates as $cand) { foreach ($names as $n) { if (stripos($n, $cand) !== false) { @@ -4632,7 +5786,10 @@ private function mdbExportToRows(string $mdbPath, array $tableCandidates): array } } } - if (!$table) return []; + if (! $table) { + return []; + } + $tmp = tempnam(sys_get_temp_dir(), 'mdbcsv_'); $cmd = sprintf( '%s -D %s -d %s -q %s -R "\\n" %s %s > %s 2>/dev/null', @@ -4645,31 +5802,43 @@ private function mdbExportToRows(string $mdbPath, array $tableCandidates): array escapeshellarg($tmp) ); @shell_exec($cmd); - if (!is_file($tmp) || filesize($tmp) === 0) { + if (! is_file($tmp) || filesize($tmp) === 0) { @unlink($tmp); return []; } $fh = fopen($tmp, 'r'); - if (!$fh) { + if (! $fh) { @unlink($tmp); return []; } $headers = fgetcsv($fh, 0, '|'); - if (!$headers) { + if (! $headers) { fclose($fh); @unlink($tmp); return []; } - $headers = array_map(fn($h) => strtolower(trim((string)$h)), $headers); - $rows = []; + $headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers); + $rows = []; while (($cols = fgetcsv($fh, 0, '|')) !== false) { - if ($cols === [null] || $cols === false) continue; + if ($cols === [null] || $cols === false) { + continue; + } + $cols = array_map(fn($v) => is_string($v) ? trim($v) : $v, $cols); - $cnt = count($headers); - if (count($cols) < $cnt) $cols = array_pad($cols, $cnt, null); - if (count($cols) > $cnt) $cols = array_slice($cols, 0, $cnt); + $cnt = count($headers); + if (count($cols) < $cnt) { + $cols = array_pad($cols, $cnt, null); + } + + if (count($cols) > $cnt) { + $cols = array_slice($cols, 0, $cnt); + } + $row = @array_combine($headers, $cols) ?: []; - if (!empty($row)) $rows[] = $row; + if (! empty($row)) { + $rows[] = $row; + } + } fclose($fh); @unlink($tmp); diff --git a/app/Console/Commands/ImportGestioniFromLegacy.php b/app/Console/Commands/ImportGestioniFromLegacy.php index 9b69efe..6293e1b 100644 --- a/app/Console/Commands/ImportGestioniFromLegacy.php +++ b/app/Console/Commands/ImportGestioniFromLegacy.php @@ -1,5 +1,4 @@ option('stabile'); - if (!$stabileCode) { + $stabileCode = (string) $this->option('stabile'); + if (! $stabileCode) { $this->error('Specificare --stabile=0000'); return 1; } $stabileCode = str_pad($stabileCode, 4, '0', STR_PAD_LEFT); - $root = rtrim((string)$this->option('root') ?: '/mnt/gescon-archives/gescon', '/'); - $tenant = (string)$this->option('tenant') ?: 'default'; - $dry = (bool)$this->option('dry-run'); + $root = rtrim((string) $this->option('root') ?: '/mnt/gescon-archives/gescon', '/'); + $tenant = (string) $this->option('tenant') ?: 'default'; + $dry = (bool) $this->option('dry-run'); // Heuristics per master MDB (Stabili) $stabiliCandidates = []; if ($this->option('stabili-mdb')) { - $stabiliCandidates[] = (string)$this->option('stabili-mdb'); + $stabiliCandidates[] = (string) $this->option('stabili-mdb'); } - foreach (['cartellestabili_e_anni.mdb', 'Stabili.mdb', 'stabili.mdb'] as $n) { + foreach (['Stabili.mdb', 'stabili.mdb', 'cartellestabili_e_anni.mdb'] as $n) { $stabiliCandidates[] = $root . '/' . $n; + $stabiliCandidates[] = $root . '/dbc/' . $n; } $stabiliMdb = null; foreach ($stabiliCandidates as $cand) { @@ -46,36 +46,42 @@ public function handle(): int break; } } - if (!$stabiliMdb) { + if (! $stabiliMdb) { $this->warn('Master MDB Stabili non trovato; continuerò usando convenzione directory.'); } // 1) Leggi mesi da Stabili (ORD_RATA_*, RIS_RATA_*) e risolvi directory - $ordMesi = []; - $risMesi = []; + $ordMesi = []; + $risMesi = []; $stableDir = $root . '/' . $stabileCode; if ($stabiliMdb) { - [$row, $headers] = $this->findRowInMdb($stabiliMdb, ['cod', 'codice', 'cod_stabile', 'stabile'], $stabileCode); + [$row, $headers] = $this->findRowInMdb($stabiliMdb, ['cod', 'codice', 'cod_stabile', 'stabile', 'nome_directory', 'codice_directory', 'internet_cod_stab'], $stabileCode); if ($row) { $hmap = array_change_key_case($headers, CASE_LOWER); // Estrai mesi foreach ($hmap as $col => $idx) { if (str_starts_with($col, 'ord_rata_')) { - $num = (int)preg_replace('/[^0-9]/', '', substr($col, strlen('ord_rata_'))); + $num = (int) preg_replace('/[^0-9]/', '', substr($col, strlen('ord_rata_'))); if ($num >= 1 && $num <= 12) { - if (self::truthy($row[$idx] ?? null)) $ordMesi[] = $num; + if (self::truthy($row[$idx] ?? null)) { + $ordMesi[] = $num; + } + } } elseif (str_starts_with($col, 'ris_rata_')) { - $num = (int)preg_replace('/[^0-9]/', '', substr($col, strlen('ris_rata_'))); + $num = (int) preg_replace('/[^0-9]/', '', substr($col, strlen('ris_rata_'))); if ($num >= 1 && $num <= 12) { - if (self::truthy($row[$idx] ?? null)) $risMesi[] = $num; + if (self::truthy($row[$idx] ?? null)) { + $risMesi[] = $num; + } + } } } // Directory se presente (campi possibili) - foreach (['directory', 'dir', 'cartella', 'path', 'folder', 'dir_stabile'] as $k) { + foreach (['directory', 'dir', 'cartella', 'path', 'folder', 'dir_stabile', 'nome_directory', 'codice_directory', 'internet_cod_stab'] as $k) { if (isset($hmap[$k])) { - $val = trim((string)($row[$hmap[$k]] ?? '')); + $val = trim((string) ($row[$hmap[$k]] ?? '')); if ($val) { $stableDir = $root . '/' . trim($val, '/'); break; @@ -85,36 +91,39 @@ public function handle(): int } } - if (!is_dir($stableDir)) { + if (! is_dir($stableDir)) { $this->warn("Directory stabile non esiste: {$stableDir}; userò {$root}/{$stabileCode}"); $stableDir = $root . '/' . $stabileCode; } // 2) Determina anni gestione da generale_stabile.mdb o fallback $generale = $stableDir . '/generale_stabile.mdb'; - $anni = []; + $anni = []; if (is_file($generale)) { $anni = $this->extractAnniFromGenerale($generale); } - if (!$anni) { + if (! $anni) { // Fallback: prova da singolo_anno.mdb (incassi anno_rif) foreach (glob($stableDir . '/*/singolo_anno.mdb') as $mdb) { $anni = array_values(array_unique(array_merge($anni, $this->extractAnniFromSingoloAnno($mdb)))); } } sort($anni); - if (!$anni) { + if (! $anni) { $this->warn('Nessun anno trovato; userò anno corrente.'); - $anni = [(int)date('Y')]; + $anni = [(int) date('Y')]; } // 2b) Fallback mesi per anno da singolo_anno.mdb (tabella rate) $mesiPerAnno = []; - if (!$ordMesi || !$risMesi) { + if (! $ordMesi || ! $risMesi) { foreach (glob($stableDir . '/*/singolo_anno.mdb') as $mdb) { $estratti = $this->extractMesiPerAnnoFromSingoloAnno($mdb); foreach ($estratti as $y => $tipi) { - if (!isset($mesiPerAnno[$y])) $mesiPerAnno[$y] = ['O' => [], 'R' => []]; + if (! isset($mesiPerAnno[$y])) { + $mesiPerAnno[$y] = ['O' => [], 'R' => []]; + } + $mesiPerAnno[$y]['O'] = array_values(array_unique(array_merge($mesiPerAnno[$y]['O'], $tipi['O'] ?? []))); $mesiPerAnno[$y]['R'] = array_values(array_unique(array_merge($mesiPerAnno[$y]['R'], $tipi['R'] ?? []))); } @@ -139,12 +148,12 @@ public function handle(): int $updated = 0; // Recupera Stabile record $stabileModel = Stabile::where('codice_stabile', $stabileCode)->first(); - $stabileId = $stabileModel?->id; - $forceUpdate = (bool)$this->option('force-update-months'); + $stabileId = $stabileModel?->id; + $forceUpdate = (bool) $this->option('force-update-months'); foreach ($anni as $anno) { foreach (['ordinaria' => $ordMesi, 'riscaldamento' => $risMesi] as $tipo => $mesi) { // Se mesi vuoti dal master, prova fallback per-anno - if (!$mesi) { + if (! $mesi) { $mesi = $tipo === 'ordinaria' ? ($mesiPerAnno[$anno]['O'] ?? []) : ($mesiPerAnno[$anno]['R'] ?? []); @@ -156,11 +165,11 @@ public function handle(): int ->first(); if ($exists) { // Aggiorna mesi se forzato o vuoti - $campo = $tipo === 'ordinaria' ? 'mesi_rate_ordinaria' : 'mesi_rate_riscaldamento'; - $valEsist = (array)($exists->{$campo} ?? []); - if ($forceUpdate || (empty($valEsist) && !empty($mesi))) { + $campo = $tipo === 'ordinaria' ? 'mesi_rate_ordinaria' : 'mesi_rate_riscaldamento'; + $valEsist = (array) ($exists->{$campo} ?? []); + if ($forceUpdate || (empty($valEsist) && ! empty($mesi))) { $exists->{$campo} = array_values(array_unique(array_map('intval', $mesi))); - if (!$dry) { + if (! $dry) { $exists->save(); } $updated++; @@ -169,15 +178,15 @@ public function handle(): int } continue; } - $gestione = new GestioneContabile(); - $gestione->tenant_id = $tenant; - $gestione->stabile_id = $stabileId; - $gestione->anno_gestione = $anno; - $gestione->tipo_gestione = $tipo; - $gestione->denominazione = ucfirst($tipo) . ' ' . $anno; - $gestione->data_inizio = sprintf('%04d-01-01', $anno); - $gestione->data_fine = sprintf('%04d-12-31', $anno); - $gestione->stato = 'aperta'; + $gestione = new GestioneContabile(); + $gestione->tenant_id = $tenant; + $gestione->stabile_id = $stabileId; + $gestione->anno_gestione = $anno; + $gestione->tipo_gestione = $tipo; + $gestione->denominazione = ucfirst($tipo) . ' ' . $anno; + $gestione->data_inizio = sprintf('%04d-01-01', $anno); + $gestione->data_fine = sprintf('%04d-12-31', $anno); + $gestione->stato = 'aperta'; $gestione->protocollo_prefix = strtoupper(substr($tipo, 0, 1)) . $anno; $gestione->ultimo_protocollo = 0; if ($tipo === 'ordinaria') { @@ -186,9 +195,9 @@ public function handle(): int if ($tipo === 'riscaldamento') { $gestione->mesi_rate_riscaldamento = array_values(array_unique(array_map('intval', $mesi))); } - $gestione->gestione_attiva = true; + $gestione->gestione_attiva = true; $gestione->codice_archivio_legacy = $stableDir; - if (!$dry) { + if (! $dry) { $gestione->save(); } $created++; @@ -201,8 +210,8 @@ public function handle(): int private static function truthy($v): bool { - $s = strtolower(trim((string)$v)); - return $s !== '' && !in_array($s, ['0', 'no', 'false', 'off', 'n'], true); + $s = strtolower(trim((string) $v)); + return $s !== '' && ! in_array($s, ['0', 'no', 'false', 'off', 'n'], true); } /** @@ -212,23 +221,35 @@ private static function truthy($v): bool private function findRowInMdb(string $mdbPath, array $keys, string $value): array { $table = $this->resolveTable($mdbPath, ['stabili', 'Stabili', 'STABILI']); - if (!$table) return [null, []]; + if (! $table) { + return [null, []]; + } + $csv = $this->mdbExport($mdbPath, $table); - if ($csv === null) return [null, []]; + if ($csv === null) { + return [null, []]; + } + $fh = fopen('php://temp', 'r+'); fwrite($fh, $csv); rewind($fh); $header = fgetcsv($fh); - if (!$header) return [null, []]; + if (! $header) { + return [null, []]; + } + $hmap = []; foreach ($header as $i => $h) { - $hmap[strtolower(trim((string)$h))] = $i; + $hmap[strtolower(trim((string) $h))] = $i; } while (($row = fgetcsv($fh)) !== false) { foreach ($keys as $k) { $i = $hmap[strtolower($k)] ?? null; - if ($i === null) continue; - if (str_pad(trim((string)($row[$i] ?? '')), 4, '0', STR_PAD_LEFT) === $value) { + if ($i === null) { + continue; + } + + if (str_pad(trim((string) ($row[$i] ?? '')), 4, '0', STR_PAD_LEFT) === $value) { return [$row, $hmap]; } } @@ -240,25 +261,34 @@ private function extractAnniFromGenerale(string $mdbPath): array { $years = []; $table = $this->resolveTable($mdbPath, ['gestioni', 'gestione', 'esercizi', 'Esercizi', 'Anni']); - if (!$table) return $years; + if (! $table) { + return $years; + } + $csv = $this->mdbExport($mdbPath, $table); - if ($csv === null) return $years; + if ($csv === null) { + return $years; + } + $fh = fopen('php://temp', 'r+'); fwrite($fh, $csv); rewind($fh); $header = fgetcsv($fh); - if (!$header) return $years; + if (! $header) { + return $years; + } + $hmap = []; foreach ($header as $i => $h) { - $hmap[strtolower(trim((string)$h))] = $i; + $hmap[strtolower(trim((string) $h))] = $i; } while (($row = fgetcsv($fh)) !== false) { $y = null; foreach (['anno', 'esercizio', 'str_anno'] as $k) { if (isset($hmap[$k])) { - $val = trim((string)($row[$hmap[$k]] ?? '')); + $val = trim((string) ($row[$hmap[$k]] ?? '')); if ($val !== '' && is_numeric($val)) { - $y = (int)$val; + $y = (int) $val; break; } } @@ -266,9 +296,9 @@ private function extractAnniFromGenerale(string $mdbPath): array if ($y === null) { foreach (['data_inizio', 'inizio', 'dal'] as $k) { if (isset($hmap[$k])) { - $val = trim((string)($row[$hmap[$k]] ?? '')); + $val = trim((string) ($row[$hmap[$k]] ?? '')); if ($val && preg_match('/^(\d{4})-/', $val, $m)) { - $y = (int)$m[1]; + $y = (int) $m[1]; break; } } @@ -284,33 +314,42 @@ private function extractAnniFromGenerale(string $mdbPath): array private function extractAnniFromSingoloAnno(string $mdbPath): array { $table = $this->resolveTable($mdbPath, ['incassi', 'Incassi']); - if (!$table) return []; + if (! $table) { + return []; + } + $csv = $this->mdbExport($mdbPath, $table); - if ($csv === null) return []; + if ($csv === null) { + return []; + } + $fh = fopen('php://temp', 'r+'); fwrite($fh, $csv); rewind($fh); $header = fgetcsv($fh); - if (!$header) return []; + if (! $header) { + return []; + } + $hmap = []; foreach ($header as $i => $h) { - $hmap[strtolower(trim((string)$h))] = $i; + $hmap[strtolower(trim((string) $h))] = $i; } $years = []; while (($row = fgetcsv($fh)) !== false) { $ar = $hmap['anno_rif'] ?? null; if ($ar !== null) { - $val = trim((string)($row[$ar] ?? '')); + $val = trim((string) ($row[$ar] ?? '')); if ($val && is_numeric($val)) { - $years[] = (int)$val; + $years[] = (int) $val; continue; } } $dt = $hmap['dt_empag'] ?? null; if ($dt !== null) { - $val = trim((string)($row[$dt] ?? '')); + $val = trim((string) ($row[$dt] ?? '')); if ($val && preg_match('/^(\d{4})-/', $val, $m)) { - $years[] = (int)$m[1]; + $years[] = (int) $m[1]; } } } @@ -319,19 +358,28 @@ private function extractAnniFromSingoloAnno(string $mdbPath): array private function resolveTable(string $mdbPath, array $candidates): ?string { - $bin = trim((string)@shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables'; + $bin = trim((string) @shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables'; $list = @shell_exec(escapeshellcmd($bin) . ' -1 ' . escapeshellarg($mdbPath)); - if (!$list) return null; + if (! $list) { + return null; + } + $names = array_filter(array_map('trim', explode("\n", $list))); foreach ($candidates as $c) { foreach ($names as $n) { - if (strcasecmp($n, $c) === 0) return $n; + if (strcasecmp($n, $c) === 0) { + return $n; + } + } } // fallback: first contains substring foreach ($candidates as $c) { foreach ($names as $n) { - if (stripos($n, $c) !== false) return $n; + if (stripos($n, $c) !== false) { + return $n; + } + } } return null; @@ -339,51 +387,69 @@ private function resolveTable(string $mdbPath, array $candidates): ?string private function mdbExport(string $mdbPath, string $table): ?string { - $bin = trim((string)@shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export'; - $p = new Process([$bin, '-D', '%Y-%m-%d', $mdbPath, $table]); + $bin = trim((string) @shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export'; + $p = new Process([$bin, '-D', '%Y-%m-%d', $mdbPath, $table]); $p->setTimeout(120); $p->run(); - if (!$p->isSuccessful()) return null; + if (! $p->isSuccessful()) { + return null; + } + return $p->getOutput(); } /** Estrae mesi per anno dalla tabella rate del singolo_anno.mdb: ritorna [anno=>['O'=>[mesi],'R'=>[mesi]]] */ private function extractMesiPerAnnoFromSingoloAnno(string $mdbPath): array { - $out = []; + $out = []; $table = $this->resolveTable($mdbPath, ['rate', 'Rate']); - if (!$table) return $out; + if (! $table) { + return $out; + } + $csv = $this->mdbExport($mdbPath, $table); - if ($csv === null) return $out; + if ($csv === null) { + return $out; + } + $fh = fopen('php://temp', 'r+'); fwrite($fh, $csv); rewind($fh); $header = fgetcsv($fh); - if (!$header) return $out; + if (! $header) { + return $out; + } + $hmap = []; foreach ($header as $i => $h) { - $hmap[strtolower(trim((string)$h))] = $i; + $hmap[strtolower(trim((string) $h))] = $i; } while (($row = fgetcsv($fh)) !== false) { $tipo = null; if (isset($hmap['o_r_s'])) { - $tipo = strtoupper(trim((string)($row[$hmap['o_r_s']] ?? ''))); + $tipo = strtoupper(trim((string) ($row[$hmap['o_r_s']] ?? ''))); } - if (!in_array($tipo, ['O', 'R'], true)) continue; + if (! in_array($tipo, ['O', 'R'], true)) { + continue; + } + $mese = null; if (isset($hmap['n_mese'])) { - $val = trim((string)($row[$hmap['n_mese']] ?? '')); + $val = trim((string) ($row[$hmap['n_mese']] ?? '')); if ($val !== '' && is_numeric($val)) { - $mese = max(1, min(12, (int)$val)); + $mese = max(1, min(12, (int) $val)); } } - if ($mese === null) continue; + if ($mese === null) { + continue; + } + $anno = null; foreach (['str_anno'] as $k) { if (isset($hmap[$k])) { - $s = trim((string)($row[$hmap[$k]] ?? '')); + $s = trim((string) ($row[$hmap[$k]] ?? '')); if ($s && preg_match('/(\d{4})/', $s, $m)) { - $anno = (int)$m[1]; + $anno = (int) $m[1]; break; } } @@ -391,16 +457,19 @@ private function extractMesiPerAnnoFromSingoloAnno(string $mdbPath): array if ($anno === null) { foreach (['dt_empag', 'dt1_da', 'data_emissione'] as $k) { if (isset($hmap[$k])) { - $s = trim((string)($row[$hmap[$k]] ?? '')); + $s = trim((string) ($row[$hmap[$k]] ?? '')); if ($s && preg_match('/^(\d{4})-/', $s, $m)) { - $anno = (int)$m[1]; + $anno = (int) $m[1]; break; } } } } - if ($anno === null) continue; - if (!isset($out[$anno])) { + if ($anno === null) { + continue; + } + + if (! isset($out[$anno])) { $out[$anno] = ['O' => [], 'R' => []]; } if ($tipo === 'O') { diff --git a/app/Console/Commands/LoadGesconMdbToStaging.php b/app/Console/Commands/LoadGesconMdbToStaging.php index 24c38c4..8e0e61d 100644 --- a/app/Console/Commands/LoadGesconMdbToStaging.php +++ b/app/Console/Commands/LoadGesconMdbToStaging.php @@ -1,16 +1,15 @@ hasTable('condomin')) { + return; + } + + // Alcuni ambienti hanno gia' la tabella al limite della row size. + // I campi storici opzionali vanno gestiti come best-effort a valle, + // senza forzare ALTER TABLE durante il reload della staging. + } + public function handle(): int { - $mdb = $this->option('mdb'); - $table = $this->option('table') ?: 'condomin'; - $limit = $this->option('limit') ? (int)$this->option('limit') : null; - $stabile = $this->option('stabile'); + $mdb = $this->option('mdb'); + $table = $this->option('table') ?: 'condomin'; + $limit = $this->option('limit') ? (int) $this->option('limit') : null; + $stabile = $this->option('stabile'); $legacyYear = $this->option('legacy-year'); - if (!$mdb || !is_file($mdb)) { + if (! $mdb || ! is_file($mdb)) { $this->error('Specifica --mdb con path valido al file .mdb'); return 1; } // Consenti alias comuni per tabella di staging $stagingTable = $table; - $tableLower = strtolower((string) $table); + $tableLower = strtolower((string) $table); if (in_array($tableLower, ['anag_casse', 'anagr_casse', 'anagcasse', 'anagrcasse'], true)) { $stagingTable = 'anag_casse'; $this->ensureAnagCasseTable(); } - if (!Schema::connection('gescon_import')->hasTable($stagingTable)) { + if (in_array($tableLower, ['condomin', 'condomini'], true)) { + $stagingTable = 'condomin'; + $this->ensureCondominColumns(); + } + if (! Schema::connection('gescon_import')->hasTable($stagingTable)) { $aliases = [ - 'condomini' => 'condomin', - 'condomin' => 'condomini', - 'anag_casse' => 'anag_casse', - 'Anag_casse' => 'anag_casse', + 'condomini' => 'condomin', + 'condomin' => 'condomini', + 'anag_casse' => 'anag_casse', + 'Anag_casse' => 'anag_casse', 'Anagr_casse' => 'anag_casse', 'anagr_casse' => 'anag_casse', - 'operazioni' => 'operazioni', - 'incassi' => 'incassi', - 'rate' => 'rate', - 'fornitori' => 'mdb_fornitori', - 'Fornitori' => 'mdb_fornitori', - 'tabelle' => 'tabelle_millesimali', - 'TABELLE' => 'tabelle_millesimali', - 'Fraz_gen' => 'fraz_gen', - 'fraz_gen' => 'fraz_gen', - 'fraz_dett' => 'fraz_dett', - 'emes_det' => Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio') + 'operazioni' => 'operazioni', + 'incassi' => 'incassi', + 'rate' => 'rate', + 'fornitori' => 'mdb_fornitori', + 'Fornitori' => 'mdb_fornitori', + 'tabelle' => 'tabelle_millesimali', + 'TABELLE' => 'tabelle_millesimali', + 'Fraz_gen' => 'fraz_gen', + 'fraz_gen' => 'fraz_gen', + 'fraz_dett' => 'fraz_dett', + 'emes_det' => Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio') ? 'rate_emissioni_dettaglio' : 'rate_dettaglio_gescon', - 'emes_gen' => Schema::connection('gescon_import')->hasTable('rate_emissioni') + 'emes_gen' => Schema::connection('gescon_import')->hasTable('rate_emissioni') ? 'rate_emissioni' : 'rate_generale_gescon', ]; @@ -97,7 +111,7 @@ public function handle(): int break; } } - if (!Schema::connection('gescon_import')->hasTable($stagingTable)) { + if (! Schema::connection('gescon_import')->hasTable($stagingTable)) { $this->error("Tabella di staging inesistente: {$table}"); return 1; } @@ -112,15 +126,15 @@ public function handle(): int if ($stabile && Schema::hasTable('amministratori')) { $amm = DB::table('amministratori')->first(); if ($amm) { - $base = getenv('HOME') ?: '/home/' . trim((string)shell_exec('whoami')); - $root = rtrim($base, '/') . '/netgescon'; - $code = $amm->codice_amministratore ?? $amm->codice_univoco ?? ('AMM' . str_pad((string)$amm->id, 4, '0', STR_PAD_LEFT)); + $base = getenv('HOME') ?: '/home/' . trim((string) shell_exec('whoami')); + $root = rtrim($base, '/') . '/netgescon'; + $code = $amm->codice_amministratore ?? $amm->codice_univoco ?? ('AMM' . str_pad((string) $amm->id, 4, '0', STR_PAD_LEFT)); $adminDir = $root . '/' . $code; if (empty($amm->cartella_dati)) { @mkdir($adminDir, 0755, true); DB::table('amministratori')->where('id', $amm->id)->update([ 'cartella_dati' => $adminDir, - 'updated_at' => now(), + 'updated_at' => now(), ]); } } @@ -131,8 +145,8 @@ public function handle(): int // Nome tabella MDB corretto (case sensitive) $tableEffective = $table; try { - $binTables = trim((string)shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables'; - $list = @shell_exec(escapeshellcmd($binTables) . ' -1 ' . escapeshellarg($mdb)); + $binTables = trim((string) shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables'; + $list = @shell_exec(escapeshellcmd($binTables) . ' -1 ' . escapeshellarg($mdb)); if ($list) { foreach (array_filter(array_map('trim', explode("\n", $list))) as $nm) { if (strcasecmp($nm, $table) === 0) { @@ -187,16 +201,16 @@ public function handle(): int } // Estrai CSV con header - $binExport = trim((string)shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export'; - $process = new Process([$binExport, '-D', '%Y-%m-%d', $mdb, $tableEffective]); + $binExport = trim((string) shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export'; + $process = new Process([$binExport, '-D', '%Y-%m-%d', $mdb, $tableEffective]); $process->setTimeout(120); $process->run(); - if (!$process->isSuccessful()) { + if (! $process->isSuccessful()) { $this->error('mdb-export fallito: ' . $process->getErrorOutput()); return 1; } $csv = $process->getOutput(); - if (!$csv) { + if (! $csv) { $this->warn('Nessun dato esportato.'); return 0; } @@ -205,12 +219,12 @@ public function handle(): int fwrite($fh, $csv); rewind($fh); $rawHeader = fgetcsv($fh); - if (!$rawHeader) { + if (! $rawHeader) { $this->warn('Header CSV non letto.'); return 0; } - $header = array_map(fn($h) => strtolower(trim((string)$h)), $rawHeader); - $cols = Schema::connection('gescon_import')->getColumnListing($stagingTable); + $header = array_map(fn($h) => strtolower(trim((string) $h)), $rawHeader); + $cols = Schema::connection('gescon_import')->getColumnListing($stagingTable); $colMap = []; foreach ($cols as $c) { $colMap[strtolower($c)] = $c; @@ -218,21 +232,39 @@ public function handle(): int $hasColumn = fn(string $c) => in_array($c, $cols, true); $normalizeDate = function ($v) { - if ($v === null || $v === '') return null; - $s = trim((string)$v); - if (preg_match('/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/', $s)) return $s; + if ($v === null || $v === '') { + return null; + } + + $s = trim((string) $v); + if (preg_match('/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/', $s)) { + return $s; + } + $dt = \DateTime::createFromFormat('m/d/y H:i:s', $s) ?: \DateTime::createFromFormat('d/m/y H:i:s', $s); - if ($dt) return $dt->format('Y-m-d H:i:s'); + if ($dt) { + return $dt->format('Y-m-d H:i:s'); + } + $dt = \DateTime::createFromFormat('m/d/Y', $s) ?: \DateTime::createFromFormat('d/m/Y', $s); - if ($dt) return $dt->format('Y-m-d'); + if ($dt) { + return $dt->format('Y-m-d'); + } + $ts = @strtotime($s); return $ts ? date('Y-m-d H:i:s', $ts) : null; }; $normalizeNumber = function ($v) { - if ($v === null) return null; - $s = trim((string)$v); - if ($s === '' || $s === '- - -' || $s === '---') return null; + if ($v === null) { + return null; + } + + $s = trim((string) $v); + if ($s === '' || $s === '- - -' || $s === '---') { + return null; + } + $s = str_replace(["\r", "\n", "\t"], '', $s); $s = str_replace(' ', '', $s); $s = str_replace('.', '', $s); @@ -240,12 +272,18 @@ public function handle(): int return is_numeric($s) ? (float) $s : null; }; - $count = 0; - $failCount = 0; + $count = 0; + $failCount = 0; $firstError = null; while (($row = fgetcsv($fh)) !== false) { - if ($row === [null]) continue; - if ($limit && $count >= $limit) break; + if ($row === [null]) { + continue; + } + + if ($limit && $count >= $limit) { + break; + } + $assoc = []; foreach ($header as $i => $col) { $assoc[$col] = $row[$i] ?? null; @@ -257,12 +295,17 @@ public function handle(): int $filtered[$colMap[$key]] = $v; } } - if ($stabile && $hasColumn('cod_stabile')) $filtered['cod_stabile'] = $stabile; - if ($legacyYear && $hasColumn('legacy_year')) $filtered['legacy_year'] = (string)$legacyYear; + if ($stabile && $hasColumn('cod_stabile')) { + $filtered['cod_stabile'] = $stabile; + } + + if ($legacyYear && $hasColumn('legacy_year')) { + $filtered['legacy_year'] = (string) $legacyYear; + } if (strtolower($stagingTable) === 'mdb_fornitori') { if ($hasColumn('legacy_key') && empty($filtered['legacy_key'])) { - $filtered['legacy_key'] = trim((string)($assoc['cod_forn'] ?? $assoc['id_fornitore'] ?? '')); + $filtered['legacy_key'] = trim((string) ($assoc['cod_forn'] ?? $assoc['id_fornitore'] ?? '')); } if ($hasColumn('row_hash') && empty($filtered['row_hash'])) { $seed = json_encode([ @@ -309,7 +352,7 @@ public function handle(): int $filtered['indirizzo_corrispondenza'] = trim((string) ($assoc['ind'] ?? $assoc['indirizzo_corrispondenza'] ?? null)) ?: null; } if ($hasColumn('inquilino') && empty($filtered['inquilino'])) { - $filtered['inquilino'] = !empty($assoc['inquil_nome']) ? 1 : 0; + $filtered['inquilino'] = ! empty($assoc['inquil_nome']) ? 1 : 0; } if ($hasColumn('proprietario') && empty($filtered['proprietario'])) { $filtered['proprietario'] = 1; @@ -359,15 +402,15 @@ public function handle(): int $filtered['totale_inquilini'] = $normalizeNumber($assoc['tot_inq'] ?? $assoc['totale_inquilini'] ?? null); } if ($hasColumn('selezionato')) { - $raw = $assoc['selezionato'] ?? $filtered['selezionato'] ?? null; + $raw = $assoc['selezionato'] ?? $filtered['selezionato'] ?? null; $filtered['selezionato'] = is_numeric($raw) ? (int) $raw : 0; } if ($hasColumn('nord')) { - $raw = $assoc['nord'] ?? $filtered['nord'] ?? null; + $raw = $assoc['nord'] ?? $filtered['nord'] ?? null; $filtered['nord'] = is_numeric($raw) ? (int) $raw : 0; } if ($hasColumn('is_detrazione') && empty($filtered['is_detrazione'])) { - $raw = $assoc['is_detr'] ?? $assoc['is_detrazione'] ?? null; + $raw = $assoc['is_detr'] ?? $assoc['is_detrazione'] ?? null; $filtered['is_detrazione'] = is_numeric($raw) ? (int) $raw : 0; } break; @@ -397,24 +440,24 @@ public function handle(): int $filtered['id'] = null; } if ($hasColumn('unico')) { - $raw = $assoc['unico'] ?? $filtered['unico'] ?? null; - $num = is_numeric($raw) ? (int)$raw : 0; + $raw = $assoc['unico'] ?? $filtered['unico'] ?? null; + $num = is_numeric($raw) ? (int) $raw : 0; $filtered['unico'] = $num > 0 ? 1 : 0; } if ($hasColumn('n_stra')) { - $raw = $assoc['n_stra'] ?? $filtered['n_stra'] ?? 0; - $filtered['n_stra'] = is_numeric($raw) ? (int)$raw : 0; + $raw = $assoc['n_stra'] ?? $filtered['n_stra'] ?? 0; + $filtered['n_stra'] = is_numeric($raw) ? (int) $raw : 0; } break; case 'cre_deb_preced': if ($hasColumn('incluso')) { - $raw = $assoc['incluso'] ?? $filtered['incluso'] ?? null; - $val = is_numeric($raw) ? (int)$raw : (in_array(strtoupper(trim((string)$raw)), ['S', 'Y', 'SI', 'YES', '1'], true) ? 1 : 0); + $raw = $assoc['incluso'] ?? $filtered['incluso'] ?? null; + $val = is_numeric($raw) ? (int) $raw : (in_array(strtoupper(trim((string) $raw)), ['S', 'Y', 'SI', 'YES', '1'], true) ? 1 : 0); $filtered['incluso'] = $val ? 1 : 0; } if ($hasColumn('n_stra')) { - $raw = $assoc['n_stra'] ?? $filtered['n_stra'] ?? 0; - $filtered['n_stra'] = is_numeric($raw) ? (int)$raw : 0; + $raw = $assoc['n_stra'] ?? $filtered['n_stra'] ?? 0; + $filtered['n_stra'] = is_numeric($raw) ? (int) $raw : 0; } break; case 'straordinarie': @@ -424,15 +467,15 @@ public function handle(): int break; case 'rate': // cod_cond (id del condomino in Gescon) - if ($hasColumn('cod_cond') && !isset($filtered['cod_cond'])) { - $filtered['cod_cond'] = (string)($assoc['id_condomino'] ?? $assoc['cod_cond'] ?? ''); + if ($hasColumn('cod_cond') && ! isset($filtered['cod_cond'])) { + $filtered['cod_cond'] = (string) ($assoc['id_condomino'] ?? $assoc['cod_cond'] ?? ''); } // Deriva anno e mese per emissione - $year = null; + $year = null; $month = null; - if (!empty($assoc['str_anno'])) { - if (preg_match('/(\d{4})/', (string)$assoc['str_anno'], $m)) { - $year = (int)$m[1]; + if (! empty($assoc['str_anno'])) { + if (preg_match('/(\d{4})/', (string) $assoc['str_anno'], $m)) { + $year = (int) $m[1]; } } if ($year === null) { @@ -442,97 +485,176 @@ public function handle(): int if ($ds) { $ts = strtotime($ds); if ($ts) { - $year = (int)date('Y', $ts); - $month = (int)date('m', $ts); + $year = (int) date('Y', $ts); + $month = (int) date('m', $ts); } } } - if (!empty($assoc['n_mese']) && is_numeric($assoc['n_mese'])) { - $month = max(1, min(12, (int)$assoc['n_mese'])); + if (! empty($assoc['n_mese']) && is_numeric($assoc['n_mese'])) { + $month = max(1, min(12, (int) $assoc['n_mese'])); } - if ($hasColumn('n_emissione') && !isset($filtered['n_emissione'])) { - $filtered['n_emissione'] = $year ? ($year * 100 + ($month ?: 1)) : 0; + if ($hasColumn('n_emissione') && ! isset($filtered['n_emissione'])) { + $filtered['n_emissione'] = $year ? ($year * 100 + ($month ?: 1)): 0; } // Date emissione/scadenza if ($hasColumn('data_emissione')) { $de = $normalizeDate($assoc['dt1_da'] ?? ($assoc['dt_empag'] ?? null)); - if (!$de && $year) { + if (! $de && $year) { $de = sprintf('%04d-%02d-01', $year, $month ?: 1); } $filtered['data_emissione'] = $de ? substr($de, 0, 10) : null; } if ($hasColumn('data_scadenza')) { $ds = $normalizeDate($assoc['dt2_a'] ?? null); - if (!$ds && $year) { + if (! $ds && $year) { $ds = sprintf('%04d-%02d-28', $year, $month ?: 1); } $filtered['data_scadenza'] = $ds ? substr($ds, 0, 10) : null; } // Importi $impR = $assoc['importo_dovuto_euro'] ?? $assoc['importo_euro'] ?? $assoc['importo'] ?? 0; - if ($hasColumn('importo_euro') && !isset($filtered['importo_euro'])) $filtered['importo_euro'] = is_numeric($impR) ? (float)$impR : 0; - if ($hasColumn('importo') && !isset($filtered['importo'])) $filtered['importo'] = is_numeric($impR) ? (float)$impR : 0; + if ($hasColumn('importo_euro') && ! isset($filtered['importo_euro'])) { + $filtered['importo_euro'] = is_numeric($impR) ? (float) $impR : 0; + } + + if ($hasColumn('importo') && ! isset($filtered['importo'])) { + $filtered['importo'] = is_numeric($impR) ? (float) $impR : 0; + } + // Tipo rata (o/r/s) - if ($hasColumn('tipo_rata') && !isset($filtered['tipo_rata']) && !empty($assoc['o_r_s'])) { - $t = strtolower(trim((string)$assoc['o_r_s'])); + if ($hasColumn('tipo_rata') && ! isset($filtered['tipo_rata']) && ! empty($assoc['o_r_s'])) { + $t = strtolower(trim((string) $assoc['o_r_s'])); $filtered['tipo_rata'] = in_array($t, ['o', 'r', 's'], true) ? strtoupper($t) : null; } // Causale/Note - if ($hasColumn('causale') && !isset($filtered['causale']) && isset($assoc['descrizione'])) $filtered['causale'] = (string)$assoc['descrizione']; - if ($hasColumn('note') && !isset($filtered['note']) && isset($assoc['descrizione'])) $filtered['note'] = (string)$assoc['descrizione']; - if ($hasColumn('pagata') && !isset($filtered['pagata'])) $filtered['pagata'] = 0; + if ($hasColumn('causale') && ! isset($filtered['causale']) && isset($assoc['descrizione'])) { + $filtered['causale'] = (string) $assoc['descrizione']; + } + + if ($hasColumn('note') && ! isset($filtered['note']) && isset($assoc['descrizione'])) { + $filtered['note'] = (string) $assoc['descrizione']; + } + + if ($hasColumn('pagata') && ! isset($filtered['pagata'])) { + $filtered['pagata'] = 0; + } + break; case 'incassi': - if ($hasColumn('cod_cond') && isset($assoc['cod_cond'])) $filtered['cod_cond'] = (string)$assoc['cod_cond']; - if ($hasColumn('cond_inq')) $filtered['cond_inq'] = (string)($assoc['cond_inq'] ?? $assoc['cond_inquil'] ?? ''); - if ($hasColumn('o_r_s')) $filtered['o_r_s'] = (string)($assoc['o_r_s'] ?? ''); - if ($hasColumn('anno_rif')) $filtered['anno_rif'] = (string)($assoc['anno_rif'] ?? ''); - if ($hasColumn('n_mese')) $filtered['n_mese'] = is_numeric($assoc['n_mese'] ?? null) ? (int)$assoc['n_mese'] : null; - if ($hasColumn('n_stra')) $filtered['n_stra'] = is_numeric($assoc['n_stra'] ?? null) ? (int)$assoc['n_stra'] : null; - if ($hasColumn('descrizione') && isset($assoc['descrizione'])) $filtered['descrizione'] = (string)$assoc['descrizione']; - if ($hasColumn('cod_cassa') && isset($assoc['cod_cassa'])) $filtered['cod_cassa'] = (string)$assoc['cod_cassa']; - if ($hasColumn('dt_empag')) $filtered['dt_empag'] = $normalizeDate($assoc['dt_empag'] ?? null); + if ($hasColumn('cod_cond') && isset($assoc['cod_cond'])) { + $filtered['cod_cond'] = (string) $assoc['cod_cond']; + } + + if ($hasColumn('cond_inq')) { + $filtered['cond_inq'] = (string) ($assoc['cond_inq'] ?? $assoc['cond_inquil'] ?? ''); + } + + if ($hasColumn('o_r_s')) { + $filtered['o_r_s'] = (string) ($assoc['o_r_s'] ?? ''); + } + + if ($hasColumn('anno_rif')) { + $filtered['anno_rif'] = (string) ($assoc['anno_rif'] ?? ''); + } + + if ($hasColumn('n_mese')) { + $filtered['n_mese'] = is_numeric($assoc['n_mese'] ?? null) ? (int) $assoc['n_mese'] : null; + } + + if ($hasColumn('n_stra')) { + $filtered['n_stra'] = is_numeric($assoc['n_stra'] ?? null) ? (int) $assoc['n_stra'] : null; + } + + if ($hasColumn('descrizione') && isset($assoc['descrizione'])) { + $filtered['descrizione'] = (string) $assoc['descrizione']; + } + + if ($hasColumn('cod_cassa') && isset($assoc['cod_cassa'])) { + $filtered['cod_cassa'] = (string) $assoc['cod_cassa']; + } + + if ($hasColumn('dt_empag')) { + $filtered['dt_empag'] = $normalizeDate($assoc['dt_empag'] ?? null); + } + if ($hasColumn('importo_pagato_euro')) { - $imp = $assoc['importo_pagato_euro'] ?? $assoc['importo_euro'] ?? $assoc['importo'] ?? null; + $imp = $assoc['importo_pagato_euro'] ?? $assoc['importo_euro'] ?? $assoc['importo'] ?? null; $filtered['importo_pagato_euro'] = $normalizeNumber($imp) ?? 0; } if ($hasColumn('data_incasso')) { $di = $normalizeDate($assoc['dt_empag'] ?? ($filtered['data_incasso'] ?? null)); - if (!$di) { + if (! $di) { // Fallback: costruisci data da anno_rif e n_mese $year = null; - if (!empty($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) $year = (int)$assoc['anno_rif']; - elseif (!empty($assoc['anno_ricev']) && is_numeric($assoc['anno_ricev'])) $year = (int)$assoc['anno_ricev']; + if (! empty($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) { + $year = (int) $assoc['anno_rif']; + } elseif (! empty($assoc['anno_ricev']) && is_numeric($assoc['anno_ricev'])) { + $year = (int) $assoc['anno_ricev']; + } + $month = null; - if (!empty($assoc['n_mese']) && is_numeric($assoc['n_mese'])) $month = max(1, min(12, (int)$assoc['n_mese'])); + if (! empty($assoc['n_mese']) && is_numeric($assoc['n_mese'])) { + $month = max(1, min(12, (int) $assoc['n_mese'])); + } + if ($year) { - $m = $month ?: 1; + $m = $month ?: 1; $day = 15; - $di = sprintf('%04d-%02d-%02d', $year, $m, $day); + $di = sprintf('%04d-%02d-%02d', $year, $m, $day); } } $filtered['data_incasso'] = $di ? substr($di, 0, 10) : null; } // Deriva anno da header legacy se la tabella incassi di staging lo prevede - if ($hasColumn('anno') && !isset($filtered['anno'])) { + if ($hasColumn('anno') && ! isset($filtered['anno'])) { $y = null; - if (!empty($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) $y = (int)$assoc['anno_rif']; - elseif (!empty($assoc['dt_empag'])) { + if (! empty($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) { + $y = (int) $assoc['anno_rif']; + } elseif (! empty($assoc['dt_empag'])) { $ts = @strtotime($assoc['dt_empag']); - if ($ts) $y = (int)date('Y', $ts); + if ($ts) { + $y = (int) date('Y', $ts); + } + } - if ($y !== null) $filtered['anno'] = $y; + if ($y !== null) { + $filtered['anno'] = $y; + } + } $impI = $assoc['importo_pagato_euro'] ?? $assoc['importo_pagato'] ?? $assoc['importo_euro'] ?? $assoc['importo'] ?? 0; - if ($hasColumn('importo_euro') && !isset($filtered['importo_euro'])) $filtered['importo_euro'] = is_numeric($impI) ? (float)$impI : 0; - if ($hasColumn('importo') && !isset($filtered['importo'])) $filtered['importo'] = is_numeric($impI) ? (float)$impI : 0; - if ($hasColumn('modalita_pagamento') && !isset($filtered['modalita_pagamento']) && isset($assoc['cod_cassa'])) $filtered['modalita_pagamento'] = (string)$assoc['cod_cassa']; - if ($hasColumn('riferimento_pagamento') && !isset($filtered['riferimento_pagamento']) && isset($assoc['n_riferimento'])) $filtered['riferimento_pagamento'] = (string)$assoc['n_riferimento']; - if ($hasColumn('n_rata_riferimento') && !isset($filtered['n_rata_riferimento']) && isset($assoc['n_mese']) && is_numeric($assoc['n_mese'])) $filtered['n_rata_riferimento'] = (int)$assoc['n_mese']; - if ($hasColumn('riconciliato') && !isset($filtered['riconciliato'])) $filtered['riconciliato'] = 0; + if ($hasColumn('importo_euro') && ! isset($filtered['importo_euro'])) { + $filtered['importo_euro'] = is_numeric($impI) ? (float) $impI : 0; + } + + if ($hasColumn('importo') && ! isset($filtered['importo'])) { + $filtered['importo'] = is_numeric($impI) ? (float) $impI : 0; + } + + if ($hasColumn('modalita_pagamento') && ! isset($filtered['modalita_pagamento']) && isset($assoc['cod_cassa'])) { + $filtered['modalita_pagamento'] = (string) $assoc['cod_cassa']; + } + + if ($hasColumn('riferimento_pagamento') && ! isset($filtered['riferimento_pagamento']) && isset($assoc['n_riferimento'])) { + $filtered['riferimento_pagamento'] = (string) $assoc['n_riferimento']; + } + + if ($hasColumn('n_rata_riferimento') && ! isset($filtered['n_rata_riferimento']) && isset($assoc['n_mese']) && is_numeric($assoc['n_mese'])) { + $filtered['n_rata_riferimento'] = (int) $assoc['n_mese']; + } + + if ($hasColumn('riconciliato') && ! isset($filtered['riconciliato'])) { + $filtered['riconciliato'] = 0; + } + break; case 'operazioni': - foreach (['dt_spe', 'dt_fat'] as $dc) if ($hasColumn($dc) && array_key_exists($dc, $filtered)) $filtered[$dc] = $normalizeDate($filtered[$dc]); + foreach (['dt_spe', 'dt_fat'] as $dc) { + if ($hasColumn($dc) && array_key_exists($dc, $filtered)) { + $filtered[$dc] = $normalizeDate($filtered[$dc]); + } + } + break; case 'voc_spe': // Evita collisioni multi-anno sugli ID @@ -540,7 +662,10 @@ public function handle(): int unset($filtered['id_vocspe']); } foreach (['perc_proprietario', 'perc_inquilino', 'imp_propr', 'imp_inquil', 'importo', 'importo_euro', 'preventivo', 'preventivo_euro', 'consuntivo', 'consuntivo_euro', 'abit_detr36'] as $nf) { - if ($hasColumn($nf) && (!isset($filtered[$nf]) || $filtered[$nf] === '')) $filtered[$nf] = 0; + if ($hasColumn($nf) && (! isset($filtered[$nf]) || $filtered[$nf] === '')) { + $filtered[$nf] = 0; + } + } break; case 'rate_emissioni_dettaglio': @@ -719,7 +844,10 @@ public function handle(): int } foreach ($filtered as $k => $v) { - if ($v === '') $filtered[$k] = null; + if ($v === '') { + $filtered[$k] = null; + } + } try { @@ -741,11 +869,17 @@ public function handle(): int // Se manca data_incasso ed è richiesta, prova fallback con anno_rif/n_mese if ($hasColumn('data_incasso') && (empty($filtered['data_incasso']))) { $year = null; - if (isset($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) $year = (int)$assoc['anno_rif']; + if (isset($assoc['anno_rif']) && is_numeric($assoc['anno_rif'])) { + $year = (int) $assoc['anno_rif']; + } + $month = null; - if (isset($assoc['n_mese']) && is_numeric($assoc['n_mese'])) $month = max(1, min(12, (int)$assoc['n_mese'])); + if (isset($assoc['n_mese']) && is_numeric($assoc['n_mese'])) { + $month = max(1, min(12, (int) $assoc['n_mese'])); + } + if ($year) { - $m = $month ?: 1; + $m = $month ?: 1; $filtered['data_incasso'] = sprintf('%04d-%02d-15', $year, $m); } } diff --git a/app/Filament/Auth/Login.php b/app/Filament/Auth/Login.php index 98cb471..f63886c 100644 --- a/app/Filament/Auth/Login.php +++ b/app/Filament/Auth/Login.php @@ -1,10 +1,11 @@ hasRole('fornitore')) { + return TicketOperativi::getUrl(panel: 'admin-filament'); + } + return '/admin-filament'; } @@ -24,9 +31,9 @@ public function authenticate(): ?LoginResponse if ($response) { Log::info('Filament login forced redirect (backup4)', [ - 'panel_id' => Filament::getCurrentPanel()?->getId(), + 'panel_id' => Filament::getCurrentPanel()?->getId(), 'filament_guard_check' => Filament::auth()->check(), - 'filament_user_id' => Filament::auth()->id(), + 'filament_user_id' => Filament::auth()->id(), ]); $this->redirect($this->getRedirectUrl()); diff --git a/app/Filament/Pages/Condomini/LettureServiziArchivio.php b/app/Filament/Pages/Condomini/LettureServiziArchivio.php index 1635dfd..af348ad 100644 --- a/app/Filament/Pages/Condomini/LettureServiziArchivio.php +++ b/app/Filament/Pages/Condomini/LettureServiziArchivio.php @@ -13,8 +13,11 @@ use BackedEnum; use Filament\Actions\Action; use Filament\Forms\Components\DatePicker; +use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Select; +use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; +use Filament\Notifications\Notification; use Filament\Pages\Page; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Concerns\InteractsWithTable; @@ -290,6 +293,24 @@ public function table(Table $table): Table ->dateTime('d/m/Y H:i') ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('deadline_lettura_at') + ->label('Deadline campagna') + ->dateTime('d/m/Y H:i') + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('sollecito_inviato_at') + ->label('Sollecito') + ->dateTime('d/m/Y H:i') + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('rilevatore_tipo') + ->label('Rilevatore') + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('lettura_precedente_valore') + ->label('Lettura precedente') + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('riferimento_acquisizione')->label('Riferimento')->toggleable(isToggledHiddenByDefault: true), TextColumn::make('archivio_documentale_path') @@ -324,6 +345,40 @@ public function table(Table $table): Table $this->archivioScope = $this->archivioScope === 'all' ? 'active' : 'all'; }), + Action::make('programmaCampagnaAcqua') + ->label('Campagna letture acqua') + ->icon('heroicon-o-megaphone') + ->color('primary') + ->form([ + Select::make('stabile_servizio_id') + ->label('Servizio acqua') + ->options(fn() => $this->getServiziOptionsByTipo('acqua')) + ->searchable() + ->required(), + DatePicker::make('deadline_lettura_at') + ->label('Scadenza invio lettura') + ->native(false) + ->required(), + Textarea::make('messaggio_template') + ->label('Messaggio base') + ->default('Inviare la lettura del contatore acqua entro la data indicata allegando foto del contatore.') + ->rows(3), + ]) + ->action(function (array $data): void { + $created = $this->scheduleWaterCampaign($data); + Notification::make()->title('Campagna letture impostata')->body('Righe aggiornate o create: ' . $created)->success()->send(); + }), + + Action::make('sollecitaScadute') + ->label('Sollecita scadute') + ->icon('heroicon-o-bell-alert') + ->color('warning') + ->requiresConfirmation() + ->action(function (): void { + $count = $this->markOverdueWaterReminders(); + Notification::make()->title('Solleciti aggiornati')->body('Righe sollecitate: ' . $count)->success()->send(); + }), + Action::make('create') ->label('Nuova') ->icon('heroicon-o-plus') @@ -376,7 +431,25 @@ public function table(Table $table): Table TextInput::make('riferimento_acquisizione')->label('Riferimento acquisizione')->maxLength(191), DatePicker::make('richiesta_lettura_inviata_at')->label('Richiesta inviata il')->native(false), DatePicker::make('prossima_lettura_scadenza_at')->label('Prossima scadenza lettura')->native(false), + DatePicker::make('deadline_lettura_at')->label('Deadline lettura')->native(false), + Select::make('rilevatore_tipo') + ->label('Rilevatore') + ->options([ + 'condomino' => 'Condomino', + 'inquilino' => 'Inquilino', + 'letturista' => 'Letturista', + 'amministrazione' => 'Amministrazione', + ]), + TextInput::make('rilevatore_nome')->label('Nome rilevatore')->maxLength(191), + FileUpload::make('lettura_foto_upload') + ->label('Foto contatore') + ->image() + ->imageEditor(), + FileUpload::make('lettura_precedente_foto_upload') + ->label('Foto contatore precedente') + ->image(), TextInput::make('archivio_documentale_path')->label('Path archivio documentale')->maxLength(500), + TextInput::make('lettura_precedente_valore')->label('Lettura precedente')->numeric(), TextInput::make('lettura_inizio')->label('Lettura inizio')->numeric(), TextInput::make('lettura_fine')->label('Lettura fine')->numeric(), TextInput::make('consumo_valore')->label('Consumo valore')->numeric(), @@ -394,7 +467,7 @@ public function table(Table $table): Table return; } - StabileServizioLettura::query()->create([ + $record = StabileServizioLettura::query()->create([ 'stabile_id' => (int) $stabileId, 'stabile_servizio_id' => (int) ($data['stabile_servizio_id'] ?? 0), 'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null, @@ -410,7 +483,11 @@ public function table(Table $table): Table 'riferimento_acquisizione' => trim((string) ($data['riferimento_acquisizione'] ?? '')) ?: null, 'richiesta_lettura_inviata_at' => $data['richiesta_lettura_inviata_at'] ?? null, 'prossima_lettura_scadenza_at' => $data['prossima_lettura_scadenza_at'] ?? null, + 'deadline_lettura_at' => $data['deadline_lettura_at'] ?? null, + 'rilevatore_tipo' => trim((string) ($data['rilevatore_tipo'] ?? '')) ?: null, + 'rilevatore_nome' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: null, 'archivio_documentale_path' => trim((string) ($data['archivio_documentale_path'] ?? '')) ?: null, + 'lettura_precedente_valore' => $data['lettura_precedente_valore'] ?? null, 'lettura_inizio' => $data['lettura_inizio'] ?? null, 'lettura_fine' => $data['lettura_fine'] ?? null, 'consumo_valore' => $data['consumo_valore'] ?? null, @@ -418,6 +495,9 @@ public function table(Table $table): Table 'importo_totale' => $data['importo_totale'] ?? null, 'created_by' => (int) $user->id, ]); + + $this->hydrateReadingWithPreviousData($record); + $this->storeReadingUploads($record, $data); }), ]) ->actions([ @@ -458,7 +538,25 @@ public function table(Table $table): Table 'pdf_ocr' => 'PDF/OCR', ]), TextInput::make('riferimento_acquisizione')->label('Riferimento acquisizione')->maxLength(191), + DatePicker::make('deadline_lettura_at')->label('Deadline lettura')->native(false), + Select::make('rilevatore_tipo') + ->label('Rilevatore') + ->options([ + 'condomino' => 'Condomino', + 'inquilino' => 'Inquilino', + 'letturista' => 'Letturista', + 'amministrazione' => 'Amministrazione', + ]), + TextInput::make('rilevatore_nome')->label('Nome rilevatore')->maxLength(191), + FileUpload::make('lettura_foto_upload') + ->label('Foto contatore') + ->image() + ->imageEditor(), + FileUpload::make('lettura_precedente_foto_upload') + ->label('Foto contatore precedente') + ->image(), TextInput::make('lettura_inizio')->label('Lettura inizio')->numeric(), + TextInput::make('lettura_precedente_valore')->label('Lettura precedente')->numeric(), TextInput::make('lettura_fine')->label('Lettura fine')->numeric(), TextInput::make('consumo_valore')->label('Consumo valore')->numeric(), TextInput::make('consumo_unita')->label('Unità')->maxLength(16), @@ -479,7 +577,11 @@ public function table(Table $table): Table 'riferimento_acquisizione' => (string) ($record->riferimento_acquisizione ?? ''), 'richiesta_lettura_inviata_at' => $record->richiesta_lettura_inviata_at, 'prossima_lettura_scadenza_at' => $record->prossima_lettura_scadenza_at, + 'deadline_lettura_at' => $record->deadline_lettura_at, + 'rilevatore_tipo' => (string) ($record->rilevatore_tipo ?? ''), + 'rilevatore_nome' => (string) ($record->rilevatore_nome ?? ''), 'archivio_documentale_path' => (string) ($record->archivio_documentale_path ?? ''), + 'lettura_precedente_valore' => $record->lettura_precedente_valore, 'lettura_inizio' => $record->lettura_inizio, 'lettura_fine' => $record->lettura_fine, 'consumo_valore' => $record->consumo_valore, @@ -502,7 +604,11 @@ public function table(Table $table): Table 'riferimento_acquisizione' => trim((string) ($data['riferimento_acquisizione'] ?? '')) ?: null, 'richiesta_lettura_inviata_at' => $data['richiesta_lettura_inviata_at'] ?? null, 'prossima_lettura_scadenza_at' => $data['prossima_lettura_scadenza_at'] ?? null, + 'deadline_lettura_at' => $data['deadline_lettura_at'] ?? null, + 'rilevatore_tipo' => trim((string) ($data['rilevatore_tipo'] ?? '')) ?: null, + 'rilevatore_nome' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: null, 'archivio_documentale_path' => trim((string) ($data['archivio_documentale_path'] ?? '')) ?: null, + 'lettura_precedente_valore' => $data['lettura_precedente_valore'] ?? null, 'lettura_inizio' => $data['lettura_inizio'] ?? null, 'lettura_fine' => $data['lettura_fine'] ?? null, 'consumo_valore' => $data['consumo_valore'] ?? null, @@ -510,6 +616,8 @@ public function table(Table $table): Table 'importo_totale' => $data['importo_totale'] ?? null, ]); $record->save(); + $this->hydrateReadingWithPreviousData($record); + $this->storeReadingUploads($record, $data); }), Action::make('delete') @@ -670,4 +778,244 @@ private function getUnitaOptions(): array }) ->all(); } + + /** + * @return array + */ + private function getServiziOptionsByTipo(string $tipo): array + { + return StabileServizio::query() + ->where('tipo', $tipo) + ->whereIn('id', array_keys($this->getServiziOptions())) + ->orderBy('nome') + ->get(['id', 'nome', 'contatore_matricola']) + ->mapWithKeys(function (StabileServizio $servizio): array { + $label = trim((string) ($servizio->nome ?? '')) ?: ('Servizio #' . (int) $servizio->id); + if (trim((string) ($servizio->contatore_matricola ?? '')) !== '') { + $label .= ' (' . trim((string) $servizio->contatore_matricola) . ')'; + } + + return [(int) $servizio->id => $label]; + }) + ->all(); + } + + private function scheduleWaterCampaign(array $data): int + { + $user = Auth::user(); + $stabileId = $user instanceof User ? (int) StabileContext::resolveActiveStabileId($user) : 0; + $servizioId = (int) ($data['stabile_servizio_id'] ?? 0); + if ($stabileId <= 0 || $servizioId <= 0) { + return 0; + } + + $deadline = $data['deadline_lettura_at'] ?? null; + $message = trim((string) ($data['messaggio_template'] ?? '')); + $count = 0; + + $unitaIds = UnitaImmobiliare::query() + ->where('stabile_id', $stabileId) + ->pluck('id'); + + foreach ($unitaIds as $unitaId) { + $row = StabileServizioLettura::query() + ->where('stabile_id', $stabileId) + ->where('stabile_servizio_id', $servizioId) + ->where('unita_immobiliare_id', (int) $unitaId) + ->whereIn('workflow_stato', ['da_richiedere', 'richiesta_inviata', 'richiesta_sollecitata']) + ->latest('id') + ->first(); + + if (! $row instanceof StabileServizioLettura) { + $row = StabileServizioLettura::query()->create([ + 'stabile_id' => $stabileId, + 'stabile_servizio_id' => $servizioId, + 'unita_immobiliare_id' => (int) $unitaId, + 'workflow_stato' => 'richiesta_inviata', + 'canale_acquisizione' => 'manuale', + 'richiesta_lettura_inviata_at' => now(), + 'deadline_lettura_at' => $deadline, + 'raw' => ['campagna_acqua' => ['messaggio' => $message, 'creata_da' => Auth::id()]], + 'created_by' => Auth::id(), + ]); + } else { + $raw = is_array($row->raw ?? null) ? $row->raw : []; + $raw['campagna_acqua'] = [ + 'messaggio' => $message, + 'aggiornata_da' => Auth::id(), + 'updated_at' => now()->toIso8601String(), + ]; + $row->workflow_stato = 'richiesta_inviata'; + $row->richiesta_lettura_inviata_at = $row->richiesta_lettura_inviata_at ?? now(); + $row->deadline_lettura_at = $deadline; + $row->raw = $raw; + $row->save(); + } + + $count++; + } + + return $count; + } + + private function markOverdueWaterReminders(): int + { + $user = Auth::user(); + $stabileId = $user instanceof User ? (int) StabileContext::resolveActiveStabileId($user) : 0; + if ($stabileId <= 0) { + return 0; + } + + $rows = StabileServizioLettura::query() + ->where('stabile_id', $stabileId) + ->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua')) + ->whereNotNull('deadline_lettura_at') + ->where('deadline_lettura_at', '<', now()) + ->where(function (Builder $q): void { + $q->whereNull('lettura_fine')->orWhereNull('consumo_valore'); + }) + ->get(); + + foreach ($rows as $row) { + $raw = is_array($row->raw ?? null) ? $row->raw : []; + $campagna = is_array($raw['campagna_acqua'] ?? null) ? $raw['campagna_acqua'] : []; + $campagna['solleciti'] = (int) ($campagna['solleciti'] ?? 0) + 1; + $campagna['ultimo_sollecito_at'] = now()->toIso8601String(); + $raw['campagna_acqua'] = $campagna; + + $row->sollecito_inviato_at = now(); + $row->workflow_stato = 'richiesta_sollecitata'; + $row->raw = $raw; + $row->save(); + } + + return $rows->count(); + } + + private function hydrateReadingWithPreviousData(StabileServizioLettura $record): void + { + if ($record->unita_immobiliare_id === null) { + return; + } + + $previous = StabileServizioLettura::query() + ->where('stabile_servizio_id', (int) $record->stabile_servizio_id) + ->where('unita_immobiliare_id', (int) $record->unita_immobiliare_id) + ->where('id', '!=', (int) $record->id) + ->latest('periodo_al') + ->latest('id') + ->first(); + + if (! $previous instanceof StabileServizioLettura) { + return; + } + + $changed = false; + if ($record->lettura_precedente_valore === null && $previous->lettura_fine !== null) { + $record->lettura_precedente_valore = $previous->lettura_fine; + $changed = true; + } + + if (! $record->lettura_precedente_foto_path && $previous->lettura_foto_path) { + $record->lettura_precedente_foto_path = $previous->lettura_foto_path; + $changed = true; + } + + if ($changed) { + $record->save(); + } + } + + private function storeReadingUploads(StabileServizioLettura $record, array $data): void + { + $updated = false; + + foreach ([ + 'lettura_foto_upload' => ['field' => 'lettura_foto_path', 'prefix' => 'lettura'], + 'lettura_precedente_foto_upload' => ['field' => 'lettura_precedente_foto_path', 'prefix' => 'precedente'], + ] as $input => $meta) { + $upload = $data[$input] ?? null; + $stored = $this->storeReadingPhoto($upload, $record, (string) $meta['prefix']); + if (! is_array($stored)) { + continue; + } + + $record->{$meta['field']} = $stored['path']; + + $raw = is_array($record->raw ?? null) ? $record->raw : []; + $raw[$input . '_metadata'] = $stored['metadata']; + if ($input === 'lettura_foto_upload') { + $record->lettura_foto_original_name = (string) ($stored['original_name'] ?? ''); + $record->lettura_foto_metadata = $stored['metadata']; + } + $record->raw = $raw; + $updated = true; + } + + if ($updated) { + $record->save(); + } + } + + private function storeReadingPhoto($upload, StabileServizioLettura $record, string $prefix): ?array + { + if (! is_object($upload) || ! method_exists($upload, 'storeAs')) { + return null; + } + + $ext = strtolower((string) ($upload->getClientOriginalExtension() ?: 'jpg')); + $unit = (int) ($record->unita_immobiliare_id ?? 0); + $filename = implode('-', array_filter([ + 'stabile' . (int) $record->stabile_id, + 'servizio' . (int) $record->stabile_servizio_id, + $unit > 0 ? 'ui' . $unit : null, + $prefix, + now()->format('YmdHis'), + ])) . '.' . $ext; + + $path = $upload->storeAs('letture-servizi/foto', $filename, 'public'); + return [ + 'path' => $path, + 'original_name' => (string) $upload->getClientOriginalName(), + 'metadata' => $this->extractImageMetadata($path), + ]; + } + + private function extractImageMetadata(string $path): array + { + $absolutePath = \Illuminate\Support\Facades\Storage::disk('public')->path($path); + $metadata = [ + 'path' => $path, + 'size' => is_file($absolutePath) ? filesize($absolutePath) : null, + ]; + + $imageSize = @getimagesize($absolutePath); + if (is_array($imageSize)) { + $metadata['width'] = $imageSize[0] ?? null; + $metadata['height'] = $imageSize[1] ?? null; + $metadata['mime'] = $imageSize['mime'] ?? null; + } + + if (function_exists('exif_read_data')) { + try { + $exif = @exif_read_data($absolutePath); + if (is_array($exif)) { + $metadata['exif_datetime'] = $exif['DateTimeOriginal'] ?? ($exif['DateTime'] ?? null); + $metadata['gps'] = [ + 'lat' => $exif['GPSLatitude'] ?? null, + 'lat_ref' => $exif['GPSLatitudeRef'] ?? null, + 'lng' => $exif['GPSLongitude'] ?? null, + 'lng_ref' => $exif['GPSLongitudeRef'] ?? null, + ]; + $metadata['device'] = trim(implode(' ', array_filter([ + $exif['Make'] ?? null, + $exif['Model'] ?? null, + ]))); + } + } catch (\Throwable) { + } + } + + return $metadata; + } } diff --git a/app/Filament/Pages/Condomini/StabilePage.php b/app/Filament/Pages/Condomini/StabilePage.php index a64c357..0343935 100644 --- a/app/Filament/Pages/Condomini/StabilePage.php +++ b/app/Filament/Pages/Condomini/StabilePage.php @@ -1,18 +1,25 @@ , riscaldamento: array, straordinaria: array} */ public array $periodiEmissione = [ - 'ordinaria' => [], + 'ordinaria' => [], 'riscaldamento' => [], 'straordinaria' => [], ]; @@ -60,11 +67,36 @@ class StabilePage extends Page /** @var array */ public array $checklistFineAnnoState = []; + public ?int $selectedInsurancePolicyId = null; + + public array $insurancePolicyForm = [ + 'broker_rubrica_id' => null, + 'company_rubrica_id' => null, + 'policy_name' => '', + 'policy_number' => '', + 'policy_reference' => '', + 'status' => 'attiva', + 'started_at' => null, + 'expires_at' => null, + 'renewal_at' => null, + 'annual_amount' => null, + 'payment_split' => '', + 'payment_schedule_notes' => '', + 'coverage_summary' => '', + 'special_conditions' => '', + 'claim_form_template' => '', + 'notes' => '', + ]; + + public $insurancePolicyDocumentUpload = null; + + public $insuranceSignatureUpload = null; + public static function canAccess(): bool { $user = Auth::user(); return $user instanceof User - && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } public function mount(): void @@ -99,19 +131,27 @@ public function mount(): void ]); $config = (array) ($this->stabile->configurazione_avanzata ?? []); - $this->mostraRiscaldamento = (bool) ($config['mostra_riscaldamento'] ?? false); - $this->gestioneOrdinariaId = isset($config['gestione_ordinaria_id']) ? (int) $config['gestione_ordinaria_id'] : null; + $this->gestioneOrdinariaId = isset($config['gestione_ordinaria_id']) ? (int) $config['gestione_ordinaria_id'] : null; $this->checklistFineAnnoState = is_array($config['checklist_fine_anno_state'] ?? null) ? $config['checklist_fine_anno_state'] : []; - $periodi = (array) ($config['periodi_emissione'] ?? []); + $periodi = (array) ($config['periodi_emissione'] ?? []); + $fallbackOrdinaria = $this->normalizeMesi((array) ($this->stabile->rate_ordinarie_mesi ?? [])); + if ($fallbackOrdinaria === []) { + $fallbackOrdinaria = $this->extractLegacyRateMonthsFromConfig($config, 'ord'); + } + $fallbackRiscaldamento = $this->normalizeMesi((array) ($this->stabile->rate_riscaldamento_mesi ?? [])); + if ($fallbackRiscaldamento === []) { + $fallbackRiscaldamento = $this->extractLegacyRateMonthsFromConfig($config, 'ris'); + } $this->periodiEmissione = [ - 'ordinaria' => $this->normalizeMesi((array) ($periodi['ordinaria'] ?? [])), - 'riscaldamento' => $this->normalizeMesi((array) ($periodi['riscaldamento'] ?? [])), + 'ordinaria' => $this->normalizeMesi((array) ($periodi['ordinaria'] ?? [])) ?: $fallbackOrdinaria, + 'riscaldamento' => $this->normalizeMesi((array) ($periodi['riscaldamento'] ?? [])) ?: $fallbackRiscaldamento, 'straordinaria' => $this->normalizeMesi((array) ($periodi['straordinaria'] ?? [])), ]; + $this->mostraRiscaldamento = (bool) ($config['mostra_riscaldamento'] ?? ($this->periodiEmissione['riscaldamento'] !== [])); $gestioneSelezionataId = request()->integer('gestione_id'); if ($gestioneSelezionataId > 0) { @@ -139,30 +179,32 @@ public function mount(): void public function tabs(): array { return [ - 'dati-generali' => 'Dati generali', - 'unita-immobiliari' => 'Unità / Locali tecnici', - 'palazzine' => 'Palazzine', - 'tabelle-millesimali' => 'Tabelle millesimali', - 'rate-emesse' => 'Rate emesse', - 'gestioni' => 'Gestioni', + 'dati-generali' => 'Dati generali', + 'unita-immobiliari' => 'Unità / Locali tecnici', + 'palazzine' => 'Palazzine', + 'assicurazioni' => 'Assicurazioni', + 'tabelle-millesimali' => 'Tabelle millesimali', + 'rate-emesse' => 'Rate emesse', + 'gestioni' => 'Gestioni', 'gestione-documentale' => 'Gestione documentale', - 'documenti-collegati' => 'Documenti collegati', - 'dati-bancari' => 'Banche e casse', + 'documenti-collegati' => 'Documenti collegati', + 'dati-bancari' => 'Banche e casse', ]; } public function tabView(): string { return match ($this->tab) { - 'rate-emesse' => 'admin.stabili.tabs.rate-emesse', - 'palazzine' => 'admin.stabili.tabs.palazzine', - 'dati-bancari' => 'filament.pages.condomini.tabs.dati-bancari', - 'tabelle-millesimali' => 'filament.pages.condomini.tabelle-millesimali-tab', - 'gestioni' => 'admin.stabili.tabs.gestioni', - 'unita-immobiliari' => 'admin.stabili.tabs.unita-immobiliari', + 'rate-emesse' => 'admin.stabili.tabs.rate-emesse', + 'palazzine' => 'admin.stabili.tabs.palazzine', + 'assicurazioni' => 'filament.pages.condomini.tabs.assicurazioni', + 'dati-bancari' => 'filament.pages.condomini.tabs.dati-bancari', + 'tabelle-millesimali' => 'filament.pages.condomini.tabelle-millesimali-tab', + 'gestioni' => 'admin.stabili.tabs.gestioni', + 'unita-immobiliari' => 'admin.stabili.tabs.unita-immobiliari', 'gestione-documentale' => 'admin.stabili.tabs.gestione-documentale', - 'documenti-collegati' => 'admin.stabili.tabs.documenti-collegati', - default => 'admin.stabili.tabs.dati-generali', + 'documenti-collegati' => 'admin.stabili.tabs.documenti-collegati', + default => 'admin.stabili.tabs.dati-generali', }; } @@ -172,8 +214,8 @@ public function toggleMostraRiscaldamento(): void return; } - $config = (array) ($this->stabile->configurazione_avanzata ?? []); - $value = ! (bool) ($config['mostra_riscaldamento'] ?? false); + $config = (array) ($this->stabile->configurazione_avanzata ?? []); + $value = ! (bool) ($config['mostra_riscaldamento'] ?? false); $config['mostra_riscaldamento'] = $value; $this->stabile->configurazione_avanzata = $config; @@ -190,9 +232,9 @@ public function savePeriodiEmissione(): void return; } - $config = (array) ($this->stabile->configurazione_avanzata ?? []); + $config = (array) ($this->stabile->configurazione_avanzata ?? []); $config['periodi_emissione'] = [ - 'ordinaria' => $this->normalizeMesi((array) ($this->periodiEmissione['ordinaria'] ?? [])), + 'ordinaria' => $this->normalizeMesi((array) ($this->periodiEmissione['ordinaria'] ?? [])), 'riscaldamento' => $this->normalizeMesi((array) ($this->periodiEmissione['riscaldamento'] ?? [])), 'straordinaria' => $this->normalizeMesi((array) ($this->periodiEmissione['straordinaria'] ?? [])), ]; @@ -220,23 +262,23 @@ public function saveGestioneSelezionata(): void } $gestione->data_inizio = $this->gestioneDataInizio ?: null; - $gestione->data_fine = $this->gestioneDataFine ?: null; + $gestione->data_fine = $this->gestioneDataFine ?: null; $mesi = $this->normalizeMesi($this->gestioneMesi); $field = match ($gestione->tipo_gestione) { - 'ordinaria' => 'mesi_rate_ordinaria', + 'ordinaria' => 'mesi_rate_ordinaria', 'riscaldamento' => 'mesi_rate_riscaldamento', 'straordinaria' => 'mesi_rate_straordinaria', - default => 'mesi_rate_ordinaria', + default => 'mesi_rate_ordinaria', }; $gestione->{$field} = $mesi; if ($gestione->tipo_gestione === 'straordinaria') { - $piano = (array) ($gestione->piano_straordinario ?? []); - $piano['numero_rate'] = $this->gestioneNumeroRate; - $piano['cadenza'] = $this->gestioneCadenza; + $piano = (array) ($gestione->piano_straordinario ?? []); + $piano['numero_rate'] = $this->gestioneNumeroRate; + $piano['cadenza'] = $this->gestioneCadenza; $gestione->piano_straordinario = $piano; } @@ -254,9 +296,9 @@ public function saveChecklistFineAnno(): void return; } - $config = (array) ($this->stabile->configurazione_avanzata ?? []); - $config['gestione_ordinaria_id'] = $this->gestioneOrdinariaId; - $config['checklist_fine_anno_state'] = $this->checklistFineAnnoState; + $config = (array) ($this->stabile->configurazione_avanzata ?? []); + $config['gestione_ordinaria_id'] = $this->gestioneOrdinariaId; + $config['checklist_fine_anno_state'] = $this->checklistFineAnnoState; $this->stabile->configurazione_avanzata = $config; $this->stabile->save(); @@ -282,22 +324,22 @@ private function loadGestioneSelezionata(): void return; } - $this->gestioneSelezionataId = $gestione->id; + $this->gestioneSelezionataId = $gestione->id; $this->gestioneSelezionataTipo = $gestione->tipo_gestione; - $this->gestioneDataInizio = $gestione->data_inizio?->format('Y-m-d'); - $this->gestioneDataFine = $gestione->data_fine?->format('Y-m-d'); + $this->gestioneDataInizio = $gestione->data_inizio?->format('Y-m-d'); + $this->gestioneDataFine = $gestione->data_fine?->format('Y-m-d'); $mesi = match ($gestione->tipo_gestione) { - 'ordinaria' => (array) ($gestione->mesi_rate_ordinaria ?? []), + 'ordinaria' => (array) ($gestione->mesi_rate_ordinaria ?? []), 'riscaldamento' => (array) ($gestione->mesi_rate_riscaldamento ?? []), 'straordinaria' => (array) ($gestione->mesi_rate_straordinaria ?? []), - default => [], + default => [], }; $this->gestioneMesi = $this->normalizeMesi($mesi); - $piano = (array) ($gestione->piano_straordinario ?? []); + $piano = (array) ($gestione->piano_straordinario ?? []); $this->gestioneNumeroRate = isset($piano['numero_rate']) ? (int) $piano['numero_rate'] : null; - $this->gestioneCadenza = $piano['cadenza'] ?? null; + $this->gestioneCadenza = $piano['cadenza'] ?? null; } /** @param array $mesi */ @@ -307,4 +349,266 @@ private function normalizeMesi(array $mesi): array sort($mesi); return $mesi; } + + /** + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getInsurancePoliciesProperty() + { + if (! $this->stabile instanceof StabileModel) { + return InsurancePolicy::query()->whereRaw('1 = 0')->get(); + } + + return InsurancePolicy::query() + ->with(['brokerRubrica', 'companyRubrica']) + ->where('stabile_id', (int) $this->stabile->id) + ->orderByDesc('renewal_at') + ->orderByDesc('expires_at') + ->orderByDesc('id') + ->get(); + } + + /** + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getInsuranceClaimsRowsProperty() + { + if (! $this->stabile instanceof StabileModel) { + return InsuranceClaim::query()->whereRaw('1 = 0')->get(); + } + + return InsuranceClaim::query() + ->with(['ticket', 'insurancePolicy']) + ->where('stabile_id', (int) $this->stabile->id) + ->when( + (int) ($this->selectedInsurancePolicyId ?? 0) > 0, + fn($query) => $query->where('insurance_policy_id', (int) $this->selectedInsurancePolicyId) + ) + ->latest('opened_at') + ->latest('id') + ->get(); + } + + /** + * @return array + */ + public function getInsuranceRubricaOptionsProperty(): array + { + return RubricaUniversale::query() + ->whereNull('deleted_at') + ->whereIn('categoria', ['assicurazione', 'fornitore', 'altro']) + ->orderBy('ragione_sociale') + ->orderBy('cognome') + ->orderBy('nome') + ->limit(300) + ->get(['id', 'ragione_sociale', 'nome', 'cognome']) + ->mapWithKeys(function (RubricaUniversale $rubrica): array { + $label = trim((string) ($rubrica->ragione_sociale ?: trim(($rubrica->nome ?? '') . ' ' . ($rubrica->cognome ?? '')))); + return [(int) $rubrica->id => ($label !== '' ? $label : ('Rubrica #' . (int) $rubrica->id))]; + }) + ->all(); + } + + public function selectInsurancePolicy(int $policyId): void + { + $policy = $this->insurancePolicies->firstWhere('id', $policyId); + if (! $policy instanceof InsurancePolicy) { + return; + } + + $this->selectedInsurancePolicyId = (int) $policy->id; + $this->insurancePolicyForm = [ + 'broker_rubrica_id' => $policy->broker_rubrica_id, + 'company_rubrica_id' => $policy->company_rubrica_id, + 'policy_name' => (string) ($policy->policy_name ?? ''), + 'policy_number' => (string) ($policy->policy_number ?? ''), + 'policy_reference' => (string) ($policy->policy_reference ?? ''), + 'status' => (string) ($policy->status ?? 'attiva'), + 'started_at' => optional($policy->started_at)->format('Y-m-d'), + 'expires_at' => optional($policy->expires_at)->format('Y-m-d'), + 'renewal_at' => optional($policy->renewal_at)->format('Y-m-d'), + 'annual_amount' => $policy->annual_amount, + 'payment_split' => (string) ($policy->payment_split ?? ''), + 'payment_schedule_notes' => (string) ($policy->payment_schedule_notes ?? ''), + 'coverage_summary' => (string) ($policy->coverage_summary ?? ''), + 'special_conditions' => (string) ($policy->special_conditions ?? ''), + 'claim_form_template' => (string) ($policy->claim_form_template ?? ''), + 'notes' => (string) ($policy->notes ?? ''), + ]; + } + + public function resetInsurancePolicyForm(): void + { + $this->selectedInsurancePolicyId = null; + $this->insurancePolicyForm = [ + 'broker_rubrica_id' => null, + 'company_rubrica_id' => null, + 'policy_name' => '', + 'policy_number' => '', + 'policy_reference' => '', + 'status' => 'attiva', + 'started_at' => null, + 'expires_at' => null, + 'renewal_at' => null, + 'annual_amount' => null, + 'payment_split' => '', + 'payment_schedule_notes' => '', + 'coverage_summary' => '', + 'special_conditions' => '', + 'claim_form_template' => '', + 'notes' => '', + ]; + $this->insurancePolicyDocumentUpload = null; + $this->insuranceSignatureUpload = null; + } + + public function saveInsurancePolicy(): void + { + if (! $this->stabile instanceof StabileModel) { + return; + } + + $data = validator($this->insurancePolicyForm, [ + 'broker_rubrica_id' => ['nullable', 'integer', 'exists:rubrica_universale,id'], + 'company_rubrica_id' => ['nullable', 'integer', 'exists:rubrica_universale,id'], + 'policy_name' => ['nullable', 'string', 'max:255'], + 'policy_number' => ['nullable', 'string', 'max:255'], + 'policy_reference' => ['nullable', 'string', 'max:255'], + 'status' => ['required', 'string', 'max:50'], + 'started_at' => ['nullable', 'date'], + 'expires_at' => ['nullable', 'date'], + 'renewal_at' => ['nullable', 'date'], + 'annual_amount' => ['nullable', 'numeric'], + 'payment_split' => ['nullable', 'string', 'max:255'], + 'payment_schedule_notes' => ['nullable', 'string', 'max:5000'], + 'coverage_summary' => ['nullable', 'string', 'max:10000'], + 'special_conditions' => ['nullable', 'string', 'max:20000'], + 'claim_form_template' => ['nullable', 'string', 'max:20000'], + 'notes' => ['nullable', 'string', 'max:10000'], + ])->validate(); + + $policy = $this->selectedInsurancePolicyId + ? InsurancePolicy::query()->where('stabile_id', (int) $this->stabile->id)->find($this->selectedInsurancePolicyId) + : new InsurancePolicy(); + + if (! $policy instanceof InsurancePolicy) { + $policy = new InsurancePolicy(); + } + + $policy->fill([ + 'stabile_id' => (int) $this->stabile->id, + 'broker_rubrica_id' => (int) ($data['broker_rubrica_id'] ?? 0) ?: null, + 'company_rubrica_id' => (int) ($data['company_rubrica_id'] ?? 0) ?: null, + 'policy_name' => $this->cleanInsuranceNullable($data['policy_name'] ?? null), + 'policy_number' => $this->cleanInsuranceNullable($data['policy_number'] ?? null), + 'policy_reference' => $this->cleanInsuranceNullable($data['policy_reference'] ?? null), + 'status' => $this->cleanInsuranceNullable($data['status'] ?? 'attiva') ?: 'attiva', + 'started_at' => $data['started_at'] ?? null, + 'expires_at' => $data['expires_at'] ?? null, + 'renewal_at' => $data['renewal_at'] ?? null, + 'annual_amount' => $data['annual_amount'] ?? null, + 'payment_split' => $this->cleanInsuranceNullable($data['payment_split'] ?? null), + 'payment_schedule_notes' => $this->cleanInsuranceNullable($data['payment_schedule_notes'] ?? null), + 'coverage_summary' => $this->cleanInsuranceNullable($data['coverage_summary'] ?? null), + 'special_conditions' => $this->cleanInsuranceNullable($data['special_conditions'] ?? null), + 'claim_form_template' => $this->cleanInsuranceNullable($data['claim_form_template'] ?? null), + 'notes' => $this->cleanInsuranceNullable($data['notes'] ?? null), + 'created_by' => Auth::id(), + ]); + $policy->save(); + + $this->storeInsuranceUploads($policy); + $this->selectedInsurancePolicyId = (int) $policy->id; + + Notification::make()->title('Polizza assicurativa salvata')->success()->send(); + } + + public function deleteInsurancePolicy(int $policyId): void + { + if (! $this->stabile instanceof StabileModel) { + return; + } + + $policy = InsurancePolicy::query()->where('stabile_id', (int) $this->stabile->id)->find($policyId); + if (! $policy instanceof InsurancePolicy) { + return; + } + + $policy->delete(); + + if ((int) ($this->selectedInsurancePolicyId ?? 0) === (int) $policyId) { + $this->resetInsurancePolicyForm(); + } + + Notification::make()->title('Polizza assicurativa eliminata')->success()->send(); + } + + public function getInsuranceDocumentUrl(InsurancePolicy $policy): ?string + { + $path = trim((string) ($policy->policy_document_path ?? '')); + return $path !== '' ? Storage::disk('public')->url($path) : null; + } + + public function getInsuranceSignatureUrl(InsurancePolicy $policy): ?string + { + $path = trim((string) ($policy->signature_image_path ?? '')); + return $path !== '' ? Storage::disk('public')->url($path) : null; + } + + public function getInsuranceTicketUrl(?Ticket $ticket): ?string + { + if (! $ticket instanceof Ticket) { + return null; + } + + return \App\Filament\Pages\Supporto\TicketGestione::getUrl(['ticket' => (int) $ticket->id, 'tab' => 'scheda'], panel: 'admin-filament'); + } + + private function storeInsuranceUploads(InsurancePolicy $policy): void + { + if (is_object($this->insurancePolicyDocumentUpload) && method_exists($this->insurancePolicyDocumentUpload, 'storeAs')) { + $ext = strtolower((string) ($this->insurancePolicyDocumentUpload->getClientOriginalExtension() ?: 'pdf')); + $fileName = 'stabile-' . (int) $policy->stabile_id . '-polizza-' . (int) $policy->id . '-documento.' . $ext; + $path = $this->insurancePolicyDocumentUpload->storeAs('assicurazioni/polizze', $fileName, 'public'); + $policy->policy_document_path = $path; + $policy->policy_document_name = (string) $this->insurancePolicyDocumentUpload->getClientOriginalName(); + $policy->policy_document_mime = (string) $this->insurancePolicyDocumentUpload->getClientMimeType(); + } + + if (is_object($this->insuranceSignatureUpload) && method_exists($this->insuranceSignatureUpload, 'storeAs')) { + $ext = strtolower((string) ($this->insuranceSignatureUpload->getClientOriginalExtension() ?: 'png')); + $fileName = 'stabile-' . (int) $policy->stabile_id . '-polizza-' . (int) $policy->id . '-firma.' . $ext; + $policy->signature_image_path = $this->insuranceSignatureUpload->storeAs('assicurazioni/firme', $fileName, 'public'); + } + + $policy->save(); + $this->insurancePolicyDocumentUpload = null; + $this->insuranceSignatureUpload = null; + } + + private function cleanInsuranceNullable($value): ?string + { + $value = trim((string) $value); + return $value !== '' ? $value : null; + } + + private function extractLegacyRateMonthsFromConfig(array $config, string $section): array + { + $flags = (array) data_get($config, 'rata_flags.' . $section, []); + $months = []; + + foreach (range(1, 12) as $month) { + $value = $flags[$section . '_rata_' . $month] ?? null; + if ($value === null) { + continue; + } + + $normalized = strtolower(trim((string) $value)); + if ($normalized !== '' && ! in_array($normalized, ['0', 'false', 'no', 'off', 'n'], true)) { + $months[] = $month; + } + } + + return $this->normalizeMesi($months); + } } diff --git a/app/Filament/Pages/Condomini/StabilePosta.php b/app/Filament/Pages/Condomini/StabilePosta.php index 06495d8..a9d5f09 100644 --- a/app/Filament/Pages/Condomini/StabilePosta.php +++ b/app/Filament/Pages/Condomini/StabilePosta.php @@ -1,5 +1,4 @@ hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } public function mount(): void @@ -66,15 +65,15 @@ public function mount(): void $posta = (array) Arr::get($stabile->configurazione_avanzata ?? [], 'posta', []); $this->getSchema('form')?->fill([ - 'pec_condominio' => (string) ($stabile->pec_condominio ?? ''), + 'pec_condominio' => (string) ($stabile->pec_condominio ?? ''), 'pec_amministratore' => (string) ($stabile->pec_amministratore ?? ''), - 'posta' => array_merge([ - 'caselle' => [], + 'posta' => array_merge([ + 'caselle' => [], 'acquisizione' => [ - 'salva_documenti' => true, - 'salva_contabilita' => true, + 'salva_documenti' => true, + 'salva_contabilita' => true, 'mittenti_assicurazione' => '', - 'descrizione_default' => 'Documento acquisito da casella stabile', + 'descrizione_default' => 'Documento acquisito da casella stabile', ], ], $posta), ]); @@ -112,8 +111,8 @@ public function form(Schema $schema): Schema ->label('Tipo casella') ->options([ 'gmail' => 'Gmail / Google Workspace', - 'imap' => 'IMAP ordinaria', - 'pec' => 'PEC via IMAP', + 'imap' => 'IMAP ordinaria', + 'pec' => 'PEC via IMAP', ]) ->default('imap') ->required(), @@ -134,8 +133,8 @@ public function form(Schema $schema): Schema Select::make('encryption') ->label('Cifratura') ->options([ - 'ssl' => 'SSL', - 'tls' => 'TLS', + 'ssl' => 'SSL', + 'tls' => 'TLS', 'none' => 'Nessuna', ]) ->default('ssl'), @@ -187,12 +186,12 @@ public function save(): void { abort_unless($this->stabile instanceof Stabile, 404); - $state = is_array($this->data ?? null) ? $this->data : []; - $config = (array) ($this->stabile->configurazione_avanzata ?? []); + $state = is_array($this->data ?? null) ? $this->data : []; + $config = (array) ($this->stabile->configurazione_avanzata ?? []); $config['posta'] = is_array($state['posta'] ?? null) ? $state['posta'] : []; - $this->stabile->pec_condominio = trim((string) ($state['pec_condominio'] ?? '')) ?: null; - $this->stabile->pec_amministratore = trim((string) ($state['pec_amministratore'] ?? '')) ?: null; + $this->stabile->pec_condominio = trim((string) ($state['pec_condominio'] ?? '')) ?: null; + $this->stabile->pec_amministratore = trim((string) ($state['pec_amministratore'] ?? '')) ?: null; $this->stabile->configurazione_avanzata = $config; $this->stabile->save(); @@ -201,4 +200,4 @@ public function save(): void ->success() ->send(); } -} \ No newline at end of file +} diff --git a/app/Filament/Pages/Fiscale/FiscaleDashboard.php b/app/Filament/Pages/Fiscale/FiscaleDashboard.php new file mode 100644 index 0000000..231b894 --- /dev/null +++ b/app/Filament/Pages/Fiscale/FiscaleDashboard.php @@ -0,0 +1,177 @@ +hasAnyRole(['super-admin', 'admin'])) { + return true; + } + + return $user->hasAnyRole(['amministratore', 'collaboratore']); + } + + public function mount(): void + { + $this->annoFiscale = (int) now()->format('Y'); + $this->adempimentiInScadenza = collect(); + + $user = Auth::user(); + if (! $user instanceof User) { + return; + } + + $activeId = StabileContext::resolveActiveStabileId($user); + if (! $activeId) { + $this->stabileAttivo = null; + $this->summary = $this->emptySummary(); + $this->templates = $this->defaultTemplates(); + return; + } + + $this->stabileAttivo = StabileContext::accessibleStabili($user)->firstWhere('id', $activeId); + $this->summary = $this->buildSummary($activeId, $this->annoFiscale); + $this->adempimentiInScadenza = AdempimentoFiscale::query() + ->where('stabile_id', $activeId) + ->orderByRaw('CASE WHEN scadenza IS NULL THEN 1 ELSE 0 END') + ->orderBy('scadenza') + ->limit(12) + ->get(); + $this->templates = $this->defaultTemplates(); + } + + public static function getRegistroRaUrl(): string + { + return RegistroRitenuteAccontoArchivio::getUrl(panel: 'admin-filament'); + } + + protected function emptySummary(): array + { + return [ + 'ritenute_count' => 0, + 'ritenute_da_versare' => 0, + 'detrazioni_count' => 0, + 'straordinarie_fiscali' => 0, + 'adempimenti_count' => 0, + 'adempimenti_aperti' => 0, + ]; + } + + protected function buildSummary(int $stabileId, int $annoFiscale): array + { + $summary = $this->emptySummary(); + + $summary['ritenute_count'] = RegistroRitenuteAcconto::query() + ->whereHas('gestione', fn($q) => $q->where('stabile_id', $stabileId)) + ->where('anno_dichiarazione', $annoFiscale) + ->count(); + + $summary['ritenute_da_versare'] = RegistroRitenuteAcconto::query() + ->whereHas('gestione', fn($q) => $q->where('stabile_id', $stabileId)) + ->where('anno_dichiarazione', $annoFiscale) + ->whereIn('stato_versamento', ['da_versare', 'scaduto', 'ritardo']) + ->count(); + + if (Schema::hasTable('detrazioni_fiscali_dom')) { + $summary['detrazioni_count'] = DB::table('detrazioni_fiscali_dom as dfd') + ->leftJoin('gestioni_contabili as gc', 'gc.id', '=', 'dfd.gestione_id') + ->where(function ($q) use ($stabileId): void { + $q->where('gc.stabile_id', $stabileId); + + if (Schema::hasColumn('detrazioni_fiscali_dom', 'stabile_id')) { + $q->orWhere('dfd.stabile_id', $stabileId); + } + }) + ->where(function ($q) use ($annoFiscale): void { + $q->whereYear('dfd.created_at', $annoFiscale); + + if (Schema::hasColumn('detrazioni_fiscali_dom', 'anno_fiscale')) { + $q->orWhere('dfd.anno_fiscale', $annoFiscale); + } + + if (Schema::hasColumn('detrazioni_fiscali_dom', 'legacy_rif')) { + $q->orWhere('dfd.legacy_rif', 'like', $annoFiscale . '%'); + } + }) + ->count(); + } + + if (Schema::hasTable('lavori_straordinari')) { + $summary['straordinarie_fiscali'] = DB::table('lavori_straordinari as ls') + ->join('gestioni_contabili as gc', 'gc.id', '=', 'ls.gestione_id') + ->where('gc.stabile_id', $stabileId) + ->where('gc.anno_gestione', $annoFiscale) + ->whereNotNull('ls.gestione_id') + ->where('ls.note_json', 'like', '%"is_fiscal_declaration":true%') + ->count(); + } + + $summary['adempimenti_count'] = AdempimentoFiscale::query() + ->where('stabile_id', $stabileId) + ->count(); + + $summary['adempimenti_aperti'] = AdempimentoFiscale::query() + ->where('stabile_id', $stabileId) + ->whereNotIn('stato', ['completato', 'annullato']) + ->count(); + + return $summary; + } + + protected function defaultTemplates(): array + { + return [ + ['tipo' => 'ritenuta_acconto', 'titolo' => 'Ritenute d\'acconto', 'descrizione' => 'Versamenti, F24, CU e quadratura come sostituto d\'imposta.'], + ['tipo' => 'certificazione_detrazione', 'titolo' => 'Certificazioni detrazioni fiscali', 'descrizione' => 'Invio certificazioni e archivio documentale per lavori agevolati.'], + ['tipo' => 'quadro_ac', 'titolo' => 'Quadro AC amministratore', 'descrizione' => 'Raccolta dati e controllo annuale dei fornitori e compensi rilevanti.'], + ['tipo' => 'dichiarazione_770', 'titolo' => 'Modello 770', 'descrizione' => 'Preparazione dichiarazione, quadratura ritenute e storico invii.'], + ['tipo' => 'rendita_condominiale', 'titolo' => 'Rendite condominiali e affitti', 'descrizione' => 'Monitoraggio rendite, contratti, certificazioni e imponibili collegati.'], + ['tipo' => 'compenso_amministratore', 'titolo' => 'Compensi amministratore', 'descrizione' => 'Preventivo, rinnovi annuali e voci addebitabili per servizi e pratiche.'], + ]; + } +} diff --git a/app/Filament/Pages/Fornitore/Collaboratori.php b/app/Filament/Pages/Fornitore/Collaboratori.php index c4de4a1..a83d704 100644 --- a/app/Filament/Pages/Fornitore/Collaboratori.php +++ b/app/Filament/Pages/Fornitore/Collaboratori.php @@ -4,6 +4,7 @@ use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext; use App\Models\Fornitore; use App\Models\FornitoreDipendente; +use App\Models\RubricaUniversale; use App\Models\User; use BackedEnum; use Filament\Notifications\Notification; @@ -36,14 +37,46 @@ class Collaboratori extends Page public bool $missingAdminContext = false; + public string $activeTab = 'nuovo'; + public string $nome = ''; public string $cognome = ''; + public string $tipoContatto = 'persona_fisica'; + + public string $ragioneSociale = ''; + + public string $codiceFiscale = ''; + + public string $partitaIva = ''; + + public string $pec = ''; + + public string $sesso = ''; + + public string $dataNascita = ''; + + public string $luogoNascita = ''; + + public string $provinciaNascita = ''; + public string $email = ''; public string $telefono = ''; + public string $indirizzo = ''; + + public string $civico = ''; + + public string $cap = ''; + + public string $citta = ''; + + public string $provincia = ''; + + public string $nazione = 'Italia'; + public string $collaboratoreTipo = FornitoreDipendente::TIPO_INTERNO; public string $fornitoreSearch = ''; @@ -84,6 +117,7 @@ public function mount(): void $this->fornitore = $fornitore; $this->refreshFornitoreOptions(); $this->refreshRows(); + $this->activeTab = $this->rows !== [] ? 'elenco' : 'nuovo'; } public function updatedCollaboratoreTipo(): void @@ -110,10 +144,25 @@ public function createCollaboratore(): void $validated = $this->validate([ 'collaboratoreTipo' => ['required', 'string', 'in:' . implode(',', [FornitoreDipendente::TIPO_INTERNO, FornitoreDipendente::TIPO_FORNITORE_ESTERNO])], + 'tipoContatto' => ['nullable', 'string', 'in:persona_fisica,persona_giuridica'], 'nome' => ['nullable', 'string', 'max:100'], 'cognome' => ['nullable', 'string', 'max:100'], + 'ragioneSociale' => ['nullable', 'string', 'max:255'], + 'codiceFiscale' => ['nullable', 'string', 'max:50'], + 'partitaIva' => ['nullable', 'string', 'max:50'], + 'pec' => ['nullable', 'string', 'max:255'], + 'sesso' => ['nullable', 'string', 'max:20'], + 'dataNascita' => ['nullable', 'date'], + 'luogoNascita' => ['nullable', 'string', 'max:255'], + 'provinciaNascita' => ['nullable', 'string', 'max:10'], 'email' => ['nullable', 'email', 'max:255'], 'telefono' => ['nullable', 'string', 'max:50'], + 'indirizzo' => ['nullable', 'string', 'max:255'], + 'civico' => ['nullable', 'string', 'max:20'], + 'cap' => ['nullable', 'string', 'max:20'], + 'citta' => ['nullable', 'string', 'max:255'], + 'provincia' => ['nullable', 'string', 'max:10'], + 'nazione' => ['nullable', 'string', 'max:100'], 'note' => ['nullable', 'string', 'max:2000'], 'createUserAccess' => ['nullable', 'boolean'], 'accessPassword' => ['nullable', 'string', 'min:8', 'max:100'], @@ -121,10 +170,21 @@ public function createCollaboratore(): void ]); $tipo = (string) $validated['collaboratoreTipo']; + $tipoContatto = (string) ($validated['tipoContatto'] ?? 'persona_fisica'); - if ($tipo === FornitoreDipendente::TIPO_INTERNO && trim((string) ($validated['nome'] ?? '')) === '') { - Notification::make()->title('Inserisci il nome del collaboratore')->warning()->send(); - return; + if ($tipo === FornitoreDipendente::TIPO_INTERNO) { + $hasPersonalName = trim((string) ($validated['nome'] ?? '')) !== ''; + $hasCompanyName = trim((string) ($validated['ragioneSociale'] ?? '')) !== ''; + + if ($tipoContatto === 'persona_giuridica' && ! $hasCompanyName) { + Notification::make()->title('Inserisci la ragione sociale del collaboratore')->warning()->send(); + return; + } + + if ($tipoContatto !== 'persona_giuridica' && ! $hasPersonalName) { + Notification::make()->title('Inserisci il nome del collaboratore')->warning()->send(); + return; + } } $fornitoreEsterno = null; @@ -170,8 +230,10 @@ public function createCollaboratore(): void $dipendente->tipo_collaboratore = $tipo; $dipendente->nome = $tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO ? (string) ($fornitoreEsterno?->ragione_sociale ?: $fornitoreEsterno?->nome ?: 'Subfornitore') - : trim((string) $validated['nome']); - $dipendente->cognome = $tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO + : ($tipoContatto === 'persona_giuridica' + ? trim((string) ($validated['ragioneSociale'] ?? '')) + : trim((string) $validated['nome'])); + $dipendente->cognome = $tipo === FornitoreDipendente::TIPO_FORNITORE_ESTERNO || $tipoContatto === 'persona_giuridica' ? null : (trim((string) ($validated['cognome'] ?? '')) ?: null); $dipendente->email = $email !== '' ? $email : null; @@ -215,10 +277,15 @@ public function createCollaboratore(): void $dipendente->user_id = (int) $user->id; } + if ($tipo === FornitoreDipendente::TIPO_INTERNO) { + $dipendente->rubrica_id = $this->upsertRubricaInterna($validated, $dipendente); + } + $dipendente->save(); $this->resetForm(); $this->refreshRows(); + $this->activeTab = 'elenco'; $notification = Notification::make()->title('Collaboratore salvato')->success(); if ($createdPassword !== null && $email !== '') { @@ -388,6 +455,7 @@ protected function refreshRows(): void 'note' => (string) ($dipendente->note ?? ''), 'attivo' => (bool) $dipendente->attivo, 'user_id' => $dipendente->user_id ? (int) $dipendente->user_id : null, + 'rubrica_id' => $dipendente->rubrica_id ? (int) $dipendente->rubrica_id : null, 'tipo' => (string) $dipendente->tipo_collaboratore, 'tipo_label' => (string) $dipendente->tipo_label, 'fornitore_esterno_nome' => (string) ($dipendente->fornitoreEsterno?->ragione_sociale ?: trim((string) (($dipendente->fornitoreEsterno?->nome ?? '') . ' ' . ($dipendente->fornitoreEsterno?->cognome ?? '')))), @@ -447,10 +515,25 @@ protected function resolveFornitoreEsterno(int $fornitoreId): ?Fornitore protected function resetForm(): void { + $this->tipoContatto = 'persona_fisica'; $this->nome = ''; $this->cognome = ''; + $this->ragioneSociale = ''; + $this->codiceFiscale = ''; + $this->partitaIva = ''; + $this->pec = ''; + $this->sesso = ''; + $this->dataNascita = ''; + $this->luogoNascita = ''; + $this->provinciaNascita = ''; $this->email = ''; $this->telefono = ''; + $this->indirizzo = ''; + $this->civico = ''; + $this->cap = ''; + $this->citta = ''; + $this->provincia = ''; + $this->nazione = 'Italia'; $this->collaboratoreTipo = FornitoreDipendente::TIPO_INTERNO; $this->fornitoreSearch = ''; $this->fornitoreEsternoId = null; @@ -459,4 +542,60 @@ protected function resetForm(): void $this->accessPassword = ''; $this->refreshFornitoreOptions(); } + + protected function upsertRubricaInterna(array $validated, FornitoreDipendente $dipendente): int + { + $email = trim((string) ($validated['email'] ?? '')); + $rubrica = null; + + if ($email !== '') { + $rubrica = RubricaUniversale::query() + ->whereRaw('LOWER(email) = ?', [mb_strtolower($email)]) + ->where('categoria', 'fornitore') + ->orderByDesc('id') + ->first(); + } + + if (! $rubrica instanceof RubricaUniversale) { + $rubrica = new RubricaUniversale(); + $rubrica->data_inserimento = now()->toDateString(); + $rubrica->creato_da = Auth::id(); + } + + $tipoContatto = (string) ($validated['tipoContatto'] ?? 'persona_fisica'); + $telefono = trim((string) ($validated['telefono'] ?? '')); + $ragioneSociale = trim((string) ($validated['ragioneSociale'] ?? '')); + + $rubrica->titolo_id = null; + $rubrica->nome = $tipoContatto === 'persona_giuridica' ? null : (trim((string) ($validated['nome'] ?? '')) ?: null); + $rubrica->cognome = $tipoContatto === 'persona_giuridica' ? null : (trim((string) ($validated['cognome'] ?? '')) ?: null); + $rubrica->ragione_sociale = $tipoContatto === 'persona_giuridica' ? ($ragioneSociale ?: null) : null; + $rubrica->tipo_contatto = $tipoContatto; + $rubrica->codice_fiscale = trim((string) ($validated['codiceFiscale'] ?? '')) ?: null; + $rubrica->partita_iva = trim((string) ($validated['partitaIva'] ?? '')) ?: null; + $rubrica->sesso = trim((string) ($validated['sesso'] ?? '')) ?: null; + $rubrica->data_nascita = trim((string) ($validated['dataNascita'] ?? '')) ?: null; + $rubrica->luogo_nascita = trim((string) ($validated['luogoNascita'] ?? '')) ?: null; + $rubrica->provincia_nascita = trim((string) ($validated['provinciaNascita'] ?? '')) ?: null; + $rubrica->indirizzo = trim((string) ($validated['indirizzo'] ?? '')) ?: null; + $rubrica->civico = trim((string) ($validated['civico'] ?? '')) ?: null; + $rubrica->cap = trim((string) ($validated['cap'] ?? '')) ?: null; + $rubrica->citta = trim((string) ($validated['citta'] ?? '')) ?: null; + $rubrica->provincia = trim((string) ($validated['provincia'] ?? '')) ?: null; + $rubrica->nazione = trim((string) ($validated['nazione'] ?? '')) ?: 'Italia'; + $rubrica->telefono_ufficio = $tipoContatto === 'persona_giuridica' ? ($telefono ?: null) : null; + $rubrica->telefono_cellulare = $telefono ?: null; + $rubrica->email = $email !== '' ? $email : null; + $rubrica->pec = trim((string) ($validated['pec'] ?? '')) ?: null; + $rubrica->note = trim((string) ($validated['note'] ?? '')) ?: null; + $rubrica->categoria = 'fornitore'; + $rubrica->stato = 'attivo'; + $rubrica->data_ultima_modifica = now()->toDateString(); + $rubrica->modificato_da = Auth::id(); + $rubrica->save(); + + $dipendente->rubrica_id = (int) $rubrica->id; + + return (int) $rubrica->id; + } } diff --git a/app/Filament/Pages/Fornitore/Contabilita.php b/app/Filament/Pages/Fornitore/Contabilita.php index d208131..6c1b0c9 100644 --- a/app/Filament/Pages/Fornitore/Contabilita.php +++ b/app/Filament/Pages/Fornitore/Contabilita.php @@ -40,10 +40,10 @@ class Contabilita extends Page /** @var array{aperte:int,pagate:int,registrate:int,totale_aperto:float,totale_registrato:float} */ public array $stats = [ - 'aperte' => 0, - 'pagate' => 0, - 'registrate' => 0, - 'totale_aperto' => 0.0, + 'aperte' => 0, + 'pagate' => 0, + 'registrate' => 0, + 'totale_aperto' => 0.0, 'totale_registrato' => 0.0, ]; @@ -58,7 +58,7 @@ public static function canAccess(): bool $user = Auth::user(); return $user instanceof User - && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']); } public function mount(): void @@ -70,7 +70,7 @@ public function mount(): void return; } - $this->fornitore = $fornitore; + $this->fornitore = $fornitore; $this->fornitoreLabel = $this->getFornitoreLabel($fornitore); $this->loadData(); } @@ -107,14 +107,14 @@ protected function loadData(): void ->limit(40) ->get() ->map(fn(TicketIntervento $intervento) => [ - 'ticket_id' => (int) $intervento->ticket_id, - 'intervento_id' => (int) $intervento->id, - 'stato' => (string) $intervento->stato, - 'stabile' => (string) ($intervento->ticket?->stabile?->denominazione ?? '-'), + 'ticket_id' => (int) $intervento->ticket_id, + 'intervento_id' => (int) $intervento->id, + 'stato' => (string) $intervento->stato, + 'stabile' => (string) ($intervento->ticket?->stabile?->denominazione ?? '-'), 'amministratore' => (string) ($intervento->ticket?->stabile?->amministratore?->denominazione_studio ?? '-'), - 'titolo' => (string) ($intervento->ticket?->titolo ?? '-'), - 'aggiornato' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-', - 'url' => TicketInterventoScheda::getUrl(['record' => (int) $intervento->id], panel: 'admin-filament'), + 'titolo' => (string) ($intervento->ticket?->titolo ?? '-'), + 'aggiornato' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-', + 'url' => TicketInterventoScheda::getUrl(['record' => (int) $intervento->id], panel: 'admin-filament'), ]) ->all(); @@ -134,25 +134,25 @@ protected function loadData(): void ->get(); $this->stats = [ - 'aperte' => $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->count(), - 'pagate' => $fatture->where('stato', 'pagato')->count(), - 'registrate' => $fatture->count(), - 'totale_aperto' => (float) $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->sum('netto_da_pagare'), + 'aperte' => $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->count(), + 'pagate' => $fatture->where('stato', 'pagato')->count(), + 'registrate' => $fatture->count(), + 'totale_aperto' => (float) $fatture->whereIn('stato', ['inserito', 'contabilizzato'])->sum('netto_da_pagare'), 'totale_registrato' => (float) $fatture->sum('totale'), ]; $this->fattureRows = $fatture ->map(fn(FatturaFornitore $fattura) => [ - 'id' => (int) $fattura->id, - 'data_documento' => optional($fattura->data_documento)->format('d/m/Y') ?: '-', + 'id' => (int) $fattura->id, + 'data_documento' => optional($fattura->data_documento)->format('d/m/Y') ?: '-', 'numero_documento' => (string) ($fattura->numero_documento ?? '-'), - 'stato' => (string) ($fattura->stato ?? 'inserito'), - 'stabile' => (string) ($fattura->stabile?->denominazione ?? '-'), - 'amministratore' => (string) ($fattura->stabile?->amministratore?->denominazione_studio ?? '-'), - 'descrizione' => (string) ($fattura->descrizione ?? ''), - 'totale' => (float) ($fattura->totale ?? 0), - 'netto' => (float) ($fattura->netto_da_pagare ?? 0), + 'stato' => (string) ($fattura->stato ?? 'inserito'), + 'stabile' => (string) ($fattura->stabile?->denominazione ?? '-'), + 'amministratore' => (string) ($fattura->stabile?->amministratore?->denominazione_studio ?? '-'), + 'descrizione' => (string) ($fattura->descrizione ?? ''), + 'totale' => (float) ($fattura->totale ?? 0), + 'netto' => (float) ($fattura->netto_da_pagare ?? 0), ]) ->all(); } -} \ No newline at end of file +} diff --git a/app/Filament/Pages/Fornitore/TicketInterventoScheda.php b/app/Filament/Pages/Fornitore/TicketInterventoScheda.php index 502df40..21d1cd3 100644 --- a/app/Filament/Pages/Fornitore/TicketInterventoScheda.php +++ b/app/Filament/Pages/Fornitore/TicketInterventoScheda.php @@ -4,6 +4,7 @@ use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext; use App\Models\Fornitore; use App\Models\FornitoreDipendente; +use App\Models\Ticket; use App\Models\TicketAttachment; use App\Models\TicketIntervento; use App\Models\User; @@ -38,11 +39,14 @@ class TicketInterventoScheda extends Page public ?FornitoreDipendente $dipendente = null; - /** @var array{contatto:string,telefono:string,problema:string} */ + /** @var array{contatto:string,telefono:string,email:string,problema:string,sorgente:string,riferimento:string} */ public array $caller = [ 'contatto' => '-', 'telefono' => '', + 'email' => '', 'problema' => '', + 'sorgente' => '', + 'riferimento' => '', ]; /** @var array> */ @@ -342,6 +346,8 @@ protected function reloadIntervento(): void { $this->intervento->load([ 'ticket.stabile', + 'ticket.unitaImmobiliare', + 'ticket.soggettoRichiedente', 'ticket.messages.user', 'ticket.attachments.user', 'ticket.apertoDaUser', @@ -353,6 +359,8 @@ protected function reloadIntervento(): void $this->intervento->refresh(); $this->intervento->load([ 'ticket.stabile', + 'ticket.unitaImmobiliare', + 'ticket.soggettoRichiedente', 'ticket.messages.user', 'ticket.attachments.user', 'ticket.apertoDaUser', @@ -361,7 +369,7 @@ protected function reloadIntervento(): void 'eseguitoDaDipendente', ]); - $this->caller = $this->extractCallerData((string) ($this->intervento->ticket?->descrizione ?? '')); + $this->caller = $this->resolveCallerData($this->intervento->ticket); $this->storicoRows = TicketIntervento::query() ->with(['ticket']) ->where('fornitore_id', (int) $this->fornitore->id) @@ -401,4 +409,78 @@ protected function reloadIntervento(): void $this->articoliUtilizzati = array_pad((array) ($this->intervento->articoli_utilizzati ?? []), 3, ''); $this->qrToken = ''; } + + /** + * @return array{contatto:string,telefono:string,email:string,problema:string,sorgente:string,riferimento:string} + */ + protected function resolveCallerData(?Ticket $ticket): array + { + $parsed = $this->extractCallerData((string) ($ticket?->descrizione ?? '')); + + $caller = [ + 'contatto' => $parsed['contatto'], + 'telefono' => $parsed['telefono'], + 'email' => '', + 'problema' => $parsed['problema'] !== '' ? $parsed['problema'] : (string) ($ticket?->titolo ?? ''), + 'sorgente' => 'Descrizione ticket', + 'riferimento' => $this->buildCallerReference($ticket), + ]; + + $soggetto = $ticket?->soggettoRichiedente; + if ($soggetto) { + $label = trim((string) ($soggetto->ragione_sociale ?: trim(($soggetto->nome ?? '') . ' ' . ($soggetto->cognome ?? '')))); + if ($label !== '') { + $caller['contatto'] = $label; + } + + $caller['telefono'] = trim((string) ($soggetto->telefono ?? '')) ?: $caller['telefono']; + $caller['email'] = trim((string) ($soggetto->email ?? '')); + $caller['sorgente'] = 'Richiedente collegato al ticket'; + + return $caller; + } + + $openedBy = $ticket?->apertoDaUser; + if ($openedBy instanceof User) { + $caller['contatto'] = trim((string) ($openedBy->name ?? '')) ?: $caller['contatto']; + $caller['email'] = trim((string) ($openedBy->email ?? '')); + $caller['sorgente'] = 'Utente che ha aperto il ticket'; + } + + return $caller; + } + + protected function buildCallerReference(?Ticket $ticket): string + { + if (! $ticket instanceof Ticket) { + return ''; + } + + $parts = []; + + $stabile = $ticket->stabile; + if ($stabile) { + $denominazione = trim((string) ($stabile->denominazione ?? '')); + $indirizzo = trim(implode(' ', array_filter([ + $stabile->indirizzo ?? null, + $stabile->cap ?? null, + $stabile->citta ?? null, + ]))); + + if ($denominazione !== '') { + $parts[] = $denominazione; + } + + if ($indirizzo !== '') { + $parts[] = $indirizzo; + } + } + + $luogoIntervento = trim((string) ($ticket->luogo_intervento ?? '')); + if ($luogoIntervento !== '') { + $parts[] = 'Luogo: ' . $luogoIntervento; + } + + return implode(' · ', array_unique($parts)); + } } diff --git a/app/Filament/Pages/Gescon/Anagrafica.php b/app/Filament/Pages/Gescon/Anagrafica.php index e76da54..51e5e25 100644 --- a/app/Filament/Pages/Gescon/Anagrafica.php +++ b/app/Filament/Pages/Gescon/Anagrafica.php @@ -1,5 +1,4 @@ hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } } diff --git a/app/Filament/Pages/Gescon/FornitoriArchivio.php b/app/Filament/Pages/Gescon/FornitoriArchivio.php index 3441d5b..0bc6e69 100644 --- a/app/Filament/Pages/Gescon/FornitoriArchivio.php +++ b/app/Filament/Pages/Gescon/FornitoriArchivio.php @@ -1,6 +1,7 @@ activeTab = 'elenco'; } + public function importaTagLegacy(bool $soloSelezionato = false, bool $dryRun = false): void + { + if (! Schema::hasColumn('fornitori', 'tags')) { + Notification::make()->title('Colonna tags non presente')->warning()->send(); + return; + } + + $params = []; + if ($dryRun) { + $params['--dry-run'] = true; + } + + if ($soloSelezionato) { + $fornitoreId = (int) ($this->selectedFornitoreId ?? 0); + if ($fornitoreId <= 0) { + Notification::make()->title('Seleziona prima un fornitore')->warning()->send(); + return; + } + + $params['--fornitore-id'] = [$fornitoreId]; + } + + try { + Artisan::call(ImportLegacyFornitoriTagsCommand::class, $params); + $output = trim(Artisan::output()); + } catch (\Throwable $e) { + Notification::make()->title('Riallineamento tag legacy fallito')->body($e->getMessage())->danger()->send(); + return; + } + + if ((int) ($this->selectedFornitoreId ?? 0) > 0) { + $this->hydrateSchedaFromSelected(); + } + + $this->searchFornitoreMatches(); + + Notification::make() + ->title($dryRun ? 'Anteprima riallineamento completata' : 'Riallineamento tag legacy completato') + ->body($output !== '' ? $output : 'Operazione eseguita.') + ->success() + ->send(); + } + public function removeSearchFilter(string $token): void { $tokens = array_values(array_filter( diff --git a/app/Filament/Pages/Gescon/GesconHome.php b/app/Filament/Pages/Gescon/GesconHome.php index c5ddfc0..e40f4ec 100644 --- a/app/Filament/Pages/Gescon/GesconHome.php +++ b/app/Filament/Pages/Gescon/GesconHome.php @@ -1,5 +1,4 @@ hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } } diff --git a/app/Filament/Pages/Gescon/Ordinarie.php b/app/Filament/Pages/Gescon/Ordinarie.php index 0e9a3b4..22907ad 100644 --- a/app/Filament/Pages/Gescon/Ordinarie.php +++ b/app/Filament/Pages/Gescon/Ordinarie.php @@ -56,7 +56,7 @@ public static function canAccess(): bool $user = Auth::user(); return $user instanceof User - && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } public ?string $mastrinoTabella = null; diff --git a/app/Filament/Pages/Gescon/Riscaldamento.php b/app/Filament/Pages/Gescon/Riscaldamento.php index 340015a..5f2da5f 100644 --- a/app/Filament/Pages/Gescon/Riscaldamento.php +++ b/app/Filament/Pages/Gescon/Riscaldamento.php @@ -1,5 +1,4 @@ hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } } diff --git a/app/Filament/Pages/Gescon/RubricaUniversaleArchivio.php b/app/Filament/Pages/Gescon/RubricaUniversaleArchivio.php index 38be7b8..8055541 100644 --- a/app/Filament/Pages/Gescon/RubricaUniversaleArchivio.php +++ b/app/Filament/Pages/Gescon/RubricaUniversaleArchivio.php @@ -144,7 +144,7 @@ public function table(Table $table): Table } foreach ($tokens as $token) { - $like = '%' . mb_strtolower($token) . '%'; + $like = '%' . mb_strtolower($token) . '%'; $digits = preg_replace('/\D+/', '', $token) ?: ''; $query->where(function (Builder $q) use ($like, $digits) { $q->whereRaw("LOWER(COALESCE(ragione_sociale, '')) LIKE ?", [$like]) diff --git a/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php b/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php index 79e4817..4757d9b 100644 --- a/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php +++ b/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php @@ -91,6 +91,25 @@ class RubricaUniversaleScheda extends Page /** @var array */ public array $dipendentePbxExtension = []; + /** @var array */ + public array $studioCollaboratoreStabileIds = []; + + /** @var array */ + public array $studioCollaboratoreStabileOptions = []; + + public ?int $studioCollaboratoreUserId = null; + + public ?string $studioCollaboratoreUserEmail = null; + + public ?string $studioCollaboratoreUserName = null; + + public ?string $studioCollaboratoreGeneratedPassword = null; + + public string $studioCollaboratorePbxExtension = ''; + + /** @var array */ + public array $studioCollaboratoreRoles = []; + /** @var array */ public array $inlineForm = []; @@ -125,6 +144,17 @@ public function mount(int | string $record): void $this->adminStabileIds = $stabiliAdminIds; + $this->studioCollaboratoreStabileOptions = Stabile::query() + ->when($adminId > 0, fn(Builder $q) => $q->where('amministratore_id', $adminId)) + ->when($adminId <= 0 && ! empty($stabiliAdminIds), fn(Builder $q) => $q->whereIn('id', $stabiliAdminIds)) + ->orderBy('codice_stabile') + ->orderBy('denominazione') + ->get(['id', 'codice_stabile', 'denominazione']) + ->mapWithKeys(fn(Stabile $stabile) => [ + (int) $stabile->id => trim((string) ($stabile->codice_stabile ?: ('#' . $stabile->id)) . ' - ' . (string) ($stabile->denominazione ?: 'Stabile')), + ]) + ->all(); + $this->initArchivioNavigation(); $this->fornitori = Fornitore::query() @@ -268,6 +298,7 @@ public function mount(int | string $record): void $this->hydrateEmailMultiple(); $this->fillInlineForm(); $this->hydrateFornitoriWorkspace(); + $this->hydrateStudioCollaboratoreWorkspace(); } public function startInlineEdit(): void @@ -1751,4 +1782,155 @@ public function salvaInternoDipendente(int $dipendenteId): void $this->hydrateFornitoriWorkspace(); Notification::make()->title('Interno PBX aggiornato')->success()->send(); } + + public function abilitaAccessoCollaboratoreStudio(): void + { + $email = mb_strtolower(trim((string) ($this->rubrica->email ?? ''))); + if ($email === '') { + Notification::make() + ->title('Email rubrica mancante') + ->warning() + ->body('Per creare o collegare il collaboratore di studio serve un indirizzo email nella rubrica.') + ->send(); + return; + } + + $name = trim((string) ($this->rubrica->nome_completo ?: $this->rubrica->ragione_sociale ?: 'Collaboratore Studio')); + + $user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first(); + if (! $user) { + $tmpPwd = Str::random(12); + $user = User::query()->create([ + 'name' => $name, + 'email' => $email, + 'password' => Hash::make($tmpPwd), + 'email_verified_at' => now(), + 'is_active' => true, + 'registration_status' => 'approved', + ]); + $this->studioCollaboratoreGeneratedPassword = $tmpPwd; + } + + $user->assignRole('collaboratore'); + + $extension = trim($this->studioCollaboratorePbxExtension); + if (Schema::hasColumn('users', 'pbx_extension')) { + $user->pbx_extension = $extension !== '' ? $extension : null; + $user->save(); + } + + $this->syncStudioCollaboratoreStabili($user); + $this->hydrateStudioCollaboratoreWorkspace(); + + Notification::make() + ->title('Collaboratore di studio abilitato') + ->success() + ->body('Utente collegato alla rubrica e pronto per la gestione centralino/stabili.') + ->send(); + } + + public function salvaCollaboratoreStudio(): void + { + $userId = (int) ($this->studioCollaboratoreUserId ?? 0); + if ($userId <= 0) { + Notification::make() + ->title('Collaboratore non ancora abilitato') + ->warning() + ->body('Crea o collega prima l\'utente collaboratore da questa rubrica.') + ->send(); + return; + } + + $user = User::query()->find($userId); + if (! $user) { + Notification::make()->title('Utente collaboratore non trovato')->danger()->send(); + return; + } + + if (Schema::hasColumn('users', 'pbx_extension')) { + $extension = trim($this->studioCollaboratorePbxExtension); + $user->pbx_extension = $extension !== '' ? $extension : null; + $user->save(); + } + + $this->syncStudioCollaboratoreStabili($user); + $this->hydrateStudioCollaboratoreWorkspace(); + + Notification::make() + ->title('Collaboratore studio aggiornato') + ->success() + ->send(); + } + + private function hydrateStudioCollaboratoreWorkspace(): void + { + $this->studioCollaboratoreUserId = null; + $this->studioCollaboratoreUserEmail = trim((string) ($this->rubrica->email ?? '')) ?: null; + $this->studioCollaboratoreUserName = trim((string) ($this->rubrica->nome_completo ?: $this->rubrica->ragione_sociale ?: '')) ?: null; + $this->studioCollaboratorePbxExtension = ''; + $this->studioCollaboratoreRoles = []; + $this->studioCollaboratoreGeneratedPassword = $this->studioCollaboratoreGeneratedPassword ?: null; + + $defaultStabili = array_values(array_unique(array_filter(array_map( + fn(array $row): int => (int) ($row['id'] ?? 0), + $this->stabili + )))); + + if ($defaultStabili === [] && count($this->adminStabileIds) === 1) { + $defaultStabili = $this->adminStabileIds; + } + + $email = $this->studioCollaboratoreUserEmail; + if ($email === null) { + $this->studioCollaboratoreStabileIds = array_map('strval', $defaultStabili); + return; + } + + $user = User::query()->with('roles')->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first(); + if (! $user) { + $this->studioCollaboratoreStabileIds = array_map('strval', $defaultStabili); + return; + } + + $this->studioCollaboratoreUserId = (int) $user->id; + $this->studioCollaboratoreUserName = (string) $user->name; + $this->studioCollaboratorePbxExtension = (string) ($user->pbx_extension ?? ''); + $this->studioCollaboratoreRoles = $user->roles->pluck('name')->values()->all(); + + $assignedIds = method_exists($user, 'stabiliAssegnati') + ? $user->stabiliAssegnati()->pluck('stabili.id')->map(fn($v) => (int) $v)->all() + : []; + + $assignedForAdmin = ! empty($this->adminStabileIds) + ? array_values(array_intersect($assignedIds, $this->adminStabileIds)) + : $assignedIds; + + $this->studioCollaboratoreStabileIds = array_map('strval', $assignedForAdmin !== [] ? $assignedForAdmin : $defaultStabili); + } + + private function syncStudioCollaboratoreStabili(User $user): void + { + if (! Schema::hasTable('collaboratore_stabile') || ! method_exists($user, 'stabiliAssegnati')) { + return; + } + + $requested = []; + foreach ($this->studioCollaboratoreStabileIds as $value) { + if (is_numeric($value)) { + $requested[] = (int) $value; + } + } + + $requested = array_values(array_unique($requested)); + if (! empty($this->adminStabileIds)) { + $requested = array_values(array_intersect($requested, $this->adminStabileIds)); + } + + $currentIds = $user->stabiliAssegnati()->pluck('stabili.id')->map(fn($v) => (int) $v)->all(); + $outsideAdminIds = ! empty($this->adminStabileIds) + ? array_values(array_diff($currentIds, $this->adminStabileIds)) + : []; + + $user->stabiliAssegnati()->sync(array_values(array_unique(array_merge($outsideAdminIds, $requested)))); + } } diff --git a/app/Filament/Pages/Gescon/StabiliArchivio.php b/app/Filament/Pages/Gescon/StabiliArchivio.php index d674cae..10963d0 100644 --- a/app/Filament/Pages/Gescon/StabiliArchivio.php +++ b/app/Filament/Pages/Gescon/StabiliArchivio.php @@ -6,6 +6,7 @@ use App\Models\Stabile; use App\Models\User; use App\Services\GesconImport\EssentialImportService; +use App\Services\Stabili\StabileTransferService; use App\Support\StabileContext; use App\Filament\Pages\Condomini\CruscottoStabile; use Filament\Notifications\Notification; @@ -22,10 +23,13 @@ use Illuminate\Support\Facades\Schema as SchemaFacade; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Select; +use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; +use Illuminate\Contracts\View\View; use Illuminate\Support\Str; use App\Models\Amministratore; use App\Models\Palazzina; +use App\Models\StabileAmministratoreTransfer; use App\Models\UnitaImmobiliare; use App\Models\Persona; use App\Models\PersonaUnitaRelazione; @@ -389,10 +393,10 @@ public function table(Table $table): Table ->toggleable(), TextColumn::make('cod_stabile') - ->label('Codice operatore') + ->label('Codice mnemonico') ->searchable() ->sortable() - ->toggleable(isToggledHiddenByDefault: true), + ->toggleable(), TextColumn::make('denominazione') ->label('Denominazione') @@ -400,6 +404,25 @@ public function table(Table $table): Table ->sortable() ->wrap(), + TextColumn::make('amministratore_corrente') + ->label('Amministratore') + ->getStateUsing(function (Stabile $record): string { + $amministratore = $record->amministratore; + if (! $amministratore instanceof Amministratore) { + return '-'; + } + + $label = trim((string) ($amministratore->denominazione_studio ?: $amministratore->nome_completo)); + if ($label === '') { + $label = 'Amministratore'; + } + + return $label . ' (ID ' . (int) $amministratore->id . ')'; + }) + ->searchable(false) + ->toggleable() + ->wrap(), + TextColumn::make('indirizzo_completo') ->label('Indirizzo') ->toggleable() @@ -558,7 +581,7 @@ public function table(Table $table): Table }), Action::make('trasferisci') - ->label('Trasferisci') + ->label('Cambia amministratore') ->icon('heroicon-o-arrow-right-circle') ->visible(function (): bool { $user = Auth::user(); @@ -584,12 +607,19 @@ public function table(Table $table): Table }) ->searchable() ->required(), + Textarea::make('reason') + ->label('Motivazione') + ->helperText('Traccia il motivo del passaggio: riallineamento, presa in carico interna, correzione operativa, ecc.') + ->rows(3) + ->required() + ->maxLength(5000), Checkbox::make('also_rubrica') ->label('Aggiorna anche Rubrica collegata (se presente)') ->default(true), ]) ->requiresConfirmation() ->action(function (Stabile $record, array $data): void { + $user = Auth::user(); $toId = (int) ($data['to_amministratore_id'] ?? 0); if ($toId <= 0) { Notification::make()->title('Amministratore non valido')->danger()->send(); @@ -606,31 +636,51 @@ public function table(Table $table): Table return; } - DB::transaction(function () use ($record, $toId, $dest, $data): void { - $record->amministratore_id = $toId; - $record->save(); - - if (! empty($data['also_rubrica']) && (int) ($record->rubrica_id ?? 0) > 0) { - if (SchemaFacade::hasTable('rubrica_universale') && SchemaFacade::hasColumn('rubrica_universale', 'amministratore_id')) { - DB::table('rubrica_universale') - ->where('id', (int) $record->rubrica_id) - ->update(['amministratore_id' => $toId, 'updated_at' => now()]); - } - } - - // Assicura cartelle archivio nel nuovo tenant. - $dest->provisionArchiveIfMissing(); - $record->refresh(); - $record->provisionArchiveIfMissing(); - }); + $transfer = app(StabileTransferService::class)->transfer( + stabile: $record, + destination: $dest, + actor: $user instanceof User ? $user : null, + alsoRubrica: (bool) ($data['also_rubrica'] ?? true), + reason: trim((string) ($data['reason'] ?? '')), + source: 'portal-superadmin', + ipAddress: request()->ip(), + meta: [ + 'panel' => 'admin-filament', + 'action' => 'stabili-archivio-trasferisci', + ], + ); Notification::make() ->title('Trasferimento completato') - ->body('Nuovo amministratore: ' . ($dest->codice_univoco ?: ('ID ' . $dest->id))) + ->body('Nuovo amministratore: ' . ($dest->codice_univoco ?: ('ID ' . $dest->id)) . ' · storico #' . (int) $transfer->id) ->success() ->send(); }), + Action::make('storico_trasferimenti') + ->label('Storico passaggi') + ->icon('heroicon-o-clock') + ->visible(function (): bool { + $user = Auth::user(); + return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin']); + }) + ->modalHeading('Storico passaggi amministratore') + ->modalSubmitAction(false) + ->modalCancelActionLabel('Chiudi') + ->modalContent(function (Stabile $record): View { + $transfers = StabileAmministratoreTransfer::query() + ->with(['fromAmministratore', 'toAmministratore', 'changedByUser']) + ->where('stabile_id', (int) $record->id) + ->latest('id') + ->limit(25) + ->get(); + + return view('filament.pages.gescon.partials.stabile-transfer-history', [ + 'stabile' => $record, + 'transfers' => $transfers, + ]); + }), + Action::make('entra') ->label('Entra') ->icon('heroicon-o-arrow-right') diff --git a/app/Filament/Pages/Gescon/Utilita.php b/app/Filament/Pages/Gescon/Utilita.php index a3878ec..f915198 100644 --- a/app/Filament/Pages/Gescon/Utilita.php +++ b/app/Filament/Pages/Gescon/Utilita.php @@ -1,5 +1,4 @@ hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } } diff --git a/app/Filament/Pages/Gescon/Varie.php b/app/Filament/Pages/Gescon/Varie.php index 3670cac..71aa9f7 100644 --- a/app/Filament/Pages/Gescon/Varie.php +++ b/app/Filament/Pages/Gescon/Varie.php @@ -1,5 +1,4 @@ hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } } diff --git a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php index 6b43226..c0870e6 100644 --- a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php +++ b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php @@ -380,8 +380,8 @@ public function form(Schema $schema): Schema ->label('Tipo') ->options([ 'gmail' => 'Gmail / Google Workspace', - 'imap' => 'IMAP ordinaria', - 'pec' => 'PEC via IMAP', + 'imap' => 'IMAP ordinaria', + 'pec' => 'PEC via IMAP', ]) ->default('imap') ->required(), @@ -402,8 +402,8 @@ public function form(Schema $schema): Schema Select::make('encryption') ->label('Cifratura') ->options([ - 'ssl' => 'SSL', - 'tls' => 'TLS', + 'ssl' => 'SSL', + 'tls' => 'TLS', 'none' => 'Nessuna', ]) ->default('ssl'), @@ -442,6 +442,13 @@ public function form(Schema $schema): Schema ->maxLength(255), ]), + Section::make('Operativita posta / Google') + ->schema([ + Placeholder::make('mail_ops_panel') + ->hiddenLabel() + ->content(fn() => view('filament.pages.impostazioni.partials.scheda-amministratore-posta-operativita')), + ]), + Section::make('WhatsApp Cloud API (Meta)') ->columns(2) ->schema([ @@ -681,6 +688,13 @@ public function form(Schema $schema): Schema ->default('gdrive:NetGesconBackups/prod/data-incremental') ->maxLength(255), ]), + + Section::make('Azioni operative') + ->schema([ + Placeholder::make('ops_distribution_panel') + ->hiddenLabel() + ->content(fn() => view('filament.pages.impostazioni.partials.scheda-amministratore-distribuzione-backup')), + ]), ]), Tab::make('Operativita / Accessi') @@ -890,6 +904,16 @@ public function runGoogleDriveCheck(): void ); } + public function runGoogleOAuthReadinessCheck(): void + { + $this->runOpsProcess( + ['bash', 'scripts/ops/netgescon-google-check.sh'], + 'Controllo Google OAuth completato', + 'Controllo Google OAuth fallito', + 120, + ); + } + public function connectGoogle(): void { $google = Arr::get($this->amministratore->impostazioni ?? [], 'google', []); diff --git a/app/Filament/Pages/Strumenti/PassaggiDiConsegne.php b/app/Filament/Pages/Strumenti/PassaggiDiConsegne.php index c4e2221..455b363 100644 --- a/app/Filament/Pages/Strumenti/PassaggiDiConsegne.php +++ b/app/Filament/Pages/Strumenti/PassaggiDiConsegne.php @@ -1,5 +1,4 @@ hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } } diff --git a/app/Filament/Pages/Strumenti/PostItGestione.php b/app/Filament/Pages/Strumenti/PostItGestione.php index 2fd6f8f..4aef800 100644 --- a/app/Filament/Pages/Strumenti/PostItGestione.php +++ b/app/Filament/Pages/Strumenti/PostItGestione.php @@ -35,12 +35,23 @@ class PostItGestione extends Page public string $activeTab = 'storico'; + public string $tecnicoCallView = 'esterne'; + + public string $tecnicoDirectionFilter = 'tutte'; + + public string $tecnicoSearch = ''; + + public string $tecnicoScopeFilter = 'tutti'; + /** @var array */ public array $riaperturaNote = []; /** @var array */ private array $rubricaPhoneCache = []; + /** @var array */ + private array $pbxExtensionCache = []; + public function getPostItInserimentoUrl(): string { return PostIt::getUrl(panel: 'admin-filament'); @@ -201,12 +212,40 @@ public function getChiamateTecnicheProperty() } try { - return CommunicationMessage::query() + $query = CommunicationMessage::query() ->with(['assignedUser:id,name', 'stabile:id,denominazione']) ->where('channel', 'smdr') - ->latest('id') + ->latest('id'); + + if ($this->tecnicoCallView === 'interne') { + $query->where('direction', 'internal'); + } elseif ($this->tecnicoCallView === 'esterne') { + $query->where('direction', '!=', 'internal'); + } + + if (in_array($this->tecnicoDirectionFilter, ['inbound', 'outbound', 'internal'], true)) { + $query->where('direction', $this->tecnicoDirectionFilter); + } + + $search = trim($this->tecnicoSearch); + if ($search !== '') { + $query->where(function ($q) use ($search): void { + $q->where('phone_number', 'like', '%' . $search . '%') + ->orWhere('target_extension', 'like', '%' . $search . '%') + ->orWhere('message_text', 'like', '%' . $search . '%') + ->orWhere('sender_name', 'like', '%' . $search . '%'); + }); + } + + $items = $query ->limit(250) ->get(); + + if (in_array($this->tecnicoScopeFilter, ['studio', 'esterno', 'interno'], true)) { + $items = $items->filter(fn(CommunicationMessage $message): bool => $this->getTecnicoScope($message) === $this->tecnicoScopeFilter)->values(); + } + + return $items; } catch (QueryException) { return collect(); } @@ -302,6 +341,98 @@ public function getRubricaNomeByPhone(?string $phone): ?string return $nome !== '' ? $nome : null; } + public function getTecnicoNominativo(CommunicationMessage $message): ?string + { + $extension = $this->extractExtensionFromMessage($message); + if ($this->isInternalSmdrMessage($message)) { + return $this->getCollaboratoreNomeByExtension($extension); + } + + $phone = (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', '')); + + return $this->getRubricaNomeByPhone($phone); + } + + public function getCollaboratoreNomeByExtension(?string $extension): ?string + { + $normalized = $this->normalizeExtension($extension); + if ($normalized === null) { + return null; + } + + if (array_key_exists($normalized, $this->pbxExtensionCache)) { + return $this->pbxExtensionCache[$normalized]['name']; + } + + if (! Schema::hasColumn('users', 'pbx_extension')) { + $this->pbxExtensionCache[$normalized] = ['id' => null, 'name' => null]; + return null; + } + + $user = User::query() + ->select(['id', 'name', 'pbx_extension']) + ->where('pbx_extension', $normalized) + ->first(); + + $result = [ + 'id' => $user ? (int) $user->id : null, + 'name' => $user ? trim((string) $user->name) : null, + ]; + + $this->pbxExtensionCache[$normalized] = $result; + + return $result['name']; + } + + public function isInternalSmdrMessage(CommunicationMessage $message): bool + { + if ((string) $message->direction === 'internal') { + return true; + } + + $extension = $this->extractExtensionFromMessage($message); + $phone = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', ''))); + + return $extension !== null && $phone !== '' && $phone === $extension; + } + + public function getTecnicoInternoLabel(CommunicationMessage $message): string + { + $extension = $this->extractExtensionFromMessage($message); + if ($extension === null) { + return '-'; + } + + $name = $this->getCollaboratoreNomeByExtension($extension); + if ($name === null) { + return $extension; + } + + return $extension . ' - ' . $name; + } + + public function getTecnicoScope(CommunicationMessage $message): string + { + if ($this->isInternalSmdrMessage($message)) { + return 'interno'; + } + + if ($this->isManagedStudioNumberMessage($message)) { + return 'studio'; + } + + return 'esterno'; + } + + public function getTecnicoScopeLabel(CommunicationMessage $message): string + { + return match ($this->getTecnicoScope($message)) { + 'interno' => 'Interno collaboratore', + 'studio' => 'Numero studio gestito', + default => 'Numero esterno', + }; + } + private function resolveRubricaIdByPhone(?string $phone): ?int { $digits = preg_replace('/\D+/', '', (string) $phone); @@ -309,6 +440,10 @@ private function resolveRubricaIdByPhone(?string $phone): ?int return null; } + if ($this->isKnownPbxExtension($digits)) { + return null; + } + if (array_key_exists($digits, $this->rubricaPhoneCache)) { return $this->rubricaPhoneCache[$digits]; } @@ -338,6 +473,61 @@ private function resolveRubricaIdByPhone(?string $phone): ?int return $result; } + private function extractExtensionFromMessage(CommunicationMessage $message): ?string + { + $extension = (string) ($message->target_extension ?: data_get($message->metadata, 'smdr.extension', '')); + + return $this->normalizeExtension($extension); + } + + private function normalizeExtension(?string $extension): ?string + { + $digits = preg_replace('/\D+/', '', (string) $extension); + + return is_string($digits) && $digits !== '' ? $digits : null; + } + + private function isKnownPbxExtension(string $digits): bool + { + if (strlen($digits) > 6 || ! Schema::hasColumn('users', 'pbx_extension')) { + return false; + } + + return User::query()->where('pbx_extension', $digits)->exists(); + } + + private function isManagedStudioNumberMessage(CommunicationMessage $message): bool + { + $phoneDigits = preg_replace('/\D+/', '', (string) ($message->phone_number ?: data_get($message->metadata, 'smdr.dial_number', ''))); + if (! is_string($phoneDigits) || $phoneDigits === '') { + return false; + } + + return in_array($phoneDigits, $this->getManagedStudioNumbers(), true); + } + + /** @return array */ + private function getManagedStudioNumbers(): array + { + $user = Auth::user(); + if (! $user) { + return []; + } + + $impostazioni = (array) (($user->amministratore?->impostazioni ?? [])); + $centralino = (array) ($impostazioni['centralino'] ?? []); + + $numbers = []; + foreach (['numero_principale', 'numero_backup', 'numero_emergenza'] as $key) { + $digits = preg_replace('/\D+/', '', (string) ($centralino[$key] ?? '')); + if (is_string($digits) && $digits !== '') { + $numbers[] = $digits; + } + } + + return array_values(array_unique($numbers)); + } + public static function canAccess(): bool { $user = Auth::user(); diff --git a/app/Filament/Pages/Supporto/Modifiche.php b/app/Filament/Pages/Supporto/Modifiche.php index fbdb7e8..464102a 100644 --- a/app/Filament/Pages/Supporto/Modifiche.php +++ b/app/Filament/Pages/Supporto/Modifiche.php @@ -284,10 +284,17 @@ public function runMaintenanceViewRebuild(): void */ public function getUpdatePlannedStepsProperty(): array { + $requireDrive = $this->shouldRequireDrivePreupdateBackup(); + $driveEnabled = $this->shouldAttemptDrivePreupdateBackup(); + $steps = [ 'Verifica canale update: ' . $this->updateChannel, 'Backup pre-update automatico (snapshot differenziale + indice record)', - 'Upload backup su Google Drive obbligatorio prima dell update', + $requireDrive + ? 'Upload backup su Google Drive obbligatorio prima dell update' + : ($driveEnabled + ? 'Upload backup su Google Drive tentato se configurato sul sito corrente' + : 'Upload backup su Google Drive non obbligatorio per questo nodo'), 'Esecuzione in modalita ' . ($this->updateDryRun ? 'dry-run (nessuna scrittura)' : 'apply (aggiornamento reale)'), 'Flag force: ' . ($this->updateForce ? 'abilitato' : 'disabilitato'), 'Check endpoint update consigliato prima del lancio', @@ -444,7 +451,10 @@ private function startUpdateJob(bool $fallback): void ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $adminId = $this->resolveAmministratoreId(); - if ($adminId <= 0) { + $requireDrive = $this->shouldRequireDrivePreupdateBackup(); + $driveEnabled = $this->shouldAttemptDrivePreupdateBackup(); + + if ($requireDrive && $adminId <= 0) { $this->updateInProgress = false; $this->updateProgressStatus = 'failed'; $this->updateProgressPercent = 100; @@ -463,13 +473,19 @@ private function startUpdateJob(bool $fallback): void } $backupParams = [ - '--differential' => true, - '--drive' => true, - '--require-drive' => true, - '--admin-id' => (string) $adminId, - '--tag' => 'update-' . $jobId, + '--differential' => true, + '--tag' => 'update-' . $jobId, ]; + if ($driveEnabled && $adminId > 0) { + $backupParams['--drive'] = true; + $backupParams['--admin-id'] = (string) $adminId; + + if ($requireDrive) { + $backupParams['--require-drive'] = true; + } + } + try { $backupExit = Artisan::call('netgescon:preupdate-backup', $backupParams); $backupOut = trim((string) Artisan::output()); @@ -574,6 +590,36 @@ private function resolveAmministratoreId(): int return 0; } + private function shouldAttemptDrivePreupdateBackup(): bool + { + if (! (bool) config('distribution.preupdate_drive_enabled', false)) { + return false; + } + + $adminId = $this->resolveAmministratoreId(); + if ($adminId <= 0) { + return false; + } + + $admin = Amministratore::query()->find($adminId); + if (! $admin instanceof Amministratore) { + return false; + } + + $google = (array) data_get($admin->impostazioni ?? [], 'google', []); + $oauth = (array) data_get($google, 'oauth', []); + + $accessToken = trim((string) ($oauth['access_token'] ?? '')); + $refreshToken = trim((string) ($oauth['refresh_token'] ?? '')); + + return $accessToken !== '' || $refreshToken !== ''; + } + + private function shouldRequireDrivePreupdateBackup(): bool + { + return (bool) config('distribution.preupdate_require_drive', false); + } + private function registerPlannedUpdateSummary(): void { $summary = implode(' | ', $this->getUpdatePlannedStepsProperty()); diff --git a/app/Filament/Pages/Supporto/TicketGestione.php b/app/Filament/Pages/Supporto/TicketGestione.php index 5c7c2f5..0711ef4 100644 --- a/app/Filament/Pages/Supporto/TicketGestione.php +++ b/app/Filament/Pages/Supporto/TicketGestione.php @@ -4,6 +4,8 @@ use App\Models\CategoriaTicket; use App\Models\Fornitore; use App\Models\FornitoreDipendente; +use App\Models\InsuranceClaim; +use App\Models\InsurancePolicy; use App\Models\Ticket; use App\Models\TicketAttachment; use App\Models\TicketIntervento; @@ -48,8 +50,18 @@ class TicketGestione extends Page public ?int $fornitoreId = null; + public string $fornitoreSearch = ''; + public ?string $noteAssegnazione = null; + public ?string $insurancePolicyReference = null; + + public ?int $insurancePolicyId = null; + + public ?string $insuranceClaimNumber = null; + + public ?string $insuranceNotes = null; + /** @var array */ public array $nuoviAllegati = []; @@ -69,8 +81,8 @@ class TicketGestione extends Page /** @var array */ public array $categorieOptions = []; - /** @var array */ - public array $fornitoriOptions = []; + /** @var array> */ + public array $fornitoreMatches = []; /** @var array> */ public array $fornitoriAttiviRows = []; @@ -111,7 +123,6 @@ public function mount(): void $this->status = 'all'; } - $this->loadFornitoriOptions(); $this->loadCategorieTicketOptions(); $this->refreshFornitoriAttiviRows(); if ($this->selectedTicket) { @@ -192,11 +203,17 @@ public function updatedStatus(): void $this->refreshData(); } + public function updatedFornitoreSearch(): void + { + $this->searchFornitori(); + } + public function refreshData(): void { $this->loadCounters(); $this->loadTickets(); $this->refreshFornitoriAttiviRows(); + $this->syncSelectedTicketState(); } public function getTicketInserimentoUrl(): string @@ -211,6 +228,7 @@ public function apriScheda(int $ticketId): void $ticket = $this->selectedTicket; $this->fornitoreId = $ticket ? ((int) ($ticket->assegnato_a_fornitore_id ?? 0) ?: null): null; + $this->syncSelectedTicketState(); } public function apriElenco(): void @@ -220,7 +238,7 @@ public function apriElenco(): void public function getFornitoreOperativoUrl(?int $fornitoreId = null): string { - $base = route('fornitore.tickets.index'); + $base = \App\Filament\Pages\Fornitore\TicketOperativi::getUrl(panel: 'admin-filament'); if ($fornitoreId) { return $base . '?fornitore=' . $fornitoreId; } @@ -293,6 +311,7 @@ public function getSelectedTicketProperty(): ?Ticket 'stabile:id,denominazione', 'categoriaTicket:id,nome', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome', + 'insuranceClaim', 'attachments.user:id,name', 'messages.user:id,name', 'interventi.fornitore:id,ragione_sociale,nome,cognome', @@ -351,6 +370,31 @@ public function assegnaFornitore(): void $this->refreshData(); } + public function selezionaFornitore(int $fornitoreId): void + { + $match = collect($this->fornitoreMatches)->firstWhere('id', $fornitoreId); + + if (! is_array($match)) { + $fornitore = $this->resolveFornitoriBaseQuery()->find($fornitoreId); + if (! $fornitore instanceof Fornitore) { + return; + } + + $match = $this->mapFornitoreSearchRow($fornitore); + } + + $this->fornitoreId = (int) $match['id']; + $this->fornitoreSearch = (string) $match['nome']; + $this->fornitoreMatches = []; + } + + public function resetFornitoreSelection(): void + { + $this->fornitoreId = null; + $this->fornitoreSearch = ''; + $this->fornitoreMatches = []; + } + public function aggiungiNotaInterna(): void { $ticket = $this->selectedTicket; @@ -377,6 +421,50 @@ public function aggiungiNotaInterna(): void Notification::make()->title('Nota aggiunta')->success()->send(); } + public function salvaSinistroAssicurativo(): void + { + $ticket = $this->selectedTicket; + if (! $ticket) { + Notification::make()->title('Ticket non trovato')->danger()->send(); + return; + } + + $this->validate([ + 'insurancePolicyId' => ['nullable', 'integer', 'exists:insurance_policies,id'], + 'insurancePolicyReference' => ['nullable', 'string', 'max:255'], + 'insuranceClaimNumber' => ['nullable', 'string', 'max:255'], + 'insuranceNotes' => ['nullable', 'string', 'max:4000'], + ]); + + $claim = InsuranceClaim::query()->updateOrCreate( + ['ticket_id' => (int) $ticket->id], + [ + 'insurance_policy_id' => (int) ($this->insurancePolicyId ?? 0) ?: null, + 'stabile_id' => (int) $ticket->stabile_id, + 'policy_reference' => filled($this->insurancePolicyReference) ? trim((string) $this->insurancePolicyReference) : null, + 'claim_number' => filled($this->insuranceClaimNumber) ? trim((string) $this->insuranceClaimNumber) : null, + 'status' => 'aperta', + 'opened_at' => $ticket->insuranceClaim?->opened_at ?? now(), + 'notes' => filled($this->insuranceNotes) ? trim((string) $this->insuranceNotes) : null, + ] + ); + + $ticket->messages()->create([ + 'user_id' => Auth::id(), + 'messaggio' => 'Apertura sinistro assicurativo collegata al ticket. Riferimento sinistro: ' . ($claim->claim_number ?: 'da definire'), + 'canale' => 'assicurazione', + 'direzione' => 'outbound', + 'inviato_il' => now(), + 'metadata' => [ + 'insurance_claim_id' => (int) $claim->id, + ], + ]); + + $this->refreshData(); + + Notification::make()->title('Sinistro assicurativo aggiornato')->success()->send(); + } + public function caricaAllegati(): void { $ticket = $this->selectedTicket; @@ -754,18 +842,83 @@ private function abilitaAccessoDipendente(FornitoreDipendente $dipendente): void Notification::make()->title('Accesso fornitore abilitato')->body($body)->success()->send(); } - private function loadFornitoriOptions(): void + private function searchFornitori(): void + { + $raw = trim($this->fornitoreSearch); + + if ($raw === '') { + if ($this->fornitoreId) { + $selected = $this->resolveFornitoriBaseQuery()->find((int) $this->fornitoreId); + $this->fornitoreMatches = $selected instanceof Fornitore ? [$this->mapFornitoreSearchRow($selected)] : []; + return; + } + + $this->fornitoreMatches = []; + return; + } + + $digits = preg_replace('/\D+/', '', $raw) ?: ''; + $needleText = '%' . mb_strtolower($raw) . '%'; + $needleDigits = '%' . $digits . '%'; + $canonicalNeedles = $this->normalizeSearchTags($raw); + + $matches = $this->resolveFornitoriBaseQuery() + ->where(function ($query) use ($needleText, $needleDigits, $digits, $canonicalNeedles): void { + $query->orWhereRaw("LOWER(COALESCE(ragione_sociale, '')) LIKE ?", [$needleText]) + ->orWhereRaw("LOWER(COALESCE(nome, '')) LIKE ?", [$needleText]) + ->orWhereRaw("LOWER(COALESCE(cognome, '')) LIKE ?", [$needleText]) + ->orWhereRaw("LOWER(COALESCE(email, '')) LIKE ?", [$needleText]) + ->orWhereRaw("LOWER(COALESCE(tags, '')) LIKE ?", [$needleText]) + ->orWhereRaw("LOWER(COALESCE(note, '')) LIKE ?", [$needleText]); + + foreach ($canonicalNeedles as $canonicalNeedle) { + $query->orWhereRaw("LOWER(COALESCE(tags, '')) LIKE ?", ['%' . $canonicalNeedle . '%']); + } + + if ($digits !== '') { + $query->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needleDigits]) + ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needleDigits]); + } + }) + ->limit(60) + ->get(); + + $this->fornitoreMatches = $matches + ->map(function (Fornitore $fornitore) use ($raw, $digits, $canonicalNeedles): array { + $row = $this->mapFornitoreSearchRow($fornitore); + $row['score'] = $this->scoreFornitoreMatch($row, $raw, $digits, $canonicalNeedles); + + return $row; + }) + ->sortByDesc('score') + ->take(12) + ->values() + ->map(function (array $row): array { + unset($row['score']); + + return $row; + }) + ->all(); + + if ($this->fornitoreId && ! collect($this->fornitoreMatches)->contains('id', $this->fornitoreId)) { + $selected = $this->resolveFornitoriBaseQuery()->find((int) $this->fornitoreId); + if ($selected instanceof Fornitore) { + array_unshift($this->fornitoreMatches, $this->mapFornitoreSearchRow($selected)); + $this->fornitoreMatches = array_slice($this->fornitoreMatches, 0, 12); + } + } + } + + private function resolveFornitoriBaseQuery() { $user = Auth::user(); if (! $user instanceof User) { - $this->fornitoriOptions = []; - return; + return Fornitore::query()->whereRaw('1 = 0'); } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { - $this->fornitoriOptions = []; - return; + return Fornitore::query()->whereRaw('1 = 0'); } $stabile = \App\Models\Stabile::query()->find($stabileId); @@ -773,18 +926,134 @@ private function loadFornitoriOptions(): void $query = Fornitore::query()->orderBy('ragione_sociale')->orderBy('cognome')->orderBy('nome'); if ($adminId > 0) { - $query->where(function ($q) use ($adminId): void { - $q->where('amministratore_id', $adminId)->orWhereNull('amministratore_id'); + $query->where(function ($builder) use ($adminId): void { + $builder->where('amministratore_id', $adminId)->orWhereNull('amministratore_id'); }); } - $this->fornitoriOptions = $query->limit(400)->get()->map(function (Fornitore $f): array { - $label = trim((string) ($f->ragione_sociale ?: trim(($f->nome ?? '') . ' ' . ($f->cognome ?? '')))); - return [ - 'id' => (int) $f->id, - 'nome' => $label !== '' ? $label : ('Fornitore #' . $f->id), - ]; - })->all(); + return $query; + } + + /** + * @return array + */ + private function mapFornitoreSearchRow(Fornitore $fornitore): array + { + $tags = $this->splitFornitoreTags((string) ($fornitore->tags ?? '')); + $label = trim((string) ($fornitore->ragione_sociale ?: trim(($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? '')))); + + return [ + 'id' => (int) $fornitore->id, + 'nome' => $label !== '' ? $label : ('Fornitore #' . $fornitore->id), + 'email' => (string) ($fornitore->email ?? ''), + 'telefono' => (string) ($fornitore->telefono ?: $fornitore->cellulare), + 'tags' => $tags, + 'tags_label' => $tags !== [] ? implode(', ', $tags) : '', + 'note' => Str::limit(trim((string) ($fornitore->note ?? '')), 120), + ]; + } + + /** + * @param array $row + * @param array $canonicalNeedles + */ + private function scoreFornitoreMatch(array $row, string $raw, string $digits, array $canonicalNeedles): int + { + $score = (int) (($row['id'] ?? 0) === (int) ($this->fornitoreId ?? 0) ? 1000 : 0); + $haystack = mb_strtolower(implode(' ', array_filter([ + (string) ($row['nome'] ?? ''), + (string) ($row['email'] ?? ''), + (string) ($row['telefono'] ?? ''), + (string) ($row['tags_label'] ?? ''), + (string) ($row['note'] ?? ''), + ]))); + + if ($raw !== '' && str_contains($haystack, mb_strtolower($raw))) { + $score += 60; + } + + if ($digits !== '') { + $phoneDigits = preg_replace('/\D+/', '', (string) ($row['telefono'] ?? '')) ?: ''; + if ($phoneDigits !== '' && str_contains($phoneDigits, $digits)) { + $score += 45; + } + } + + $rowTags = array_map(fn(string $tag): string => mb_strtolower($tag), (array) ($row['tags'] ?? [])); + foreach ($canonicalNeedles as $canonicalNeedle) { + foreach ($rowTags as $rowTag) { + if ($rowTag === $canonicalNeedle) { + $score += 90; + } elseif (str_contains($rowTag, $canonicalNeedle) || str_contains($canonicalNeedle, $rowTag)) { + $score += 55; + } + } + } + + return $score; + } + + /** + * @return array + */ + private function normalizeSearchTags(string $input): array + { + $tags = []; + foreach ($this->splitFornitoreTags($input) as $tag) { + $normalized = $this->canonicalizeFornitoreTag($tag); + if ($normalized !== null) { + $tags[] = $normalized; + } + } + + return array_values(array_unique($tags)); + } + + /** + * @return array + */ + private function splitFornitoreTags(string $value): array + { + $parts = preg_split('/[,;|\n\r\/]+/', $value) ?: []; + + return array_values(array_filter(array_map(function (string $part): string { + $clean = trim($part); + $clean = preg_replace('/\s+/', ' ', $clean) ?? ''; + + return $clean; + }, $parts), fn(string $part): bool => $part !== '')); + } + + private function canonicalizeFornitoreTag(string $raw): ?string + { + $clean = trim(mb_strtolower($raw)); + if ($clean === '' || mb_strlen($clean) < 3) { + return null; + } + + $map = [ + 'idr' => 'idraulico', + 'idraul' => 'idraulico', + 'elett' => 'elettricista', + 'elettric' => 'elettricista', + 'ascens' => 'ascensorista', + 'puliz' => 'pulizie', + 'giardin' => 'giardiniere', + 'assicur' => 'assicurazione', + 'manut' => 'manutenzione', + 'spurgh' => 'spurghi', + 'fogn' => 'spurghi', + 'serr' => 'serrature', + 'cald' => 'caldaia', + ]; + + foreach ($map as $prefix => $normalized) { + if (str_starts_with($clean, $prefix)) { + return $normalized; + } + } + + return $clean; } private function loadCategorieTicketOptions(): void @@ -825,6 +1094,66 @@ public function getTipoInterventoLabel(Ticket $ticket): string return 'N/D'; } + private function syncSelectedTicketState(): void + { + $ticket = $this->selectedTicket; + + if (! $ticket) { + $this->fornitoreSearch = ''; + $this->fornitoreMatches = []; + $this->insurancePolicyId = null; + $this->insurancePolicyReference = null; + $this->insuranceClaimNumber = null; + $this->insuranceNotes = null; + return; + } + + $this->fornitoreId = (int) ($ticket->assegnato_a_fornitore_id ?? 0) ?: null; + if ($this->fornitoreId) { + $selected = $this->resolveFornitoriBaseQuery()->find((int) $this->fornitoreId); + if ($selected instanceof Fornitore) { + $this->fornitoreSearch = $this->mapFornitoreSearchRow($selected)['nome']; + $this->fornitoreMatches = [$this->mapFornitoreSearchRow($selected)]; + } + } else { + $this->fornitoreSearch = ''; + $this->fornitoreMatches = []; + } + + $this->insurancePolicyId = (int) ($ticket->insuranceClaim?->insurance_policy_id ?? 0) ?: null; + $this->insurancePolicyReference = (string) ($ticket->insuranceClaim?->policy_reference ?? ''); + $this->insuranceClaimNumber = (string) ($ticket->insuranceClaim?->claim_number ?? ''); + $this->insuranceNotes = (string) ($ticket->insuranceClaim?->notes ?? ''); + } + + /** + * @return array + */ + public function getInsurancePolicyOptionsProperty(): array + { + $ticket = $this->selectedTicket; + if (! $ticket || ! class_exists(InsurancePolicy::class)) { + return []; + } + + return InsurancePolicy::query() + ->where('stabile_id', (int) $ticket->stabile_id) + ->orderByDesc('renewal_at') + ->orderByDesc('expires_at') + ->orderBy('policy_number') + ->get() + ->mapWithKeys(function (InsurancePolicy $policy): array { + $label = trim(implode(' · ', array_filter([ + $policy->display_name, + $policy->policy_number, + $policy->expires_at?->format('d/m/Y'), + ]))); + + return [(int) $policy->id => ($label !== '' ? $label : ('Polizza #' . (int) $policy->id))]; + }) + ->all(); + } + private function loadCounters(): void { $user = Auth::user(); diff --git a/app/Filament/Pages/Supporto/TicketMobile.php b/app/Filament/Pages/Supporto/TicketMobile.php index 61acbb8..26fec62 100644 --- a/app/Filament/Pages/Supporto/TicketMobile.php +++ b/app/Filament/Pages/Supporto/TicketMobile.php @@ -353,6 +353,11 @@ public function getTicketArchivioUrl(): string return TicketGestione::getUrl(panel: 'admin-filament', parameters: ['status' => 'all']); } + public function getHelpUrl(): string + { + return TicketMobileHelp::getUrl(panel: 'admin-filament'); + } + public function getTicketDettaglioUrl(int $ticketId): string { return TicketScheda::getUrl(panel: 'admin-filament') . '?ticket=' . $ticketId; diff --git a/app/Filament/Pages/Supporto/TicketMobileHelp.php b/app/Filament/Pages/Supporto/TicketMobileHelp.php new file mode 100644 index 0000000..4a24a6c --- /dev/null +++ b/app/Filament/Pages/Supporto/TicketMobileHelp.php @@ -0,0 +1,41 @@ +hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } + + public function getBackUrl(): string + { + return TicketMobile::getUrl(panel: 'admin-filament'); + } + + public function getGestioneUrl(): string + { + return TicketGestione::getUrl(panel: 'admin-filament'); + } +} diff --git a/app/Filament/Pages/UnitaImmobiliarePage.php b/app/Filament/Pages/UnitaImmobiliarePage.php index 838499b..0566b6c 100644 --- a/app/Filament/Pages/UnitaImmobiliarePage.php +++ b/app/Filament/Pages/UnitaImmobiliarePage.php @@ -1,18 +1,16 @@ hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } protected int $stabileId = 0; @@ -178,19 +174,19 @@ public function hydrate(): void public function dehydrate(): void { - $this->unita = null; - $this->millesimiPerTabella = []; - $this->dirittiProprieta = []; - $this->relazioniPerTipo = []; - $this->nominativiStorici = []; - $this->ripartizioniPerTabella = []; - $this->preventiviPerTabella = []; - $this->totaliPerGestione = []; - $this->rateEmessePerCategoria = []; - $this->estrattoCompattoRateRows = []; - $this->estrattoCompattoIncassi = []; + $this->unita = null; + $this->millesimiPerTabella = []; + $this->dirittiProprieta = []; + $this->relazioniPerTipo = []; + $this->nominativiStorici = []; + $this->ripartizioniPerTabella = []; + $this->preventiviPerTabella = []; + $this->totaliPerGestione = []; + $this->rateEmessePerCategoria = []; + $this->estrattoCompattoRateRows = []; + $this->estrattoCompattoIncassi = []; $this->estrattoConguagliIniziali = []; - $this->legacyCondominRow = null; + $this->legacyCondominRow = null; } public function unitaPicker(Schema $schema): Schema @@ -202,13 +198,13 @@ public function unitaPicker(Schema $schema): Schema ->label('Unità') ->hiddenLabel() ->placeholder('Cerca unità…') - ->options(fn(): array => $this->getUnitaOptions()) + ->options(fn(): array=> $this->getUnitaOptions()) ->searchable() ->preload() ->native(false) ->live() ->afterStateUpdated(function ($state): void { - $id = is_numeric($state) ? (int) $state : null; + $id = is_numeric($state) ? (int) $state : null; $this->unitaId = $this->pickUnitaId($id); if (! $this->unitaId) { return; @@ -305,11 +301,11 @@ protected function getHeaderActions(): array protected function getRuoliLabels(): array { return [ - 'condomino' => 'Condomino', - 'inquilino' => 'Inquilino', + 'condomino' => 'Condomino', + 'inquilino' => 'Inquilino', 'proprietario' => 'Proprietario', - 'fornitore' => 'Fornitore', - 'altro' => 'Altro', + 'fornitore' => 'Fornitore', + 'altro' => 'Altro', ]; } @@ -334,7 +330,7 @@ protected function getPrevUnitaId(): ?int return null; } - $ids = array_map('intval', array_keys($this->unitaOptionsCache)); + $ids = array_map('intval', array_keys($this->unitaOptionsCache)); $index = array_search((int) $this->unitaId, $ids, true); if ($index === false || $index <= 0) { return null; @@ -349,7 +345,7 @@ protected function getNextUnitaId(): ?int return null; } - $ids = array_map('intval', array_keys($this->unitaOptionsCache)); + $ids = array_map('intval', array_keys($this->unitaOptionsCache)); $index = array_search((int) $this->unitaId, $ids, true); if ($index === false) { return null; @@ -392,10 +388,10 @@ protected function refreshUnitaOptions(): void $unita = $unita->filter(function (UnitaImmobiliare $u): bool { $denominazione = trim((string) ($u->denominazione ?? '')); - $scala = trim((string) ($u->scala ?? '')); - $interno = trim((string) ($u->interno ?? '')); - $piano = $u->piano; - $codice = trim((string) ($u->codice_unita ?? '')); + $scala = trim((string) ($u->scala ?? '')); + $interno = trim((string) ($u->interno ?? '')); + $piano = $u->piano; + $codice = trim((string) ($u->codice_unita ?? '')); $isEmptyCore = $denominazione === '' && $scala === '' @@ -495,8 +491,8 @@ protected function refreshUnitaOptions(): void $this->unitaOptionsCache = $sorted ->mapWithKeys(function (UnitaImmobiliare $u) { $codice = $u->codice_unita ?: ('ID ' . $u->id); - $owner = $this->ownerByUnitaLocal[(int) $u->id] ?? null; - $label = trim($codice); + $owner = $this->ownerByUnitaLocal[(int) $u->id] ?? null; + $label = trim($codice); if (is_string($owner) && trim($owner) !== '') { $label .= ' — ' . trim($owner); } @@ -532,7 +528,7 @@ public function setEstrattoTipo(string $tipo): void */ protected function internoSortKey(?string $interno): array { - $s = trim((string)($interno ?? '')); + $s = trim((string) ($interno ?? '')); if ($s === '' || $s === '—') { return ['~', PHP_INT_MAX, '']; } @@ -540,10 +536,10 @@ protected function internoSortKey(?string $interno): array $m = null; if (preg_match('/^([A-Z]+)\s*[-\/]\s*(\d+)(.*)$/', $u, $m)) { - return [$m[1], (int)$m[2], trim((string)($m[3] ?? ''))]; + return [$m[1], (int) $m[2], trim((string) ($m[3] ?? ''))]; } if (preg_match('/^(\d+)(.*)$/', $u, $m)) { - return ['', (int)$m[1], trim((string)($m[2] ?? ''))]; + return ['', (int) $m[1], trim((string) ($m[2] ?? ''))]; } return [$u, PHP_INT_MAX, $u]; @@ -565,14 +561,14 @@ protected function pickUnitaId(?int $candidate): ?int protected function loadUnita(): void { - $this->unita = null; - $this->millesimiPerTabella = []; - $this->dirittiProprieta = []; - $this->relazioniPerTipo = []; - $this->nominativiStorici = []; + $this->unita = null; + $this->millesimiPerTabella = []; + $this->dirittiProprieta = []; + $this->relazioniPerTipo = []; + $this->nominativiStorici = []; $this->ripartizioniPerTabella = []; - $this->preventiviPerTabella = []; - $this->totaliPerGestione = []; + $this->preventiviPerTabella = []; + $this->totaliPerGestione = []; $this->rateEmessePerCategoria = []; if (! $this->unitaId) { @@ -617,11 +613,11 @@ public function hydrateNominativiStorici(): void return; } - $rows = $this->unita->nominativiStorici() - ->orderBy('data_inizio') - ->orderByDesc('ruolo') - ->orderByDesc('id') - ->get(); + $rows = $this->unita->nominativiStorici() + ->orderBy('data_inizio') + ->orderByDesc('ruolo') + ->orderByDesc('id') + ->get(); if ($rows->isEmpty()) { return; @@ -629,13 +625,13 @@ public function hydrateNominativiStorici(): void $this->nominativiStorici = $rows->map(function ($row): array { $rawInizio = method_exists($row, 'getRawOriginal') ? $row->getRawOriginal('data_inizio') : null; - $rawFine = method_exists($row, 'getRawOriginal') ? $row->getRawOriginal('data_fine') : null; + $rawFine = method_exists($row, 'getRawOriginal') ? $row->getRawOriginal('data_fine') : null; $inizioVal = $this->normalizeLegacyDateValue($rawInizio ?? $row->data_inizio ?? null); - $fineVal = $this->normalizeLegacyDateValue($rawFine ?? $row->data_fine ?? null); + $fineVal = $this->normalizeLegacyDateValue($rawFine ?? $row->data_fine ?? null); - $inizio = $inizioVal ? Carbon::parse($inizioVal)->format('d/m/Y') : null; - $fine = $fineVal ? Carbon::parse($fineVal)->format('d/m/Y') : null; + $inizio = $inizioVal ? Carbon::parse($inizioVal)->format('d/m/Y') : null; + $fine = $fineVal ? Carbon::parse($fineVal)->format('d/m/Y') : null; $periodo = null; if ($inizio && $fine) { $periodo = 'Da ' . $inizio . ' · fino a ' . $fine; @@ -653,11 +649,11 @@ public function hydrateNominativiStorici(): void } return [ - 'ruolo' => $row->ruolo ?: '—', - 'nominativo' => $row->nominativo ?: '—', - 'periodo' => $periodo, + 'ruolo' => $row->ruolo ?: '—', + 'nominativo' => $row->nominativo ?: '—', + 'periodo' => $periodo, 'percentuale' => $percentuale, - 'fonte' => $row->fonte ?: null, + 'fonte' => $row->fonte ?: null, ]; })->all(); } @@ -709,7 +705,7 @@ protected function hydrateRateEmesse(): void ->map(function ($items, $soggettoId) use ($ownerLookup): array { $first = $items->first(); - $s = $first?->soggettoResponsabile; + $s = $first?->soggettoResponsabile; $nome = '—'; if ($s) { $nome = trim((string) ($s->ragione_sociale ?? '')); @@ -722,14 +718,14 @@ protected function hydrateRateEmesse(): void } $totAddebitato = (float) $items->sum(fn(RataEmessaNg $r) => (float) $r->importo_addebitato_soggetto); - $totPagato = (float) $items->sum(fn(RataEmessaNg $r) => (float) $r->importo_pagato); - $residuo = $totAddebitato - $totPagato; + $totPagato = (float) $items->sum(fn(RataEmessaNg $r) => (float) $r->importo_pagato); + $residuo = $totAddebitato - $totPagato; $scadute = $items->filter(function (RataEmessaNg $r): bool { try { return strtoupper((string) $r->stato_rata) === 'EMESSA' - && $r->data_scadenza - && $r->data_scadenza->isPast(); + && $r->data_scadenza + && $r->data_scadenza->isPast(); } catch (\Throwable $e) { return false; } @@ -738,22 +734,22 @@ protected function hydrateRateEmesse(): void $categoria = isset($ownerLookup[(int) $soggettoId]) ? 'condomini' : 'inquilini'; return [ - 'categoria' => $categoria, - 'soggetto_id' => is_numeric($soggettoId) ? (int) $soggettoId : null, - 'nome' => $nome, - 'codice_fiscale' => $s?->codice_fiscale, - 'numero_rate' => $items->count(), - 'scadute' => $scadute, + 'categoria' => $categoria, + 'soggetto_id' => is_numeric($soggettoId) ? (int) $soggettoId : null, + 'nome' => $nome, + 'codice_fiscale' => $s?->codice_fiscale, + 'numero_rate' => $items->count(), + 'scadute' => $scadute, 'totale_addebitato' => $totAddebitato, - 'totale_pagato' => $totPagato, - 'residuo' => $residuo, - 'ultimo_piano' => $first?->pianoRateizzazione?->descrizione, + 'totale_pagato' => $totPagato, + 'residuo' => $residuo, + 'ultimo_piano' => $first?->pianoRateizzazione?->descrizione, ]; }) ->values(); foreach ($groups as $g) { - $cat = $g['categoria'] ?? 'inquilini'; + $cat = $g['categoria'] ?? 'inquilini'; $this->rateEmessePerCategoria[$cat][] = $g; } @@ -778,7 +774,7 @@ protected function hydrateRateEmesse(): void protected function hydrateEstrattoCompatto(): void { $this->estrattoCompattoRateRows = []; - $this->estrattoCompattoIncassi = []; + $this->estrattoCompattoIncassi = []; if (! DbSchema::hasTable('rate_emesse')) { return; @@ -804,21 +800,21 @@ protected function hydrateEstrattoCompatto(): void $tipo = (string) ($m[1] ?? null); } - $dovuto = (float) ($r->importo_addebitato_soggetto ?? 0); - $pagato = (float) ($r->importo_pagato ?? 0); + $dovuto = (float) ($r->importo_addebitato_soggetto ?? 0); + $pagato = (float) ($r->importo_pagato ?? 0); $residuo = $dovuto - $pagato; return [ - 'id' => (int) $r->id, + 'id' => (int) $r->id, 'data_emissione' => $r->data_emissione?->format('d/m/Y'), - 'data_scadenza' => $r->data_scadenza?->format('d/m/Y'), + 'data_scadenza' => $r->data_scadenza?->format('d/m/Y'), 'data_pagamento' => $r->data_ultimo_pagamento?->format('d/m/Y'), - 'descrizione' => (string) ($r->descrizione ?? ''), - 'avviso' => (int) ($r->numero_rata_progressivo ?? 0), - 'tipo' => $tipo, - 'dovuto' => $dovuto, - 'pagato' => $pagato, - 'residuo' => $residuo, + 'descrizione' => (string) ($r->descrizione ?? ''), + 'avviso' => (int) ($r->numero_rata_progressivo ?? 0), + 'tipo' => $tipo, + 'dovuto' => $dovuto, + 'pagato' => $pagato, + 'residuo' => $residuo, ]; })->all(); } @@ -875,12 +871,12 @@ protected function hydrateConguagliIniziali(): void } $legacyYear = trim((string) ($r->legacy_year ?? '')); - $label = $this->resolveLegacyYearLabel($codStabile, $legacyYear); - $year = $this->extractYearFromLabel($label); - $data = $year ? ('31/12/' . $year) : null; + $label = $this->resolveLegacyYearLabel($codStabile, $legacyYear); + $year = $this->extractYearFromLabel($label); + $data = $year ? ('31/12/' . $year) : null; $codTab = trim((string) ($r->cod_tab ?? '')); - $tipo = 'Straordinaria'; + $tipo = 'Straordinaria'; if (str_starts_with($codTab, 'CONG.O')) { $tipo = 'Ordinaria'; } elseif (str_starts_with($codTab, 'CONG.R')) { @@ -893,13 +889,13 @@ protected function hydrateConguagliIniziali(): void $descr = 'Conguaglio iniziale ' . $tipo; $this->estrattoConguagliIniziali[] = [ - 'legacy_year' => $legacyYear, + 'legacy_year' => $legacyYear, 'gestione_label' => $label, - 'data' => $data, - 'cod_tab' => $codTab, - 'tipo' => $tipo, - 'importo' => $importo, - 'descrizione' => $descr, + 'data' => $data, + 'cod_tab' => $codTab, + 'tipo' => $tipo, + 'importo' => $importo, + 'descrizione' => $descr, ]; } } @@ -964,7 +960,7 @@ private function loadIncassiForUnita($rateItems): array continue; } $tipo = ''; - $cod = ''; + $cod = ''; if (preg_match('/\btipo_soggetto=([CI])\b/', $note, $mTipo)) { $tipo = (string) ($mTipo[1] ?? ''); @@ -1007,7 +1003,7 @@ private function loadIncassiForUnita($rateItems): array } }); - $user = Auth::user(); + $user = Auth::user(); $annoGestione = $user instanceof User ? AnnoGestioneContext::resolveActiveAnno($user) : null; if ($annoGestione) { $annoCol = DbSchema::hasColumn('incassi', 'anno_rif') @@ -1042,7 +1038,7 @@ private function loadIncassiForUnita($rateItems): array return $incassi->map(function (Incasso $i) use ($codCol, $tipoCol): array { $importoRaw = $i->importo_pagato_euro ?? $i->importo_pagato ?? $i->importo_euro ?? $i->importo ?? 0; - $importo = $this->normalizeLegacyIncassoImporto(is_numeric($importoRaw) ? (float) $importoRaw : 0.0); + $importo = $this->normalizeLegacyIncassoImporto(is_numeric($importoRaw) ? (float) $importoRaw : 0.0); $dt = null; if (! empty($i->dt_empag)) { @@ -1060,13 +1056,13 @@ private function loadIncassiForUnita($rateItems): array } return [ - 'id' => (int) $i->id, - 'data' => $dt, - 'tipo' => (string) (data_get($i, $tipoCol) ?? ''), - 'cod_cond' => (string) (data_get($i, $codCol) ?? ''), - 'importo' => $importo, + 'id' => (int) $i->id, + 'data' => $dt, + 'tipo' => (string) (data_get($i, $tipoCol) ?? ''), + 'cod_cond' => (string) (data_get($i, $codCol) ?? ''), + 'importo' => $importo, 'descrizione' => (string) ($i->descrizione ?? ''), - 'n_ricevuta' => $i->n_ricevuta ?? $i->n_riferimento ?? null, + 'n_ricevuta' => $i->n_ricevuta ?? $i->n_riferimento ?? null, ]; })->all(); } @@ -1091,22 +1087,22 @@ protected function hydrateMillesimi(): void $this->millesimiPerTabella = $this->unita->dettagliMillesimi ->sortBy(function ($d) { $tabella = $d->tabellaMillesimale; - $tipo = (string) ($tabella?->tipo_tabella ?? ($tabella?->tipo ?? '')); - $ord = (int) ($d->nord ?? $tabella?->nord ?? $tabella?->ordinamento ?? 999999); + $tipo = (string) ($tabella?->tipo_tabella ?? ($tabella?->tipo ?? '')); + $ord = (int) ($d->nord ?? $tabella?->nord ?? $tabella?->ordinamento ?? 999999); return sprintf('%s|%09d', $tipo, $ord); }) ->map(function ($d) { $tabella = $d->tabellaMillesimale; - $tipo = (string) ($tabella?->tipo_tabella ?? ($tabella?->tipo ?? '')); + $tipo = (string) ($tabella?->tipo_tabella ?? ($tabella?->tipo ?? '')); if ($tipo === '') { $tipo = 'altro'; } return [ - 'tipo' => $tipo, - 'gruppo' => $this->mapTipoToGruppo($tipo), - 'id' => $tabella?->id, - 'codice' => $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? 'TAB', - 'nome' => $tabella?->denominazione ?? $tabella?->nome_tabella_millesimale ?? 'Tabella millesimale', + 'tipo' => $tipo, + 'gruppo' => $this->mapTipoToGruppo($tipo), + 'id' => $tabella?->id, + 'codice' => $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? 'TAB', + 'nome' => $tabella?->denominazione ?? $tabella?->nome_tabella_millesimale ?? 'Tabella millesimale', 'millesimi' => (float) ($d->millesimi ?? 0), ]; }) @@ -1159,7 +1155,7 @@ protected function hydrateDiritti(): void ->get() ->map(function (Proprieta $diritto) use ($formatDate) { $soggetto = $diritto->soggetto; - $nome = $soggetto?->ragione_sociale + $nome = $soggetto?->ragione_sociale ?: trim(($soggetto?->cognome ?? '') . ' ' . ($soggetto?->nome ?? '')); if ($nome === '') { @@ -1176,17 +1172,17 @@ protected function hydrateDiritti(): void } return [ - 'id' => $diritto->id, - 'soggetto_id' => $soggetto?->id, - 'nome' => $nome, - 'codice_fiscale' => $codiceFiscale, - 'tipo' => $diritto->tipo_diritto, - 'percentuale' => $percentuale, + 'id' => $diritto->id, + 'soggetto_id' => $soggetto?->id, + 'nome' => $nome, + 'codice_fiscale' => $codiceFiscale, + 'tipo' => $diritto->tipo_diritto, + 'percentuale' => $percentuale, 'percentuale_label' => $percentuale !== null ? number_format($percentuale, 2, ',', '.') : null, - 'data_inizio' => $formatDate($diritto->data_inizio), - 'data_fine' => $formatDate($diritto->data_fine), + 'data_inizio' => $formatDate($diritto->data_inizio), + 'data_fine' => $formatDate($diritto->data_fine), ]; }) ->filter() @@ -1212,61 +1208,61 @@ protected function hydrateRelazioni(): void 'condomino' => 'Condomino', 'inquilino' => 'Inquilino', 'fornitore' => 'Fornitore', - 'altro' => 'Altro', + 'altro' => 'Altro', ]; $labelsRuoli = [ 'condomino' => 'Condomino', 'inquilino' => 'Inquilino', 'fornitore' => 'Fornitore', - 'altro' => 'Altro', + 'altro' => 'Altro', ]; $tipoDirittoLabels = [ 'piena_proprieta' => 'Condomino (piena_proprietà)', - 'proprieta' => 'Condomino (proprietà)', - 'comproprieta' => 'Condomino (comproprietà)', - 'nuda_proprieta' => 'Condomino (nuda proprietà)', - 'usufrutto' => 'Condomino (usufrutto)', - 'usufruttuario' => 'Condomino (usufrutto)', + 'proprieta' => 'Condomino (proprietà)', + 'comproprieta' => 'Condomino (comproprietà)', + 'nuda_proprieta' => 'Condomino (nuda proprietà)', + 'usufrutto' => 'Condomino (usufrutto)', + 'usufruttuario' => 'Condomino (usufrutto)', ]; - $legacyRow = $this->getLegacyCondominRow(); + $legacyRow = $this->getLegacyCondominRow(); $legacyOwnerStart = $legacyRow?->subentrato_dal ?? null; - $legacyOwnerEnd = $legacyRow?->attivo_fino_al ?? null; - $legacyInqStart = $legacyRow?->inquil_dal ?? null; - $legacyInqEnd = $legacyRow?->inquil_al ?? null; - $legacyOwnerName = trim((string) ($legacyRow?->nom_cond ?? '')); - $legacyInqName = trim((string) ($legacyRow?->inquil_nome ?? '')); + $legacyOwnerEnd = $legacyRow?->attivo_fino_al ?? null; + $legacyInqStart = $legacyRow?->inquil_dal ?? null; + $legacyInqEnd = $legacyRow?->inquil_al ?? null; + $legacyOwnerName = trim((string) ($legacyRow?->nom_cond ?? '')); + $legacyInqName = trim((string) ($legacyRow?->inquil_nome ?? '')); $proprietari = collect($this->dirittiProprieta) ->filter(fn(array $d) => ! empty($d['soggetto_id'])) ->map(function (array $d) use ($tipoDirittoLabels): array { - $tipoRaw = strtolower(trim((string) ($d['tipo'] ?? 'condomino'))); + $tipoRaw = strtolower(trim((string) ($d['tipo'] ?? 'condomino'))); $tipoLabel = $tipoDirittoLabels[$tipoRaw] ?? 'Condomino'; $quota = $d['percentuale']; $quota = is_numeric($quota) ? (float) $quota : null; return [ - 'id' => (int) ($d['id'] ?? 0), - 'soggetto_id' => (int) ($d['soggetto_id'] ?? 0), - 'nome' => (string) ($d['nome'] ?? '—'), + 'id' => (int) ($d['id'] ?? 0), + 'soggetto_id' => (int) ($d['soggetto_id'] ?? 0), + 'nome' => (string) ($d['nome'] ?? '—'), 'codice_fiscale' => $d['codice_fiscale'] ?? null, - 'tipo_raw' => 'condomino', - 'tipo' => $tipoLabel, - 'ruolo_rate' => 'C', - 'quota' => $quota, - 'quota_label' => $quota !== null ? number_format($quota, 2, ',', '.') : null, - 'data_inizio' => $d['data_inizio'] ?? null, - 'data_fine' => $d['data_fine'] ?? null, - 'attivo' => true, + 'tipo_raw' => 'condomino', + 'tipo' => $tipoLabel, + 'ruolo_rate' => 'C', + 'quota' => $quota, + 'quota_label' => $quota !== null ? number_format($quota, 2, ',', '.') : null, + 'data_inizio' => $d['data_inizio'] ?? null, + 'data_fine' => $d['data_fine'] ?? null, + 'attivo' => true, ]; }) ->unique(fn(array $d) => (int) ($d['soggetto_id'] ?? 0)) ->values(); - $today = now()->toDateString(); + $today = now()->toDateString(); $relazioniMapped = RubricaRuolo::query() ->with(['contatto']) ->where('stabile_id', $this->stabileId) @@ -1286,12 +1282,12 @@ protected function hydrateRelazioni(): void ->filter(fn(RubricaRuolo $relazione) => (bool) $relazione->contatto) ->map(function (RubricaRuolo $relazione) use ($formatDate, $labelsRuoli) { $contatto = $relazione->contatto; - $nome = trim((string) ($contatto?->nome_completo ?? '')); + $nome = trim((string) ($contatto?->nome_completo ?? '')); if ($nome === '') { $nome = $contatto ? 'Contatto #' . $contatto->id : 'Contatto non definito'; } - $tipoRaw = strtolower(trim((string) ($relazione->ruolo_standard ?? ''))); + $tipoRaw = strtolower(trim((string) ($relazione->ruolo_standard ?? ''))); $tipoLabel = $labelsRuoli[$tipoRaw] ?? ($tipoRaw !== '' ? $tipoRaw : 'Altro'); if (! empty($relazione->ruolo_custom)) { $tipoLabel .= ' (' . (string) $relazione->ruolo_custom . ')'; @@ -1315,21 +1311,21 @@ protected function hydrateRelazioni(): void } return [ - 'id' => (int) $relazione->id, - 'persona_id' => (int) ($relazione->rubrica_id ?? 0), - 'nome' => $nome, - 'codice_fiscale' => $contatto?->codice_fiscale, - 'tipo_raw' => $tipoRaw, - 'tipo' => $tipoLabel, - '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, + 'id' => (int) $relazione->id, + 'persona_id' => (int) ($relazione->rubrica_id ?? 0), + 'nome' => $nome, + 'codice_fiscale' => $contatto?->codice_fiscale, + 'tipo_raw' => $tipoRaw, + 'tipo' => $tipoLabel, + '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, 'riceve_comunicazioni' => false, - 'riceve_convocazioni' => false, - 'vota_assemblea' => false, + 'riceve_convocazioni' => false, + 'vota_assemblea' => false, ]; }); @@ -1403,13 +1399,13 @@ protected function hydrateRelazioni(): void ->values(); } - $legacyOwnerStartC = $this->parseLegacyDateToCarbon($legacyOwnerStart); - $legacyOwnerEndC = $this->parseLegacyDateToCarbon($legacyOwnerEnd); - $legacyOwnerNewStartC = $legacyOwnerEndC ? $legacyOwnerEndC->copy()->addDay() : $legacyOwnerStartC; + $legacyOwnerStartC = $this->parseLegacyDateToCarbon($legacyOwnerStart); + $legacyOwnerEndC = $this->parseLegacyDateToCarbon($legacyOwnerEnd); + $legacyOwnerNewStartC = $legacyOwnerEndC ? $legacyOwnerEndC->copy()->addDay() : $legacyOwnerStartC; if ($legacyOwnerStartC || $legacyOwnerEndC) { $proprietari = $proprietari->values(); - $count = $proprietari->count(); + $count = $proprietari->count(); if ($count === 1) { $proprietari = $proprietari->map(function (array $p) use ($legacyOwnerStartC, $legacyOwnerEndC): array { if (empty($p['data_inizio'])) { @@ -1431,7 +1427,7 @@ protected function hydrateRelazioni(): void } } } - $idxOld = $idxNew === 0 ? ($count - 1) : 0; + $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'); @@ -1445,10 +1441,10 @@ protected function hydrateRelazioni(): void } $legacyInqStartC = $this->parseLegacyDateToCarbon($legacyInqStart); - $legacyInqEndC = $this->parseLegacyDateToCarbon($legacyInqEnd); + $legacyInqEndC = $this->parseLegacyDateToCarbon($legacyInqEnd); if ($legacyInqStartC || $legacyInqEndC) { $inquilini = $inquilini->values(); - $count = $inquilini->count(); + $count = $inquilini->count(); if ($count === 1) { $inquilini = $inquilini->map(function (array $i) use ($legacyInqStartC, $legacyInqEndC): array { if (empty($i['data_inizio'])) { @@ -1470,7 +1466,7 @@ protected function hydrateRelazioni(): void } } } - $idxOld = $idxNew === 0 ? ($count - 1) : 0; + $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'); @@ -1485,8 +1481,8 @@ protected function hydrateRelazioni(): void $this->relazioniPerTipo = [ 'proprietari' => $proprietari->values()->all(), - 'inquilini' => $inquilini->values()->all(), - 'altri' => $altri->values()->all(), + 'inquilini' => $inquilini->values()->all(), + 'altri' => $altri->values()->all(), ]; } @@ -1509,7 +1505,7 @@ private function getLegacyCondominRow(): ?object return $this->legacyCondominRow = null; } - $scala = trim((string) ($this->unita->scala ?? '')); + $scala = trim((string) ($this->unita->scala ?? '')); $interno = trim((string) ($this->unita->interno ?? '')); if ($interno === '') { @@ -1543,8 +1539,8 @@ private function getLegacyCondominRow(): ?object $row->subentrato_dal = $this->normalizeLegacyDateValue($row->subentrato_dal ?? null); $row->attivo_fino_al = $this->normalizeLegacyDateValue($row->attivo_fino_al ?? null); - $row->inquil_dal = $this->normalizeLegacyDateValue($row->inquil_dal ?? null); - $row->inquil_al = $this->normalizeLegacyDateValue($row->inquil_al ?? null); + $row->inquil_dal = $this->normalizeLegacyDateValue($row->inquil_dal ?? null); + $row->inquil_al = $this->normalizeLegacyDateValue($row->inquil_al ?? null); return $this->legacyCondominRow = $row; } @@ -1594,7 +1590,7 @@ private function parseLegacyDateToCarbon(mixed $value): ?Carbon protected function hydrateRipartizioni(): void { - if (!DbSchema::hasTable('dettaglio_ripartizione_spese')) { + if (! DbSchema::hasTable('dettaglio_ripartizione_spese')) { $this->ripartizioniPerTabella = $this->fallbackVociSpesa(); return; } @@ -1609,38 +1605,38 @@ protected function hydrateRipartizioni(): void ->get(); $grouped = $details->map(function (DettaglioRipartizioneSpese $d) { - $rip = $d->ripartizioneSpese; + $rip = $d->ripartizioneSpese; $tabella = $rip?->tabellaMillesimale; - $voce = $rip?->voceSpesa; + $voce = $rip?->voceSpesa; return [ - 'tabella_id' => $tabella?->id, - 'tabella_codice' => $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? 'TAB', - 'tabella_nome' => $tabella?->denominazione ?? $tabella?->nome_tabella_millesimale ?? 'Tabella millesimale', - 'voce_codice' => $voce?->codice, - 'voce_descrizione' => $voce?->descrizione, - 'tipo_gestione' => $voce?->tipo_gestione, - 'calcolo' => $rip?->tipo_ripartizione, + 'tabella_id' => $tabella?->id, + 'tabella_codice' => $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? 'TAB', + 'tabella_nome' => $tabella?->denominazione ?? $tabella?->nome_tabella_millesimale ?? 'Tabella millesimale', + 'voce_codice' => $voce?->codice, + 'voce_descrizione' => $voce?->descrizione, + 'tipo_gestione' => $voce?->tipo_gestione, + 'calcolo' => $rip?->tipo_ripartizione, 'percentuale_applicata' => (float) ($d->percentuale_applicata ?? 0), 'percentuale_inquilino' => (float) ($d->percentuale_inquilino ?? 0), - 'quota_finale' => (float) ($d->quota_finale ?? 0), - 'importo_proprietario' => (float) ($d->importo_proprietario ?? 0), - 'importo_inquilino' => (float) ($d->importo_inquilino ?? 0), + 'quota_finale' => (float) ($d->quota_finale ?? 0), + 'importo_proprietario' => (float) ($d->importo_proprietario ?? 0), + 'importo_inquilino' => (float) ($d->importo_inquilino ?? 0), ]; })->groupBy('tabella_id'); $this->ripartizioniPerTabella = $grouped->map(function ($items) { return [ 'tabella_codice' => $items->first()['tabella_codice'] ?? 'TAB', - 'tabella_nome' => $items->first()['tabella_nome'] ?? 'Tabella millesimale', - 'righe' => $items->values()->all(), + 'tabella_nome' => $items->first()['tabella_nome'] ?? 'Tabella millesimale', + 'righe' => $items->values()->all(), ]; })->values()->all(); } protected function fallbackVociSpesa(): array { - if (!DbSchema::hasTable('voci_spesa')) { + if (! DbSchema::hasTable('voci_spesa')) { return []; } @@ -1653,26 +1649,26 @@ protected function fallbackVociSpesa(): array $grouped = $voci->map(function (VoceSpesa $voce) { $tabella = $voce->tabellaMillesimaleDefault; return [ - 'tabella_id' => $tabella?->id, - 'tabella_codice' => $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? 'TAB', - 'tabella_nome' => $tabella?->denominazione ?? $tabella?->nome_tabella_millesimale ?? 'Tabella millesimale', - 'voce_codice' => $voce->codice, - 'voce_descrizione' => $voce->descrizione, - 'tipo_gestione' => $voce->tipo_gestione, - 'calcolo' => $voce->categoria, + 'tabella_id' => $tabella?->id, + 'tabella_codice' => $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? 'TAB', + 'tabella_nome' => $tabella?->denominazione ?? $tabella?->nome_tabella_millesimale ?? 'Tabella millesimale', + 'voce_codice' => $voce->codice, + 'voce_descrizione' => $voce->descrizione, + 'tipo_gestione' => $voce->tipo_gestione, + 'calcolo' => $voce->categoria, 'percentuale_applicata' => 0.0, 'percentuale_inquilino' => 0.0, - 'quota_finale' => 0.0, - 'importo_proprietario' => 0.0, - 'importo_inquilino' => 0.0, + 'quota_finale' => 0.0, + 'importo_proprietario' => 0.0, + 'importo_inquilino' => 0.0, ]; })->groupBy('tabella_id'); return $grouped->map(function ($items) { return [ 'tabella_codice' => $items->first()['tabella_codice'] ?? 'TAB', - 'tabella_nome' => $items->first()['tabella_nome'] ?? 'Tabella millesimale', - 'righe' => $items->values()->all(), + 'tabella_nome' => $items->first()['tabella_nome'] ?? 'Tabella millesimale', + 'righe' => $items->values()->all(), ]; })->values()->all(); } @@ -1680,7 +1676,7 @@ protected function fallbackVociSpesa(): array protected function hydratePreventivi(): void { $this->preventiviPerTabella = []; - $this->totaliPerGestione = []; + $this->totaliPerGestione = []; // Preferenza: usa le voci dal dominio (pagina "Voci di spesa") così le modifiche importi/% // si riflettono direttamente nella scheda Unità immobiliare. @@ -1723,7 +1719,7 @@ protected function hydratePreventivi(): void $vociDomain = VoceSpesa::query() ->where('stabile_id', $this->stabileId) - ->when(!empty($gestioneIds), fn($q) => $q->whereIn('gestione_contabile_id', $gestioneIds)) + ->when(! empty($gestioneIds), fn($q) => $q->whereIn('gestione_contabile_id', $gestioneIds)) ->with('tabellaMillesimaleDefault') ->orderBy('ordinamento') ->orderBy('codice') @@ -1748,7 +1744,7 @@ protected function hydratePreventivi(): void // Importi reali (prev_euro) per unità e tabella, separati per ruolo C/I. $importiPerTabella = collect(); - if (DbSchema::hasTable('dettaglio_importi_tabella') && !empty($tabellaCodes)) { + if (DbSchema::hasTable('dettaglio_importi_tabella') && ! empty($tabellaCodes)) { $baseImporti = DB::table('dettaglio_importi_tabella as dit') ->join('tabelle_millesimali as tm', 'tm.id', '=', 'dit.tabella_millesimale_id') ->where('tm.stabile_id', $this->stabileId) @@ -1776,7 +1772,7 @@ protected function hydratePreventivi(): void $tabelleDomain = DB::table('tabelle_millesimali') ->where('stabile_id', $this->stabileId) - ->when(!empty($tabellaCodes), fn($q) => $q->whereIn('codice_tabella', $tabellaCodes)) + ->when(! empty($tabellaCodes), fn($q) => $q->whereIn('codice_tabella', $tabellaCodes)) ->get(['id', 'codice_tabella', 'denominazione', 'tipo_tabella', 'tipo_calcolo', 'totale_millesimi']) ->keyBy('codice_tabella'); @@ -1788,32 +1784,32 @@ protected function hydratePreventivi(): void ->map(function ($items, $tabellaCode) use ($importiPerTabella, $tabelleDomain, $millesimiPerCodice, &$totaliGestione) { $totalPreventivo = (float) $items->sum(fn(VoceSpesa $v) => (float) ($v->importo_default ?? 0)); - $domainTab = $tabelleDomain->get($tabellaCode); + $domainTab = $tabelleDomain->get($tabellaCode); $tabellaNome = $domainTab?->denominazione ?: (string) $tabellaCode; // Tipo gestione (O/R/S) per i box totali - $tipoRaw = (string) ($items->first()?->tipo_gestione ?? 'ordinaria'); + $tipoRaw = (string) ($items->first()?->tipo_gestione ?? 'ordinaria'); $tipoGestioneKey = match ($tipoRaw) { 'riscaldamento' => 'R', 'straordinaria' => 'S', - default => 'O', + default => 'O', }; $tabellaMillesimi = $millesimiPerCodice->get($tabellaCode); - $unitMillesimi = (float) ($tabellaMillesimi->millesimi ?? 0); - $tabTotalMm = (float) ($domainTab?->totale_millesimi ?? 0); - $ratio = ($tabTotalMm > 0) ? ($unitMillesimi / $tabTotalMm) : 0.0; + $unitMillesimi = (float) ($tabellaMillesimi->millesimi ?? 0); + $tabTotalMm = (float) ($domainTab?->totale_millesimi ?? 0); + $ratio = ($tabTotalMm > 0) ? ($unitMillesimi / $tabTotalMm) : 0.0; // Importi reali per tabella e unità (prev_euro) $impRows = $importiPerTabella->get($tabellaCode) ?? collect(); $impProp = (float) $impRows - ->filter(fn($r) => (string)($r->ruolo_legacy ?? '') !== 'I') + ->filter(fn($r) => (string) ($r->ruolo_legacy ?? '') !== 'I') ->sum(fn($r) => (float) ($r->prev_euro ?? 0)); $impInq = (float) $impRows - ->filter(fn($r) => (string)($r->ruolo_legacy ?? '') === 'I') + ->filter(fn($r) => (string) ($r->ruolo_legacy ?? '') === 'I') ->sum(fn($r) => (float) ($r->prev_euro ?? 0)); - $impTot = $impProp + $impInq; - $hasImporti = abs($impTot) > 0.00001; + $impTot = $impProp + $impInq; + $hasImporti = abs($impTot) > 0.00001; $useImportiSplit = $hasImporti && abs($impInq) > 0.00001; // Quota unità tabella @@ -1823,10 +1819,10 @@ protected function hydratePreventivi(): void $rows = $items->map(function (VoceSpesa $voce) use ($hasImporti, $useImportiSplit, $impInq, $quotaUnitTabella, $totalPreventivo, $ratio) { $importoVoce = (float) ($voce->importo_default ?? 0); - $percInq = is_numeric($voce->percentuale_inquilino ?? null) ? (float) $voce->percentuale_inquilino : 0.0; + $percInq = is_numeric($voce->percentuale_inquilino ?? null) ? (float) $voce->percentuale_inquilino : 0.0; if ($hasImporti) { - $share = ($totalPreventivo != 0.0) ? ($importoVoce / $totalPreventivo) : 0.0; + $share = ($totalPreventivo != 0.0) ? ($importoVoce / $totalPreventivo) : 0.0; $quotaUnita = $quotaUnitTabella * $share; if ($useImportiSplit) { $quotaInquilinoVoce = $impInq * $share; @@ -1834,34 +1830,34 @@ protected function hydratePreventivi(): void $quotaInquilinoVoce = $quotaUnita * ($percInq / 100); } } else { - $quotaUnita = $importoVoce * $ratio; + $quotaUnita = $importoVoce * $ratio; $quotaInquilinoVoce = $quotaUnita * ($percInq / 100); } - $quotaProprietarioVoce = $quotaUnita - $quotaInquilinoVoce; + $quotaProprietarioVoce = $quotaUnita - $quotaInquilinoVoce; $percentualeInquilinoVoce = $quotaUnita != 0.0 ? ($quotaInquilinoVoce / $quotaUnita) * 100 : 0.0; return [ - 'codice' => (string) ($voce->codice ?? ''), - 'descrizione' => (string) ($voce->descrizione ?? ''), - 'importo_preventivato' => $importoVoce, - 'quota_unita' => $quotaUnita, - 'importo_proprietario' => $quotaProprietarioVoce, - 'importo_inquilino' => $quotaInquilinoVoce, + 'codice' => (string) ($voce->codice ?? ''), + 'descrizione' => (string) ($voce->descrizione ?? ''), + 'importo_preventivato' => $importoVoce, + 'quota_unita' => $quotaUnita, + 'importo_proprietario' => $quotaProprietarioVoce, + 'importo_inquilino' => $quotaInquilinoVoce, 'percentuale_inquilino' => $percentualeInquilinoVoce, ]; })->values()->all(); if ($useImportiSplit) { - $quotaProprietario = $impProp; - $quotaInquilino = $impInq; + $quotaProprietario = $impProp; + $quotaInquilino = $impInq; $percentualeInquilino = ($impProp + $impInq) != 0.0 ? ($impInq / ($impProp + $impInq)) * 100 : 0.0; } else { $quotaProprietario = 0.0; - $quotaInquilino = 0.0; + $quotaInquilino = 0.0; foreach ($rows as $row) { $quotaProprietario += (float) ($row['importo_proprietario'] ?? 0); - $quotaInquilino += (float) ($row['importo_inquilino'] ?? 0); + $quotaInquilino += (float) ($row['importo_inquilino'] ?? 0); } $percentualeInquilino = ($quotaProprietario + $quotaInquilino) != 0.0 ? ($quotaInquilino / ($quotaProprietario + $quotaInquilino)) * 100 @@ -1873,17 +1869,17 @@ protected function hydratePreventivi(): void } return [ - 'tabella_codice' => $tabellaCode, - 'tabella_nome' => $tabellaNome, - 'tipo' => $tipoGestioneKey, - 'total_preventivo' => $totalPreventivo, - 'quota_unit' => $quotaUnitTabella, - 'importo_proprietario' => $quotaProprietario, - 'importo_inquilino' => $quotaInquilino, + 'tabella_codice' => $tabellaCode, + 'tabella_nome' => $tabellaNome, + 'tipo' => $tipoGestioneKey, + 'total_preventivo' => $totalPreventivo, + 'quota_unit' => $quotaUnitTabella, + 'importo_proprietario' => $quotaProprietario, + 'importo_inquilino' => $quotaInquilino, 'percentuale_inquilino' => $percentualeInquilino, - 'tabella_millesimi' => $tabTotalMm, - 'unit_millesimi' => $unitMillesimi, - 'voci' => $rows, + 'tabella_millesimi' => $tabTotalMm, + 'unit_millesimi' => $unitMillesimi, + 'voci' => $rows, ]; }) ->values() @@ -1895,16 +1891,14 @@ protected function hydratePreventivi(): void } $importConn = 'gescon_import'; - $hasVocSpe = DbSchema::connection($importConn)->hasTable('voc_spe'); - $hasDetTab = DbSchema::connection($importConn)->hasTable('dett_tab'); + $hasVocSpe = DbSchema::connection($importConn)->hasTable('voc_spe'); + $hasDetTab = DbSchema::connection($importConn)->hasTable('dett_tab'); - if (!$hasVocSpe || !$hasDetTab) { + if (! $hasVocSpe || ! $hasDetTab) { return; } - $codiceStabile = $this->unita->stabile?->codice_stabile - ?? $this->unita->stabile?->codice_interno - ?? $this->unita->stabile?->old_id; + $codiceStabile = $this->unita->stabile?->codice_stabile ?? $this->unita->stabile?->codice_interno ?? $this->unita->stabile?->old_id; $voci = DB::connection($importConn) ->table('voc_spe') @@ -1973,28 +1967,28 @@ protected function hydratePreventivi(): void return (float) ($v->preventivo_euro ?? 0); }); - $domainTab = $tabelleDomain->get($tabellaCode); + $domainTab = $tabelleDomain->get($tabellaCode); $tabellaNome = $domainTab?->denominazione ?: ($items->first()->tabella_descrizione ?? $items->first()->descrizione ?? $tabellaCode); - $tipoGestione = $items->first()->v_ors ?? $items->first()->tipo_gestione ?? $items->first()->tipo ?? 'O'; + $tipoGestione = $items->first()->v_ors ?? $items->first()->tipo_gestione ?? $items->first()->tipo ?? 'O'; $tipoGestioneKey = strtoupper(substr((string) $tipoGestione, 0, 1)); $tabellaMillesimi = $millesimiPerCodice->get($tabellaCode); - $unitMillesimi = (float) ($tabellaMillesimi->millesimi ?? 0); - $tabTotalMm = (float) ($domainTab?->totale_millesimi ?? 0); - $ratio = ($tabTotalMm > 0) ? ($unitMillesimi / $tabTotalMm) : 0.0; + $unitMillesimi = (float) ($tabellaMillesimi->millesimi ?? 0); + $tabTotalMm = (float) ($domainTab?->totale_millesimi ?? 0); + $ratio = ($tabTotalMm > 0) ? ($unitMillesimi / $tabTotalMm) : 0.0; // Importi reali per tabella e unità (prev_euro) se presenti $impRows = $importiPerTabella->get($tabellaCode) ?? collect(); $impProp = (float) $impRows - ->filter(fn($r) => (string)($r->ruolo_legacy ?? '') !== 'I') + ->filter(fn($r) => (string) ($r->ruolo_legacy ?? '') !== 'I') ->sum(fn($r) => (float) ($r->prev_euro ?? 0)); $impInq = (float) $impRows - ->filter(fn($r) => (string)($r->ruolo_legacy ?? '') === 'I') + ->filter(fn($r) => (string) ($r->ruolo_legacy ?? '') === 'I') ->sum(fn($r) => (float) ($r->prev_euro ?? 0)); - $impTot = $impProp + $impInq; - $hasImporti = abs($impTot) > 0.00001; + $impTot = $impProp + $impInq; + $hasImporti = abs($impTot) > 0.00001; $percentInqFromImporti = $impTot != 0.0 ? ($impInq / $impTot) * 100 : 0.0; // Usa lo split importato solo quando c'è effettivamente una quota Inquilino. @@ -2007,46 +2001,38 @@ protected function hydratePreventivi(): void : ($totalPreventivo * $ratio); $rows = $items->map(function ($voce) use ($hasImporti, $useImportiSplit, $impInq, $quotaUnitTabella, $totalPreventivo, $ratio) { - $voceCodice = $voce->codice - ?? $voce->cod_voce - ?? $voce->codice_voce - ?? $voce->cod - ?? null; + $voceCodice = $voce->codice ?? $voce->cod_voce ?? $voce->codice_voce ?? $voce->cod ?? null; - $voceDescrizione = $voce->descrizione - ?? $voce->descr - ?? $voce->voce - ?? $voce->voce_descrizione - ?? ''; + $voceDescrizione = $voce->descrizione ?? $voce->descr ?? $voce->voce ?? $voce->voce_descrizione ?? ''; $importoVoce = (float) ($voce->preventivo_euro ?? 0); // Quota unità voce if ($hasImporti) { - $share = ($totalPreventivo != 0.0) ? ($importoVoce / $totalPreventivo) : 0.0; + $share = ($totalPreventivo != 0.0) ? ($importoVoce / $totalPreventivo) : 0.0; $quotaUnita = $quotaUnitTabella * $share; if ($useImportiSplit) { $quotaInquilinoVoce = $impInq * $share; } else { - $percInq = is_numeric($voce->perc_inquilino ?? null) ? (float) $voce->perc_inquilino : 0.0; + $percInq = is_numeric($voce->perc_inquilino ?? null) ? (float) $voce->perc_inquilino : 0.0; $quotaInquilinoVoce = $quotaUnita * ($percInq / 100); } } else { - $quotaUnita = $importoVoce * $ratio; - $percInq = is_numeric($voce->perc_inquilino ?? null) ? (float) $voce->perc_inquilino : 0.0; + $quotaUnita = $importoVoce * $ratio; + $percInq = is_numeric($voce->perc_inquilino ?? null) ? (float) $voce->perc_inquilino : 0.0; $quotaInquilinoVoce = $quotaUnita * ($percInq / 100); } - $quotaProprietarioVoce = $quotaUnita - $quotaInquilinoVoce; + $quotaProprietarioVoce = $quotaUnita - $quotaInquilinoVoce; $percentualeInquilinoVoce = $quotaUnita != 0.0 ? ($quotaInquilinoVoce / $quotaUnita) * 100 : 0.0; return [ - 'codice' => $voceCodice, - 'descrizione' => $voceDescrizione, - 'importo_preventivato' => $importoVoce, - 'quota_unita' => $quotaUnita, - 'importo_proprietario' => $quotaProprietarioVoce, - 'importo_inquilino' => $quotaInquilinoVoce, + 'codice' => $voceCodice, + 'descrizione' => $voceDescrizione, + 'importo_preventivato' => $importoVoce, + 'quota_unita' => $quotaUnita, + 'importo_proprietario' => $quotaProprietarioVoce, + 'importo_inquilino' => $quotaInquilinoVoce, 'percentuale_inquilino' => $percentualeInquilinoVoce, ]; })->values()->all(); @@ -2055,15 +2041,15 @@ protected function hydratePreventivi(): void // - se importi reali disponibili, usa quelli // - altrimenti deriva dalle percentuali voce if ($useImportiSplit) { - $quotaProprietario = $impProp; - $quotaInquilino = $impInq; + $quotaProprietario = $impProp; + $quotaInquilino = $impInq; $percentualeInquilino = ($impProp + $impInq) != 0.0 ? ($impInq / ($impProp + $impInq)) * 100 : 0.0; } else { $quotaProprietario = 0.0; - $quotaInquilino = 0.0; + $quotaInquilino = 0.0; foreach ($rows as $row) { $quotaProprietario += (float) ($row['importo_proprietario'] ?? 0); - $quotaInquilino += (float) ($row['importo_inquilino'] ?? 0); + $quotaInquilino += (float) ($row['importo_inquilino'] ?? 0); } $percentualeInquilino = ($quotaProprietario + $quotaInquilino) != 0.0 ? ($quotaInquilino / ($quotaProprietario + $quotaInquilino)) * 100 @@ -2075,17 +2061,17 @@ protected function hydratePreventivi(): void } return [ - 'tabella_codice' => $tabellaCode, - 'tabella_nome' => $tabellaNome, - 'tipo' => $tipoGestione, - 'total_preventivo' => $totalPreventivo, - 'quota_unit' => $quotaUnitTabella, - 'importo_proprietario' => $quotaProprietario, - 'importo_inquilino' => $quotaInquilino, + 'tabella_codice' => $tabellaCode, + 'tabella_nome' => $tabellaNome, + 'tipo' => $tipoGestione, + 'total_preventivo' => $totalPreventivo, + 'quota_unit' => $quotaUnitTabella, + 'importo_proprietario' => $quotaProprietario, + 'importo_inquilino' => $quotaInquilino, 'percentuale_inquilino' => $percentualeInquilino, - 'tabella_millesimi' => $tabTotalMm, - 'unit_millesimi' => $unitMillesimi, - 'voci' => $rows, + 'tabella_millesimi' => $tabTotalMm, + 'unit_millesimi' => $unitMillesimi, + 'voci' => $rows, ]; }) ->values() diff --git a/app/Filament/Pages/UnitaImmobiliareV2.php b/app/Filament/Pages/UnitaImmobiliareV2.php index 557a474..f74374d 100644 --- a/app/Filament/Pages/UnitaImmobiliareV2.php +++ b/app/Filament/Pages/UnitaImmobiliareV2.php @@ -1,5 +1,4 @@ hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } public function mount(): void { // Manteniamo la route storica ma riportiamo alla pagina unica. $unitaId = request()->query('unita_id'); - $url = UnitaImmobiliarePage::getUrl(panel: 'admin-filament'); + $url = UnitaImmobiliarePage::getUrl(panel: 'admin-filament'); if (is_numeric($unitaId)) { $url .= '?unita_id=' . (int) $unitaId; } diff --git a/app/Filament/Pages/UnitaImmobiliareV3.php b/app/Filament/Pages/UnitaImmobiliareV3.php index 12d6b0a..09d296b 100644 --- a/app/Filament/Pages/UnitaImmobiliareV3.php +++ b/app/Filament/Pages/UnitaImmobiliareV3.php @@ -1,5 +1,4 @@ hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } public function mount(): void { // Manteniamo la route storica ma riportiamo alla pagina unica. $unitaId = request()->query('unita_id'); - $url = UnitaImmobiliarePage::getUrl(panel: 'admin-filament'); + $url = UnitaImmobiliarePage::getUrl(panel: 'admin-filament'); if (is_numeric($unitaId)) { $url .= '?unita_id=' . (int) $unitaId; } diff --git a/app/Http/Controllers/Admin/MobileHubController.php b/app/Http/Controllers/Admin/MobileHubController.php index 34110b9..26b8c2b 100644 --- a/app/Http/Controllers/Admin/MobileHubController.php +++ b/app/Http/Controllers/Admin/MobileHubController.php @@ -3,6 +3,7 @@ use App\Http\Controllers\Controller; use App\Models\RubricaUniversale; +use App\Models\Stabile; use App\Models\Ticket; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -33,6 +34,14 @@ public function index(Request $request) ->limit(25) ->get(); + $stabili = Stabile::query() + ->when($adminId, function ($q) use ($adminId): void { + $q->where('amministratore_id', $adminId); + }) + ->orderBy('denominazione') + ->limit(12) + ->get(['id', 'denominazione', 'indirizzo', 'citta', 'stato']); + $callerMatches = collect(); if ($phoneQuery !== '') { $digits = preg_replace('/\D+/', '', $phoneQuery) ?: ''; @@ -51,6 +60,7 @@ public function index(Request $request) return view('admin.mobile.hub', [ 'tickets' => $tickets, + 'stabili' => $stabili, 'status' => $status, 'phoneQuery' => $phoneQuery, 'callerMatches' => $callerMatches, diff --git a/app/Http/Controllers/Admin/TicketAttachmentViewController.php b/app/Http/Controllers/Admin/TicketAttachmentViewController.php index 586d704..878f9c6 100644 --- a/app/Http/Controllers/Admin/TicketAttachmentViewController.php +++ b/app/Http/Controllers/Admin/TicketAttachmentViewController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Models\Fornitore; +use App\Models\FornitoreDipendente; use App\Models\TicketAttachment; use App\Models\User; use App\Support\StabileContext; @@ -18,7 +20,7 @@ public function show(TicketAttachment $attachment): BinaryFileResponse abort(403); } - if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) { + if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore'])) { abort(403); } @@ -27,9 +29,13 @@ public function show(TicketAttachment $attachment): BinaryFileResponse abort(404); } - $stabileId = StabileContext::resolveActiveStabileId($user); - if (! $stabileId || (int) $ticket->stabile_id !== (int) $stabileId) { - abort(403); + if ($user->hasRole('fornitore')) { + $this->authorizeSupplierAttachment($user, $attachment); + } else { + $stabileId = StabileContext::resolveActiveStabileId($user); + if (! $stabileId || (int) $ticket->stabile_id !== (int) $stabileId) { + abort(403); + } } $disk = Storage::disk('public'); @@ -47,4 +53,73 @@ public function show(TicketAttachment $attachment): BinaryFileResponse 'Content-Disposition' => 'inline; filename="' . addslashes((string) ($attachment->original_file_name ?: basename($path))) . '"', ]); } + + protected function authorizeSupplierAttachment(User $user, TicketAttachment $attachment): void + { + $fornitoreIds = []; + $currentSupplier = $this->resolveCurrentUserSupplier($user); + + if ($currentSupplier instanceof Fornitore) { + $fornitoreIds[] = (int) $currentSupplier->id; + } + + $dipendente = $this->resolveCollaboratoreForUser($user, $currentSupplier?->id); + if ($dipendente instanceof FornitoreDipendente) { + $fornitoreIds[] = (int) $dipendente->fornitore_id; + + if ((int) ($dipendente->fornitore_esterno_id ?? 0) > 0) { + $fornitoreIds[] = (int) $dipendente->fornitore_esterno_id; + } + } + + $fornitoreIds = array_values(array_unique(array_filter($fornitoreIds))); + abort_if($fornitoreIds === [], 403); + + $ticket = $attachment->ticket; + abort_unless($ticket, 404); + + if (in_array((int) ($ticket->assegnato_a_fornitore_id ?? 0), $fornitoreIds, true)) { + return; + } + + $hasIntervento = $ticket->interventi() + ->whereIn('fornitore_id', $fornitoreIds) + ->exists(); + + abort_unless($hasIntervento, 403); + } + + protected function resolveCurrentUserSupplier(User $user): ?Fornitore + { + $email = mb_strtolower(trim((string) $user->email)); + if ($email === '') { + return null; + } + + return Fornitore::query() + ->whereRaw('LOWER(email) = ?', [$email]) + ->withCount(['ticketInterventi', 'dipendenti']) + ->orderByDesc('ticket_interventi_count') + ->orderByDesc('dipendenti_count') + ->orderByDesc('id') + ->first(); + } + + protected function resolveCollaboratoreForUser(User $user, ?int $currentSupplierId = null): ?FornitoreDipendente + { + return FornitoreDipendente::query() + ->where('attivo', true) + ->where(function ($builder) use ($user, $currentSupplierId): void { + $builder->where('user_id', (int) $user->id) + ->orWhereRaw('LOWER(email) = ?', [mb_strtolower((string) $user->email)]); + + if (($currentSupplierId ?? 0) > 0) { + $builder->orWhere('fornitore_esterno_id', (int) $currentSupplierId); + } + }) + ->orderByRaw('CASE WHEN user_id = ? THEN 0 ELSE 1 END', [(int) $user->id]) + ->orderByRaw('CASE WHEN fornitore_esterno_id IS NULL THEN 1 ELSE 0 END') + ->orderByDesc('id') + ->first(); + } } diff --git a/app/Http/Controllers/Admin/TicketController.php b/app/Http/Controllers/Admin/TicketController.php index 7133035..d665df5 100755 --- a/app/Http/Controllers/Admin/TicketController.php +++ b/app/Http/Controllers/Admin/TicketController.php @@ -3,6 +3,7 @@ use App\Http\Controllers\Controller; use App\Models\CategoriaTicket; +use App\Models\CommunicationMessage; use App\Models\Documento; use App\Models\Fornitore; use App\Models\InsuranceClaim; @@ -197,6 +198,26 @@ public function storeEmailMessage(Request $request, Ticket $ticket) ], ]); + CommunicationMessage::query()->create([ + 'channel' => 'email', + 'direction' => 'inbound', + 'external_message_id' => $externalId !== '' ? $externalId : null, + 'stabile_id' => $ticket->stabile_id, + 'sender_name' => $mittente !== '' ? $mittente : null, + 'message_text' => trim((string) ($validated['messaggio'] ?? '')) ?: ($oggetto !== '' ? $oggetto : 'Email collegata al ticket #' . $ticket->id), + 'ticket_id' => (int) $ticket->id, + 'status' => 'linked_to_ticket', + 'received_at' => $inviatoIl, + 'metadata' => [ + 'source' => 'admin_ticket_eml_import', + 'email_mittente' => $mittente !== '' ? $mittente : null, + 'email_destinatario' => $destinatario !== '' ? $destinatario : null, + 'eml_documento_id' => (int) $documento->id, + 'original_file_name' => $file->getClientOriginalName(), + 'stored_path' => $path, + ], + ]); + return redirect()->route('admin.tickets.show', $ticket) ->with('success', 'Email archiviata e collegata al ticket.'); } diff --git a/app/Http/Controllers/Condomino/AnagrafeController.php b/app/Http/Controllers/Condomino/AnagrafeController.php new file mode 100644 index 0000000..1db45e7 --- /dev/null +++ b/app/Http/Controllers/Condomino/AnagrafeController.php @@ -0,0 +1,389 @@ +resolveUserUnita($user)->values(); + $persona = $this->resolvePortalPersona($user, $unitaImmobiliari); + $serviceLabels = UnitaRecapitoServizio::serviceLabels(); + + return view('condomino.anagrafe.show', [ + 'persona' => $persona, + 'unitaImmobiliari' => $unitaImmobiliari, + 'serviceLabels' => $serviceLabels, + 'emailRows' => $this->buildEmailRows($persona), + 'serviceContactMatrix' => $this->buildServiceContactMatrix($persona, $unitaImmobiliari), + ]); + } + + public function update(Request $request): RedirectResponse + { + /** @var User|null $user */ + $user = Auth::user(); + $unitaImmobiliari = $this->resolveUserUnita($user)->values(); + $persona = $this->resolvePortalPersona($user, $unitaImmobiliari); + + if (! $persona instanceof Persona) { + return back()->withErrors([ + 'anagrafe' => 'Non e stato possibile collegare con certezza il tuo profilo a una scheda anagrafica digitale.', + ])->withInput(); + } + + $validated = $request->validate([ + 'cognome' => 'nullable|string|max:100', + 'nome' => 'nullable|string|max:100', + 'codice_fiscale' => 'nullable|string|max:16', + 'partita_iva' => 'nullable|string|max:16', + 'data_nascita' => 'nullable|date', + 'residenza_via' => 'nullable|string|max:255', + 'telefono_principale' => 'nullable|string|max:50', + 'email_principale' => 'nullable|email|max:255', + 'email_pec' => 'nullable|email|max:255', + 'whatsapp' => 'nullable|string|max:50', + 'lingua_preferita' => 'nullable|string|max:10', + 'modalita_comunicazione_preferita' => 'nullable|in:email,sms,whatsapp,telefono,posta', + 'consenso_privacy' => 'nullable|boolean', + 'note' => 'nullable|string|max:4000', + 'email_multiple' => 'nullable|array', + 'email_multiple.*.id' => 'nullable|integer', + 'email_multiple.*.email' => 'nullable|email|max:255', + 'email_multiple.*.tipo_email' => 'nullable|string|max:50', + 'email_multiple.*.attiva' => 'nullable|boolean', + 'service_contacts' => 'nullable|array', + ]); + + DB::transaction(function () use ($validated, $persona, $unitaImmobiliari): void { + $this->updatePersona($persona, $validated); + $this->syncAdditionalEmails($persona, (array) ($validated['email_multiple'] ?? [])); + $this->syncServiceContacts($persona, $unitaImmobiliari, (array) ($validated['service_contacts'] ?? [])); + }); + + return redirect()->route('condomino.anagrafe.show') + ->with('success', 'Scheda anagrafica aggiornata. Le modifiche sono tracciate con data, utente e IP.'); + } + + public function downloadModulo(): Response + { + /** @var User|null $user */ + $user = Auth::user(); + $unitaImmobiliari = $this->resolveUserUnita($user)->values(); + $persona = $this->resolvePortalPersona($user, $unitaImmobiliari); + + $html = view('condomino.anagrafe.modulo-pdf', [ + 'persona' => $persona, + 'utente' => $user, + 'unitaImmobiliari' => $unitaImmobiliari, + 'serviceLabels' => UnitaRecapitoServizio::serviceLabels(), + 'emailRows' => $this->buildEmailRows($persona), + 'serviceContactMatrix' => $this->buildServiceContactMatrix($persona, $unitaImmobiliari), + 'generatedAt' => now(), + ])->render(); + + $options = new Options(); + $options->set('isRemoteEnabled', true); + $options->set('defaultFont', 'DejaVu Sans'); + + $dompdf = new Dompdf($options); + $dompdf->setPaper('A4', 'portrait'); + $dompdf->loadHtml($html, 'UTF-8'); + $dompdf->render(); + + return response($dompdf->output(), 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'attachment; filename="modulo-anagrafe-condominiale.pdf"', + ]); + } + + private function updatePersona(Persona $persona, array $validated): void + { + $fields = [ + 'cognome', + 'nome', + 'codice_fiscale', + 'partita_iva', + 'data_nascita', + 'residenza_via', + 'telefono_principale', + 'email_principale', + 'email_pec', + 'whatsapp', + 'lingua_preferita', + 'modalita_comunicazione_preferita', + 'note', + ]; + + foreach ($fields as $field) { + $newValue = $validated[$field] ?? null; + $oldValue = $persona->getAttribute($field); + + if ((string) $oldValue === (string) $newValue) { + continue; + } + + PersonaAudit::registraModifica($persona->id, $field, $oldValue, $newValue); + $persona->setAttribute($field, $newValue); + } + + $consensoPrivacy = (bool) ($validated['consenso_privacy'] ?? false); + if ((bool) $persona->consenso_privacy !== $consensoPrivacy) { + PersonaAudit::registraModifica($persona->id, 'consenso_privacy', $persona->consenso_privacy ? '1' : '0', $consensoPrivacy ? '1' : '0'); + $persona->consenso_privacy = $consensoPrivacy; + $persona->data_consenso_privacy = $consensoPrivacy ? now() : null; + } + + $persona->save(); + } + + private function syncAdditionalEmails(Persona $persona, array $rows): void + { + $before = $persona->emailMultiple() + ->orderBy('id') + ->get(['email', 'tipo_email', 'attiva']) + ->map(fn(PersonaEmailMultipla $row): array=> [ + 'email' => mb_strtolower(trim((string) $row->email)), + 'tipo_email' => (string) $row->tipo_email, + 'attiva' => (bool) $row->attiva, + ]) + ->values() + ->all(); + + $keepIds = []; + foreach ($rows as $row) { + $email = mb_strtolower(trim((string) ($row['email'] ?? ''))); + if ($email === '') { + continue; + } + + $attrs = [ + 'email' => $email, + 'tipo_email' => trim((string) ($row['tipo_email'] ?? 'secondaria')) ?: 'secondaria', + 'attiva' => (bool) ($row['attiva'] ?? true), + ]; + + $id = (int) ($row['id'] ?? 0); + if ($id > 0) { + $existing = $persona->emailMultiple()->whereKey($id)->first(); + if ($existing instanceof PersonaEmailMultipla) { + $existing->fill($attrs); + $existing->save(); + $keepIds[] = (int) $existing->id; + continue; + } + } + + $created = $persona->emailMultiple()->create($attrs); + $keepIds[] = (int) $created->id; + } + + $deleteQuery = $persona->emailMultiple(); + if ($keepIds !== []) { + $deleteQuery->whereNotIn('id', $keepIds); + } + $deleteQuery->delete(); + + $after = $persona->emailMultiple() + ->orderBy('id') + ->get(['email', 'tipo_email', 'attiva']) + ->map(fn(PersonaEmailMultipla $row): array=> [ + 'email' => mb_strtolower(trim((string) $row->email)), + 'tipo_email' => (string) $row->tipo_email, + 'attiva' => (bool) $row->attiva, + ]) + ->values() + ->all(); + + if (json_encode($before) !== json_encode($after)) { + PersonaAudit::registraModifica($persona->id, 'email_multiple', json_encode($before), json_encode($after)); + } + } + + private function syncServiceContacts(Persona $persona, Collection $unitaImmobiliari, array $matrix): void + { + $allowedUnitIds = $unitaImmobiliari->pluck('id')->map(fn($id) => (int) $id)->all(); + + foreach ($allowedUnitIds as $unitaId) { + $serviceRows = (array) ($matrix[$unitaId] ?? []); + + foreach (array_keys(UnitaRecapitoServizio::serviceLabels()) as $serviceType) { + $email = mb_strtolower(trim((string) ($serviceRows[$serviceType] ?? ''))); + $query = UnitaRecapitoServizio::query() + ->where('unita_id', $unitaId) + ->where('persona_id', (int) $persona->id) + ->where('tipo_servizio', $serviceType); + + $before = $query->orderBy('id')->pluck('email')->map(fn($value) => mb_strtolower(trim((string) $value)))->values()->all(); + + if ($email === '') { + $query->delete(); + $after = []; + } else { + $record = $query->orderBy('id')->first(); + if (! $record instanceof UnitaRecapitoServizio) { + $record = new UnitaRecapitoServizio([ + 'unita_id' => $unitaId, + 'persona_id' => (int) $persona->id, + 'tipo_servizio' => $serviceType, + 'ordine' => 1, + 'is_default' => true, + 'attivo' => true, + 'sorgente' => 'portale_condomino', + ]); + } + + $record->email = $email; + $record->etichetta = UnitaRecapitoServizio::serviceLabels()[$serviceType]; + $record->last_confirmed_at = now(); + $record->save(); + + UnitaRecapitoServizio::query() + ->where('unita_id', $unitaId) + ->where('persona_id', (int) $persona->id) + ->where('tipo_servizio', $serviceType) + ->where('id', '!=', (int) $record->id) + ->delete(); + + $after = [$email]; + } + + if (json_encode($before) !== json_encode($after)) { + PersonaAudit::registraModifica( + $persona->id, + 'unita_recapito:' . $unitaId . ':' . $serviceType, + json_encode($before), + json_encode($after) + ); + } + } + } + } + + /** + * @return array> + */ + private function buildEmailRows(?Persona $persona): array + { + $rows = []; + if ($persona instanceof Persona) { + $rows = $persona->emailMultiple() + ->orderByDesc('attiva') + ->orderBy('tipo_email') + ->orderBy('email') + ->get() + ->map(fn(PersonaEmailMultipla $row): array=> [ + 'id' => (int) $row->id, + 'email' => (string) $row->email, + 'tipo_email' => (string) ($row->tipo_email ?: 'secondaria'), + 'attiva' => (bool) $row->attiva, + ]) + ->values() + ->all(); + } + + while (count($rows) < 4) { + $rows[] = [ + 'id' => null, + 'email' => '', + 'tipo_email' => 'secondaria', + 'attiva' => true, + ]; + } + + return $rows; + } + + /** + * @return array>> + */ + private function buildServiceContactMatrix(?Persona $persona, Collection $unitaImmobiliari): array + { + $matrix = []; + $labels = UnitaRecapitoServizio::serviceLabels(); + + $storedRows = collect(); + if ($persona instanceof Persona) { + $storedRows = UnitaRecapitoServizio::query() + ->where('persona_id', (int) $persona->id) + ->whereIn('unita_id', $unitaImmobiliari->pluck('id')->all()) + ->get() + ->groupBy(['unita_id', 'tipo_servizio']); + } + + foreach ($unitaImmobiliari as $unita) { + $unitRows = []; + + foreach ($labels as $serviceType => $label) { + $stored = data_get($storedRows, $unita->id . '.' . $serviceType . '.0.email'); + $resolved = $this->resolver->resolveForUnita($unita, $serviceType); + + $unitRows[$serviceType] = [ + 'label' => $label, + 'value' => (string) ($stored ?? ''), + 'resolved' => $resolved, + ]; + } + + $matrix[(int) $unita->id] = $unitRows; + } + + return $matrix; + } + + private function resolvePortalPersona(?User $user, Collection $unitaImmobiliari): ?Persona + { + if (! $user) { + return null; + } + + $email = mb_strtolower(trim((string) $user->email)); + if ($email === '') { + return null; + } + + $unitIds = $unitaImmobiliari->pluck('id')->map(fn($id) => (int) $id)->all(); + + $baseQuery = Persona::query()->with(['emailMultiple', 'relazioniUnitaAttive.unitaImmobiliare.stabile']); + if ($unitIds !== []) { + $baseQuery->whereHas('relazioniUnitaAttive', fn($query) => $query->whereIn('unita_id', $unitIds)); + } + + $primaryMatch = (clone $baseQuery) + ->whereRaw('LOWER(email_principale) = ?', [$email]) + ->first(); + + if ($primaryMatch instanceof Persona) { + return $primaryMatch; + } + + return (clone $baseQuery) + ->whereHas('emailMultiple', fn($query) => $query->whereRaw('LOWER(email) = ?', [$email])->where('attiva', true)) + ->first(); + } +} diff --git a/app/Http/Controllers/Condomino/Concerns/ResolvesCondominoAccess.php b/app/Http/Controllers/Condomino/Concerns/ResolvesCondominoAccess.php new file mode 100644 index 0000000..1601710 --- /dev/null +++ b/app/Http/Controllers/Condomino/Concerns/ResolvesCondominoAccess.php @@ -0,0 +1,58 @@ +soggetto?->id_soggetto) && is_numeric($user->soggetto->id_soggetto)) { + return (int) $user->soggetto->id_soggetto; + } + + if (isset($user->soggetto?->id) && is_numeric($user->soggetto->id)) { + return (int) $user->soggetto->id; + } + + return null; + } + + protected function resolveUserUnita(?User $user): Collection + { + if (! $user) { + return collect(); + } + + $soggettoId = $this->resolveSoggettoId($user); + + $query = UnitaImmobiliare::query() + ->with(['stabile', 'soggetti']) + ->where(function ($q) use ($soggettoId, $user): void { + if ($soggettoId) { + $q->whereHas('soggetti', function ($qp) use ($soggettoId): void { + $qp->where('soggetti.id', $soggettoId); + }); + } + + $email = trim((string) $user->email); + if ($email !== '') { + $q->orWhereIn('id', function ($sub) use ($email): void { + $sub->select('ai.unita_immobiliare_id') + ->from('affitti_immobili as ai') + ->join('rubrica_universale as ru', 'ru.id', '=', 'ai.rubrica_inquilino_id') + ->whereNotNull('ai.unita_immobiliare_id') + ->whereRaw('LOWER(ru.email) = ?', [mb_strtolower($email)]); + }); + } + }); + + return $query->orderBy('stabile_id')->orderBy('scala')->orderBy('interno')->get(); + } +} diff --git a/app/Http/Controllers/Condomino/DashboardController.php b/app/Http/Controllers/Condomino/DashboardController.php index 015ac77..a2f6649 100755 --- a/app/Http/Controllers/Condomino/DashboardController.php +++ b/app/Http/Controllers/Condomino/DashboardController.php @@ -1,41 +1,38 @@ where('soggetto_id', $user->soggetto->id_soggetto ?? null); - })->with(['stabile', 'proprieta.soggetto'])->get(); + + $soggettoId = $this->resolveSoggettoId($user); + $unitaImmobiliari = $this->resolveUserUnita($user); + $stabiliIds = $unitaImmobiliari->pluck('stabile_id')->filter()->unique(); // Statistiche principali $stats = [ - 'unita_possedute' => $unitaImmobiliari->count(), - 'ticket_aperti' => Ticket::where('soggetto_richiedente_id', $user->soggetto->id_soggetto ?? null) + 'unita_possedute' => $unitaImmobiliari->count(), + 'ticket_aperti' => Ticket::where('soggetto_richiedente_id', $soggettoId) ->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(), - 'rate_scadute' => 0, // Implementeremo quando avremo le rate - 'documenti_disponibili' => Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function($q) use ($unitaImmobiliari) { - $q->whereIn('id_stabile', $unitaImmobiliari->pluck('stabile_id')); + 'rate_scadute' => 0, // Implementeremo quando avremo le rate + 'documenti_disponibili' => Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function ($q) use ($stabiliIds) { + $q->whereIn('id', $stabiliIds); })->count(), ]; // Ticket recenti - $ticketRecenti = Ticket::where('soggetto_richiedente_id', $user->soggetto->id_soggetto ?? null) + $ticketRecenti = Ticket::where('soggetto_richiedente_id', $soggettoId) ->with(['stabile', 'categoriaTicket']) ->orderBy('created_at', 'desc') ->take(5) @@ -45,16 +42,22 @@ public function index() $rateInScadenza = collect(); // Ultimi documenti - $ultimiDocumenti = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function($q) use ($unitaImmobiliari) { - $q->whereIn('id_stabile', $unitaImmobiliari->pluck('stabile_id')); + $ultimiDocumenti = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function ($q) use ($stabiliIds) { + $q->whereIn('id', $stabiliIds); })->orderBy('created_at', 'desc')->take(5)->get(); + $serviceContactsCount = UnitaRecapitoServizio::query() + ->whereIn('unita_id', $unitaImmobiliari->pluck('id')->all()) + ->where('attivo', true) + ->count(); + return view('condomino.dashboard', compact( - 'stats', - 'unitaImmobiliari', - 'ticketRecenti', - 'rateInScadenza', - 'ultimiDocumenti' + 'stats', + 'unitaImmobiliari', + 'ticketRecenti', + 'rateInScadenza', + 'ultimiDocumenti', + 'serviceContactsCount' )); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Condomino/DocumentoController.php b/app/Http/Controllers/Condomino/DocumentoController.php index c4e9b65..670c7bf 100755 --- a/app/Http/Controllers/Condomino/DocumentoController.php +++ b/app/Http/Controllers/Condomino/DocumentoController.php @@ -1,46 +1,45 @@ where('soggetto_id', $user->soggetto->id_soggetto ?? null); - })->pluck('stabile_id')->unique(); - $query = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function($q) use ($stabiliIds) { - $q->whereIn('id_stabile', $stabiliIds); + // Trova gli stabili delle unità dell'utente + $stabiliIds = $this->resolveUserUnita($user)->pluck('stabile_id')->filter()->unique(); + + $query = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function ($q) use ($stabiliIds) { + $q->whereIn('id', $stabiliIds); })->with('documentable'); // Filtri if ($request->filled('tipo_documento')) { $query->where('tipo_documento', $request->tipo_documento); } - + if ($request->filled('search')) { - $query->where(function($q) use ($request) { + $query->where(function ($q) use ($request) { $q->where('nome_file', 'like', '%' . $request->search . '%') - ->orWhere('descrizione', 'like', '%' . $request->search . '%'); + ->orWhere('descrizione', 'like', '%' . $request->search . '%'); }); } $documenti = $query->orderBy('created_at', 'desc')->paginate(20); - + // Tipi documento per filtro - $tipiDocumento = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function($q) use ($stabiliIds) { - $q->whereIn('id_stabile', $stabiliIds); + $tipiDocumento = Documento::whereHasMorph('documentable', ['App\Models\Stabile'], function ($q) use ($stabiliIds) { + $q->whereIn('id', $stabiliIds); })->distinct()->pluck('tipo_documento')->filter(); return view('condomino.documenti.index', compact('documenti', 'tipiDocumento')); @@ -49,22 +48,20 @@ public function index(Request $request) public function download(Documento $documento) { $user = Auth::user(); - + // Verifica accesso - $stabiliIds = UnitaImmobiliare::whereHas('proprieta', function($q) use ($user) { - $q->where('soggetto_id', $user->soggetto->id_soggetto ?? null); - })->pluck('stabile_id')->unique(); + $stabiliIds = $this->resolveUserUnita($user)->pluck('stabile_id')->filter()->unique(); if ($documento->documentable_type === 'App\Models\Stabile') { - if (!$stabiliIds->contains($documento->documentable_id)) { + if (! $stabiliIds->contains($documento->documentable_id)) { abort(403); } } - if (!Storage::disk('public')->exists($documento->path_file)) { + if (! Storage::disk('public')->exists($documento->path_file)) { abort(404, 'File non trovato'); } return Storage::disk('public')->download($documento->path_file, $documento->nome_file); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Condomino/LetturaAcquaController.php b/app/Http/Controllers/Condomino/LetturaAcquaController.php index 4c90dc3..c462805 100644 --- a/app/Http/Controllers/Condomino/LetturaAcquaController.php +++ b/app/Http/Controllers/Condomino/LetturaAcquaController.php @@ -1,20 +1,21 @@ route('condomino.letture-acqua.index') ->with('success', 'Autolettura registrata correttamente.'); } - - private function resolveUserUnita(?User $user): Collection - { - if (! $user) { - return collect(); - } - - $soggettoId = null; - if (isset($user->soggetto?->id_soggetto) && is_numeric($user->soggetto->id_soggetto)) { - $soggettoId = (int) $user->soggetto->id_soggetto; - } elseif (isset($user->soggetto?->id) && is_numeric($user->soggetto->id)) { - $soggettoId = (int) $user->soggetto->id; - } - - $query = UnitaImmobiliare::query() - ->with('stabile') - ->where(function ($q) use ($soggettoId, $user): void { - if ($soggettoId) { - $q->whereHas('proprieta', function ($qp) use ($soggettoId): void { - $qp->where('soggetto_id', $soggettoId); - }); - } - - $email = trim((string) $user->email); - if ($email !== '') { - $q->orWhereIn('id', function ($sub) use ($email): void { - $sub->select('ai.unita_immobiliare_id') - ->from('affitti_immobili as ai') - ->join('rubrica_universale as ru', 'ru.id', '=', 'ai.rubrica_inquilino_id') - ->whereNotNull('ai.unita_immobiliare_id') - ->whereRaw('LOWER(ru.email) = ?', [mb_strtolower($email)]); - }); - } - }); - - return $query->orderBy('stabile_id')->orderBy('scala')->orderBy('interno')->get(); - } } diff --git a/app/Http/Controllers/Condomino/TicketController.php b/app/Http/Controllers/Condomino/TicketController.php index a6110b2..39e0f7a 100755 --- a/app/Http/Controllers/Condomino/TicketController.php +++ b/app/Http/Controllers/Condomino/TicketController.php @@ -1,18 +1,19 @@ soggetto?->id_soggetto) && is_numeric($user->soggetto->id_soggetto)) { - return (int) $user->soggetto->id_soggetto; - } - - if (isset($user->soggetto?->id) && is_numeric($user->soggetto->id)) { - return (int) $user->soggetto->id; - } - - return null; - } - - private function resolveUserUnita(?User $user): Collection - { - if (! $user) { - return collect(); - } - - $soggettoId = $this->resolveSoggettoId($user); - - $query = UnitaImmobiliare::query() - ->with('stabile') - ->where(function ($q) use ($soggettoId, $user): void { - if ($soggettoId) { - $q->whereHas('proprieta', function ($qp) use ($soggettoId): void { - $qp->where('soggetto_id', $soggettoId); - }); - } - - $email = trim((string) $user->email); - if ($email !== '') { - $q->orWhereIn('id', function ($sub) use ($email): void { - $sub->select('ai.unita_immobiliare_id') - ->from('affitti_immobili as ai') - ->join('rubrica_universale as ru', 'ru.id', '=', 'ai.rubrica_inquilino_id') - ->whereNotNull('ai.unita_immobiliare_id') - ->whereRaw('LOWER(ru.email) = ?', [mb_strtolower($email)]); - }); - } - }); - - return $query->orderBy('stabile_id')->orderBy('scala')->orderBy('interno')->get(); - } } diff --git a/app/Http/Controllers/Condomino/UnitaController.php b/app/Http/Controllers/Condomino/UnitaController.php index e390ded..a448f67 100755 --- a/app/Http/Controllers/Condomino/UnitaController.php +++ b/app/Http/Controllers/Condomino/UnitaController.php @@ -1,22 +1,24 @@ where('soggetto_id', $user->soggetto->id_soggetto ?? null); - })->with(['stabile', 'proprieta.soggetto'])->get(); + + $unitaImmobiliari = $this->resolveUserUnita($user); return view('condomino.unita.index', compact('unitaImmobiliari')); } @@ -24,50 +26,63 @@ public function index() public function show(UnitaImmobiliare $unitaImmobiliare) { $user = Auth::user(); - + // Verifica accesso - $hasAccess = $unitaImmobiliare->proprieta() - ->where('soggetto_id', $user->soggetto->id_soggetto ?? null) - ->exists(); - - if (!$hasAccess) { + $hasAccess = $this->resolveUserUnita($user) + ->pluck('id') + ->contains((int) $unitaImmobiliare->id); + + if (! $hasAccess) { abort(403); } - $unitaImmobiliare->load(['stabile', 'proprieta.soggetto']); + $unitaImmobiliare->load(['stabile', 'soggetti', 'relazioniPersoneAttive.persona.emailMultiple', 'unitaRecapitiServizio']); - return view('condomino.unita.show', compact('unitaImmobiliare')); + $serviceLabels = UnitaRecapitoServizio::serviceLabels(); + $serviceRecipients = []; + $resolver = app(RecapitiServizioResolver::class); + foreach (array_keys($serviceLabels) as $serviceType) { + $serviceRecipients[$serviceType] = $resolver->resolveForUnita($unitaImmobiliare, $serviceType); + } + + return view('condomino.unita.show', compact('unitaImmobiliare', 'serviceLabels', 'serviceRecipients')); } public function richiestaModifica(Request $request, UnitaImmobiliare $unitaImmobiliare) { $user = Auth::user(); - + // Verifica accesso - $hasAccess = $unitaImmobiliare->proprieta() - ->where('soggetto_id', $user->soggetto->id_soggetto ?? null) - ->exists(); - - if (!$hasAccess) { + $hasAccess = $this->resolveUserUnita($user) + ->pluck('id') + ->contains((int) $unitaImmobiliare->id); + + if (! $hasAccess) { + abort(403); + } + + $soggettoId = $this->resolveSoggettoId($user); + + if (! $soggettoId) { abort(403); } $request->validate([ - 'tipo_modifica' => 'required|in:anagrafica,catastale,proprieta', - 'descrizione' => 'required|string', + 'tipo_modifica' => 'required|in:anagrafica,catastale,proprieta,catastale_servizio', + 'descrizione' => 'required|string', 'dati_proposti' => 'required|array', ]); RichiestaModifica::create([ - 'unita_immobiliare_id' => $unitaImmobiliare->id_unita, - 'soggetto_richiedente_id' => $user->soggetto->id_soggetto, - 'tipo_modifica' => $request->tipo_modifica, - 'descrizione' => $request->descrizione, - 'dati_attuali' => $unitaImmobiliare->toArray(), - 'dati_proposti' => $request->dati_proposti, - 'stato' => 'in_attesa', + 'unita_immobiliare_id' => $unitaImmobiliare->id, + 'soggetto_richiedente_id' => $soggettoId, + 'tipo_modifica' => $request->tipo_modifica, + 'descrizione' => $request->descrizione, + 'dati_attuali' => $unitaImmobiliare->toArray(), + 'dati_proposti' => $request->dati_proposti, + 'stato' => 'in_attesa', ]); return back()->with('success', 'Richiesta di modifica inviata con successo.'); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Fornitore/DipendenteController.php b/app/Http/Controllers/Fornitore/DipendenteController.php index 107d881..bb36895 100644 --- a/app/Http/Controllers/Fornitore/DipendenteController.php +++ b/app/Http/Controllers/Fornitore/DipendenteController.php @@ -18,18 +18,12 @@ public function index() { $fornitore = $this->resolveFornitoreForUser(); - $dipendenti = FornitoreDipendente::query() - ->with('user') - ->where('fornitore_id', $fornitore->id) - ->orderBy('attivo', 'desc') - ->orderBy('cognome') - ->orderBy('nome') - ->paginate(20); - - return view('fornitore.dipendenti.index', [ - 'fornitore' => $fornitore, - 'dipendenti' => $dipendenti, - ]); + return redirect()->to( + \App\Filament\Pages\Fornitore\Collaboratori::getUrl( + ['fornitore' => (int) $fornitore->id], + panel: 'admin-filament' + ) + ); } public function store(Request $request): RedirectResponse @@ -43,6 +37,7 @@ public function store(Request $request): RedirectResponse 'telefono' => ['nullable', 'string', 'max:50'], 'note' => ['nullable', 'string', 'max:2000'], 'create_user_access' => ['nullable', 'boolean'], + 'access_password' => ['nullable', 'string', 'min:8', 'max:100'], ]); $email = trim((string) ($validated['email'] ?? '')); @@ -67,16 +62,23 @@ public function store(Request $request): RedirectResponse $dipendente->created_by_user_id = Auth::id(); $dipendente->updated_by_user_id = Auth::id(); + $accessPassword = trim((string) ($validated['access_password'] ?? '')); + $createdPassword = null; + if ((bool) ($validated['create_user_access'] ?? false) && $email !== '') { $user = User::query()->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first(); if (! $user) { - $generatedPassword = Str::random(12); - $user = User::query()->create([ + $createdPassword = $accessPassword !== '' ? $accessPassword : Str::random(12); + $user = User::query()->create([ 'name' => trim($dipendente->nome . ' ' . ($dipendente->cognome ?? '')), 'email' => $email, - 'password' => Hash::make($generatedPassword), + 'password' => Hash::make($createdPassword), 'email_verified_at' => now(), ]); + } elseif ($accessPassword !== '') { + $user->password = Hash::make($accessPassword); + $user->save(); + $createdPassword = $accessPassword; } $role = Role::query()->where('name', 'fornitore')->first(); @@ -89,8 +91,17 @@ public function store(Request $request): RedirectResponse $dipendente->save(); - return redirect()->route('fornitore.dipendenti.index') + $redirect = redirect()->route('fornitore.dipendenti.index') ->with('success', 'Dipendente creato correttamente.'); + + if ($createdPassword !== null && $email !== '') { + $redirect->with('access_credentials', [ + 'email' => $email, + 'password' => $createdPassword, + ]); + } + + return $redirect; } public function update(Request $request, FornitoreDipendente $dipendente): RedirectResponse @@ -136,17 +147,35 @@ private function resolveFornitoreForUser(): Fornitore { $user = Auth::user(); - $fornitore = Fornitore::query() - ->where('email', (string) ($user?->email ?? '')) - ->first(); + $fornitore = null; - if (! $fornitore && $user) { - $link = FornitoreDipendente::query() - ->where('user_id', (int) $user->id) - ->where('attivo', true) - ->first(); - if ($link) { - $fornitore = Fornitore::query()->find((int) $link->fornitore_id); + if ($user) { + $email = mb_strtolower(trim((string) $user->email)); + + if ($email !== '') { + $fornitore = Fornitore::query() + ->whereRaw('LOWER(email) = ?', [$email]) + ->withCount(['ticketInterventi', 'dipendenti']) + ->orderByDesc('ticket_interventi_count') + ->orderByDesc('dipendenti_count') + ->orderByDesc('id') + ->first(); + } + + if (! $fornitore) { + $link = FornitoreDipendente::query() + ->where('attivo', true) + ->where(function ($q) use ($user): void { + $q->where('user_id', (int) $user->id) + ->orWhereRaw('LOWER(email) = ?', [mb_strtolower((string) $user->email)]); + }) + ->orderByRaw('CASE WHEN user_id = ? THEN 0 ELSE 1 END', [(int) $user->id]) + ->orderByDesc('id') + ->first(); + + if ($link) { + $fornitore = Fornitore::query()->find((int) $link->fornitore_id); + } } } diff --git a/app/Http/Controllers/Fornitore/TicketOperativoController.php b/app/Http/Controllers/Fornitore/TicketOperativoController.php index bef8051..b2dd80a 100644 --- a/app/Http/Controllers/Fornitore/TicketOperativoController.php +++ b/app/Http/Controllers/Fornitore/TicketOperativoController.php @@ -14,85 +14,81 @@ class TicketOperativoController extends Controller { public function index(Request $request) { - [$fornitore] = $this->resolveOperatoreContext(); + [$fornitore, $dipendente] = $this->resolveOperatoreContext(); + + $url = \App\Filament\Pages\Fornitore\TicketOperativi::getUrl( + ['fornitore' => (int) $fornitore->id], + panel: 'admin-filament' + ); $stato = (string) $request->query('stato', 'aperti'); - $query = TicketIntervento::query() - ->with(['ticket.stabile']) - ->where('fornitore_id', $fornitore->id) - ->orderByDesc('created_at'); - - if ($stato === 'chiusi') { - $query->whereIn('stato', ['chiuso', 'fatturato']); - } elseif ($stato === 'fatturabili') { - $query->whereIn('stato', ['fatturabile', 'fatturato']); - } else { - $query->whereNotIn('stato', ['chiuso']); + if ($stato !== '') { + $url .= '&stato=' . urlencode($stato); } - $interventi = $query->paginate(15)->withQueryString(); - - $rows = collect($interventi->items()) - ->map(fn(TicketIntervento $intervento): array=> $this->buildInterventoRow($intervento)); - - $fatturabili = TicketIntervento::query() - ->where('fornitore_id', $fornitore->id) - ->whereIn('stato', ['fatturabile', 'fatturato']) - ->orderByDesc('updated_at') - ->limit(100) - ->get(); - - return view('fornitore.tickets.index', [ - 'fornitore' => $fornitore, - 'interventi' => $interventi, - 'rows' => $rows, - 'stato' => $stato, - 'fatturabili' => $fatturabili, - ]); + return redirect()->to($url); } public function show(TicketIntervento $intervento) + { + [$fornitore, $dipendente] = $this->resolveOperatoreContext(); + $this->authorizeIntervento($intervento, $fornitore); + $this->authorizeDipendenteIntervento($intervento, $dipendente); + + return redirect()->to( + \App\Filament\Pages\Fornitore\TicketInterventoScheda::getUrl( + ['record' => (int) $intervento->id], + panel: 'admin-filament' + ) + ); + } + + public function assign(Request $request, TicketIntervento $intervento) { [$fornitore] = $this->resolveOperatoreContext(); $this->authorizeIntervento($intervento, $fornitore); - $intervento->load([ - 'ticket.stabile', - 'ticket.messages.user', - 'ticket.attachments.user', - 'ticket.apertoDaUser', - 'ticket.assegnatoAUser', - 'ticket.assegnatoAFornitore', - 'eseguitoDaDipendente', + $validated = $request->validate([ + 'dipendente_id' => 'nullable|integer', ]); - $storico = TicketIntervento::query() - ->with(['ticket']) - ->where('fornitore_id', $fornitore->id) - ->where('id', '!=', $intervento->id) - ->when( - (int) ($intervento->ticket?->soggetto_richiedente_id ?? 0) > 0, - fn($q) => $q->whereHas('ticket', fn($t) => $t->where('soggetto_richiedente_id', (int) $intervento->ticket->soggetto_richiedente_id)), - fn($q) => $q->whereHas('ticket', fn($t) => $t->where('stabile_id', (int) ($intervento->ticket?->stabile_id ?? 0))) - ) - ->latest('id') - ->limit(12) - ->get(); + $dipendenteId = (int) ($validated['dipendente_id'] ?? 0); + $dipendente = null; - $caller = $this->extractCallerData((string) ($intervento->ticket?->descrizione ?? '')); + if ($dipendenteId > 0) { + $dipendente = FornitoreDipendente::query() + ->where('fornitore_id', (int) $fornitore->id) + ->where('attivo', true) + ->find($dipendenteId); - return view('fornitore.tickets.show', [ - 'fornitore' => $fornitore, - 'intervento' => $intervento, - 'storico' => $storico, - 'caller' => $caller, + abort_unless($dipendente instanceof FornitoreDipendente, 404, 'Dipendente non valido per questo fornitore.'); + } + + $intervento->eseguito_da_dipendente_id = $dipendente?->id; + $intervento->save(); + + $stamp = now()->format('d/m/Y H:i'); + $message = $dipendente + ? '[' . $stamp . '] Intervento assegnato al dipendente fornitore: ' . $dipendente->nome_completo . '.' + : '[' . $stamp . '] Assegnazione dipendente rimossa: intervento riportato al coordinamento del fornitore.'; + + $intervento->ticket->messages()->create([ + 'user_id' => Auth::id(), + 'messaggio' => $message, + 'canale' => 'interno', + 'direzione' => 'inbound', + 'inviato_il' => now(), ]); + + return redirect()->route('fornitore.tickets.show', $intervento) + ->with('success', $dipendente ? 'Intervento assegnato al dipendente selezionato.' : 'Assegnazione dipendente rimossa.'); } public function start(TicketIntervento $intervento) { [$fornitore, $dipendente] = $this->resolveOperatoreContext(); $this->authorizeIntervento($intervento, $fornitore); + $this->authorizeDipendenteIntervento($intervento, $dipendente); $intervento->update([ 'stato' => 'in_corso', @@ -111,6 +107,7 @@ public function complete(Request $request, TicketIntervento $intervento) { [$fornitore, $dipendente] = $this->resolveOperatoreContext(); $this->authorizeIntervento($intervento, $fornitore); + $this->authorizeDipendenteIntervento($intervento, $dipendente); $validated = $request->validate([ 'rapporto_fornitore' => 'required|string|max:5000', @@ -243,8 +240,9 @@ public function complete(Request $request, TicketIntervento $intervento) public function sollecito(TicketIntervento $intervento) { - [$fornitore] = $this->resolveOperatoreContext(); + [$fornitore, $dipendente] = $this->resolveOperatoreContext(); $this->authorizeIntervento($intervento, $fornitore); + $this->authorizeDipendenteIntervento($intervento, $dipendente); $stamp = now()->format('d/m/Y H:i'); $intervento->ticket->messages()->create([ @@ -283,24 +281,37 @@ private function resolveOperatoreContext(): array } $dipendente = null; - $fornitore = Fornitore::query() - ->where('email', (string) $user->email) - ->first(); + $fornitore = null; - if (! $fornitore && $user) { - $dipendente = FornitoreDipendente::query() - ->where('attivo', true) - ->where(function ($q) use ($user): void { - $q->where('user_id', (int) $user->id) - ->orWhereRaw('LOWER(email) = ?', [mb_strtolower((string) $user->email)]); - }) - ->orderByDesc('id') - ->first(); + if ($user) { + $email = mb_strtolower(trim((string) $user->email)); - if ($dipendente) { - $fornitore = Fornitore::query()->find((int) $dipendente->fornitore_id); - $dipendente->ultimo_accesso_at = now(); - $dipendente->save(); + if ($email !== '') { + $fornitore = Fornitore::query() + ->whereRaw('LOWER(email) = ?', [$email]) + ->withCount(['ticketInterventi', 'dipendenti']) + ->orderByDesc('ticket_interventi_count') + ->orderByDesc('dipendenti_count') + ->orderByDesc('id') + ->first(); + } + + if (! $fornitore) { + $dipendente = FornitoreDipendente::query() + ->where('attivo', true) + ->where(function ($q) use ($user): void { + $q->where('user_id', (int) $user->id) + ->orWhereRaw('LOWER(email) = ?', [mb_strtolower((string) $user->email)]); + }) + ->orderByRaw('CASE WHEN user_id = ? THEN 0 ELSE 1 END', [(int) $user->id]) + ->orderByDesc('id') + ->first(); + + if ($dipendente) { + $fornitore = Fornitore::query()->find((int) $dipendente->fornitore_id); + $dipendente->ultimo_accesso_at = now(); + $dipendente->save(); + } } } @@ -314,6 +325,16 @@ private function authorizeIntervento(TicketIntervento $intervento, Fornitore $fo abort_unless((int) $intervento->fornitore_id === (int) $fornitore->id, 403); } + private function authorizeDipendenteIntervento(TicketIntervento $intervento, ?FornitoreDipendente $dipendente): void + { + if (! $dipendente instanceof FornitoreDipendente) { + return; + } + + $assignedDipendenteId = (int) ($intervento->eseguito_da_dipendente_id ?? 0); + abort_if($assignedDipendenteId > 0 && $assignedDipendenteId !== (int) $dipendente->id, 403, 'Intervento assegnato a un altro operatore del fornitore.'); + } + /** * @return array{ingresso:string,contatto:string,telefono:string,problema:string,apparato:string} */ diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index fdec747..b2159a9 100755 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -1,14 +1,12 @@ check()) { // Se l'utente è autenticato... - $user = Auth::user(); // ...recupera l'utente + $user = Auth::user(); // ...recupera l'utente // Reindirizza l'utente alla dashboard specifica del suo ruolo - if ($user->hasRole('super-admin')) { return redirect()->route('superadmin.dashboard'); } - if ($user->hasRole(['admin', 'amministratore'])) { return redirect()->route('admin.dashboard'); } - if ($user->hasRole('condomino')) { return redirect()->route('condomino.dashboard'); } + if ($user->hasRole('super-admin')) {return redirect('/admin-filament');} + if ($user->hasRole(['admin', 'amministratore', 'collaboratore'])) {return redirect('/admin-filament');} + if ($user->hasRole('fornitore')) {return redirect()->to(TicketOperativi::getUrl(panel: 'admin-filament'));} + if ($user->hasRole('condomino')) {return redirect()->route('condomino.dashboard');} // Fallback per utenti autenticati senza un ruolo specifico o con un ruolo non gestito - return redirect()->route('dashboard'); + return redirect()->route('dashboard'); } } // Se l'utente non è autenticato, continua con la richiesta // Questo permette di accedere alle rotte pubbliche come login, registrazione, ecc - - return $next($request); + + return $next($request); } } diff --git a/app/Models/AdempimentoFiscale.php b/app/Models/AdempimentoFiscale.php new file mode 100644 index 0000000..5d80546 --- /dev/null +++ b/app/Models/AdempimentoFiscale.php @@ -0,0 +1,42 @@ + 'integer', + 'scadenza' => 'date', + 'data_completamento' => 'date', + 'metadati' => 'array', + ]; + + public function stabile(): BelongsTo + { + return $this->belongsTo(Stabile::class, 'stabile_id'); + } + + public function gestione(): BelongsTo + { + return $this->belongsTo(GestioneContabile::class, 'gestione_id'); + } +} diff --git a/app/Models/InsuranceClaim.php b/app/Models/InsuranceClaim.php index 36a8dfa..4f84820 100644 --- a/app/Models/InsuranceClaim.php +++ b/app/Models/InsuranceClaim.php @@ -11,6 +11,7 @@ class InsuranceClaim extends Model protected $fillable = [ 'ticket_id', 'stabile_id', + 'insurance_policy_id', 'policy_reference', 'claim_number', 'status', @@ -29,6 +30,11 @@ public function ticket() return $this->belongsTo(Ticket::class, 'ticket_id'); } + public function insurancePolicy() + { + return $this->belongsTo(InsurancePolicy::class, 'insurance_policy_id'); + } + public function communicationMessages() { return $this->hasMany(CommunicationMessage::class, 'insurance_claim_id'); diff --git a/app/Models/InsurancePolicy.php b/app/Models/InsurancePolicy.php new file mode 100644 index 0000000..e4eac52 --- /dev/null +++ b/app/Models/InsurancePolicy.php @@ -0,0 +1,79 @@ + 'date', + 'expires_at' => 'date', + 'renewal_at' => 'date', + 'annual_amount' => 'decimal:2', + 'metadata' => 'array', + ]; + + public function stabile() + { + return $this->belongsTo(Stabile::class, 'stabile_id'); + } + + public function brokerRubrica() + { + return $this->belongsTo(RubricaUniversale::class, 'broker_rubrica_id'); + } + + public function companyRubrica() + { + return $this->belongsTo(RubricaUniversale::class, 'company_rubrica_id'); + } + + public function claims() + { + return $this->hasMany(InsuranceClaim::class, 'insurance_policy_id'); + } + + public function getDisplayNameAttribute(): string + { + $label = trim((string) ($this->policy_name ?? '')); + if ($label !== '') { + return $label; + } + + $label = trim((string) ($this->policy_number ?? '')); + if ($label !== '') { + return $label; + } + + return 'Polizza #' . (int) $this->id; + } +} \ No newline at end of file diff --git a/app/Models/Persona.php b/app/Models/Persona.php index 5eea419..3bac370 100755 --- a/app/Models/Persona.php +++ b/app/Models/Persona.php @@ -1,11 +1,10 @@ 'date', + 'data_nascita' => 'date', 'data_consenso_privacy' => 'datetime', - 'consenso_privacy' => 'boolean', - 'consenso_marketing' => 'boolean', - 'attivo' => 'boolean' + 'consenso_privacy' => 'boolean', + 'consenso_marketing' => 'boolean', + 'attivo' => 'boolean', ]; /** @@ -65,7 +64,7 @@ public function unitaImmobiliari(): BelongsToMany 'riceve_comunicazioni', 'riceve_convocazioni', 'vota_assemblea', - 'note_relazione' + 'note_relazione', ]) ->withTimestamps() ->wherePivot('attivo', true); @@ -95,6 +94,11 @@ public function emailMultiple(): HasMany return $this->hasMany(PersonaEmailMultipla::class); } + public function unitaRecapitiServizio(): HasMany + { + return $this->hasMany(UnitaRecapitoServizio::class, 'persona_id'); + } + /** * Società di cui è rappresentante legale */ @@ -226,7 +230,7 @@ public static function verificaDuplicati($datiPersona, $escludiId = null) $duplicati = []; // 1️⃣ CONTROLLO CODICE FISCALE (Critico) - if (!empty($datiPersona['codice_fiscale'])) { + if (! empty($datiPersona['codice_fiscale'])) { $query = static::where('codice_fiscale', $datiPersona['codice_fiscale']); if ($escludiId) { $query->where('id', '!=', $escludiId); @@ -234,34 +238,34 @@ public static function verificaDuplicati($datiPersona, $escludiId = null) if ($existing = $query->first()) { $duplicati[] = [ - 'tipo' => 'CRITICO', - 'campo' => 'Codice Fiscale', + 'tipo' => 'CRITICO', + 'campo' => 'Codice Fiscale', 'persona_esistente' => $existing, - 'azione' => 'BLOCCA_O_MERGE' + 'azione' => 'BLOCCA_O_MERGE', ]; } } // 2️⃣ CONTROLLO TELEFONO PRINCIPALE (Critico) - if (!empty($datiPersona['telefono_principale'])) { + if (! empty($datiPersona['telefono_principale'])) { $telefono = preg_replace('/[^\d+]/', '', $datiPersona['telefono_principale']); - $query = static::where('telefono_principale', $telefono); + $query = static::where('telefono_principale', $telefono); if ($escludiId) { $query->where('id', '!=', $escludiId); } if ($existing = $query->first()) { $duplicati[] = [ - 'tipo' => 'CRITICO', - 'campo' => 'Telefono Principale', + 'tipo' => 'CRITICO', + 'campo' => 'Telefono Principale', 'persona_esistente' => $existing, - 'azione' => 'BLOCCA_O_MERGE' + 'azione' => 'BLOCCA_O_MERGE', ]; } } // 3️⃣ CONTROLLO EMAIL (Warning) - if (!empty($datiPersona['email_principale'])) { + if (! empty($datiPersona['email_principale'])) { $query = static::where('email_principale', $datiPersona['email_principale']); if ($escludiId) { $query->where('id', '!=', $escludiId); @@ -269,16 +273,16 @@ public static function verificaDuplicati($datiPersona, $escludiId = null) if ($existing = $query->first()) { $duplicati[] = [ - 'tipo' => 'WARNING', - 'campo' => 'Email Principale', + 'tipo' => 'WARNING', + 'campo' => 'Email Principale', 'persona_esistente' => $existing, - 'azione' => 'AVVISA' + 'azione' => 'AVVISA', ]; } } // 4️⃣ CONTROLLO OMONIMIA (Warning) - if (!empty($datiPersona['cognome']) && !empty($datiPersona['nome']) && !empty($datiPersona['data_nascita'])) { + if (! empty($datiPersona['cognome']) && ! empty($datiPersona['nome']) && ! empty($datiPersona['data_nascita'])) { $query = static::where('cognome', $datiPersona['cognome']) ->where('nome', $datiPersona['nome']) ->where('data_nascita', $datiPersona['data_nascita']); @@ -288,10 +292,10 @@ public static function verificaDuplicati($datiPersona, $escludiId = null) if ($existing = $query->first()) { $duplicati[] = [ - 'tipo' => 'WARNING', - 'campo' => 'Omonimia Completa', + 'tipo' => 'WARNING', + 'campo' => 'Omonimia Completa', 'persona_esistente' => $existing, - 'azione' => 'VERIFICA_MANUALE' + 'azione' => 'VERIFICA_MANUALE', ]; } } @@ -315,7 +319,7 @@ protected static function boot() // Audit delle modifiche static::updating(function ($persona) { - $changes = $persona->getChanges(); + $changes = $persona->getChanges(); $original = $persona->getOriginal(); foreach ($changes as $campo => $nuovoValore) { diff --git a/app/Models/Stabile.php b/app/Models/Stabile.php index 97cd0ba..60e457a 100755 --- a/app/Models/Stabile.php +++ b/app/Models/Stabile.php @@ -5,6 +5,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use App\Models\RubricaUniversale; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; class Stabile extends Model { @@ -163,6 +165,16 @@ public function amministratore() return $this->belongsTo(Amministratore::class, 'amministratore_id', 'id'); } + public function amministratoreTransfers(): HasMany + { + return $this->hasMany(StabileAmministratoreTransfer::class, 'stabile_id', 'id'); + } + + public function latestAmministratoreTransfer(): HasOne + { + return $this->hasOne(StabileAmministratoreTransfer::class, 'stabile_id', 'id')->latestOfMany(); + } + /** * Rubrica universale collegata (opzionale) */ @@ -187,6 +199,14 @@ public function tickets() return $this->hasMany(Ticket::class, 'stabile_id', 'id'); } + public function insurancePolicies() + { + return $this->hasMany(InsurancePolicy::class, 'stabile_id', 'id') + ->orderByDesc('renewal_at') + ->orderByDesc('expires_at') + ->orderByDesc('id'); + } + /** * Relazione con RipartizioneSpese */ diff --git a/app/Models/StabileAmministratoreTransfer.php b/app/Models/StabileAmministratoreTransfer.php new file mode 100644 index 0000000..517ce33 --- /dev/null +++ b/app/Models/StabileAmministratoreTransfer.php @@ -0,0 +1,54 @@ + 'boolean', + 'meta' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + public function stabile(): BelongsTo + { + return $this->belongsTo(Stabile::class, 'stabile_id', 'id'); + } + + public function fromAmministratore(): BelongsTo + { + return $this->belongsTo(Amministratore::class, 'from_amministratore_id', 'id'); + } + + public function toAmministratore(): BelongsTo + { + return $this->belongsTo(Amministratore::class, 'to_amministratore_id', 'id'); + } + + public function changedByUser(): BelongsTo + { + return $this->belongsTo(User::class, 'changed_by_user_id', 'id'); + } +} \ No newline at end of file diff --git a/app/Models/StabileServizioLettura.php b/app/Models/StabileServizioLettura.php index 2ef3d61..b6693b2 100644 --- a/app/Models/StabileServizioLettura.php +++ b/app/Models/StabileServizioLettura.php @@ -25,10 +25,21 @@ class StabileServizioLettura extends Model 'protocollo_numero', 'richiesta_lettura_inviata_at', 'prossima_lettura_scadenza_at', + 'deadline_lettura_at', + 'sollecito_inviato_at', + 'rilevatore_tipo', + 'rilevatore_nome', 'archivio_documentale_path', 'archivio_documentale_sha256', + 'lettura_precedente_valore', 'lettura_inizio', 'lettura_fine', + 'lettura_foto_path', + 'lettura_foto_original_name', + 'lettura_foto_metadata', + 'lettura_precedente_foto_path', + 'lettura_ocr_valore', + 'lettura_ocr_confidenza', 'consumo_valore', 'consumo_unita', 'importo_totale', @@ -41,8 +52,13 @@ class StabileServizioLettura extends Model 'periodo_al' => 'date', 'richiesta_lettura_inviata_at' => 'datetime', 'prossima_lettura_scadenza_at' => 'datetime', + 'deadline_lettura_at' => 'datetime', + 'sollecito_inviato_at' => 'datetime', + 'lettura_precedente_valore' => 'decimal:3', 'lettura_inizio' => 'decimal:3', 'lettura_fine' => 'decimal:3', + 'lettura_foto_metadata' => 'array', + 'lettura_ocr_confidenza' => 'decimal:2', 'consumo_valore' => 'decimal:3', 'importo_totale' => 'decimal:2', 'raw' => 'array', diff --git a/app/Models/UnitaImmobiliare.php b/app/Models/UnitaImmobiliare.php index 72f6f50..80d2aab 100755 --- a/app/Models/UnitaImmobiliare.php +++ b/app/Models/UnitaImmobiliare.php @@ -1,13 +1,12 @@ 'decimal:2', - 'rendita_catastale' => 'decimal:2', - 'superficie_commerciale' => 'decimal:2', - 'superficie_calpestabile' => 'decimal:2', - 'superficie_balconi' => 'decimal:2', - 'superficie_giardino_privato' => 'decimal:2', - 'ha_balconi' => 'boolean', - 'ha_giardino_privato' => 'boolean', - 'ha_posto_auto_assegnato' => 'boolean', - 'ha_cantina_assegnata' => 'boolean', - 'ha_soffitta_assegnata' => 'boolean', - 'ha_climatizzazione' => 'boolean', - 'ha_impianto_gas' => 'boolean', - 'ha_cambio_destinazione_uso' => 'boolean', - 'millesimi_generali' => 'decimal:3', - 'millesimi_riscaldamento' => 'decimal:3', - 'millesimi_acqua' => 'decimal:3', - 'millesimi_ascensore' => 'decimal:3', - 'millesimi_scale' => 'decimal:3', - 'valore_commerciale_stimato' => 'decimal:2', - 'canone_locazione_mensile' => 'decimal:2', + 'consistenza' => 'decimal:2', + 'rendita_catastale' => 'decimal:2', + 'superficie_commerciale' => 'decimal:2', + 'superficie_calpestabile' => 'decimal:2', + 'superficie_balconi' => 'decimal:2', + 'superficie_giardino_privato' => 'decimal:2', + 'ha_balconi' => 'boolean', + 'ha_giardino_privato' => 'boolean', + 'ha_posto_auto_assegnato' => 'boolean', + 'ha_cantina_assegnata' => 'boolean', + 'ha_soffitta_assegnata' => 'boolean', + 'ha_climatizzazione' => 'boolean', + 'ha_impianto_gas' => 'boolean', + 'ha_cambio_destinazione_uso' => 'boolean', + 'millesimi_generali' => 'decimal:3', + 'millesimi_riscaldamento' => 'decimal:3', + 'millesimi_acqua' => 'decimal:3', + 'millesimi_ascensore' => 'decimal:3', + 'millesimi_scale' => 'decimal:3', + 'valore_commerciale_stimato' => 'decimal:2', + 'canone_locazione_mensile' => 'decimal:2', 'data_scadenza_autorizzazioni' => 'date', - 'data_ultima_valutazione' => 'date', - 'attiva' => 'boolean', - 'unita_demo' => 'boolean', + 'data_ultima_valutazione' => 'date', + 'attiva' => 'boolean', + 'unita_demo' => 'boolean', ]; /** @@ -225,6 +224,23 @@ public function rubricaRuoliAttivi(): HasMany }); } + public function relazioniPersone(): HasMany + { + return $this->hasMany(PersonaUnitaRelazione::class, 'unita_id'); + } + + public function relazioniPersoneAttive(): HasMany + { + return $this->relazioniPersone()->where('attivo', true)->where(function ($query) { + $query->whereNull('data_fine')->orWhere('data_fine', '>=', now()->toDateString()); + }); + } + + public function unitaRecapitiServizio(): HasMany + { + return $this->hasMany(UnitaRecapitoServizio::class, 'unita_id'); + } + // Scope methods public function scopeAttive($query) { @@ -322,27 +338,27 @@ public function getUnitaVicine() // Unità sovrastante if ($sovrastante = $this->getUnitaSovrastante()) { $vicine->push([ - 'unita' => $sovrastante, - 'relazione' => 'sovrastante', - 'descrizione' => 'Piano superiore' + 'unita' => $sovrastante, + 'relazione' => 'sovrastante', + 'descrizione' => 'Piano superiore', ]); } // Unità sottostante if ($sottostante = $this->getUnitaSottostante()) { $vicine->push([ - 'unita' => $sottostante, - 'relazione' => 'sottostante', - 'descrizione' => 'Piano inferiore' + 'unita' => $sottostante, + 'relazione' => 'sottostante', + 'descrizione' => 'Piano inferiore', ]); } // Unità laterali foreach ($this->getUnitaLaterali() as $laterale) { $vicine->push([ - 'unita' => $laterale, - 'relazione' => 'laterale', - 'descrizione' => 'Stesso piano' + 'unita' => $laterale, + 'relazione' => 'laterale', + 'descrizione' => 'Stesso piano', ]); } @@ -373,7 +389,7 @@ public function isLibera() public function hasAutorizzazioniScadute() { return $this->data_scadenza_autorizzazioni && - $this->data_scadenza_autorizzazioni->isPast(); + $this->data_scadenza_autorizzazioni->isPast(); } /** diff --git a/app/Models/UnitaRecapitoServizio.php b/app/Models/UnitaRecapitoServizio.php new file mode 100644 index 0000000..c961d39 --- /dev/null +++ b/app/Models/UnitaRecapitoServizio.php @@ -0,0 +1,67 @@ + 'integer', + 'is_default' => 'boolean', + 'attivo' => 'boolean', + 'last_confirmed_at' => 'datetime', + ]; + + public static function serviceLabels(): array + { + return [ + 'comunicazioni' => 'Comunicazioni generali', + 'convocazioni' => 'Convocazioni e verbali', + 'ticket' => 'Ticket e segnalazioni', + 'solleciti' => 'Solleciti e scadenze', + 'contabilita' => 'Contabilita e rate', + 'amministrazione' => 'Amministrazione', + 'anagrafe' => 'Anagrafe condominiale', + ]; + } + + public function unitaImmobiliare(): BelongsTo + { + return $this->belongsTo(UnitaImmobiliare::class, 'unita_id'); + } + + public function persona(): BelongsTo + { + return $this->belongsTo(Persona::class, 'persona_id'); + } + + public function scopeAttivi($query) + { + return $query->where('attivo', true); + } + + public function scopePerServizio($query, string $tipoServizio) + { + return $query->where('tipo_servizio', $tipoServizio); + } +} diff --git a/app/Services/Comunicazioni/RecapitiServizioResolver.php b/app/Services/Comunicazioni/RecapitiServizioResolver.php new file mode 100644 index 0000000..6cadced --- /dev/null +++ b/app/Services/Comunicazioni/RecapitiServizioResolver.php @@ -0,0 +1,110 @@ +loadMissing([ + 'unitaRecapitiServizio.persona', + 'relazioniPersoneAttive.persona.emailMultiple', + ]); + + $resolved = []; + + foreach ($unita->unitaRecapitiServizio as $row) { + if (! $row->attivo || $row->tipo_servizio !== $tipoServizio) { + continue; + } + + $resolved[$this->normalizeEmailKey($row->email)] = [ + 'email' => mb_strtolower(trim((string) $row->email)), + 'source' => 'override_unita', + 'label' => $row->etichetta ?: $labels[$tipoServizio], + 'persona' => $row->persona?->nome_completo, + ]; + } + + if ($resolved !== []) { + return array_values($resolved); + } + + foreach ($unita->relazioniPersoneAttive as $relazione) { + if (! $this->relationAllowsService($relazione, $tipoServizio)) { + continue; + } + + $persona = $relazione->persona; + if (! $persona instanceof Persona) { + continue; + } + + foreach ($this->emailsForPersona($persona) as $email) { + $key = $this->normalizeEmailKey($email); + if ($key === '') { + continue; + } + + $resolved[$key] = [ + 'email' => $key, + 'source' => 'persona', + 'label' => $labels[$tipoServizio], + 'persona' => $persona->nome_completo, + ]; + } + } + + return array_values($resolved); + } + + private function relationAllowsService(PersonaUnitaRelazione $relazione, string $tipoServizio): bool + { + return match ($tipoServizio) { + 'convocazioni' => (bool) $relazione->riceve_convocazioni, + 'solleciti', 'contabilita', 'comunicazioni', 'ticket', 'amministrazione', 'anagrafe' => (bool) $relazione->riceve_comunicazioni, + default => false, + }; + } + + /** + * @return array + */ + private function emailsForPersona(Persona $persona): array + { + $emails = []; + + $primary = mb_strtolower(trim((string) $persona->email_principale)); + if ($primary !== '') { + $emails[] = $primary; + } + + foreach ($persona->emailMultiple as $row) { + if (! $row instanceof PersonaEmailMultipla || ! $row->attiva) { + continue; + } + + $email = mb_strtolower(trim((string) $row->email)); + if ($email !== '') { + $emails[] = $email; + } + } + + return array_values(array_unique($emails)); + } + + private function normalizeEmailKey(?string $email): string + { + return mb_strtolower(trim((string) $email)); + } +} diff --git a/app/Services/GesconImport/StabileEnrichmentService.php b/app/Services/GesconImport/StabileEnrichmentService.php index 5698f5e..bc74a71 100644 --- a/app/Services/GesconImport/StabileEnrichmentService.php +++ b/app/Services/GesconImport/StabileEnrichmentService.php @@ -1,10 +1,9 @@ $legacyCode, 'catasto' => false, 'banca_inline' => false, 'mdb_found' => false]; - if (!$legacyCode) return $result; - if (!Schema::hasTable('stabili')) return $result; + if (! $legacyCode) { + return $result; + } + + if (! Schema::hasTable('stabili')) { + return $result; + } + // Trova record stabile di dominio $stabile = DB::table('stabili')->where(function ($q) use ($legacyCode) { - if (Schema::hasColumn('stabili', 'codice_stabile')) $q->orWhere('codice_stabile', $legacyCode); + if (Schema::hasColumn('stabili', 'codice_stabile')) { + $q->orWhere('codice_stabile', $legacyCode); + } + $q->orWhere('denominazione', $legacyCode); // fallback })->first(); - if (!$stabile) return $result; + if (! $stabile) { + return $result; + } + + $mdbPath = $this->resolveMdbPath($basePath); + if (! is_file($mdbPath) || ! is_readable($mdbPath)) { + return $result; + } - $mdbPath = rtrim($basePath, '/') . '/Stabili.mdb'; - if (!is_file($mdbPath) || !is_readable($mdbPath)) return $result; $result['mdb_found'] = true; $row = $this->extractRow($mdbPath, $legacyCode); - if (!$row) return $result; + if (! $row) { + return $result; + } $updates = []; // Catasto fields mapping heuristic (legacy → logical field) $mapCatasto = [ - 'foglio' => ['foglio', 'ac_foglio', 'foglio_catasto'], - 'particella' => ['particella', 'ac_particella', 'mappale', 'catasto_particella'], - 'subalterno' => ['sub', 'subalterno', 'ac_sub', 'ac_subalterno'], - 'sezione' => ['sezione', 'ac_sezione', 'sez'], - 'categoria' => ['categoria_catastale', 'categoria', 'cat_categoria'], - 'classe' => ['classe', 'cat_classe'], - 'consistenza' => ['consistenza', 'cat_consistenza'], - 'rendita_catastale' => ['rendita_catastale', 'rendita', 'cat_rendita'], + 'foglio' => ['foglio', 'ac_foglio', 'foglio_catasto'], + 'particella' => ['particella', 'ac_particella', 'mappale', 'catasto_particella'], + 'subalterno' => ['sub', 'subalterno', 'ac_sub', 'ac_subalterno'], + 'sezione' => ['sezione', 'ac_sezione', 'sez'], + 'categoria' => ['categoria_catastale', 'categoria', 'cat_categoria'], + 'classe' => ['classe', 'cat_classe'], + 'consistenza' => ['consistenza', 'cat_consistenza'], + 'rendita_catastale' => ['rendita_catastale', 'rendita', 'cat_rendita'], 'superficie_catastale' => ['superficie_catastale', 'superficie'], ]; $targetColumnAliases = [ - 'foglio' => ['foglio', 'foglio_catasto'], - 'particella' => ['particella', 'particella_catasto', 'mappale'], - 'subalterno' => ['subalterno'], - 'sezione' => ['sezione'], - 'categoria' => ['categoria_catastale', 'categoria'], - 'classe' => ['classe', 'classe_catastale'], - 'consistenza' => ['consistenza', 'consistenza_catastale'], - 'rendita_catastale' => ['rendita_catastale'], + 'foglio' => ['foglio', 'foglio_catasto'], + 'particella' => ['particella', 'particella_catasto', 'mappale'], + 'subalterno' => ['subalterno'], + 'sezione' => ['sezione'], + 'categoria' => ['categoria_catastale', 'categoria'], + 'classe' => ['classe', 'classe_catastale'], + 'consistenza' => ['consistenza', 'consistenza_catastale'], + 'rendita_catastale' => ['rendita_catastale'], 'superficie_catastale' => ['superficie_catastale'], ]; foreach ($mapCatasto as $logical => $candidates) { $column = $this->resolveStabileColumn($targetColumnAliases[$logical] ?? [$logical]); - if (!$column) { + if (! $column) { continue; } $current = $stabile->$column ?? null; - if (!empty($current) && $current !== 'ND') { + if (! empty($current) && $current !== 'ND') { continue; } foreach ($candidates as $cand) { @@ -73,7 +88,7 @@ public function enrich(string $legacyCode, string $basePath = '/mnt/gescon-archi break; } } - if (!empty(array_intersect(array_keys($updates), array_filter(array_map(function ($aliases) { + if (! empty(array_intersect(array_keys($updates), array_filter(array_map(function ($aliases) { foreach ($aliases as $alias) { if (Schema::hasColumn('stabili', $alias)) { return $alias; @@ -90,7 +105,7 @@ public function enrich(string $legacyCode, string $basePath = '/mnt/gescon-archi $cc = $row['cod_comune'] ?? ($row['codice_catastale'] ?? ($row['cod_catastale'] ?? null)); if ($cc && is_string($cc) && trim($cc) !== '') { $updates[$codiceComuneColumn] = strtoupper(trim($cc)); - $result['catasto'] = true; + $result['catasto'] = true; } } @@ -100,14 +115,14 @@ public function enrich(string $legacyCode, string $basePath = '/mnt/gescon-archi $currentIban = $stabile->$ibanColumn ?? null; if (empty($currentIban)) { $ibanKey = $this->firstAvailable($row, ['iban_condominio', 'iban', 'iban1', 'iban_1']); - if ($ibanKey && !empty($row[$ibanKey])) { - $updates[$ibanColumn] = trim($row[$ibanKey]); + if ($ibanKey && ! empty($row[$ibanKey])) { + $updates[$ibanColumn] = trim($row[$ibanKey]); $result['banca_inline'] = true; } } } - if (!empty($updates)) { + if (! empty($updates)) { $updates['updated_at'] = now(); DB::table('stabili')->where('id', $stabile->id)->update($updates); } @@ -117,13 +132,19 @@ public function enrich(string $legacyCode, string $basePath = '/mnt/gescon-archi private function extractRow(string $mdbPath, string $legacyCode): ?array { try { - $binTables = trim((string)@shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables'; - $binExport = trim((string)@shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export'; - if (!is_file($mdbPath) || !is_readable($mdbPath)) return null; - $tables = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: ''; + $binTables = trim((string) @shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables'; + $binExport = trim((string) @shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export'; + if (! is_file($mdbPath) || ! is_readable($mdbPath)) { + return null; + } + + $tables = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: ''; $candidateTables = []; foreach (['Stabili', 'stabili', 'Condomin', 'condomin'] as $t) { - if (preg_match('/(^|\n)' . preg_quote($t, '/') . '(\n|$)/', $tables)) $candidateTables[] = $t; + if (preg_match('/(^|\n)' . preg_quote($t, '/') . '(\n|$)/', $tables)) { + $candidateTables[] = $t; + } + } $candidateTables = array_unique($candidateTables); foreach ($candidateTables as $table) { @@ -139,27 +160,33 @@ private function extractRow(string $mdbPath, string $legacyCode): ?array escapeshellarg($tmp) ); @shell_exec($cmd); - if (!is_file($tmp) || filesize($tmp) === 0) { + if (! is_file($tmp) || filesize($tmp) === 0) { @unlink($tmp); continue; } $fh = fopen($tmp, 'r'); - if (!$fh) { + if (! $fh) { @unlink($tmp); continue; } $headers = fgetcsv($fh, 0, '|'); - if (!$headers) { + if (! $headers) { fclose($fh); @unlink($tmp); continue; } - $headers = array_map(fn($h) => strtolower(trim((string)$h)), $headers); + $headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers); while (($cols = fgetcsv($fh, 0, '|')) !== false) { - if (count($cols) !== count($headers)) continue; + if (count($cols) !== count($headers)) { + continue; + } + $row = @array_combine($headers, $cols) ?: []; - if (!$row) continue; - $codeFields = ['cod_stabile', 'codice', 'codice_stabile', 'stabile', 'cod']; + if (! $row) { + continue; + } + + $codeFields = ['cod_stabile', 'codice', 'codice_stabile', 'stabile', 'cod', 'nome_directory', 'codice_directory', 'internet_cod_stab']; foreach ($codeFields as $cf) { if (isset($row[$cf]) && trim($row[$cf]) === $legacyCode) { fclose($fh); @@ -180,7 +207,10 @@ private function extractRow(string $mdbPath, string $legacyCode): ?array private function firstAvailable(array $row, array $keys): ?string { foreach ($keys as $k) { - if (isset($row[$k]) && trim((string)$row[$k]) !== '') return $k; + if (isset($row[$k]) && trim((string) $row[$k]) !== '') { + return $k; + } + } return null; } @@ -194,4 +224,23 @@ private function resolveStabileColumn(array $candidates): ?string } return null; } + + private function resolveMdbPath(string $basePath): string + { + $basePath = rtrim($basePath, '/'); + $candidates = [ + $basePath . '/dbc/Stabili.mdb', + $basePath . '/dbc/stabili.mdb', + $basePath . '/Stabili.mdb', + $basePath . '/stabili.mdb', + ]; + + foreach ($candidates as $candidate) { + if (is_file($candidate) && is_readable($candidate)) { + return $candidate; + } + } + + return $candidates[0]; + } } diff --git a/app/Services/Stabili/StabileTransferService.php b/app/Services/Stabili/StabileTransferService.php new file mode 100644 index 0000000..25eece2 --- /dev/null +++ b/app/Services/Stabili/StabileTransferService.php @@ -0,0 +1,61 @@ +amministratore_id ?? 0); + $toId = (int) $destination->id; + + if ($fromId === $toId) { + throw new \InvalidArgumentException('Lo stabile e` gia` assegnato a questo amministratore.'); + } + + return DB::transaction(function () use ($stabile, $destination, $actor, $alsoRubrica, $reason, $source, $ipAddress, $meta, $fromId, $toId): StabileAmministratoreTransfer { + $stabile->amministratore_id = $toId; + $stabile->save(); + + if ($alsoRubrica && (int) ($stabile->rubrica_id ?? 0) > 0) { + if (Schema::hasTable('rubrica_universale') && Schema::hasColumn('rubrica_universale', 'amministratore_id')) { + DB::table('rubrica_universale') + ->where('id', (int) $stabile->rubrica_id) + ->update(['amministratore_id' => $toId, 'updated_at' => now()]); + } + } + + if (method_exists($destination, 'provisionArchiveIfMissing')) { + $destination->provisionArchiveIfMissing(); + } + + return StabileAmministratoreTransfer::query()->create([ + 'stabile_id' => (int) $stabile->id, + 'from_amministratore_id' => $fromId > 0 ? $fromId : null, + 'to_amministratore_id' => $toId, + 'changed_by_user_id' => $actor?->id, + 'changed_by_name' => $actor?->name ?: 'Sistema', + 'source' => $source, + 'reason' => $reason !== null ? trim($reason) : null, + 'also_rubrica' => $alsoRubrica, + 'ip_address' => $ipAddress, + 'meta' => $meta, + ]); + }); + } +} \ No newline at end of file diff --git a/config/distribution.php b/config/distribution.php index ea02dba..bcaebec 100644 --- a/config/distribution.php +++ b/config/distribution.php @@ -24,6 +24,12 @@ // Timeout client per check/download. 'http_timeout_seconds' => (int) env('NETGESCON_UPDATE_HTTP_TIMEOUT', 60), + // Backup pre-update: abilita upload su Google Drive solo se richiesto dal nodo. + 'preupdate_drive_enabled' => (bool) env('NETGESCON_PREUPDATE_DRIVE_ENABLED', false), + + // Se true, l'update fallisce quando la copia Drive non puo essere completata. + 'preupdate_require_drive' => (bool) env('NETGESCON_PREUPDATE_REQUIRE_DRIVE', false), + // Override opzionale DNS per richieste update (formato CSV host:port:ip). // Esempio: updates.netgescon.it:443:192.168.0.53 'http_resolve' => env('NETGESCON_UPDATE_RESOLVE', ''), diff --git a/config/netgescon.php b/config/netgescon.php index 673ebc0..3814568 100755 --- a/config/netgescon.php +++ b/config/netgescon.php @@ -3,7 +3,7 @@ return [ 'version' => env('NETGESCON_VERSION', '0.8.1'), 'ui' => [ - 'force_filament' => env('NETGESCON_FORCE_FILAMENT', false), + 'force_filament' => env('NETGESCON_FORCE_FILAMENT', true), ], /* |-------------------------------------------------------------------------- diff --git a/database/migrations/2026_03_18_090000_create_unita_recapiti_servizio_table.php b/database/migrations/2026_03_18_090000_create_unita_recapiti_servizio_table.php new file mode 100644 index 0000000..fa06573 --- /dev/null +++ b/database/migrations/2026_03_18_090000_create_unita_recapiti_servizio_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignId('unita_id')->constrained('unita_immobiliari')->cascadeOnDelete(); + $table->foreignId('persona_id')->nullable()->constrained('persone')->nullOnDelete(); + $table->string('tipo_servizio', 50); + $table->string('email', 255); + $table->string('etichetta', 120)->nullable(); + $table->text('note')->nullable(); + $table->unsignedInteger('ordine')->default(1); + $table->boolean('is_default')->default(true); + $table->boolean('attivo')->default(true); + $table->string('sorgente', 50)->default('manuale'); + $table->timestamp('last_confirmed_at')->nullable(); + $table->timestamps(); + + $table->index(['unita_id', 'tipo_servizio']); + $table->index(['persona_id', 'tipo_servizio']); + $table->index(['attivo', 'tipo_servizio']); + $table->unique(['unita_id', 'persona_id', 'tipo_servizio', 'email'], 'unita_recapito_servizio_unico'); + }); + } + + public function down(): void + { + Schema::dropIfExists('unita_recapiti_servizio'); + } +}; diff --git a/database/migrations/2026_03_19_120000_create_adempimenti_fiscali_table.php b/database/migrations/2026_03_19_120000_create_adempimenti_fiscali_table.php new file mode 100644 index 0000000..48dce1e --- /dev/null +++ b/database/migrations/2026_03_19_120000_create_adempimenti_fiscali_table.php @@ -0,0 +1,39 @@ +id(); + $table->string('tenant_id', 50)->nullable()->index(); + $table->foreignId('stabile_id')->constrained('stabili')->cascadeOnDelete(); + $table->foreignId('gestione_id')->nullable()->constrained('gestioni_contabili')->nullOnDelete(); + $table->string('tipo', 50)->index(); + $table->integer('anno_fiscale')->nullable()->index(); + $table->string('titolo', 255); + $table->text('descrizione')->nullable(); + $table->string('stato', 30)->default('bozza')->index(); + $table->date('scadenza')->nullable()->index(); + $table->date('data_completamento')->nullable(); + $table->string('periodicita', 30)->nullable(); + $table->json('metadati')->nullable(); + $table->timestamps(); + + $table->index(['stabile_id', 'anno_fiscale', 'tipo'], 'adempimenti_fiscali_stabile_anno_tipo_idx'); + }); + } + + public function down(): void + { + Schema::dropIfExists('adempimenti_fiscali'); + } +}; diff --git a/database/migrations/2026_03_20_160000_create_stabile_amministratore_transfers_table.php b/database/migrations/2026_03_20_160000_create_stabile_amministratore_transfers_table.php new file mode 100644 index 0000000..b796cd4 --- /dev/null +++ b/database/migrations/2026_03_20_160000_create_stabile_amministratore_transfers_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('stabile_id')->constrained('stabili')->cascadeOnDelete(); + $table->foreignId('from_amministratore_id')->nullable()->constrained('amministratori')->nullOnDelete(); + $table->foreignId('to_amministratore_id')->constrained('amministratori')->cascadeOnDelete(); + $table->foreignId('changed_by_user_id')->nullable()->constrained('users')->nullOnDelete(); + $table->string('changed_by_name', 150)->nullable(); + $table->string('source', 50)->default('portal-superadmin'); + $table->text('reason')->nullable(); + $table->boolean('also_rubrica')->default(true); + $table->string('ip_address', 45)->nullable(); + $table->json('meta')->nullable(); + $table->timestamps(); + + $table->index(['stabile_id', 'created_at'], 'stabile_transfer_stabile_created_idx'); + $table->index(['to_amministratore_id', 'created_at'], 'stabile_transfer_dest_created_idx'); + }); + } + + public function down(): void + { + Schema::dropIfExists('stabile_amministratore_transfers'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_03_21_101000_create_insurance_policies_table.php b/database/migrations/2026_03_21_101000_create_insurance_policies_table.php new file mode 100644 index 0000000..2cc0ea6 --- /dev/null +++ b/database/migrations/2026_03_21_101000_create_insurance_policies_table.php @@ -0,0 +1,66 @@ +id(); + $table->foreignId('stabile_id')->constrained('stabili')->cascadeOnDelete(); + $table->foreignId('broker_rubrica_id')->nullable()->constrained('rubrica_universale')->nullOnDelete(); + $table->foreignId('company_rubrica_id')->nullable()->constrained('rubrica_universale')->nullOnDelete(); + $table->string('policy_name')->nullable(); + $table->string('policy_number')->nullable(); + $table->string('policy_reference')->nullable(); + $table->string('status', 50)->default('attiva'); + $table->date('started_at')->nullable(); + $table->date('expires_at')->nullable(); + $table->date('renewal_at')->nullable(); + $table->decimal('annual_amount', 12, 2)->nullable(); + $table->string('payment_split')->nullable(); + $table->text('payment_schedule_notes')->nullable(); + $table->text('coverage_summary')->nullable(); + $table->longText('special_conditions')->nullable(); + $table->longText('claim_form_template')->nullable(); + $table->string('policy_document_path')->nullable(); + $table->string('policy_document_name')->nullable(); + $table->string('policy_document_mime')->nullable(); + $table->string('signature_image_path')->nullable(); + $table->text('notes')->nullable(); + $table->json('metadata')->nullable(); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + $table->index(['stabile_id', 'status']); + $table->index(['policy_number']); + $table->index(['expires_at']); + }); + } + + if (Schema::hasTable('insurance_claims') && ! Schema::hasColumn('insurance_claims', 'insurance_policy_id')) { + Schema::table('insurance_claims', function (Blueprint $table): void { + $table->foreignId('insurance_policy_id') + ->nullable() + ->after('stabile_id') + ->constrained('insurance_policies') + ->nullOnDelete(); + }); + } + } + + public function down(): void + { + if (Schema::hasTable('insurance_claims') && Schema::hasColumn('insurance_claims', 'insurance_policy_id')) { + Schema::table('insurance_claims', function (Blueprint $table): void { + $table->dropConstrainedForeignId('insurance_policy_id'); + }); + } + + Schema::dropIfExists('insurance_policies'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_03_21_102000_extend_stabile_servizio_letture_for_water_module.php b/database/migrations/2026_03_21_102000_extend_stabile_servizio_letture_for_water_module.php new file mode 100644 index 0000000..07ba0f6 --- /dev/null +++ b/database/migrations/2026_03_21_102000_extend_stabile_servizio_letture_for_water_module.php @@ -0,0 +1,88 @@ +dateTime('deadline_lettura_at')->nullable()->after('prossima_lettura_scadenza_at'); + } + + if (! Schema::hasColumn('stabile_servizio_letture', 'sollecito_inviato_at')) { + $table->dateTime('sollecito_inviato_at')->nullable()->after('deadline_lettura_at'); + } + + if (! Schema::hasColumn('stabile_servizio_letture', 'rilevatore_tipo')) { + $table->string('rilevatore_tipo', 40)->nullable()->after('sollecito_inviato_at'); + } + + if (! Schema::hasColumn('stabile_servizio_letture', 'rilevatore_nome')) { + $table->string('rilevatore_nome')->nullable()->after('rilevatore_tipo'); + } + + if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_precedente_valore')) { + $table->decimal('lettura_precedente_valore', 12, 3)->nullable()->after('lettura_inizio'); + } + + if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_foto_path')) { + $table->string('lettura_foto_path')->nullable()->after('lettura_fine'); + } + + if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_foto_original_name')) { + $table->string('lettura_foto_original_name')->nullable()->after('lettura_foto_path'); + } + + if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_foto_metadata')) { + $table->json('lettura_foto_metadata')->nullable()->after('lettura_foto_original_name'); + } + + if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_precedente_foto_path')) { + $table->string('lettura_precedente_foto_path')->nullable()->after('lettura_foto_metadata'); + } + + if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_ocr_valore')) { + $table->string('lettura_ocr_valore', 64)->nullable()->after('lettura_precedente_foto_path'); + } + + if (! Schema::hasColumn('stabile_servizio_letture', 'lettura_ocr_confidenza')) { + $table->decimal('lettura_ocr_confidenza', 5, 2)->nullable()->after('lettura_ocr_valore'); + } + }); + } + + public function down(): void + { + if (! Schema::hasTable('stabile_servizio_letture')) { + return; + } + + Schema::table('stabile_servizio_letture', function (Blueprint $table): void { + foreach ([ + 'lettura_ocr_confidenza', + 'lettura_ocr_valore', + 'lettura_precedente_foto_path', + 'lettura_foto_metadata', + 'lettura_foto_original_name', + 'lettura_foto_path', + 'lettura_precedente_valore', + 'rilevatore_nome', + 'rilevatore_tipo', + 'sollecito_inviato_at', + 'deadline_lettura_at', + ] as $column) { + if (Schema::hasColumn('stabile_servizio_letture', $column)) { + $table->dropColumn($column); + } + } + }); + } +}; \ No newline at end of file diff --git a/docs/00-COPILOT-MASTER-GUIDE.md b/docs/00-COPILOT-MASTER-GUIDE.md index 38f3781..4ed19ed 100755 --- a/docs/00-COPILOT-MASTER-GUIDE.md +++ b/docs/00-COPILOT-MASTER-GUIDE.md @@ -1,4 +1,5 @@ # 🤖 NETGESCON - GUIDA PER GITHUB COPILOT/AI + ## 📋 Istruzioni Complete per Continuare lo Sviluppo > **🎯 DOCUMENTO MASTER** per GitHub Copilot/AI @@ -7,19 +8,38 @@ ## 📋 Istruzioni Complete per Continuare lo Sviluppo --- +## Stato Documento + +Questo documento contiene materiale storico utile come contesto, ma non definisce piu il workspace attivo. + +Regola corrente: + +- workspace attivo: `/home/michele/netgescon/netgescon-day0` +- documentazione autorevole: `/home/michele/netgescon/netgescon-day0/docs` +- repository Git da usare per commit, push e distribuzione: `netgescon-day0` +- `netgescon-laravel` resta archivio tecnico e non va usato come base operativa corrente + +Per le regole aggiornate prevale sempre [docs/DOCS-AND-WORKSPACE-POLICY-DAY0.md](/home/michele/netgescon/netgescon-day0/docs/DOCS-AND-WORKSPACE-POLICY-DAY0.md). + +--- + ## 🚨 **ATTENZIONE: PRIMA DI INIZIARE** ### 📍 **Percorso di lavoro attuale (DEV)** -- Usare **/home/michele/netgescon/netgescon-laravel** per tutti i comandi (composer, artisan, import). -- Non usare **/var/www/netgescon**: è l'albero produzione ed è stato escluso dal workspace finché non validiamo le importazioni. + +- Usare **/home/michele/netgescon/netgescon-day0** per tutti i comandi (composer, artisan, import, test, git). +- Non usare **netgescon-laravel** come base di sviluppo corrente: resta archivio storico. +- Non usare **/var/www/netgescon** come albero di sviluppo. ### ⚠️ **REGOLA D'ORO: NON PERDERE I DATI** + 1. **MAI** modificare direttamente il database di produzione 2. **SEMPRE** fare backup prima di modifiche strutturali 3. **TESTARE** ogni modifica su ambiente di sviluppo 4. **VERIFICARE** autenticazione e permessi dopo ogni modifica ### 🔑 **CREDENZIALI E ACCESSI ATTUALI** + ```bash # Login applicazione URL: http://192.168.0.200:8000 @@ -42,6 +62,7 @@ # Database ## 📂 **STRUTTURA PROGETTO - MAPPA COMPLETA** ### 🏠 **DIRECTORY PRINCIPALE** + ``` ~/netgescon/ ├── netgescon-laravel/ # 🌐 APPLICAZIONE PRINCIPALE LARAVEL @@ -65,12 +86,15 @@ ### 🏠 **DIRECTORY PRINCIPALE** ### 🎯 **ENTRY POINT - DA DOVE INIZIARE** #### 1️⃣ **DOCUMENTO PRINCIPALE** + **📍 INIZIO QUI:** [`docs/00-INDICE-DOCS-UNIFICATA.md`](00-INDICE-DOCS-UNIFICATA.md) + - Contiene **navigazione completa** di tutta la documentazione - Link diretti a tutti i documenti chiave - Scenari di navigazione per ogni situazione #### 2️⃣ **DOCUMENTI CHIAVE FONDAMENTALI** + 1. **[`docs/00-transizione-linux/README-TRANSITION-COMPLETE.md`](00-transizione-linux/README-TRANSITION-COMPLETE.md)** - 🔑 **GUIDA TRANSIZIONE COMPLETA** - Struttura progetto, configurazione, troubleshooting @@ -95,6 +119,7 @@ #### 2️⃣ **DOCUMENTI CHIAVE FONDAMENTALI** ## 🔧 **WORKFLOW SVILUPPO SICURO** ### ✅ **PROCEDURA STANDARD** + ```bash # 1. Backup prima di qualsiasi modifica cd ~/netgescon/netgescon-laravel @@ -116,6 +141,7 @@ # 5. Deploy step-by-step ``` ### ⚠️ **COSA NON FARE MAI** + - ❌ `php artisan migrate:fresh` in produzione - ❌ Modificare .env senza backup - ❌ Cancellare migrations esistenti @@ -127,6 +153,7 @@ ### ⚠️ **COSA NON FARE MAI** ## 🎯 **TASK PRIORITARI - COSA FARE DOPO** ### 🚀 **IMMEDIATE (Prossimi giorni)** + 1. **Completare gestione economica** - File: `app/Http/Controllers/EconomicaController.php` - Riferimento: [`docs/00-transizione-linux/FEATURES-INVENTORY-COMPLETE.md`](00-transizione-linux/FEATURES-INVENTORY-COMPLETE.md) sezione "GESTIONE ECONOMICA" @@ -140,6 +167,7 @@ ### 🚀 **IMMEDIATE (Prossimi giorni)** - Riferimento: [`docs/07-API-INTEGRAZIONI.md`](07-API-INTEGRAZIONI.md) ### 📋 **BREVE TERMINE (1-2 settimane)** + 1. **API REST complete** - Directory: `routes/api.php` - Riferimento: [`docs/02-architettura-laravel/ARCHITETTURA_MODULARE_COMPLETATA.md`](02-architettura-laravel/ARCHITETTURA_MODULARE_COMPLETATA.md) @@ -155,15 +183,18 @@ ## 🛠️ **SCRIPT E AUTOMAZIONE DISPONIBILI** ### 📁 **Directory:** `docs/03-scripts-automazione/` #### 🔧 **Setup e Manutenzione** + - `setup-complete-environment.sh` - Setup ambiente completo - `fix-vscode-install.sh` - Fix installazione VS Code - `manage-database.sh` - Gestione database (backup/restore) #### 🔄 **Sincronizzazione** + - `sync-bidirectional.sh` - Sync bidirezionale file - `sync-to-remote.sh` - Sync verso server remoto #### ⚡ **Testing e Deploy** + - `test-dashboard.sh` - Test dashboard e funzionalità - `quick-deploy.sh` - Deploy rapido - `verify-system.sh` - Verifica sistema @@ -173,6 +204,7 @@ #### ⚡ **Testing e Deploy** ## 🗄️ **COMPRENSIONE DATABASE** ### 📊 **Tabelle Principali** + ```sql -- Riferimento: docs/04-DATABASE-STRUTTURE.md stabili # Edifici/condomini @@ -183,6 +215,7 @@ ### 📊 **Tabelle Principali** ``` ### 🔑 **Migrations Critiche** + ```bash # Directory: database/migrations/ # ATTENZIONE: Non modificare quelle esistenti @@ -190,6 +223,7 @@ # Per nuove modifiche: php artisan make:migration nome_modifica ``` ### 👥 **Sistema Autenticazione** + - **Controller:** `app/Http/Controllers/Auth/SecureDashboardController.php` - **Middleware:** `app/Http/Middleware/` - **Spatie Roles:** Sistema ruoli e permessi @@ -200,16 +234,19 @@ ### 👥 **Sistema Autenticazione** ## 🎨 **SISTEMA INTERFACCIA** ### 🏗️ **Layout Universale** + - **File principale:** `resources/views/components/layout/universal.blade.php` - **Sistema sidebar:** `resources/views/components/sidebar-dynamic.blade.php` - **AJAX navigation:** `public/js/dashboard.js` ### 📱 **Componenti UI** + - **Cards dashboard:** `resources/views/dashboard/` - **Form modali:** `resources/views/forms/` - **Tabelle data:** `resources/views/tables/` ### 🎯 **Riferimenti Interfaccia** + - [`docs/05-INTERFACCIA-UNIVERSALE.md`](05-INTERFACCIA-UNIVERSALE.md) - **Sistema completo** - [`docs/02-architettura-laravel/sidebar-modulare.md`](02-architettura-laravel/sidebar-modulare.md) - **Sidebar** - [`docs/08-FRONTEND-UX.md`](08-FRONTEND-UX.md) - **Frontend guidelines** @@ -219,6 +256,7 @@ ### 🎯 **Riferimenti Interfaccia** ## 🔍 **TROUBLESHOOTING RAPIDO** ### ❌ **Problema: Dashboard non carica** + ```bash # Soluzione rapida php artisan optimize:clear @@ -227,6 +265,7 @@ # Riferimento: docs/01-manuali-aggiuntivi/QUICK_REFERENCE_LARAVEL.md ``` ### ❌ **Problema: Errori permessi** + ```bash # Fix permessi sudo chown -R www-data:www-data ~/netgescon/netgescon-laravel @@ -235,6 +274,7 @@ # Fix permessi ``` ### ❌ **Problema: Database connection** + ```bash # Verifica configurazione cat .env | grep DB_ @@ -243,6 +283,7 @@ # Riferimento: docs/00-transizione-linux/README-TRANSITION-COMPLETE.md ``` ### ❌ **Problema: Autenticazione rotta** + ```bash # Verifica ruoli php artisan tinker @@ -255,6 +296,7 @@ # Riferimento: docs/06-SISTEMA-MULTI-RUOLO.md ## 🔄 **SINCRONIZZAZIONE SERVER REMOTO** ### 📡 **Script Rsync Pronto** + ```bash # Da eseguire per sincronizzare con server remoto cd ~/netgescon @@ -267,6 +309,7 @@ # Da eseguire per sincronizzare con server remoto ``` ### 🎯 **Cartelle da Sincronizzare SEMPRE** + - `docs/` - Documentazione completa - `netgescon-laravel/app/` - Codice applicazione - `netgescon-laravel/resources/` - Template e asset @@ -274,6 +317,7 @@ ### 🎯 **Cartelle da Sincronizzare SEMPRE** - `netgescon-laravel/.env.example` - Configurazione template ### ⚠️ **Cartelle da NON Sincronizzare** + - `netgescon-laravel/storage/logs/` - Log locali - `netgescon-laravel/vendor/` - Dipendenze (ricostruire con composer) - `netgescon-laravel/node_modules/` - Dipendenze npm @@ -284,6 +328,7 @@ ### ⚠️ **Cartelle da NON Sincronizzare** ## 🤖 **MESSAGGI PER IL TUO ALTER EGO** ### 💬 **Cosa dirgli per iniziare** + ``` "Ciao! Sono il tuo 'alter ego' Michele. Ho preparato tutto per te: @@ -305,6 +350,7 @@ ### 💬 **Cosa dirgli per iniziare** ``` ### 🎯 **Come risolvere primi problemi** + ``` "Per i primi problemi: @@ -322,6 +368,7 @@ ### 🎯 **Come risolvere primi problemi** ## 📋 **CHECKLIST FINALE HANDOFF** ### ✅ **Verifica Completezza** + - [ ] Documentazione unificata in docs/ - [ ] Script automazione testati - [ ] Database funzionante @@ -330,6 +377,7 @@ ### ✅ **Verifica Completezza** - [ ] Rsync script preparato ### ✅ **Test Funzionalità** + - [ ] Login admin funziona - [ ] Dashboard carica - [ ] CRUD stabili operativo @@ -337,6 +385,7 @@ ### ✅ **Test Funzionalità** - [ ] Sidebar AJAX funziona ### ✅ **Documentazione Accessibile** + - [ ] Indici navigabili - [ ] Cross-reference funzionanti - [ ] Screenshot disponibili @@ -347,12 +396,12 @@ ### ✅ **Documentazione Accessibile** ## 🚀 **PASSAGGIO DI CONSEGNE COMPLETATO** > **🎯 Michele → GitHub Copilot/AI** -> +> > **Data handoff:** 18/07/2025 > **Stato sistema:** ✅ Funzionante e documentato > **Copertura documentazione:** 301 file, 11MB > **Entry point:** `docs/00-INDICE-DOCS-UNIFICATA.md` -> +> > **🔑 Tutto è pronto per continuare lo sviluppo in autonomia!** --- diff --git a/docs/00-STRATEGIA-GIT-DISTRIBUZIONE.md b/docs/00-STRATEGIA-GIT-DISTRIBUZIONE.md index cda143e..a6b8dda 100755 --- a/docs/00-STRATEGIA-GIT-DISTRIBUZIONE.md +++ b/docs/00-STRATEGIA-GIT-DISTRIBUZIONE.md @@ -6,9 +6,23 @@ # 🚀 NETGESCON - STRATEGIA GIT DISTRIBUITO E SISTEMA DISTRIBUZIONE --- +## Nota Day0 + +Per l'operativita corrente del progetto vale questa regola prioritaria: + +- il solo repository attivo e distribuibile e `netgescon-day0` +- il solo workspace di sviluppo da usare e `/home/michele/netgescon/netgescon-day0` +- la documentazione attuale da seguire e `/home/michele/netgescon/netgescon-day0/docs` +- eventuali riferimenti storici a `~/netgescon/docs` o `netgescon-laravel` sono da considerare archivio o contesto legacy + +In caso di conflitto prevalgono sempre le regole Day0 e la policy [docs/DOCS-AND-WORKSPACE-POLICY-DAY0.md](/home/michele/netgescon/netgescon-day0/docs/DOCS-AND-WORKSPACE-POLICY-DAY0.md). + +--- + ## 🏗️ **ARCHITETTURA GIT DISTRIBUITA** ### 🎯 **FILOSOFIA SISTEMA** + - **Autonomia completa** del team di sviluppo - **Controllo versioni** professionale interno - **Distribuzione sicura** agli utilizzatori finali @@ -41,6 +55,7 @@ ### 🌐 **STRUTTURA GIT MULTI-LIVELLO** ## ✅ **STATUS IMPLEMENTAZIONE** ### 🎯 **COMPLETATO** + - ✅ Repository Git inizializzato con commit iniziale - ✅ Struttura branches creata: master, development, release, hotfix - ✅ Scripts di automazione Git workflow @@ -49,6 +64,7 @@ ### 🎯 **COMPLETATO** - ✅ Plugin system progettato ### 🔄 **IN CORSO** + - 🚧 Setup Git server (Gitea) su macchina master - 🚧 Configurazione domini: git.netgescon.it - 🚧 Sistema distribuzione pacchetti @@ -60,6 +76,7 @@ ## 🔧 **SETUP GIT SERVER INTERNO** ### 📋 **Configurazione Git Server su MASTER** #### 1️⃣ **Setup Gitea/GitLab Self-Hosted** + ```bash # Opzione A: Gitea (leggero e veloce) - SCRIPT PREPARATO sudo docs/03-scripts-automazione/setup-git-server-master.sh @@ -81,6 +98,7 @@ # Opzione B: GitLab CE (più completo) ``` #### 2️⃣ **Configurazione Accessi** + ```bash # URL Git server interno https://git.netgescon.local # Web interface @@ -95,6 +113,7 @@ # Users del team ### 🗂️ **Repository Structure** #### 📦 **netgescon-core.git** + ``` netgescon-core/ ├── app/ # Laravel application @@ -109,6 +128,7 @@ #### 📦 **netgescon-core.git** ``` #### 🔌 **netgescon-plugins.git** + ``` netgescon-plugins/ ├── core-plugins/ # Plugin essenziali @@ -122,6 +142,7 @@ #### 🔌 **netgescon-plugins.git** ``` #### 🎨 **netgescon-themes.git** + ``` netgescon-themes/ ├── default/ # Tema di default @@ -139,6 +160,7 @@ ## 📋 **WORKFLOW DI SVILUPPO** ### 🔄 **Processo di Sviluppo Interno** #### 1️⃣ **Sviluppo Locale (Michele + AI)** + ```bash # Clone del repo principale git clone git@git.netgescon.local:netgescon/netgescon-core.git @@ -155,6 +177,7 @@ # Merge request via web interface ``` #### 2️⃣ **Review e Testing** + ```bash # AI Remoto testa la feature git checkout feature/nuova-funzionalita @@ -168,6 +191,7 @@ # Se OK, merge in develop ``` #### 3️⃣ **Release Process** + ```bash # Quando develop è stabile git checkout main @@ -180,6 +204,7 @@ # Trigger deploy automatico ``` ### 🚀 **Branching Strategy** + ``` main # Produzione stabile ├── develop # Sviluppo attivo @@ -195,6 +220,7 @@ ## 📦 **SISTEMA DISTRIBUZIONE AUTOMATICA** ### 🎯 **Multi-Platform Distribution** #### 1️⃣ **Distribuzione via APT Repository** + ```bash # Setup repository NetGescon curl -fsSL https://packages.netgescon.it/gpg | sudo apt-key add - @@ -209,6 +235,7 @@ # Auto-update ``` #### 2️⃣ **Distribuzione via Docker** + ```bash # Pull immagine ufficiale docker pull netgescon/netgescon:latest @@ -219,6 +246,7 @@ # Deploy completo con docker-compose ``` #### 3️⃣ **Distribuzione via VM Template** + ```bash # Download template VM curl -fsSL https://get.netgescon.it/netgescon-vm-latest.ova -o netgescon.ova @@ -230,6 +258,7 @@ # Auto-configurazione al primo boot ### ⚙️ **Auto-Updater System** #### 📋 **Script Auto-Update** + ```bash #!/bin/bash # /usr/local/bin/netgescon-updater @@ -255,6 +284,7 @@ # Check for updates ``` #### 🔐 **Gestione Permessi Update** + ```php // Nel core NetGescon class UpdateManager { @@ -279,6 +309,7 @@ ## 🔌 **PLUGIN SYSTEM ARCHITECTURE** ### 🏗️ **Framework Plugin** #### 📋 **Plugin Structure** + ```php ticket senza passare solo da allegati manuali. + +## Deploy e staging + +Dopo aver copiato i file in staging eseguire sempre: + +```bash +php artisan migrate --force +php artisan optimize:clear +``` + +La migration da applicare e: + +- `2026_03_18_090000_create_unita_recapiti_servizio_table.php` + +## Limiti attuali + +- il collegamento portale -> persona avviene solo quando l'utente autenticato e riconoscibile con sicurezza tramite email principale o email multipla della persona e una relazione attiva con le unita accessibili; +- la gestione completa di mailbox Google/IMAP non e ancora automatizzata; +- il servizio catastale e registrato come richiesta strutturata, non ancora come workflow economico/fatturabile completo. diff --git a/docs/OPERATIVITA-STUDIO-COLLABORATORI-POSTA.md b/docs/OPERATIVITA-STUDIO-COLLABORATORI-POSTA.md new file mode 100644 index 0000000..30d3f9e --- /dev/null +++ b/docs/OPERATIVITA-STUDIO-COLLABORATORI-POSTA.md @@ -0,0 +1,69 @@ +# Operativita Studio: Collaboratori, SMDR e Posta + +## Collaboratori studio da Rubrica Universale + +Percorso operativo previsto: + +1. Crea o aggiorna il nominativo in Rubrica Universale con email reale. +2. Apri la scheda rubrica del nominativo. +3. Usa la tab `Collaboratore studio`. +4. Abilita l'accesso collaboratore. +5. Assegna l'interno PBX. +6. Seleziona gli stabili del tenant da associare al collaboratore. +7. Salva interno e stabili. + +Regole applicative: + +- Se esiste gia un utente con la stessa email, il sistema riusa quell'utente. +- Viene aggiunto il solo ruolo `collaboratore` senza rimuovere eventuali altri ruoli gia presenti. +- La sincronizzazione degli stabili agisce solo sugli stabili dell'amministratore corrente. +- Gli stabili assegnati fuori tenant non vengono rimossi. + +## SMDR / Post-It tecnico + +La tab tecnica Post-It ora distingue tre casi: + +- `Interno collaboratore`: numero riconosciuto come `users.pbx_extension`. +- `Numero studio gestito`: numero esterno che coincide con uno dei numeri centralino configurati nella scheda amministratore. +- `Numero esterno`: tutto cio che non rientra nei due casi precedenti. + +Filtri disponibili: + +- vista chiamate: esterne, tutte, interne +- ambito numero: tutti, studio, interno, esterno +- direzione: inbound, outbound, internal +- ricerca libera su numero, interno o raw line + +Nota tecnica: + +- gli interni PBX non vengono piu risolti tramite Rubrica Universale come se fossero telefoni esterni. + +## Posta: stato reale e percorso consigliato + +Stato attuale: + +- configurazione SMTP presente +- configurazione Google OAuth presente +- configurazione caselle Gmail/IMAP/PEC presente +- archiviazione manuale EML su ticket presente +- polling automatico caselle non ancora implementato come flusso applicativo stabile + +Percorso consigliato per rendere la posta pronta via web: + +1. Attiva un dominio posta reale, ad esempio `netgescon.it` su Google Workspace. +2. Crea la casella operativa, ad esempio `info@netgescon.it` o `studio@netgescon.it`. +3. Mantieni `netgescon@gmail.com` solo come account storico o tecnico, non come casella principale di produzione. +4. Configura in Scheda Amministratore: + - client id + - client secret + - redirect uri + - workspace email + - label/query Gmail +5. Usa il pulsante web `Collega Google`. +6. Usa il pulsante web `Verifica Google OAuth` per controllare APP_URL, redirect e credenziali. +7. Quando il check e pulito, il passo successivo di sviluppo da fare e un importer applicativo per Gmail/IMAP che trasformi i messaggi in `communication_messages`, ticket o documenti. + +Indicazione architetturale: + +- tutte le verifiche e le azioni operative vanno esposte da pagina web amministrativa +- la shell deve restare uno strumento di manutenzione, non il percorso operativo principale del prodotto ospitato diff --git a/docs/README.md b/docs/README.md index a2a0823..592f52e 100755 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,17 @@ # 🏢 NetGescon - Sistema di Gestione Condominiale > **Sistema Unificato** per la gestione completa di condomini, stabili, unità immobiliari e amministrazione condominiale. +## Nota Operativa Day0 + +Questo repository e questo workspace sono la sola base attiva di sviluppo, test, commit, push e distribuzione. + +- Workspace attivo: `/home/michele/netgescon/netgescon-day0` +- Documentazione autorevole: `/home/michele/netgescon/netgescon-day0/docs` +- Repository Git da usare: `netgescon-day0` +- La cartella storica `netgescon-laravel` va considerata archivio tecnico e non base di lavoro corrente. + +Se un documento riporta path o workflow precedenti, prevalgono sempre le regole Day0. + ## 📎 Descrizione NetGescon è una piattaforma web sviluppata in **Laravel** per la gestione completa di condomini e amministrazioni condominiali. Il sistema offre funzionalità avanzate per: @@ -23,20 +34,23 @@ ## � **Tecnologie** ## 🛠️ **Installazione** ### 📋 **Prerequisiti** + - PHP 8.3+ - Composer - MySQL/MariaDB - Node.js + NPM (per asset) ### ⚡ Setup rapido + 1) Installa dipendenze PHP: composer install 2) Copia env e chiave: cp .env.example .env; php artisan key:generate 3) Migrazioni/seed: php artisan migrate:fresh --seed 4) Avvio server: php artisan serve --host=0.0.0.0 --port=8000 ### 🔑 **Primo Accesso** -- **URL:** http://localhost:8000 -- **Email:** admin@example.com + +- **URL:** +- **Email:** - **Password:** password ## 🧭 Documentazione & Indici @@ -46,34 +60,41 @@ ## 🧭 Documentazione & Indici - `docs/automation/` – commit log mensile, summary giornalieri e protocollo handoff automatici. - `07-IMPORT-UNITA-IMMOBILIARI.md` – blueprint operativo per importare unità e collegare proprietari/inquilini. - `08-ANAGRAFICA-UNICA-UI.md` – specifica UX della scheda anagrafica unica e relativi widget condominiali. +- `OPERATIVITA-STUDIO-COLLABORATORI-POSTA.md` – flusso operativo per collaboratori studio, filtri SMDR/Post-It e roadmap posta Google/IMAP via web. +- `ANAGRAFE-CONDOMINIALE-PORTALE-POSTA.md` – base tecnica e operativa per anagrafe self-service, recapiti email per servizio e primo aggancio posta-ticket. ## 📁 Struttura Progetto (workspace) -- `/var/www/netgescon` – app Laravel principale (PHP 8.3, Laravel 12) -- `/home/michele/netgescon/docs` – documentazione completa (vedi indici sopra) -- `/home/michele/netgescon/scripts` – script import/export e riconciliazioni +- `/home/michele/netgescon/netgescon-day0` – app Laravel principale e repository Git attivo +- `/home/michele/netgescon/netgescon-day0/docs` – documentazione attuale e autorevole +- `/home/michele/netgescon/netgescon-day0/scripts` – script operativi del workspace attivo +- `netgescon-laravel` – archivio storico, non base attiva per sviluppo o distribuzione ## ✨ **Funzionalità Principali** ### 🏢 **Gestione Stabili** + - Anagrafica completa con dati catastali - Multi-palazzine per complessi residenziali - Gestione dati bancari e coordinate IBAN - Upload documenti e planimetrie -### 👥 **Anagrafica Condominiale** +### 👥 **Anagrafica Condominiale** + - Proprietari e inquilini - Cariche e deleghe amministrative - Storico variazioni quote millesimali - Gestione incarichi (portiere, pulizie, etc.) ### � **Area Finanziaria** + - Conti correnti multipli - Budget preventivi e consuntivi - Ripartizione spese per criterio - Estratti conto e solleciti ### � **Reports e Stampe** + - Bilanci consuntivi - Situazione debitoria - Comunicazioni personalizzate @@ -98,6 +119,7 @@ # Debug in tempo reale ## 🤝 **Contributi** Per contribuire al progetto: + 1. Fork del repository 2. Creazione branch feature (`git checkout -b feature/nome-feature`) 3. Commit modifiche (`git commit -am 'Aggiunta nuova feature'`) @@ -111,13 +133,15 @@ ## 📄 **Licenza** ## 📞 **Supporto** Per supporto tecnico o domande: + - **Issues:** Usa il sistema issues di GitHub -- **Email:** info@netgescon.it +- **Email:** - **Documentazione:** Disponibile nel repository --- Link rapidi + - Guida import GESCON (Stabili.mdb): `docs/000-IMPORT/01-GESCON/GUIDA-IMPORT-GESCON-MDB.md` - Piano import unità → anagrafica unica: `docs/07-IMPORT-UNITA-IMMOBILIARI.md` - Specifica UI anagrafica unica: `docs/08-ANAGRAFICA-UNICA-UI.md` @@ -125,4 +149,4 @@ ## 📞 **Supporto** — 🏢 NetGescon – Sistema di Gestione Condominiale Unificato -📧 Info: info@netgescon.it | 🌐 Demo: https://demo.netgescon.it \ No newline at end of file +📧 Info: | 🌐 Demo: diff --git a/docs/SMDR-INTEGRAZIONE-OPERATIVA.md b/docs/SMDR-INTEGRAZIONE-OPERATIVA.md index ab8091d..932dac4 100644 --- a/docs/SMDR-INTEGRAZIONE-OPERATIVA.md +++ b/docs/SMDR-INTEGRAZIONE-OPERATIVA.md @@ -21,6 +21,61 @@ ## Comando base --reconnect-delay=5 ``` +## Wrapper operativo consigliato + +Per evitare comandi manuali lunghi e mantenere la stessa configurazione tra sviluppo e staging: + +```bash +cd /home/michele/netgescon/netgescon-day0 +bash scripts/ops/netgescon-smdr-listener.sh +``` + +Configurazione opzionale via file dedicato `.env.smdr` nella root applicativa: + +```env +NETGESCON_SMDR_HOST=192.168.0.101 +NETGESCON_SMDR_PORT=2300 +NETGESCON_SMDR_USER=SMDR +NETGESCON_SMDR_PASS=PCCSMDR +NETGESCON_SMDR_RECONNECT=1 +NETGESCON_SMDR_RECONNECT_DELAY=5 +NETGESCON_SMDR_WRITE_POSTIT=1 +NETGESCON_SMDR_WRITE_COMMUNICATIONS=1 +NETGESCON_SMDR_TIMEOUT=0 +NETGESCON_SMDR_MAX_LINES=0 +NETGESCON_SMDR_NO_AUTH=0 +``` + +## Daemon systemd consigliato + +Il blocco osservato il `13/03/2026` e stato coerente con un listener non piu in esecuzione. La soluzione stabile e tenerlo sotto `systemd` con restart automatico. + +Template repo: + +- `scripts/systemd/netgescon-smdr-listener.service.template` +- `scripts/ops/install-smdr-systemd.sh` + +Installazione tipica sviluppo: + +```bash +cd /home/michele/netgescon/netgescon-day0 +RUN_AS=michele RUN_GROUP=michele bash scripts/ops/install-smdr-systemd.sh +``` + +Installazione tipica staging: + +```bash +cd /var/www/netgescon +RUN_AS=www-data RUN_GROUP=www-data bash scripts/ops/install-smdr-systemd.sh +``` + +Verifica: + +```bash +systemctl status netgescon-smdr-listener.service --no-pager +journalctl -u netgescon-smdr-listener.service -n 100 --no-pager +``` + ## Opzioni utili - `--no-auth`: se il centralino non richiede credenziali su socket. @@ -32,6 +87,7 @@ ## Verifica dati acquisiti ```bash php artisan tinker --execute="echo App\\Models\\CommunicationMessage::where('channel','smdr')->count();" php artisan tinker --execute="echo App\\Models\\ChiamataPostIt::count();" +php artisan tinker --execute="echo optional(App\\Models\\CommunicationMessage::where('channel','smdr')->latest('created_at')->first())->created_at;" ``` ## URL operativi correlati diff --git a/docs/checklists/DAY0-GITEA-UPDATES-CHECKLIST.md b/docs/checklists/DAY0-GITEA-UPDATES-CHECKLIST.md new file mode 100644 index 0000000..0939edf --- /dev/null +++ b/docs/checklists/DAY0-GITEA-UPDATES-CHECKLIST.md @@ -0,0 +1,157 @@ +# Day0 Gitea + Updates Checklist + +Data: 17-03-2026 + +## Stato chiarito + +- La sola versione realmente prevista oggi e attiva e `free`. +- La versione `licensed` e solo prevista sulla carta. +- Quindi un canale `licensed` vuoto, non pubblicato o non configurato non e un errore bloccante in questa fase. +- Il focus corretto e allineare `netgescon-day0`, Gitea e `updates.netgescon.it` sul solo canale `free`. + +## Cosa risulta gia verificato + +- Repository Git attivo: `ssh://git@192.168.0.53:2222/michele/netgescon-day0.git` +- Branch visibile da Day0: solo `main` +- Route Day0 locale presenti: + - `GET api/v1/distribution/updates/manifest` + - `GET api/v1/distribution/updates/package` +- Artefatti locali Day0 presenti: + - `storage/app/distribution/free/manifest.json` + - `storage/app/distribution/free/netgescon-0.8.1.zip` +- `updates.netgescon.it` ora risolve verso `192.168.0.53` +- `https://updates.netgescon.it/api/v1/distribution/health` risponde correttamente +- `https://updates.netgescon.it/api/v1/distribution/updates/manifest?channel=free` attualmente restituisce `404` + +## Cosa chiedere all'altro agent + +Chiedere di verificare sulla macchina `192.168.0.53` solo questi punti. + +### 1. Repository e working copy corretti + +Verificare: + +- qual e la cartella reale dell'app che serve `updates.netgescon.it` +- se quella cartella deriva da `netgescon-day0` +- se il commit checked out sulla macchina `.53` coincide con `origin/main` di `netgescon-day0` + +Comandi utili: + +```bash +pwd +git remote -v +git branch --show-current +git rev-parse HEAD +git log --oneline -n 5 +``` + +### 2. Route Laravel sul server update + +Verificare se sull'app che gira sulla `.53` esistono davvero le route distribution update: + +```bash +php artisan route:list | rg "distribution|updates" +``` + +Ci aspettiamo almeno: + +- `api/v1/distribution/health` +- `api/v1/distribution/updates/manifest` +- `api/v1/distribution/updates/package` + +### 3. Cache Laravel e route cache + +Se le route nel codice esistono ma il server risponde `404`, chiedere di verificare e pulire cache: + +```bash +php artisan optimize:clear +php artisan route:clear +php artisan config:clear +php artisan cache:clear +php artisan route:list | rg "distribution|updates" +``` + +### 4. Document root e virtual host di updates.netgescon.it + +Verificare che nginx/apache per `updates.netgescon.it` punti al `public/` dell'app Day0 corretta, non a un'altra installazione Laravel. + +Chiedere: + +- file di configurazione del virtual host usato da `updates.netgescon.it` +- document root impostata +- eventuale symlink verso cartella sbagliata + +Controlli utili: + +```bash +nginx -T | rg -n "updates.netgescon.it|root |server_name" +``` + +### 5. Artefatti distribution free sul server `.53` + +Verificare che sulla macchina `.53` esistano davvero: + +```bash +ls -lah storage/app/distribution/free/ +cat storage/app/distribution/free/manifest.json +sha256sum storage/app/distribution/free/*.zip +``` + +Se mancano, il server update non puo servire il canale `free`. + +### 6. Configurazione distribution + +Verificare questi valori reali dentro l'app della `.53`: + +```bash +php artisan tinker --execute='echo "APP_URL=".config("app.url").PHP_EOL; echo "DIST_ROOT=".config("distribution.storage_root").PHP_EOL; echo "LICENSED_PAIRS=".(config("distribution.licensed_pairs") ?: "").PHP_EOL;' +``` + +Ci serve sapere: + +- `app.url` +- `distribution.storage_root` +- se `distribution.storage_root` punta davvero alla cartella giusta + +### 7. Test HTTP locale dal server `.53` + +Chiedere di testare dalla macchina stessa: + +```bash +curl -i http://127.0.0.1/api/v1/distribution/health +curl -i "http://127.0.0.1/api/v1/distribution/updates/manifest?channel=free" +``` + +Se da localhost funziona ma dal dominio no, il problema e nginx/vhost. +Se da localhost non funziona, il problema e app/codice/cache/storage. + +### 8. Allineamento staging con sviluppo Day0 + +Per capire se staging e allineato alla nostra versione di sviluppo, chiedere: + +- commit SHA della macchina `.53` +- commit SHA locale di `netgescon-day0` +- se il pacchetto `free` e stato rigenerato dopo gli ultimi cambi Day0 + +## Risposta minima che ci serve dall'altro agent + +Chiedere di rispondere in questo formato: + +1. Path reale dell'app che serve `updates.netgescon.it` +2. Commit SHA attuale sulla `.53` +3. Output di `php artisan route:list | rg "distribution|updates"` +4. Presenza o assenza di `storage/app/distribution/free/manifest.json` +5. Output di `cat storage/app/distribution/free/manifest.json` +6. Conferma del virtual host e document root di `updates.netgescon.it` +7. Esito di `curl` locale a `updates/manifest?channel=free` +8. Eventuale motivo del `404` + +## Nota importante + +Per ora non serve trattare `licensed` come errore. + +Il requisito corretto di questa fase e: + +- `free` deve funzionare +- Gitea Day0 deve essere la sola sorgente Git attiva +- `updates.netgescon.it` deve servire il manifest e il package del canale `free` diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index dd0654e..9a5f4c9 100755 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -216,11 +216,11 @@
- + Ticket Operativi Fornitore - + Dipendenti Fornitore diff --git a/resources/views/admin/mobile/hub.blade.php b/resources/views/admin/mobile/hub.blade.php index 63612b1..4e60369 100644 --- a/resources/views/admin/mobile/hub.blade.php +++ b/resources/views/admin/mobile/hub.blade.php @@ -40,6 +40,33 @@ @endif +
+
+

Stabili essenziali

+ Vista rapida mobile +
+ +
+ @forelse($stabili as $stabile) +
+
+
+

{{ $stabile->denominazione ?: 'Stabile #' . $stabile->id }}

+

{{ trim(collect([$stabile->indirizzo, $stabile->citta])->filter()->implode(' · ')) ?: 'Indirizzo non valorizzato' }}

+
+ {{ $stabile->stato ?: 'n.d.' }} +
+ +
+ @empty +

Nessuno stabile disponibile per l'utente corrente.

+ @endforelse +
+
+

Ticket rapidi

diff --git a/resources/views/admin/tickets/show.blade.php b/resources/views/admin/tickets/show.blade.php index 3ad1699..53e7d65 100755 --- a/resources/views/admin/tickets/show.blade.php +++ b/resources/views/admin/tickets/show.blade.php @@ -187,7 +187,7 @@ class="bg-light0 hover:bg-gray-700 text-white fw-bold py-2 px-3 rounded">
- Apri area manutentore + Apri area manutentore @if($ticket->interventi->count() > 0) diff --git a/resources/views/condomino/anagrafe/modulo-pdf.blade.php b/resources/views/condomino/anagrafe/modulo-pdf.blade.php new file mode 100644 index 0000000..b7a0c92 --- /dev/null +++ b/resources/views/condomino/anagrafe/modulo-pdf.blade.php @@ -0,0 +1,90 @@ + + + + + + + +
+

Modulo anagrafe condominiale

+

Generato il {{ $generatedAt->format('d/m/Y H:i') }}. Il dichiarante si impegna a comunicare entro 30 giorni le variazioni rilevanti per l'anagrafe condominiale.

+
+ +
+

Dati del dichiarante

+ + + + + + + + + +
Cognome e nome{{ trim((string) ($persona?->cognome ?? '') . ' ' . (string) ($persona?->nome ?? '')) ?: ($utente?->name ?? 'Da compilare') }}
Codice fiscale{{ $persona?->codice_fiscale ?? '____________________________' }}
Partita IVA{{ $persona?->partita_iva ?? '____________________________' }}
Data nascita{{ optional($persona?->data_nascita)->format('d/m/Y') ?? '____________________________' }}
Residenza{{ $persona?->residenza_via ?? '______________________________________________________________' }}
Telefono{{ $persona?->telefono_principale ?? '____________________________' }}
Email principale{{ $persona?->email_principale ?? ($utente?->email ?? '____________________________') }}
PEC{{ $persona?->email_pec ?? '____________________________' }}
+
+ +
+

Email aggiuntive

+ + + + + + + @forelse ($emailRows as $row) + + + + + + @empty + + @endforelse +
EmailTipoAttiva
{{ $row['email'] ?: '____________________________' }}{{ $row['tipo_email'] ?: 'secondaria' }}{{ ! empty($row['email']) ? (! empty($row['attiva']) ? 'Si' : 'No') : '___' }}
Nessun recapito aggiuntivo indicato.
+
+ +
+

Unità immobiliari e recapiti per servizio

+ @forelse ($unitaImmobiliari as $unita) + @php $matrix = $serviceContactMatrix[$unita->id] ?? []; @endphp +

{{ $unita->stabile->denominazione ?? 'Stabile' }} · {{ $unita->denominazione ?? ('Unità #' . $unita->id) }}

+ + + + + + @foreach ($matrix as $row) + + + + + @endforeach +
ServizioRecapito
{{ $row['label'] }}{{ $row['value'] ?: collect($row['resolved'] ?? [])->pluck('email')->implode(', ') ?: '____________________________' }}
+ @empty +

Nessuna unità collegata al profilo.

+ @endforelse +
+ +
+

Dichiarazione e firma

+

Il sottoscritto dichiara che i dati riportati sono aggiornati e si impegna a comunicare entro 30 giorni ogni variazione utile alla gestione condominiale e all'anagrafe.

+ + + + + +
Data ____________________________Firma ____________________________
+

Per chi non dispone di strumenti elettronici il modulo può essere stampato, firmato e consegnato allo studio.

+
+ + \ No newline at end of file diff --git a/resources/views/condomino/anagrafe/show.blade.php b/resources/views/condomino/anagrafe/show.blade.php new file mode 100644 index 0000000..3803dd0 --- /dev/null +++ b/resources/views/condomino/anagrafe/show.blade.php @@ -0,0 +1,189 @@ + + + + + + + Scheda anagrafica - NetGescon + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+
+
+

Scheda anagrafica condominiale

+

Puoi aggiornare i tuoi recapiti, le email aggiuntive e i recapiti per servizio. Ogni modifica viene tracciata.

+
+ +
+ + @if (session('success')) +
{{ session('success') }}
+ @endif + + @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + + @if (! $persona) +
+ Non è stato possibile associare con certezza il tuo account a una scheda anagrafica digitale. Puoi comunque scaricare il modulo e usare la scheda unità per inviare richieste strutturate. +
+ @endif + +
+ @csrf + +
+
+

Dati anagrafici

+
+ + + + + + +
+
+ +
+

Recapiti principali

+
+ + + + + + + + +
+
+
+ +
+
+

Email aggiuntive

+

Puoi indicare più caselle per lavoro, personale, studio o altri recapiti.

+
+
+ @foreach ($emailRows as $index => $row) +
+ + + + +
+ @endforeach +
+
+ +
+
+

Recapiti per servizio e unità

+

Se compili questi campi, NetGescon userà questi indirizzi come default per la specifica unità e il servizio indicato.

+
+ + @forelse ($unitaImmobiliari as $unita) + @php $matrix = $serviceContactMatrix[$unita->id] ?? []; @endphp +
+
+

{{ $unita->stabile->denominazione ?? 'Stabile' }} · {{ $unita->denominazione ?? ('Unità #' . $unita->id) }}

+

{{ trim(($unita->scala ? 'Scala ' . $unita->scala : '') . ' ' . ($unita->interno ? 'Interno ' . $unita->interno : '')) ?: 'Unità senza dettaglio interno' }}

+
+
+ @foreach ($matrix as $serviceType => $row) + + @endforeach +
+
+ @empty +

Nessuna unità collegata al profilo corrente.

+ @endforelse +
+ +
+ + Scarica il modulo da firmare o stampare +
+
+
+ + \ No newline at end of file diff --git a/resources/views/condomino/dashboard.blade.php b/resources/views/condomino/dashboard.blade.php index 0bcbcd9..e0a94b2 100644 --- a/resources/views/condomino/dashboard.blade.php +++ b/resources/views/condomino/dashboard.blade.php @@ -26,6 +26,33 @@

Letture acqua

Invia autoletture con foto e controlla lo storico consumo.

+ + +

Anagrafe

+

Scheda anagrafica

+

Aggiorna recapiti, email multiple e indirizzi di recapito per servizio.

+
+ + +

Unità immobiliari

+

Le mie unità

+

Consulta dati unità, recapiti attivi e richiedi servizi catastali.

+
+ + +
+
+

Unità accessibili

+

{{ $stats['unita_possedute'] ?? 0 }}

+
+
+

Ticket aperti

+

{{ $stats['ticket_aperti'] ?? 0 }}

+
+
+

Recapiti di servizio attivi

+

{{ $serviceContactsCount ?? 0 }}

+
diff --git a/resources/views/condomino/unita/index.blade.php b/resources/views/condomino/unita/index.blade.php new file mode 100644 index 0000000..db1be5f --- /dev/null +++ b/resources/views/condomino/unita/index.blade.php @@ -0,0 +1,41 @@ + + + + + + + Le mie unità - NetGescon + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+
+
+

Le mie unità immobiliari

+

Consulta le unità collegate al tuo profilo e i relativi recapiti operativi.

+
+ +
+ + @forelse ($unitaImmobiliari as $unita) +
+
+
+

{{ $unita->stabile->denominazione ?? 'Stabile' }} · {{ $unita->denominazione ?? ('Unità #' . $unita->id) }}

+

+ {{ trim(($unita->scala ? 'Scala ' . $unita->scala : '') . ' ' . ($unita->piano !== null ? 'Piano ' . $unita->piano : '') . ' ' . ($unita->interno ? 'Interno ' . $unita->interno : '')) ?: 'Posizione non dettagliata' }} +

+

Foglio {{ $unita->foglio ?: '—' }} · Particella {{ $unita->particella ?: '—' }} · Subalterno {{ $unita->subalterno ?: '—' }}

+
+ Apri scheda +
+
+ @empty +
Nessuna unità collegata al tuo account.
+ @endforelse +
+ + \ No newline at end of file diff --git a/resources/views/condomino/unita/show.blade.php b/resources/views/condomino/unita/show.blade.php new file mode 100644 index 0000000..95b820c --- /dev/null +++ b/resources/views/condomino/unita/show.blade.php @@ -0,0 +1,132 @@ + + + + + + + Scheda unità - NetGescon + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+
+
+

{{ $unitaImmobiliare->stabile->denominazione ?? 'Stabile' }} · {{ $unitaImmobiliare->denominazione ?? ('Unità #' . $unitaImmobiliare->id) }}

+

Scheda unità, recapiti effettivi per servizio e richieste di aggiornamento.

+
+ +
+ + @if (session('success')) +
{{ session('success') }}
+ @endif + + @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + +
+
+

Dati unità

+
+
Scala / piano / interno
{{ trim(($unitaImmobiliare->scala ? 'Scala ' . $unitaImmobiliare->scala : '') . ' ' . ($unitaImmobiliare->piano !== null ? 'Piano ' . $unitaImmobiliare->piano : '') . ' ' . ($unitaImmobiliare->interno ? 'Interno ' . $unitaImmobiliare->interno : '')) ?: '—' }}
+
Foglio
{{ $unitaImmobiliare->foglio ?: '—' }}
+
Particella
{{ $unitaImmobiliare->particella ?: '—' }}
+
Subalterno
{{ $unitaImmobiliare->subalterno ?: '—' }}
+
Categoria catastale
{{ $unitaImmobiliare->categoria_catastale ?: '—' }}
+
Rendita catastale
{{ $unitaImmobiliare->rendita_catastale ?: '—' }}
+
+
+ +
+

Soggetti collegati

+ @forelse ($unitaImmobiliare->soggetti as $soggetto) +
+
{{ $soggetto->nominativo ?? $soggetto->nome ?? ('Soggetto #' . $soggetto->id) }}
+
{{ $soggetto->email ?? 'Email non disponibile' }}
+
+ @empty +

Nessun soggetto legacy collegato in modo esplicito.

+ @endforelse +
+
+ +
+

Recapiti effettivi per servizio

+
+ @foreach ($serviceLabels as $serviceType => $label) +
+

{{ $label }}

+ @php $rows = $serviceRecipients[$serviceType] ?? []; @endphp + @if (empty($rows)) +

Nessun recapito definito.

+ @else +
    + @foreach ($rows as $row) +
  • {{ $row['email'] }} @if(! empty($row['persona']))· {{ $row['persona'] }}@endif
  • + @endforeach +
+ @endif +
+ @endforeach +
+
+ +
+
+

Richiesta di aggiornamento

+
+ @csrf + + + + +
+
+ +
+

Servizio aggiuntivo: aggiornamento catastale

+

Puoi richiedere l'avvio del servizio a pagamento per aggiornare la posizione catastale dell'unità. La richiesta viene registrata e presa in carico dallo studio.

+
+ @csrf + + + + + + +
+
+
+
+ + \ No newline at end of file diff --git a/resources/views/filament/components/footer.blade.php b/resources/views/filament/components/footer.blade.php index 6c7f3d7..7be5041 100644 --- a/resources/views/filament/components/footer.blade.php +++ b/resources/views/filament/components/footer.blade.php @@ -40,10 +40,6 @@
- @if($user instanceof \App\Models\User && $user->hasAnyRole(['super-admin','admin','amministratore','collaboratore'])) - Apri UI classica - @endif - Richiedi help / segnala bug diff --git a/resources/views/filament/pages/condomini/tabs/assicurazioni.blade.php b/resources/views/filament/pages/condomini/tabs/assicurazioni.blade.php new file mode 100644 index 0000000..18b4f64 --- /dev/null +++ b/resources/views/filament/pages/condomini/tabs/assicurazioni.blade.php @@ -0,0 +1,201 @@ +
+
+
+
+
Modulo assicurazione stabile
+
Storico polizze, clausole, PDF, firma immagine e collegamento dei sinistri ai ticket.
+
+
+ +
+
+
+ +
+
+
Elenco polizze
+
+ @forelse($this->insurancePolicies as $policy) +
+
+
+
{{ $policy->display_name }}
+
{{ $policy->policy_reference ?: 'Riferimento non indicato' }}
+
Scadenza {{ optional($policy->expires_at)->format('d/m/Y') ?: '-' }} · Rinnovo {{ optional($policy->renewal_at)->format('d/m/Y') ?: '-' }}
+
+
+ + +
+
+
+ Stato: {{ $policy->status ?: '-' }} + Premio annuo: {{ $policy->annual_amount !== null ? number_format((float) $policy->annual_amount, 2, ',', '.') . ' EUR' : '-' }} +
+
+ @empty +
Nessuna polizza registrata per questo stabile.
+ @endforelse +
+
+ +
+
Scheda polizza
+
+ + + + + + + + + + + + + + + + + + +
+ + @if(($selectedPolicy = $this->insurancePolicies->firstWhere('id', (int) ($this->selectedInsurancePolicyId ?? 0))) instanceof \App\Models\InsurancePolicy) +
+
+
Documento polizza
+ @if($this->getInsuranceDocumentUrl($selectedPolicy)) + Apri PDF / allegato polizza + @else +
Nessun documento caricato.
+ @endif +
+
+
Firma immagine
+ @if($this->getInsuranceSignatureUrl($selectedPolicy)) + Firma + @else +
Nessuna firma immagine caricata.
+ @endif +
+
+ @endif + +
+ + +
+
+
+ +
+
Sinistri collegati ai ticket
+
Separati dalla sola scheda ticket: qui rimangono tracciati per polizza e per stabile.
+
+ + + + + + + + + + + + @forelse($this->insuranceClaimsRows as $claim) + + + + + + + + @empty + + + + @endforelse + +
AperturaPolizzaSinistroTicketNote
{{ optional($claim->opened_at)->format('d/m/Y H:i') ?: '-' }}{{ $claim->insurancePolicy?->display_name ?: ($claim->policy_reference ?: '-') }}{{ $claim->claim_number ?: '-' }}
{{ $claim->status ?: 'aperta' }}
+ @if($claim->ticket) + Ticket #{{ (int) $claim->ticket->id }} +
{{ $claim->ticket->titolo ?: '-' }}
+ @else + - + @endif +
{{ $claim->notes ?: '-' }}
Nessun sinistro registrato per questo stabile.
+
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/fiscale/cruscotto.blade.php b/resources/views/filament/pages/fiscale/cruscotto.blade.php new file mode 100644 index 0000000..b3c26ca --- /dev/null +++ b/resources/views/filament/pages/fiscale/cruscotto.blade.php @@ -0,0 +1,86 @@ + +
+
+
+
+

Archivio fiscale condominiale

+

+ @if($this->stabileAttivo) + Stabile attivo: {{ $this->stabileAttivo->denominazione }} · anno fiscale {{ $this->annoFiscale }} + @else + Seleziona uno stabile attivo per vedere ritenute, certificazioni e scadenze fiscali. + @endif +

+
+ +
+
+ +
+
+
Ritenute
+
{{ $summary['ritenute_count'] ?? 0 }}
+

Registrazioni incluse nell'anno fiscale selezionato.

+
Da versare: {{ $summary['ritenute_da_versare'] ?? 0 }}
+
+
+
Detrazioni
+
{{ $summary['detrazioni_count'] ?? 0 }}
+

Certificazioni e posizioni fiscali da lavori agevolati.

+
Dichiarazioni fiscali da straordinarie: {{ $summary['straordinarie_fiscali'] ?? 0 }}
+
+
+
Adempimenti
+
{{ $summary['adempimenti_count'] ?? 0 }}
+

Archivio operativo fiscale del condominio.

+
Aperti o da completare: {{ $summary['adempimenti_aperti'] ?? 0 }}
+
+
+ +
+
+

Scadenze e archivio

+
+ @forelse($adempimentiInScadenza as $adempimento) +
+
+
+
{{ $adempimento->titolo }}
+
{{ str_replace('_', ' ', $adempimento->tipo) }}
+
+ {{ $adempimento->stato }} +
+
{{ $adempimento->descrizione ?: 'Nessuna descrizione operativa.' }}
+
+ Anno fiscale: {{ $adempimento->anno_fiscale ?: 'n.d.' }} + @if($adempimento->scadenza) + · Scadenza {{ $adempimento->scadenza->format('d/m/Y') }} + @endif +
+
+ @empty +
+ Nessun adempimento archiviato. Il menu Fiscale e` pronto per raccogliere ritenute, certificazioni, Quadro AC, 770 e compensi amministratore. +
+ @endforelse +
+
+ +
+

Struttura iniziale

+
+ @foreach($templates as $template) +
+
{{ $template['titolo'] }}
+
{{ str_replace('_', ' ', $template['tipo']) }}
+

{{ $template['descrizione'] }}

+
+ @endforeach +
+
+
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/fornitore/collaboratori.blade.php b/resources/views/filament/pages/fornitore/collaboratori.blade.php index 30e0f5d..8f6e17e 100644 --- a/resources/views/filament/pages/fornitore/collaboratori.blade.php +++ b/resources/views/filament/pages/fornitore/collaboratori.blade.php @@ -24,75 +24,162 @@ Questa vista per amministratori richiede un fornitore selezionato.
@else -
-
-
Nuovo collaboratore
-
Puoi registrare sia un collaboratore interno sia un altro fornitore da usare come subfornitore operativo.
-
- - - @if($collaboratoreTipo === 'fornitore_esterno') - - - @else - - - - - @endif - - @if($collaboratoreTipo === 'interno') - - - @else -
- Il subfornitore usa il suo account esistente. Qui salvi solo il collegamento operativo. -
- @endif - Salva collaboratore -
+
+
+ +
-
-
Elenco collaboratori
+ @if($activeTab === 'nuovo') +
+
Nuovo collaboratore
+
Scheda semplificata per il fornitore, con salvataggio anagrafico su rubrica universale per i collaboratori interni.
+
+
+ + + @if($collaboratoreTipo === 'fornitore_esterno') + + +
+ Il subfornitore usa il suo account esistente. Qui salvi solo il collegamento operativo. +
+ @else +
+ + @if($tipoContatto === 'persona_giuridica') + + @else + + + + + + + @endif + + + + + + + + + + + +
+ @endif +
+ +
+ + @if($collaboratoreTipo === 'interno') + + +
+ Per i collaboratori interni viene creata o aggiornata anche la relativa scheda in rubrica, senza sezione conti bancari. +
+ @endif +
+ Salva collaboratore +
+
+
+
+ @else +
+
Elenco collaboratori
@@ -130,6 +217,9 @@ @else Nessun account @endif + @if($row['rubrica_id']) +
Rubrica #{{ $row['rubrica_id'] }}
+ @endif
{{ $row['attivo'] ? 'Attivo' : 'Disattivo' }} @@ -155,7 +245,8 @@
-
+
+ @endif
@endif
diff --git a/resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php b/resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php index 65c91ea..9e7b61f 100644 --- a/resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php +++ b/resources/views/filament/pages/fornitore/ticket-intervento-scheda.blade.php @@ -35,6 +35,20 @@ @endif
+
+
Email
+
+ @if($caller['email'] !== '') + {{ $caller['email'] }} + @else + - + @endif +
+
+
+
Origine dato
+
{{ $caller['sorgente'] !== '' ? $caller['sorgente'] : '-' }}
+
Stato intervento
{{ $this->intervento->stato }}
@@ -43,6 +57,10 @@
Operatore
{{ $this->intervento->operatore_assegnato_label }}
+
+
Riferimento stabile
+
{{ $caller['riferimento'] !== '' ? $caller['riferimento'] : '-' }}
+
{{ $ticket->descrizione }}
@@ -202,10 +220,11 @@
{{ $attachment->original_file_name }}
{{ $attachment->description ?: 'Nessuna descrizione' }}
+
Caricato da {{ $attachment->user->name ?? 'Sistema' }} il {{ optional($attachment->created_at)->format('d/m/Y H:i') ?: '-' }}
Apri allegato
@empty -
Nessun allegato disponibile.
+
Nessun allegato disponibile. Se il ticket e stato creato senza file persistiti, qui non verra mostrato nulla finche non vengono caricati allegati reali.
@endforelse diff --git a/resources/views/filament/pages/gescon/partials/stabile-transfer-history.blade.php b/resources/views/filament/pages/gescon/partials/stabile-transfer-history.blade.php new file mode 100644 index 0000000..40c3022 --- /dev/null +++ b/resources/views/filament/pages/gescon/partials/stabile-transfer-history.blade.php @@ -0,0 +1,48 @@ +
+
+
{{ $stabile->codice_stabile }} · {{ $stabile->denominazione }}
+
Cronologia dei passaggi amministratore registrati dal portale.
+
+ + @if($transfers->isEmpty()) +
+ Nessun passaggio registrato per questo stabile. +
+ @else +
+ @foreach($transfers as $transfer) +
+
+
+
+ {{ trim((string) ($transfer->fromAmministratore?->denominazione_studio ?: $transfer->fromAmministratore?->nome_completo ?: 'N/D')) }} + -> + {{ trim((string) ($transfer->toAmministratore?->denominazione_studio ?: $transfer->toAmministratore?->nome_completo ?: 'N/D')) }} +
+
+ Storico #{{ $transfer->id }} · {{ optional($transfer->created_at)->format('d/m/Y H:i') }} +
+
+
+
{{ $transfer->changed_by_name ?: ($transfer->changedByUser?->name ?? 'Sistema') }}
+
{{ $transfer->source ?: 'portal-superadmin' }}
+
+
+ + @if(!empty($transfer->reason)) +
+ {{ $transfer->reason }} +
+ @endif + +
+ Rubrica collegata aggiornata: {{ $transfer->also_rubrica ? 'si' : 'no' }} + @if(!empty($transfer->ip_address)) + · IP {{ $transfer->ip_address }} + @endif +
+
+ @endforeach +
+ @endif +
\ No newline at end of file diff --git a/resources/views/filament/pages/gescon/rubrica-universale-scheda.blade.php b/resources/views/filament/pages/gescon/rubrica-universale-scheda.blade.php index c2aa41a..b959d3c 100644 --- a/resources/views/filament/pages/gescon/rubrica-universale-scheda.blade.php +++ b/resources/views/filament/pages/gescon/rubrica-universale-scheda.blade.php @@ -385,11 +385,70 @@
+
+ @if($sideTab === 'studio') + + Collegamento collaboratore studio + Trasforma questo nominativo rubrica in collaboratore di studio senza perdere eventuali altri profili già esistenti sullo stesso utente. + +
+
+
+
Utente collegato
+
{{ $studioCollaboratoreUserName ?: ($rubrica->nome_completo ?? '—') }}
+
{{ $studioCollaboratoreUserEmail ?: 'Email non presente in rubrica' }}
+
+
+
Ruoli attuali utente
+
{{ count($studioCollaboratoreRoles) > 0 ? implode(', ', $studioCollaboratoreRoles) : 'Nessun ruolo applicativo rilevato' }}
+
+
+ + @if(filled($studioCollaboratoreGeneratedPassword)) +
+ Ultima password temporanea generata: {{ $studioCollaboratoreGeneratedPassword }} +
+ @endif + +
+
+ + +
+
+
Stabili assegnati al collaboratore
+
+ @forelse($studioCollaboratoreStabileOptions as $stabileId => $stabileLabel) + + @empty +
Nessuno stabile disponibile per l'amministratore attivo.
+ @endforelse +
+
+
+ +
+ + {{ $studioCollaboratoreUserId ? 'Riabilita / conferma collaboratore' : 'Abilita collaboratore studio' }} + + Salva interno e stabili +
+ +
+ Questo collegamento usa l'email della rubrica come chiave utente. Se l'utente esiste già, aggiunge solo il ruolo collaboratore e sincronizza gli stabili dell'amministratore corrente senza rimuovere eventuali altri ruoli o profili già presenti. +
+
+
+ @endif + @if($sideTab === 'collegamenti') Collegamenti diff --git a/resources/views/filament/pages/gescon/table.blade.php b/resources/views/filament/pages/gescon/table.blade.php index b2295b8..478acf5 100644 --- a/resources/views/filament/pages/gescon/table.blade.php +++ b/resources/views/filament/pages/gescon/table.blade.php @@ -42,7 +42,13 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
Vista operativa in stile ticket-gestione. Cerca rapidamente e apri HUB/scheda inline.
- Totale fornitori importati: {{ (int) ($fornitoriCount ?? 0) }} +
+
Totale fornitori importati: {{ (int) ($fornitoriCount ?? 0) }}
+
+ + +
+
@@ -170,6 +176,7 @@ class="inline-flex h-4 w-4 items-center justify-center rounded-full text-slate-5
+ Apri vecchia scheda (immutata)
diff --git a/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-distribuzione-backup.blade.php b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-distribuzione-backup.blade.php new file mode 100644 index 0000000..fde2886 --- /dev/null +++ b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-distribuzione-backup.blade.php @@ -0,0 +1,31 @@ +
+
+
Operazioni distribuzione, backup e sincronizzazione
+
+ Aggiorna produzione + Crea pacchetto aggiornamento + Scarica ultimo pacchetto + Controlla nuovi utenti + Backup produzione + Backup incrementale dati su Drive + Verifica Google Drive + Restore dati non distruttivo + Sync aggiornamenti + archivi + Collega Google + Scollega Google +
+ + @if(filled($this->lastUpdatePackageName)) +
+ Ultimo pacchetto aggiornamento: {{ $this->lastUpdatePackageName }} +
+ @endif + + @if(filled($this->opsLastOutput)) +
+
Output operazioni
+
{{ $this->opsLastOutput }}
+
+ @endif +
+
\ No newline at end of file diff --git a/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-operativita-accessi.blade.php b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-operativita-accessi.blade.php index 2a8a5e0..75df23e 100644 --- a/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-operativita-accessi.blade.php +++ b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-operativita-accessi.blade.php @@ -48,36 +48,6 @@ -
-
Operazioni distribuzione, backup e sincronizzazione
-
- Aggiorna produzione - Crea pacchetto aggiornamento - Scarica ultimo pacchetto - Controlla nuovi utenti - Backup produzione - Backup incrementale dati su Drive - Verifica Google Drive - Restore dati non distruttivo - Sync aggiornamenti + archivi - Collega Google - Scollega Google -
- - @if(filled($this->lastUpdatePackageName)) -
- Ultimo pacchetto aggiornamento: {{ $this->lastUpdatePackageName }} -
- @endif - - @if(filled($this->opsLastOutput)) -
-
Output operazioni
-
{{ $this->opsLastOutput }}
-
- @endif -
-
Nuovi utenti da abilitare (risultato controllo)
diff --git a/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-posta-operativita.blade.php b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-posta-operativita.blade.php new file mode 100644 index 0000000..752de3c --- /dev/null +++ b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-posta-operativita.blade.php @@ -0,0 +1,18 @@ +
+
+ Questa area serve a verificare via web la prontezza dell'integrazione Google e della posta studio. La lettura automatica caselle non e ancora attiva come daemon applicativo, ma puoi validare configurazione OAuth e collegamento account senza entrare in shell. +
+ +
+ Collega Google + Verifica Google OAuth + Scollega Google +
+ + @if(filled($this->opsLastOutput)) +
+
Ultimo output verifica posta / Google
+
{{ $this->opsLastOutput }}
+
+ @endif +
\ No newline at end of file diff --git a/resources/views/filament/pages/strumenti/post-it-gestione.blade.php b/resources/views/filament/pages/strumenti/post-it-gestione.blade.php index 2838b89..6bf0573 100644 --- a/resources/views/filament/pages/strumenti/post-it-gestione.blade.php +++ b/resources/views/filament/pages/strumenti/post-it-gestione.blade.php @@ -98,6 +98,39 @@

Elenco tecnico chiamate (1 riga = 1 evento)

Qui vedi in colonne i dati disponibili dal centralino: interno, linea/trunk, numero, durata, costo, utente assegnato, stabile, raw line, ecc.

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -105,6 +138,7 @@ + @@ -123,15 +157,16 @@ @php($smdr = (array) data_get($m->metadata, 'smdr', [])) @php($cost = trim((string) (($smdr['cost_currency'] ?? '') . ' ' . ($smdr['cost_amount'] ?? '')))) @php($duration = (string) (($smdr['duration_raw'] ?? '') ?: ($smdr['ring_duration_raw'] ?? ''))) + @php($phoneValue = (string) ($m->phone_number ?: ($smdr['dial_number'] ?? ''))) + @php($rubricaNomeTech = $this->getTecnicoNominativo($m)) - + + - @php($phoneValue = (string) ($m->phone_number ?: ($smdr['dial_number'] ?? ''))) - @php($rubricaNomeTech = $this->getRubricaNomeByPhone($phoneValue)) @@ -141,7 +176,7 @@ @empty - + @endforelse diff --git a/resources/views/filament/pages/supporto/ticket-gestione.blade.php b/resources/views/filament/pages/supporto/ticket-gestione.blade.php index fe7113f..6157e65 100644 --- a/resources/views/filament/pages/supporto/ticket-gestione.blade.php +++ b/resources/views/filament/pages/supporto/ticket-gestione.blade.php @@ -220,14 +220,46 @@
Assegnazione fornitore
+
Ricerca per tag, telefono, nome, ragione sociale o email. Tocca il risultato per selezionarlo.
+ + @if($fornitoreId) +
+ Fornitore selezionato: {{ $fornitoreSearch }} + +
+ @endif + + @if(count($fornitoreMatches) > 0) +
+ @foreach($fornitoreMatches as $match) + + @endforeach +
+ @elseif(filled($fornitoreSearch) && ! $fornitoreId) +
Nessun fornitore trovato con questa ricerca.
+ @endif +
+
+
+
+
Assicurazione e sinistro
+
Gestione apertura o aggiornamento del sinistro senza uscire da Filament.
+
+ @if($ticket->insuranceClaim) + + Sinistro {{ $ticket->insuranceClaim->status ?: 'aperto' }} + + @endif +
+ +
+ + + +
+ + + + @if($ticket->insuranceClaim) +
+ Aperto il {{ optional($ticket->insuranceClaim->opened_at)->format('d/m/Y H:i') ?: '-' }} + @if($ticket->insuranceClaim->insurancePolicy) + · {{ $ticket->insuranceClaim->insurancePolicy->display_name }} + @endif + @if($ticket->insuranceClaim->claim_number) + · Sinistro {{ $ticket->insuranceClaim->claim_number }} + @endif + @if($ticket->insuranceClaim->policy_reference) + · Polizza {{ $ticket->insuranceClaim->policy_reference }} + @endif +
+ @endif + +
+ Salva sinistro +
+
+
Storico note/messaggi
@@ -341,7 +431,7 @@
@if($attachmentPreview) -
+
@@ -351,6 +441,16 @@ @endif
+ @if($attachmentPreview['is_image'] ?? false) + + + + @elseif($attachmentPreview['is_pdf'] ?? false) + + Pagina + + + @endif Apri in nuova scheda
@@ -358,9 +458,11 @@
@if($attachmentPreview['is_image'] ?? false) - Anteprima allegato +
+ Anteprima allegato +
@elseif($attachmentPreview['is_pdf'] ?? false) - + @else
Anteprima non disponibile per questo formato. Scarica o apri il file: diff --git a/resources/views/filament/pages/supporto/ticket-mobile-help.blade.php b/resources/views/filament/pages/supporto/ticket-mobile-help.blade.php new file mode 100644 index 0000000..e969c27 --- /dev/null +++ b/resources/views/filament/pages/supporto/ticket-mobile-help.blade.php @@ -0,0 +1,41 @@ + +
+
+
+
+
Help Ticket Mobile
+
Guida rapida per usare la pagina in modo lineare dal telefono senza ingombro visivo inutile.
+
+ +
+
+ +
+
+
1. Apertura rapida
+
Seleziona prima il chiamante dalla ricerca, poi compila titolo, descrizione e priorità del ticket.
+
+
+
2. Assegnazione e intervento
+
Dopo la creazione vai in Gestione Ticket per assegnare il fornitore corretto e aprire gli interventi operativi.
+
+
+
3. Verifica e chiusura
+
Chi usa la parte amministrativa verifica esito, proforma o fattura e chiude il ticket solo a lavoro coerente.
+
+
+ +
+
Buone pratiche
+
+
Usa la ricerca chiamante per telefono, nome, cognome, ragione sociale o email e seleziona il contatto con un solo tocco.
+
Compila il campo descrizione con il problema reale, non solo con il titolo, così il fornitore trova subito il contesto.
+
Se hai foto o file, allegali subito dal telefono e usa una descrizione breve per ogni allegato quando serve.
+
Per la gestione operativa successiva usa la pagina Gestione Ticket, che è la vista corretta per assegnazione, sinistro, note e controllo avanzamento.
+
+
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/supporto/ticket-mobile.blade.php b/resources/views/filament/pages/supporto/ticket-mobile.blade.php index 5cb25eb..dd56bd1 100644 --- a/resources/views/filament/pages/supporto/ticket-mobile.blade.php +++ b/resources/views/filament/pages/supporto/ticket-mobile.blade.php @@ -68,28 +68,6 @@
-
-
Workflow Ticket Chiaro
-
Percorso operativo consigliato per evitare passaggi persi.
-
-
-
1) Apertura rapida
-
Seleziona chiamante, compila titolo e descrizione, poi crea il ticket.
-
-
-
2) Assegnazione e intervento
-
Apri l'archivio ticket, assegna fornitore e registra gli interventi.
-
-
-
3) Verifica e chiusura
-
Verifica esito/fattura, segna risolto e chiudi definitivamente.
-
-
- -
-
Ricerca chiamante
Ricerca per telefono, nome, cognome, ragione sociale o email e seleziona il contatto con un click.
@@ -242,8 +220,9 @@
Gestione operativa separata
Per lavorazione, presa in carico, risoluzione e chiusura usa la pagina dedicata.
- diff --git a/resources/views/fornitore/dipendenti/index.blade.php b/resources/views/fornitore/dipendenti/index.blade.php deleted file mode 100644 index e904e39..0000000 --- a/resources/views/fornitore/dipendenti/index.blade.php +++ /dev/null @@ -1,125 +0,0 @@ -@extends('admin.layouts.netgescon') - -@section('title', 'Dipendenti fornitore') - -@section('content') -
-
-
-

Dipendenti - {{ $fornitore->ragione_sociale ?? $fornitore->nome ?? 'Fornitore' }}

- Gestione operatori che possono lavorare i ticket da cellulare e web -
- Torna ai ticket -
- - @if(session('success')) -
{{ session('success') }}
- @endif - - @if($errors->any()) -
-
    - @foreach($errors->all() as $error) -
  • {{ $error }}
  • - @endforeach -
-
- @endif - -
-
Nuovo dipendente
-
-
- @csrf -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
- -
- -
-
- -
-
Elenco dipendenti
-
-
-
ID Data/Ora DirezioneAmbito Interno Linea/CO Numero
{{ $m->id }} {{ optional($m->received_at)->format('d/m/Y H:i:s') }} {{ $m->direction }}{{ (string) ($m->target_extension ?: ($smdr['extension'] ?? '-')) }}{{ $this->getTecnicoScopeLabel($m) }}{{ $this->getTecnicoInternoLabel($m) }} {{ (string) ($smdr['co'] ?? '-') }} {{ (string) ($m->phone_number ?: ($smdr['dial_number'] ?? '-')) }}{{ $rubricaNomeTech ?: '-' }} {{ $duration !== '' ? $duration : '-' }} {{ $cost !== '' ? $cost : '-' }}{{ $m->message_text }} @php($rubricaUrlTech = $this->getRubricaUrlByPhone($phoneValue)) - @if($rubricaUrlTech) + @if($rubricaUrlTech && ! $this->isInternalSmdrMessage($m)) Apri Rubrica @endif @if(!$m->post_it_id) @@ -153,7 +188,7 @@
Nessun evento SMDR disponibile.Nessun evento SMDR disponibile.
- - - - - - - - - - - @forelse($dipendenti as $dipendente) - - - - - - - - @empty - - - - @endforelse - -
NomeContattiAccessoStato
- {{ $dipendente->nome_completo }} - @if($dipendente->note) -
{{ \Illuminate\Support\Str::limit($dipendente->note, 80) }}
- @endif -
-
{{ $dipendente->email ?: '-' }}
-
{{ $dipendente->telefono ?: '-' }}
-
- @if($dipendente->user_id) - Account collegato - @else - Nessun account - @endif - - {{ $dipendente->attivo ? 'Attivo' : 'Disattivo' }} - -
- @csrf - -
-
Nessun dipendente registrato.
-
-
- -
- -@endsection diff --git a/resources/views/fornitore/tickets/index.blade.php b/resources/views/fornitore/tickets/index.blade.php deleted file mode 100644 index 00226d8..0000000 --- a/resources/views/fornitore/tickets/index.blade.php +++ /dev/null @@ -1,116 +0,0 @@ -@extends('admin.layouts.netgescon') - -@section('title', 'I miei ticket operativi') - -@section('content') -
-
-
-

Ticket operativi - {{ $fornitore->ragione_sociale ?? $fornitore->nome ?? 'Fornitore' }}

- Gestione interventi, chiusure operative e raggruppamento fattura -
- -
- - - -
-
Interventi da fatturare
-
- @if($fatturabili->isEmpty()) -

Nessun intervento ancora pronto per fattura.

- @else -
- - - - - - - - - - - - @foreach($fatturabili as $item) - - - - - - - - @endforeach - -
TicketStabileStatoRif. chiusuraFattura
#{{ $item->ticket_id }} - {{ $item->ticket->titolo ?? '-' }}{{ $item->ticket->stabile->denominazione ?? '-' }}{{ $item->stato }}{{ $item->riferimento_chiusura ?: '-' }}{{ $item->fattura_numero ?: 'Da emettere' }}
-
- @endif -
-
- -
-
Pratiche / Interventi / Manutenzioni
-
-
- - - - - - - - - - - - - - - @forelse($interventi as $idx => $intervento) - @php($row = $rows[$idx] ?? ['ingresso' => '-', 'contatto' => '-', 'telefono' => '', 'problema' => '-', 'apparato' => '-']) - - - - - - - - - - - @empty - - - - @endforelse - -
IngressoContattoTelefonoProblemaApparatoStatoAggiornato
{{ $row['ingresso'] }} -
{{ $row['contatto'] }}
- Ticket #{{ $intervento->ticket_id }} - {{ $intervento->ticket->stabile->denominazione ?? '-' }} -
- @if(!empty($row['telefono'])) - - {{ $row['telefono'] }} - - @else - - - @endif - {{ \Illuminate\Support\Str::limit((string) $row['problema'], 90) }}{{ $row['apparato'] }}{{ $intervento->stato }}{{ optional($intervento->updated_at)->format('d/m/Y H:i') }} - Scheda -
- @csrf - -
-
Nessun intervento disponibile.
-
-
- -
-
-@endsection diff --git a/resources/views/fornitore/tickets/show.blade.php b/resources/views/fornitore/tickets/show.blade.php deleted file mode 100644 index 6a3378d..0000000 --- a/resources/views/fornitore/tickets/show.blade.php +++ /dev/null @@ -1,220 +0,0 @@ -@extends('admin.layouts.netgescon') - -@section('title', 'Dettaglio intervento fornitore') - -@section('content') -
-
-
-

Intervento Ticket #{{ $intervento->ticket_id }}

- {{ $intervento->ticket->titolo ?? '-' }} - {{ $intervento->ticket->stabile->denominazione ?? '-' }} -
- Torna ai ticket -
- -
-
- -
-
- -
-
-
-
-
-
Dati segnalazione
-
-
Richiedente: {{ $caller['contatto'] ?: '-' }}
-
Telefono: - @if(!empty($caller['telefono'])) - {{ $caller['telefono'] }} - @else - - - @endif -
-
Aperto da: {{ $intervento->ticket->apertoDaUser->name ?? '-' }}
-
Assegnato a fornitore da: {{ $intervento->ticket->assegnatoAUser->name ?? '-' }}
-
Descrizione: -
{{ $intervento->ticket->descrizione }}
-
-
-
- -
-
Foto e documenti segnalati
-
- @if($intervento->ticket->attachments->isEmpty()) -

Nessun allegato disponibile.

- @else -
- @foreach($intervento->ticket->attachments as $a) -
-
-
{{ $a->original_file_name }}
-
{{ $a->description ?: 'Nessuna descrizione' }}
- Apri -
-
- @endforeach -
- @endif -
-
-
- -
-
-
Stato operativo
-
-
Stato: {{ $intervento->stato }}
-
Operatore: {{ $intervento->eseguitoDaDipendente?->nome_completo ?: 'Responsabile fornitore' }}
-
Iniziato: {{ optional($intervento->iniziato_at)->format('d/m/Y H:i') ?: '-' }}
-
Terminato: {{ optional($intervento->terminato_at)->format('d/m/Y H:i') ?: '-' }}
-
Rif. chiusura: {{ $intervento->riferimento_chiusura ?: '-' }}
-
QR token: {{ $intervento->qr_token ?: '-' }}
-
-
- -
-
Storico stesso richiedente/stabile
-
- @forelse($storico as $old) -
-
#{{ $old->ticket_id }} - {{ $old->ticket->titolo ?? '-' }}
-
{{ $old->stato }} · {{ optional($old->updated_at)->format('d/m/Y H:i') }}
- @if($old->note_amministratore) -
Nota admin: {{ \Illuminate\Support\Str::limit($old->note_amministratore, 90) }}
- @endif -
- @empty -

Nessuno storico correlato.

- @endforelse -
-
-
-
-
- -
-
-
Rapporto operativo, ricambi e apparato
-
- @if($intervento->stato === 'assegnato') -
- @csrf - -
- @endif - -
- @csrf -
- - -
- -
-
-
-
-
- -
- - -
- -
- - @php($items = old('articoli_utilizzati', $intervento->articoli_utilizzati ?? [])) - @for($i = 0; $i < 10; $i++) - - @endfor -
- -
-
- - -
-
- - -
-
- - @for($i = 0; $i < 10; $i++) - - @endfor - -
- - -
- - -
-
-
-
- -
-
-
Note e comunicazioni ticket
-
- @forelse($intervento->ticket->messages as $msg) -
-
{{ $msg->user->name ?? 'Sistema' }} · {{ optional($msg->created_at)->format('d/m/Y H:i') }}
-
{{ $msg->messaggio }}
-
- @empty -

Nessuna comunicazione disponibile.

- @endforelse -
-
-
- -
-
-
Chiusura rapportino e proforma
-
-
- @csrf -
- - -
- -
-
-
-
- -
- - -
-
- - -
- -
- - -
- - -
-
-
-
-
-
-@endsection diff --git a/routes/web.php b/routes/web.php index 07fde01..c135d8a 100755 --- a/routes/web.php +++ b/routes/web.php @@ -23,6 +23,7 @@ use App\Http\Controllers\Admin\TicketInterventoController; use App\Http\Controllers\Admin\UnitaImmobiliareController; use App\Http\Controllers\Admin\VoceSpesaController; +use App\Http\Controllers\Condomino\AnagrafeController as CondominoAnagrafeController; use App\Http\Controllers\Condomino\DashboardController as CondominoDashboardController; use App\Http\Controllers\Condomino\DocumentoController as CondominoDocumentoController; use App\Http\Controllers\Condomino\TicketController as CondominoTicketController; @@ -34,6 +35,7 @@ use App\Http\Controllers\SuperAdmin\CategoriaTicketController; use App\Http\Controllers\SuperAdmin\DiagnosticaFattureElettronicheController; use App\Http\Controllers\SuperAdmin\UserController as SuperAdminUserController; +use App\Models\TicketIntervento; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; @@ -60,20 +62,20 @@ if (Auth::check()) { $user = Auth::user(); if ($user instanceof \App\Models\User && $user->hasRole('super-admin')) { - return redirect()->route('superadmin.dashboard'); + return redirect('/admin-filament'); } if ($user instanceof \App\Models\User && ($user->hasRole('amministratore') || $user->hasRole('admin'))) { - return redirect()->route('admin.dashboard'); + return redirect('/admin-filament'); } if ($user instanceof \App\Models\User && $user->hasRole('collaboratore')) { // Collaboratore (profilo 4) condivide la dashboard admin ma con meno permessi - return redirect()->route('admin.dashboard'); + return redirect('/admin-filament'); } if ($user instanceof \App\Models\User && ($user->hasRole('condomino') || $user->hasRole('inquilino'))) { return redirect()->route('condomino.dashboard'); } if ($user instanceof \App\Models\User && $user->hasRole('fornitore')) { - return redirect()->route('fornitore.tickets.index'); + return redirect()->to(\App\Filament\Pages\Fornitore\TicketOperativi::getUrl(panel: 'admin-filament')); } } // Fallback ad una semplice dashboard neutra se manca ruolo/vista @@ -91,7 +93,7 @@ // --- SUPER-ADMIN PANEL --- Route::middleware(['role:super-admin'])->prefix('superadmin')->name('superadmin.')->group(function () { Route::get('/', function () { - return view('superadmin.dashboard'); + return redirect('/admin-filament'); })->name('dashboard'); // Gestione utenti @@ -613,6 +615,11 @@ return redirect()->route('profile.edit'); })->name('profile'); + // Anagrafe condominiale self-service + Route::get('/anagrafe', [CondominoAnagrafeController::class, 'show'])->name('anagrafe.show'); + Route::post('/anagrafe', [CondominoAnagrafeController::class, 'update'])->name('anagrafe.update'); + Route::get('/anagrafe/modulo', [CondominoAnagrafeController::class, 'downloadModulo'])->name('anagrafe.modulo'); + // Documenti Route::get('/documenti', [CondominoDocumentoController::class, 'index'])->name('documenti.index'); @@ -637,13 +644,37 @@ // --- FORNITORE / MANUTENTORE PANEL --- Route::middleware(['role:fornitore|super-admin|admin|amministratore'])->prefix('fornitore')->name('fornitore.')->group(function () { - Route::get('/tickets', [FornitoreTicketOperativoController::class, 'index'])->name('tickets.index'); - Route::get('/tickets/{intervento}', [FornitoreTicketOperativoController::class, 'show'])->name('tickets.show'); + Route::get('/tickets', function () { + $url = \App\Filament\Pages\Fornitore\TicketOperativi::getUrl(panel: 'admin-filament'); + + if (request()->filled('fornitore')) { + $url .= '?fornitore=' . (int) request()->query('fornitore'); + } + + if (request()->filled('stato')) { + $separator = str_contains($url, '?') ? '&' : '?'; + $url .= $separator . 'stato=' . urlencode((string) request()->query('stato')); + } + + return redirect()->to($url); + })->name('tickets.index'); + Route::get('/tickets/{intervento}', function (TicketIntervento $intervento) { + return redirect()->to(\App\Filament\Pages\Fornitore\TicketInterventoScheda::getUrl(['record' => (int) $intervento->id], panel: 'admin-filament')); + })->name('tickets.show'); + Route::post('/tickets/{intervento}/assign', [FornitoreTicketOperativoController::class, 'assign'])->name('tickets.assign'); Route::post('/tickets/{intervento}/start', [FornitoreTicketOperativoController::class, 'start'])->name('tickets.start'); Route::post('/tickets/{intervento}/complete', [FornitoreTicketOperativoController::class, 'complete'])->name('tickets.complete'); Route::post('/tickets/{intervento}/sollecito', [FornitoreTicketOperativoController::class, 'sollecito'])->name('tickets.sollecito'); - Route::get('/dipendenti', [\App\Http\Controllers\Fornitore\DipendenteController::class, 'index'])->name('dipendenti.index'); + Route::get('/dipendenti', function () { + $url = \App\Filament\Pages\Fornitore\Collaboratori::getUrl(panel: 'admin-filament'); + + if (request()->filled('fornitore')) { + $url .= '?fornitore=' . (int) request()->query('fornitore'); + } + + return redirect()->to($url); + })->name('dipendenti.index'); Route::post('/dipendenti', [\App\Http\Controllers\Fornitore\DipendenteController::class, 'store'])->name('dipendenti.store'); Route::put('/dipendenti/{dipendente}', [\App\Http\Controllers\Fornitore\DipendenteController::class, 'update'])->name('dipendenti.update'); Route::post('/dipendenti/{dipendente}/toggle', [\App\Http\Controllers\Fornitore\DipendenteController::class, 'toggle'])->name('dipendenti.toggle'); diff --git a/scripts/ops/install-smdr-systemd.sh b/scripts/ops/install-smdr-systemd.sh new file mode 100755 index 0000000..23c6aab --- /dev/null +++ b/scripts/ops/install-smdr-systemd.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TEMPLATE="$BASE_DIR/scripts/systemd/netgescon-smdr-listener.service.template" + +SERVICE_NAME="${SERVICE_NAME:-netgescon-smdr-listener.service}" +RUN_AS="${RUN_AS:-www-data}" +RUN_GROUP="${RUN_GROUP:-$RUN_AS}" +INSTALL_DIR="${INSTALL_DIR:-/etc/systemd/system}" +TARGET_FILE="$INSTALL_DIR/$SERVICE_NAME" + +if [[ ! -f "$TEMPLATE" ]]; then + echo "Template non trovato: $TEMPLATE" >&2 + exit 1 +fi + +TMP_FILE="$(mktemp)" +trap 'rm -f "$TMP_FILE"' EXIT + +sed \ + -e "s|@BASE_DIR@|$BASE_DIR|g" \ + -e "s|@RUN_AS@|$RUN_AS|g" \ + -e "s|@RUN_GROUP@|$RUN_GROUP|g" \ + "$TEMPLATE" > "$TMP_FILE" + +echo "Installo $SERVICE_NAME in $TARGET_FILE" +sudo install -o root -g root -m 0644 "$TMP_FILE" "$TARGET_FILE" +sudo systemctl daemon-reload +sudo systemctl enable --now "$SERVICE_NAME" +sudo systemctl status "$SERVICE_NAME" --no-pager \ No newline at end of file diff --git a/scripts/ops/netgescon-smdr-listener.sh b/scripts/ops/netgescon-smdr-listener.sh new file mode 100755 index 0000000..6123967 --- /dev/null +++ b/scripts/ops/netgescon-smdr-listener.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$BASE_DIR" + +SMDR_ENV_FILE="${NETGESCON_SMDR_ENV_FILE:-$BASE_DIR/.env.smdr}" + +if [[ -f "$SMDR_ENV_FILE" ]]; then + set -a + # shellcheck disable=SC1090 + source "$SMDR_ENV_FILE" + set +a +fi + +PHP_BIN="${NETGESCON_PHP_BIN:-$(command -v php)}" +SMDR_HOST="${NETGESCON_SMDR_HOST:-192.168.0.101}" +SMDR_PORT="${NETGESCON_SMDR_PORT:-2300}" +SMDR_USER="${NETGESCON_SMDR_USER:-SMDR}" +SMDR_PASS="${NETGESCON_SMDR_PASS:-PCCSMDR}" +SMDR_TIMEOUT="${NETGESCON_SMDR_TIMEOUT:-0}" +SMDR_RECONNECT="${NETGESCON_SMDR_RECONNECT:-1}" +SMDR_RECONNECT_DELAY="${NETGESCON_SMDR_RECONNECT_DELAY:-5}" +SMDR_WRITE_POSTIT="${NETGESCON_SMDR_WRITE_POSTIT:-1}" +SMDR_WRITE_COMMUNICATIONS="${NETGESCON_SMDR_WRITE_COMMUNICATIONS:-1}" +SMDR_MAX_LINES="${NETGESCON_SMDR_MAX_LINES:-0}" +SMDR_NO_AUTH="${NETGESCON_SMDR_NO_AUTH:-0}" + +args=( + artisan smdr:listen + "--host=${SMDR_HOST}" + "--port=${SMDR_PORT}" + "--user=${SMDR_USER}" + "--pass=${SMDR_PASS}" + "--timeout=${SMDR_TIMEOUT}" + "--reconnect=${SMDR_RECONNECT}" + "--reconnect-delay=${SMDR_RECONNECT_DELAY}" + "--write-postit=${SMDR_WRITE_POSTIT}" + "--write-communications=${SMDR_WRITE_COMMUNICATIONS}" + "--max-lines=${SMDR_MAX_LINES}" +) + +if [[ "$SMDR_NO_AUTH" == "1" || "$SMDR_NO_AUTH" == "true" || "$SMDR_NO_AUTH" == "yes" ]]; then + args+=(--no-auth) +fi + +exec "$PHP_BIN" "${args[@]}" \ No newline at end of file diff --git a/scripts/systemd/netgescon-smdr-listener.service.template b/scripts/systemd/netgescon-smdr-listener.service.template new file mode 100644 index 0000000..208562b --- /dev/null +++ b/scripts/systemd/netgescon-smdr-listener.service.template @@ -0,0 +1,20 @@ +[Unit] +Description=NetGescon SMDR listener +After=network-online.target mysql.service +Wants=network-online.target + +[Service] +Type=simple +User=@RUN_AS@ +Group=@RUN_GROUP@ +WorkingDirectory=@BASE_DIR@ +ExecStart=/usr/bin/env bash -lc '@BASE_DIR@/scripts/ops/netgescon-smdr-listener.sh' +Restart=always +RestartSec=5 +KillSignal=SIGTERM +TimeoutStopSec=30 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target \ No newline at end of file