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">
Crea Intervento Operativo
- 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
+
+
+ Email
+ Tipo
+ Attiva
+
+ @forelse ($emailRows as $row)
+
+ {{ $row['email'] ?: '____________________________' }}
+ {{ $row['tipo_email'] ?: 'secondaria' }}
+ {{ ! empty($row['email']) ? (! empty($row['attiva']) ? 'Si' : 'No') : '___' }}
+
+ @empty
+ Nessun recapito aggiuntivo indicato.
+ @endforelse
+
+
+
+
+
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) }}
+
+
+ Servizio
+ Recapito
+
+ @foreach ($matrix as $row)
+
+ {{ $row['label'] }}
+ {{ $row['value'] ?: collect($row['resolved'] ?? [])->pluck('email')->implode(', ') ?: '____________________________' }}
+
+ @endforeach
+
+ @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
+
+
+
+
+
\ 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 }}
+
+
+
+
+
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
+
+
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 @@
+
+
+