diff --git a/app/Console/Commands/GesconAutoSyncAnagrafiche.php b/app/Console/Commands/GesconAutoSyncAnagrafiche.php index e26dacd..81b6c93 100644 --- a/app/Console/Commands/GesconAutoSyncAnagrafiche.php +++ b/app/Console/Commands/GesconAutoSyncAnagrafiche.php @@ -435,7 +435,7 @@ public function handle(): int } $existingMatch = $this->findExistingRubricaMatch($amministratoreId, $data); - $record = $existingMatch['record'] ?: RubricaUniversale::withTrashed()->where($match)->first(); + $record = $existingMatch['record'] ?: RubricaUniversale::withTrashed()->where($match)->first(); if ($record) { $this->mergeImportDataIntoRubrica( $record, @@ -609,7 +609,7 @@ public function handle(): int $exists ? $updated++ : $inserted++; } else { $existingMatch = $this->findExistingRubricaMatch($amministratoreId, $data); - $record = $existingMatch['record'] ?: RubricaUniversale::withTrashed()->where($match)->first(); + $record = $existingMatch['record'] ?: RubricaUniversale::withTrashed()->where($match)->first(); if ($record) { $this->mergeImportDataIntoRubrica( $record, @@ -1490,18 +1490,18 @@ private function syncRubricaEmailChannels(RubricaUniversale $record, int $stabil foreach ($emails as $index => $email) { RubricaContattoCanale::query()->updateOrCreate( [ - 'rubrica_id' => (int) $record->id, - 'tipo' => 'email', - 'valore' => $email, - 'stabile_id' => $stabileId > 0 ? $stabileId : null, + 'rubrica_id' => (int) $record->id, + 'tipo' => 'email', + 'valore' => $email, + 'stabile_id' => $stabileId > 0 ? $stabileId : null, 'unita_immobiliare_id' => $unitaId, ], [ - 'etichetta' => $index === 0 ? 'Email primaria legacy' : 'Email aggiuntiva legacy', + 'etichetta' => $index === 0 ? 'Email primaria legacy' : 'Email aggiuntiva legacy', 'is_principale' => $index === 0, - 'meta' => [ + 'meta' => [ 'source' => 'gescon_import.condomin', - 'scope' => 'auto_sync_anagrafiche', + 'scope' => 'auto_sync_anagrafiche', ], ], ); @@ -1510,18 +1510,18 @@ private function syncRubricaEmailChannels(RubricaUniversale $record, int $stabil foreach ($pecs as $index => $pec) { RubricaContattoCanale::query()->updateOrCreate( [ - 'rubrica_id' => (int) $record->id, - 'tipo' => 'pec', - 'valore' => $pec, - 'stabile_id' => $stabileId > 0 ? $stabileId : null, + 'rubrica_id' => (int) $record->id, + 'tipo' => 'pec', + 'valore' => $pec, + 'stabile_id' => $stabileId > 0 ? $stabileId : null, 'unita_immobiliare_id' => $unitaId, ], [ - 'etichetta' => $index === 0 ? 'PEC primaria legacy' : 'PEC aggiuntiva legacy', + 'etichetta' => $index === 0 ? 'PEC primaria legacy' : 'PEC aggiuntiva legacy', 'is_principale' => $index === 0, - 'meta' => [ + 'meta' => [ 'source' => 'gescon_import.condomin', - 'scope' => 'auto_sync_anagrafiche', + 'scope' => 'auto_sync_anagrafiche', ], ], ); diff --git a/app/Console/Commands/ImportGesconFullPipeline.php b/app/Console/Commands/ImportGesconFullPipeline.php index b54033c..934b33c 100644 --- a/app/Console/Commands/ImportGesconFullPipeline.php +++ b/app/Console/Commands/ImportGesconFullPipeline.php @@ -54,6 +54,10 @@ class ImportGesconFullPipeline extends Command private array $legacyYearMap = []; + private array $legacyUnitTypeMappings = []; + + private array $tipologiaCache = []; + private bool $legacyYearMapLoaded = false; private function requestedLegacyYear(): ?string @@ -571,10 +575,11 @@ private function stepUnita(?int $limit): int return 0; } - $count = 0; - $updated = 0; - $hasDetailedSchema = Schema::hasColumn('unita_immobiliari', 'codice_unita'); - $parentStabile = null; + $count = 0; + $updated = 0; + $hasDetailedSchema = Schema::hasColumn('unita_immobiliari', 'codice_unita'); + $hasTipologiaColumn = Schema::hasColumn('unita_immobiliari', 'tipologia_id'); + $parentStabile = null; if (Schema::hasTable('stabili') && $this->option('stabile')) { $stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; $parentStabile = DB::table('stabili')->where($stabileCol, $this->option('stabile'))->first(); @@ -645,9 +650,10 @@ private function stepUnita(?int $limit): int $sub = null; } // Riconosci pertinenze nel campo INT o note: cantina, box/garage, posto auto, soffitta/solaio, locale tecnico - $internoStr = is_string($interno) ? trim($interno) : (string) $interno; - $ptype = null; - $pcode = null; // tipo pertinenza e codice interno normalizzato + $ownerDescriptor = trim((string) (($r->cognome ?? '') . ' ' . ($r->nome ?? ''))); + $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'; @@ -666,6 +672,10 @@ private function stepUnita(?int $limit): int $pcode = 'LT' . ($m[2] ?? ''); } } + $unitClassification = $this->resolveLegacyUnitClassification($r, $internoStr, $ownerDescriptor, $ptype); + if ($ptype === null && ($unitClassification['pertinenza_type'] ?? null)) { + $ptype = (string) $unitClassification['pertinenza_type']; + } $scalaVal = trim((string) ($r->scala ?? 'A')) ?: 'A'; // Piano: normalizza (R=0, T=0, 1=1, -1 o S= -1) $pianoVal = 0; @@ -686,6 +696,9 @@ private function stepUnita(?int $limit): int $normalizedInterno = null; } } + if ($normalizedInterno === null) { + $normalizedInterno = $this->buildLegacyFallbackInterno($legacyCondId, $unitClassification); + } if ($hasDetailedSchema) { $exists = null; if ($legacyCondId !== null && $legacyCondId !== '' && Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) { @@ -768,8 +781,25 @@ private function stepUnita(?int $limit): int $patch['denominazione'] = $denominazione; } } - if (Schema::hasColumn('unita_immobiliari', 'tipo_unita') && (string) ($exists->tipo_unita ?? '') !== ($ptype ?: 'abitazione')) { - $patch['tipo_unita'] = $ptype ?: 'abitazione'; + $resolvedTipoUnita = $this->normalizeLegacyTipoUnita($unitClassification['tipo_unita'] ?? ($ptype ?: 'abitazione')); + if (Schema::hasColumn('unita_immobiliari', 'tipo_unita') && (string) ($exists->tipo_unita ?? '') !== $resolvedTipoUnita) { + $patch['tipo_unita'] = $resolvedTipoUnita; + } + if (Schema::hasColumn('unita_immobiliari', 'categoria_catastale')) { + $categoriaCatastale = $this->normalizeCategoriaCatastale($this->firstNonEmptyStr([ + $r->catasto_categoria ?? null, + $r->categoria_catastale ?? null, + $unitClassification['categoria_catastale'] ?? null, + ], 20)); + if ($categoriaCatastale !== null && (string) ($exists->categoria_catastale ?? '') !== $categoriaCatastale) { + $patch['categoria_catastale'] = $categoriaCatastale; + } + } + if ($hasTipologiaColumn) { + $tipologiaId = $this->resolveLegacyTipologiaId($unitClassification); + if ($tipologiaId !== null && (int) ($exists->tipologia_id ?? 0) !== $tipologiaId) { + $patch['tipologia_id'] = $tipologiaId; + } } if (Schema::hasColumn('unita_immobiliari', 'attiva') && ! (bool) ($exists->attiva ?? false)) { $patch['attiva'] = true; @@ -777,6 +807,15 @@ private function stepUnita(?int $limit): int if (Schema::hasColumn('unita_immobiliari', 'deleted_at') && ! empty($exists->deleted_at)) { $patch['deleted_at'] = null; } + if (($unitClassification['is_condominiale'] ?? false) && Schema::hasColumn('unita_immobiliari', 'denominazione')) { + $condoName = $this->firstNonEmptyStr([ + $unitClassification['denominazione'] ?? null, + $ownerDescriptor, + ], 255); + if ($condoName !== null && (string) ($exists->denominazione ?? '') !== $condoName) { + $patch['denominazione'] = $condoName; + } + } if ($this->splitMode === 'palazzine') { if (Schema::hasColumn('unita_immobiliari', 'palazzina')) { if (empty($exists->palazzina) || $exists->palazzina !== $scalaVal) { @@ -799,24 +838,35 @@ private function stepUnita(?int $limit): int } continue; } - $data = [ + $resolvedTipoUnita = $this->normalizeLegacyTipoUnita($unitClassification['tipo_unita'] ?? ($ptype ?: 'abitazione')); + $categoriaCatastale = $this->normalizeCategoriaCatastale($this->firstNonEmptyStr([ + $r->catasto_categoria ?? null, + $r->categoria_catastale ?? null, + $unitClassification['categoria_catastale'] ?? null, + ], 20)); + $tipologiaId = $hasTipologiaColumn ? $this->resolveLegacyTipologiaId($unitClassification) : null; + $data = [ 'stabile_id' => $rowStabileId, 'palazzina_id' => null, 'codice_unita' => $codice, - 'denominazione' => trim(($r->cognome ?? '') . ' ' . ($r->nome ?? '')) ?: null, + 'denominazione' => $this->firstNonEmptyStr([ + $unitClassification['denominazione'] ?? null, + $ownerDescriptor, + ], 255), 'descrizione' => null, 'palazzina' => $this->splitMode === 'palazzine' ? $scalaVal : null, 'scala' => $scalaVal, 'piano' => $pianoVal, 'interno' => $normalizedInterno, 'subalterno' => $sub, - 'categoria_catastale' => null, + 'categoria_catastale' => $categoriaCatastale, 'classe' => null, 'consistenza' => null, 'rendita_catastale' => null, 'millesimi_generali' => 0, 'legacy_cond_id' => $legacyCondId, - 'tipo_unita' => $ptype ?: 'abitazione', + 'tipo_unita' => $resolvedTipoUnita, + 'tipologia_id' => $tipologiaId, 'stato_conservazione' => 'buono', 'stato_occupazione' => 'occupata_proprietario', 'attiva' => true, @@ -824,6 +874,9 @@ private function stepUnita(?int $limit): int 'created_at' => now(), 'updated_at' => now(), ]; + if (! $hasTipologiaColumn) { + unset($data['tipologia_id']); + } $this->dynamicInsert('unita_immobiliari', $data); $unitaInserted = DB::table('unita_immobiliari') ->where('stabile_id', $rowStabileId) @@ -4881,8 +4934,8 @@ private function buildLegacyUnitCode(string $stabileCode, string $scala, string { $maxLen = 20; - $stablePart = $this->sanitizeUnitCodeSegment($stabileCode, 4) ?: 'S0'; - $scalaPart = $this->sanitizeUnitCodeSegment($scala, 4) ?: 'A'; + $stablePart = $this->sanitizeUnitCodeSegment($stabileCode, 4) ?: 'S0'; + $scalaPart = $this->sanitizeUnitCodeSegment($scala, 4) ?: 'A'; $internoPart = $this->sanitizeUnitCodeSegment($interno, 32) ?: 'INT'; $base = $stablePart . '-' . $scalaPart . '-' . $internoPart; @@ -4890,14 +4943,236 @@ private function buildLegacyUnitCode(string $stabileCode, string $scala, string return $base; } - $hash = strtoupper(substr(hash('crc32b', implode('|', [$stabileCode, $scala, $interno, (string) $salt])), 0, 4)); - $reserved = strlen($stablePart) + strlen($scalaPart) + strlen($hash) + 3; + $hash = strtoupper(substr(hash('crc32b', implode('|', [$stabileCode, $scala, $interno, (string) $salt])), 0, 4)); + $reserved = strlen($stablePart) + strlen($scalaPart) + strlen($hash) + 3; $availableInterno = max(1, $maxLen - $reserved); - $internoCompact = substr($internoPart, 0, $availableInterno); + $internoCompact = substr($internoPart, 0, $availableInterno); return $stablePart . '-' . $scalaPart . '-' . $internoCompact . '-' . $hash; } + private function resolveLegacyUnitClassification(object $row, ?string $interno, ?string $ownerDescriptor, ?string $explicitPertinenzaType = null): array + { + $descriptor = Str::upper(trim(implode(' ', array_filter([ + $interno, + $ownerDescriptor, + is_string($row->note ?? null) ? $row->note : null, + is_string($row->descrizione ?? null) ? $row->descrizione : null, + is_string($row->destinazione ?? null) ? $row->destinazione : null, + ], fn($value) => is_string($value) && trim($value) !== '')))); + + $mapping = $this->findLegacyUnitTypeMapping( + 'condomin', + 'tipo_pr', + isset($row->tipo_pr) ? trim((string) $row->tipo_pr) : null, + $descriptor, + ); + + if (str_contains($descriptor, 'MAGAZZ')) { + $mapping = array_merge($mapping, [ + 'tipo_unita' => 'magazzino_deposito', + 'tipologia_codice' => $mapping['tipologia_codice'] ?? 'MAG', + 'tipologia_nome' => $mapping['tipologia_nome'] ?? 'Magazzino', + 'tipologia_categoria' => $mapping['tipologia_categoria'] ?? 'deposito', + 'categoria_catastale' => $mapping['categoria_catastale'] ?? 'C/2', + 'fallback_interno_prefix' => $mapping['fallback_interno_prefix'] ?? 'MAG', + 'denominazione' => $this->looksLikeCondominialeDescriptor($descriptor) + ? 'Magazzino Condominiale' + : ($mapping['denominazione'] ?? null), + ]); + } + + if (str_contains($descriptor, 'BOX') || str_contains($descriptor, 'GARAGE') || str_contains($descriptor, 'AUTORIM')) { + $mapping = array_merge($mapping, [ + 'tipo_unita' => 'box_garage', + 'tipologia_codice' => $mapping['tipologia_codice'] ?? 'BOX', + 'tipologia_nome' => $mapping['tipologia_nome'] ?? 'Box', + 'tipologia_categoria' => $mapping['tipologia_categoria'] ?? 'pertinenza', + 'categoria_catastale' => $mapping['categoria_catastale'] ?? 'C/6', + 'is_pertinenza' => true, + 'fallback_interno_prefix' => $mapping['fallback_interno_prefix'] ?? 'BOX', + ]); + } + + if (str_contains($descriptor, 'CANTIN')) { + $mapping = array_merge($mapping, [ + 'tipo_unita' => 'cantina', + 'tipologia_codice' => $mapping['tipologia_codice'] ?? 'CANT', + 'tipologia_nome' => $mapping['tipologia_nome'] ?? 'Cantina', + 'tipologia_categoria' => $mapping['tipologia_categoria'] ?? 'pertinenza', + 'categoria_catastale' => $mapping['categoria_catastale'] ?? 'C/2', + 'is_pertinenza' => true, + 'fallback_interno_prefix' => $mapping['fallback_interno_prefix'] ?? 'CAN', + ]); + } + + if ($explicitPertinenzaType !== null && $explicitPertinenzaType !== '') { + $mapping['pertinenza_type'] = $explicitPertinenzaType; + } elseif (($mapping['tipo_unita'] ?? null) === 'box_garage') { + $mapping['pertinenza_type'] = 'box'; + } elseif (($mapping['tipo_unita'] ?? null) === 'cantina') { + $mapping['pertinenza_type'] = 'cantina'; + } elseif (($mapping['tipo_unita'] ?? null) === 'soffitta') { + $mapping['pertinenza_type'] = 'soffitta'; + } elseif (($mapping['tipo_unita'] ?? null) === 'locale_tecnico') { + $mapping['pertinenza_type'] = 'locale_tecnico'; + } + + if (($mapping['is_condominiale'] ?? false) && empty($mapping['denominazione'])) { + $mapping['denominazione'] = trim((string) ($ownerDescriptor ?? '')) ?: 'Unita Condominiale'; + } + + return $mapping; + } + + private function buildLegacyFallbackInterno(?string $legacyCondId, array $classification): ?string + { + $prefix = trim((string) ($classification['fallback_interno_prefix'] ?? '')); + $condId = trim((string) ($legacyCondId ?? '')); + if ($prefix === '') { + return null; + } + + return $condId !== '' ? strtoupper($prefix . $condId) : strtoupper($prefix); + } + + private function findLegacyUnitTypeMapping(string $source, string $field, ?string $value, string $descriptor): array + { + $cacheKey = implode('|', [$source, $field, strtoupper(trim((string) $value)), $descriptor]); + if (array_key_exists($cacheKey, $this->legacyUnitTypeMappings)) { + return $this->legacyUnitTypeMappings[$cacheKey]; + } + + $default = [ + 'tipo_unita' => 'abitazione', + 'tipologia_codice' => 'APP', + 'tipologia_nome' => 'Appartamento', + 'tipologia_categoria' => 'residenziale', + 'categoria_catastale' => null, + 'is_pertinenza' => false, + 'is_condominiale' => false, + 'fallback_interno_prefix' => null, + 'denominazione' => null, + ]; + + if (! Schema::hasTable('legacy_unita_tipo_mappings')) { + return $this->legacyUnitTypeMappings[$cacheKey] = $default; + } + + $rows = DB::table('legacy_unita_tipo_mappings') + ->where('legacy_source', $source) + ->where('legacy_field', $field) + ->where(function ($query) use ($value) { + if ($value !== null && trim((string) $value) !== '') { + $query->where('legacy_value', strtoupper(trim((string) $value)))->orWhereNull('legacy_value'); + } else { + $query->whereNull('legacy_value'); + } + }) + ->orderBy('sort_order') + ->orderBy('id') + ->get(); + + foreach ($rows as $row) { + $regex = trim((string) ($row->match_regex ?? '')); + if ($regex !== '' && @preg_match('/' . $regex . '/i', $descriptor) !== 1) { + continue; + } + + return $this->legacyUnitTypeMappings[$cacheKey] = array_merge($default, [ + 'tipo_unita' => trim((string) ($row->tipo_unita ?? '')) ?: $default['tipo_unita'], + 'tipologia_codice' => trim((string) ($row->tipologia_codice ?? '')) ?: $default['tipologia_codice'], + 'tipologia_nome' => trim((string) ($row->tipologia_nome ?? '')) ?: $default['tipologia_nome'], + 'tipologia_categoria' => trim((string) ($row->tipologia_categoria ?? '')) ?: $default['tipologia_categoria'], + 'categoria_catastale' => trim((string) ($row->categoria_catastale ?? '')) ?: null, + 'is_pertinenza' => (bool) ($row->is_pertinenza ?? false), + 'is_condominiale' => (bool) ($row->is_condominiale ?? false), + 'fallback_interno_prefix' => trim((string) ($row->fallback_interno_prefix ?? '')) ?: null, + 'denominazione' => trim((string) ($row->descrizione ?? '')) ?: null, + ]); + } + + return $this->legacyUnitTypeMappings[$cacheKey] = $default; + } + + private function normalizeLegacyTipoUnita(?string $value): string + { + $normalized = Str::snake(Str::lower(trim((string) $value)), '_'); + + return match ($normalized) { + 'box', 'garage', 'posto_auto', 'box_garage' => 'box_garage', + 'cantina' => 'cantina', + 'soffitta', 'solaio' => 'soffitta', + 'locale_tecnico', 'centrale_termica' => 'locale_tecnico', + 'magazzino', 'deposito', 'magazzino_deposito' => 'magazzino_deposito', + 'condominiale', 'spazio_comune_redditizio', 'area_condominiale' => 'spazio_comune_redditizio', + 'studio', 'ufficio', 'studio_professionale' => 'studio_professionale', + 'negozio', 'locale_commerciale', 'attivita_commerciale' => 'attivita_commerciale', + 'abitazione', '' => 'abitazione', + default => 'altro', + }; + } + + private function resolveLegacyTipologiaId(array $classification): ?int + { + if (! Schema::hasTable('unita_tipologie')) { + return null; + } + + $code = strtoupper(trim((string) ($classification['tipologia_codice'] ?? ''))); + if ($code === '') { + return null; + } + + if (array_key_exists($code, $this->tipologiaCache)) { + return $this->tipologiaCache[$code]; + } + + $row = DB::table('unita_tipologie')->where('codice', $code)->first(); + if ($row) { + return $this->tipologiaCache[$code] = (int) $row->id; + } + + if ($this->isDryRun) { + return $this->tipologiaCache[$code] = null; + } + + $id = DB::table('unita_tipologie')->insertGetId([ + 'codice' => $code, + 'nome' => trim((string) ($classification['tipologia_nome'] ?? $code)) ?: $code, + 'categoria' => trim((string) ($classification['tipologia_categoria'] ?? '')) ?: null, + 'is_pertinenza' => (bool) ($classification['is_pertinenza'] ?? false), + 'is_condominiale' => (bool) ($classification['is_condominiale'] ?? false), + 'meta' => json_encode([ + 'source' => 'legacy_unita_tipo_mappings', + 'categoria_catastale' => $classification['categoria_catastale'] ?? null, + ]), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return $this->tipologiaCache[$code] = (int) $id; + } + + private function looksLikeCondominialeDescriptor(string $descriptor): bool + { + return str_contains($descriptor, 'CONDOMINIO') || str_contains($descriptor, 'PARTI COMUNI'); + } + + private function normalizeCategoriaCatastale(?string $value): ?string + { + $value = strtoupper(trim((string) ($value ?? ''))); + if ($value === '') { + return null; + } + + if (preg_match('/^([A-Z])(\d+)$/', $value, $matches) === 1) { + return $matches[1] . '/' . $matches[2]; + } + + return $value; + } + private function sanitizeUnitCodeSegment(?string $value, int $maxLen): string { $normalized = strtoupper(trim((string) $value)); @@ -6232,4 +6507,20 @@ private function mdbExportToRows(string $mdbPath, array $tableCandidates): array @unlink($tmp); return $rows; } + private function firstNonEmptyStr(array $values, int $maxLen): ?string + { + foreach ($values as $v) { + if ($v === null) { + continue; + } + $s = trim((string) $v); + if ($s === '') { + continue; + } + return mb_substr($s, 0, $maxLen); + } + + return null; + } + } diff --git a/app/Console/Commands/SyncGesconFornitoriLegacyCommand.php b/app/Console/Commands/SyncGesconFornitoriLegacyCommand.php index 87bd846..d165163 100644 --- a/app/Console/Commands/SyncGesconFornitoriLegacyCommand.php +++ b/app/Console/Commands/SyncGesconFornitoriLegacyCommand.php @@ -1,5 +1,4 @@ argument('amministratore_id'); - $mdb = (string) $this->option('mdb'); - $table = (string) $this->option('table'); - $dryRun = (bool) $this->option('dry-run'); - $skipTags = (bool) $this->option('skip-tags'); + $adminId = (int) $this->argument('amministratore_id'); + $mdb = (string) $this->option('mdb'); + $table = (string) $this->option('table'); + $dryRun = (bool) $this->option('dry-run'); + $skipTags = (bool) $this->option('skip-tags'); $truncateStaging = (bool) $this->option('truncate-staging'); - $previewLimit = max(1, (int) ($this->option('preview-limit') ?: 10)); + $previewLimit = max(1, (int) ($this->option('preview-limit') ?: 10)); if ($adminId <= 0) { $this->error('amministratore_id non valido.'); @@ -36,19 +35,19 @@ public function handle(): int $this->info('Sync fornitori legacy per amministratore #' . $adminId . ($dryRun ? ' [DRY-RUN]' : '')); $stageParams = [ - 'tenant' => (string) $adminId, - '--mdb' => $mdb, + 'tenant' => (string) $adminId, + '--mdb' => $mdb, '--table' => $table, ]; if ($dryRun) { $stageParams['--preview'] = true; - $stageParams['--limit'] = $previewLimit; + $stageParams['--limit'] = $previewLimit; } if ($truncateStaging && ! $dryRun) { $stageParams['--truncate'] = true; } - $stageExit = Artisan::call('import:gescon-fornitori', $stageParams); + $stageExit = Artisan::call('import:gescon-fornitori', $stageParams); $stageOutput = trim((string) Artisan::output()); $this->line('Step staging exit=' . $stageExit); if ($stageOutput !== '') { @@ -60,14 +59,14 @@ public function handle(): int $importParams = [ 'amministratore_id' => (string) $adminId, - '--mdb' => $mdb, - '--table' => $table, + '--mdb' => $mdb, + '--table' => $table, ]; if ($dryRun) { $importParams['--dry-run'] = true; } - $importExit = Artisan::call('gescon:import-fornitori-legacy', $importParams); + $importExit = Artisan::call('gescon:import-fornitori-legacy', $importParams); $importOutput = trim((string) Artisan::output()); $this->line('Step archivio locale exit=' . $importExit); if ($importOutput !== '') { @@ -83,7 +82,7 @@ public function handle(): int $tagParams['--dry-run'] = true; } - $tagExit = Artisan::call('fornitori:import-legacy-tags', $tagParams); + $tagExit = Artisan::call('fornitori:import-legacy-tags', $tagParams); $tagOutput = trim((string) Artisan::output()); $this->line('Step tag legacy exit=' . $tagExit); if ($tagOutput !== '') { @@ -95,9 +94,9 @@ public function handle(): int } $this->info($dryRun - ? 'Dry-run sync fornitori legacy completato.' - : 'Sync fornitori legacy completato.'); + ? 'Dry-run sync fornitori legacy completato.' + : 'Sync fornitori legacy completato.'); return self::SUCCESS; } -} \ No newline at end of file +} diff --git a/app/Filament/Pages/Gescon/ImportazioneArchivi.php b/app/Filament/Pages/Gescon/ImportazioneArchivi.php index a50201d..a66a61a 100644 --- a/app/Filament/Pages/Gescon/ImportazioneArchivi.php +++ b/app/Filament/Pages/Gescon/ImportazioneArchivi.php @@ -367,6 +367,9 @@ public function form(Schema $schema): Schema ->options(fn(): array=> $this->anniOptions) ->multiple() ->searchable() + ->live() + ->dehydrateStateUsing(fn($state): array=> $this->normalizeSelectedOptionValues((array) ($state ?? []), $this->anniOptions)) + ->validationMessages(['in' => 'Seleziona anni validi dall elenco aggiornato.']) ->helperText('Seleziona uno o piu anni. Le directory legacy vengono mappate automaticamente da tabella anni (nome_dir).') ->dehydrated(), @@ -375,6 +378,9 @@ public function form(Schema $schema): Schema ->options(fn(): array=> $this->anniOptions) ->multiple() ->searchable() + ->live() + ->dehydrateStateUsing(fn($state): array=> $this->normalizeSelectedOptionValues((array) ($state ?? []), $this->anniOptions)) + ->validationMessages(['in' => 'Seleziona anni acqua validi dall elenco aggiornato.']) ->helperText('Per ACQUA (acqua_gen/acqua_dett/acqua_fatture).') ->dehydrated(), @@ -383,6 +389,9 @@ public function form(Schema $schema): Schema ->options(fn(): array=> $this->anniOptions) ->multiple() ->searchable() + ->live() + ->dehydrateStateUsing(fn($state): array=> $this->normalizeSelectedOptionValues((array) ($state ?? []), $this->anniOptions)) + ->validationMessages(['in' => 'Seleziona anni aperti validi dall elenco aggiornato.']) ->helperText('Gli anni fuori da questa selezione vengono chiusi secondo le regole di import.') ->dehydrated(), @@ -442,7 +451,7 @@ public function form(Schema $schema): Schema public function loadStabili(): void { - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $mdb = (string) ($state['stabili_mdb'] ?? ''); if ($mdb === '' || ! is_file($mdb) || ! is_readable($mdb)) { @@ -487,7 +496,7 @@ public function loadStabili(): void ksort($opts); $this->stabiliOptions = $opts; - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); if (empty($state['stabile_code'])) { $active = Auth::user() instanceof User ? StabileContext::getActiveStabile(Auth::user()) : null; $activeCode = $active?->codice_stabile ?? null; @@ -500,7 +509,7 @@ public function loadStabili(): void } } - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $this->refreshAnniOptionsForStabile( (string) ($state['stabile_code'] ?? ''), (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon') @@ -520,14 +529,14 @@ public function creaStabileDaMdb(): void abort(403); } - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code === '') { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $amm = null; if ($user->hasAnyRole(['super-admin', 'admin'])) { @@ -607,7 +616,7 @@ public function applicaEnrichmentStabile(): void abort(403); } - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); $path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'); @@ -646,7 +655,7 @@ public function aggiornaAnagraficheDaCondomin(): void abort(403); } - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code === '') { Notification::make()->title('Seleziona uno stabile')->danger()->send(); @@ -685,8 +694,8 @@ public function risincronizzaRelazioniImportate(): void abort(403); } - $state = $this->getSchema('form')?->getState() ?? []; - $code = trim((string) ($state['stabile_code'] ?? '')); + $state = $this->getImportFormState(); + $code = trim((string) ($state['stabile_code'] ?? '')); if ($code === '') { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; @@ -722,7 +731,7 @@ public function aggiornaFornitoriGescon(): void abort(403); } - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'); $amm = null; @@ -790,7 +799,7 @@ public function refreshImportTagLegacyFornitoriGlobale(): void abort(403); } - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'); $dryRun = (bool) ($state['dry_run'] ?? false); @@ -864,7 +873,7 @@ public function applicaEnrichmentTuttiStabili(): void abort(403); } - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'); $amm = null; @@ -915,7 +924,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void abort(403); } - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code === '') { Notification::make()->title('Seleziona uno stabile')->danger()->send(); @@ -1172,8 +1181,8 @@ public function eseguiImportSelezionato(EssentialImportService $service): void } $shouldSyncRelations = ! empty($options['with_unita']) - || ! empty($options['with_soggetti']) - || ! empty($options['with_diritti']); + || ! empty($options['with_soggetti']) + || ! empty($options['with_diritti']); if ($shouldSyncRelations) { $relationSync = $this->runRelationSyncSequence($code, (int) $stabile->id, (bool) $options['dry']); @@ -1213,7 +1222,7 @@ public function eseguiImportAllineamentoCompleto(): void abort(403); } - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code === '') { Notification::make()->title('Seleziona uno stabile')->danger()->send(); @@ -1288,7 +1297,7 @@ public function eseguiImportAllineamentoCompleto(): void public function caricaAnniDaGenerale(): void { - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $code = (string) ($state['stabile_code'] ?? ''); $path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'); @@ -1312,16 +1321,16 @@ public function caricaAnniDaGenerale(): void /** @return array */ public function getCommandPreviewData(): array { - $state = is_array($this->data) ? $this->data : []; + $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code !== '') { $code = str_pad($code, 4, '0', STR_PAD_LEFT); } - $path = trim((string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon')); + $path = trim((string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon')); $annoDir = trim((string) ($state['anno'] ?? '')); - $years = implode(',', array_values(array_filter(array_map( + $years = implode(',', array_values(array_filter(array_map( fn($value) => trim((string) $value), (array) ($state['align_years_selected'] ?? []) )))); @@ -1334,8 +1343,8 @@ public function getCommandPreviewData(): array (array) ($state['align_open_years_selected'] ?? []) )))); - $adminId = (int) ($state['amministratore_id'] ?? 0); - $stabile = $this->resolveSelectedStabileByCode($code); + $adminId = (int) ($state['amministratore_id'] ?? 0); + $stabile = $this->resolveSelectedStabileByCode($code); $stabileId = $stabile instanceof Stabile ? (int) $stabile->id : null; $fornitoriMdb = trim((string) ($state['fornitori_mdb'] ?? '')); @@ -1356,47 +1365,47 @@ public function getCommandPreviewData(): array : ''; return [ - 'source_mode' => (string) ($state['source_mode'] ?? 'linux_server'), - 'stabile_code' => $code, - 'stabile_id' => $stabileId, + 'source_mode' => (string) ($state['source_mode'] ?? 'linux_server'), + 'stabile_code' => $code, + 'stabile_id' => $stabileId, 'amministratore_id' => $adminId > 0 ? $adminId : null, - 'anno_dir' => $annoDir, - 'years_csv' => $years, - 'water_years_csv' => $waterYears, - 'open_years_csv' => $openYears, - 'paths' => [ - 'root' => $path, - 'stabili_mdb' => $stabiliMdb, + 'anno_dir' => $annoDir, + 'years_csv' => $years, + 'water_years_csv' => $waterYears, + 'open_years_csv' => $openYears, + 'paths' => [ + 'root' => $path, + 'stabili_mdb' => $stabiliMdb, 'fornitori_mdb' => $fornitoriMdb, - 'generale_mdb' => $generaleMdb, - 'singolo_mdb' => $singoloMdb, + 'generale_mdb' => $generaleMdb, + 'singolo_mdb' => $singoloMdb, ], - 'commands' => [ - 'load_stabili' => $stabiliMdb !== '' + 'commands' => [ + 'load_stabili' => $stabiliMdb !== '' ? 'php artisan import:gescon-fornitori ' . max(1, $adminId) . ' --mdb="' . $fornitoriMdb . '" --preview --limit=5' : null, - 'fornitori_sync' => ($adminId > 0 && $fornitoriMdb !== '') + 'fornitori_sync' => ($adminId > 0 && $fornitoriMdb !== '') ? 'php artisan gescon:sync-fornitori-legacy ' . $adminId . ' --mdb="' . $fornitoriMdb . '"' : null, - 'allineamento' => ($code !== '' && $path !== '' && $years !== '') + 'allineamento' => ($code !== '' && $path !== '' && $years !== '') ? 'php artisan gescon:import-align --stabile=' . $code . ' --path="' . $path . '" --years=' . $years . ' --water-years=' . ($waterYears !== '' ? $waterYears : $years) . ' --open-years=' . ($openYears !== '' ? $openYears : $years) : null, - 'unita' => ($code !== '' && $path !== '' && $annoDir !== '') + 'unita' => ($code !== '' && $path !== '' && $annoDir !== '') ? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=unita --anno=' . $annoDir . ' --path="' . $path . '"' : null, - 'soggetti' => ($code !== '' && $path !== '' && $annoDir !== '') + 'soggetti' => ($code !== '' && $path !== '' && $annoDir !== '') ? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=soggetti --anno=' . $annoDir . ' --path="' . $path . '"' : null, - 'diritti' => ($code !== '' && $path !== '' && $annoDir !== '') + 'diritti' => ($code !== '' && $path !== '' && $annoDir !== '') ? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=diritti --anno=' . $annoDir . ' --path="' . $path . '"' : null, 'anagrafiche_sync' => ($code !== '') ? 'php artisan gescon:auto-sync-anagrafiche --stabile=' . $code . ' --only=both --link-ruolo --include-comproprietari --apply' : null, - 'ruoli_sync' => ($stabileId !== null) + 'ruoli_sync' => ($stabileId !== null) ? 'php artisan gescon:sync-rubrica-ruoli --stabile=' . $stabileId . ' --apply' : null, - 'repair_contacts' => ($code !== '') + 'repair_contacts' => ($code !== '') ? 'php artisan gescon:repair-nominativi-contacts --stabile=' . $code . ' --apply' : null, ], @@ -1459,7 +1468,7 @@ private function refreshAnniOptionsForStabile(string $code, string $root, ?Set $ ksort($opts); $this->anniOptions = $opts; - $state = $this->getSchema('form')?->getState() ?? []; + $state = $this->getImportFormState(); $keys = array_keys($opts); $selectedYears = array_values(array_filter(array_map( @@ -1523,15 +1532,15 @@ private function resolveSelectedStabileByCode(?string $code): ?Stabile /** @return array{ok:bool,message:string,outputs:array} */ private function runRelationSyncSequence(string $code, int $stabileId, bool $dryRun): array { - $outputs = []; + $outputs = []; $statuses = []; try { $syncParams = [ - '--stabile' => $code, - '--limit' => 20000, - '--only' => 'both', - '--link-ruolo' => true, + '--stabile' => $code, + '--limit' => 20000, + '--only' => 'both', + '--link-ruolo' => true, '--include-comproprietari' => true, ]; if (! $dryRun) { @@ -1539,32 +1548,32 @@ private function runRelationSyncSequence(string $code, int $stabileId, bool $dry } $statuses[] = Artisan::call('gescon:auto-sync-anagrafiche', $syncParams); - $outputs[] = trim((string) Artisan::output()); + $outputs[] = trim((string) Artisan::output()); $ruoliParams = [ '--stabile' => (string) $stabileId, - '--limit' => 5000, + '--limit' => 5000, ]; if (! $dryRun) { $ruoliParams['--apply'] = true; } $statuses[] = Artisan::call('gescon:sync-rubrica-ruoli', $ruoliParams); - $outputs[] = trim((string) Artisan::output()); + $outputs[] = trim((string) Artisan::output()); $repairParams = [ '--stabile' => $code, - '--limit' => 5000, + '--limit' => 5000, ]; if (! $dryRun) { $repairParams['--apply'] = true; } $statuses[] = Artisan::call('gescon:repair-nominativi-contacts', $repairParams); - $outputs[] = trim((string) Artisan::output()); + $outputs[] = trim((string) Artisan::output()); } catch (\Throwable $e) { return [ - 'ok' => false, + 'ok' => false, 'message' => $e->getMessage(), 'outputs' => $outputs, ]; @@ -1577,12 +1586,54 @@ private function runRelationSyncSequence(string $code, int $stabileId, bool $dry ->implode("\n"); return [ - 'ok' => collect($statuses)->every(fn($status) => (int) $status === 0), + 'ok' => collect($statuses)->every(fn($status) => (int) $status === 0), 'message' => $tail !== '' ? $tail : 'Operazione completata.', 'outputs' => $outputs, ]; } + /** @return array */ + private function getImportFormState(): array + { + $state = is_array($this->data) ? $this->data : []; + + $state['align_years_selected'] = $this->normalizeSelectedOptionValues( + (array) ($state['align_years_selected'] ?? []), + $this->anniOptions, + ); + $state['align_water_years_selected'] = $this->normalizeSelectedOptionValues( + (array) ($state['align_water_years_selected'] ?? []), + $this->anniOptions, + ); + $state['align_open_years_selected'] = $this->normalizeSelectedOptionValues( + (array) ($state['align_open_years_selected'] ?? []), + $this->anniOptions, + ); + + return $state; + } + + /** + * @param array $values + * @param array $options + * @return array + */ + private function normalizeSelectedOptionValues(array $values, array $options): array + { + $normalized = array_values(array_filter(array_map( + static fn($value): string => trim((string) $value), + $values, + ), static fn(string $value): bool => $value !== '')); + + if ($options === []) { + return $normalized; + } + + $allowed = array_map('strval', array_keys($options)); + + return array_values(array_filter($normalized, static fn(string $value): bool => in_array($value, $allowed, true))); + } + private function firstNonEmpty(array $row, array $keys): mixed { foreach ($keys as $k) { diff --git a/app/Models/LegacyUnitaTipoMapping.php b/app/Models/LegacyUnitaTipoMapping.php new file mode 100644 index 0000000..7957a35 --- /dev/null +++ b/app/Models/LegacyUnitaTipoMapping.php @@ -0,0 +1,34 @@ + 'boolean', + 'is_condominiale' => 'boolean', + 'sort_order' => 'integer', + 'meta' => 'array', + ]; +} diff --git a/database/migrations/2026_04_09_120000_create_legacy_unita_tipo_mappings_table.php b/database/migrations/2026_04_09_120000_create_legacy_unita_tipo_mappings_table.php new file mode 100644 index 0000000..be9944f --- /dev/null +++ b/database/migrations/2026_04_09_120000_create_legacy_unita_tipo_mappings_table.php @@ -0,0 +1,123 @@ +id(); + $table->string('legacy_source', 50)->default('condomin'); + $table->string('legacy_field', 50)->default('tipo_pr'); + $table->string('legacy_value', 50)->nullable(); + $table->string('descrizione', 255)->nullable(); + $table->string('match_regex', 255)->nullable(); + $table->string('tipo_unita', 50); + $table->string('tipologia_codice', 50)->nullable(); + $table->string('tipologia_nome', 100)->nullable(); + $table->string('tipologia_categoria', 100)->nullable(); + $table->string('categoria_catastale', 20)->nullable(); + $table->boolean('is_pertinenza')->default(false); + $table->boolean('is_condominiale')->default(false); + $table->string('fallback_interno_prefix', 20)->nullable(); + $table->unsignedInteger('sort_order')->default(100); + $table->json('meta')->nullable(); + $table->timestamps(); + + $table->index(['legacy_source', 'legacy_field', 'legacy_value'], 'legacy_unita_tipo_lookup_idx'); + }); + } + + $now = now(); + $rows = [ + [ + 'legacy_source' => 'condomin', + 'legacy_field' => 'tipo_pr', + 'legacy_value' => 'B', + 'descrizione' => 'Box / autorimessa da archivio condomin', + 'tipo_unita' => 'box_garage', + 'tipologia_codice' => 'BOX', + 'tipologia_nome' => 'Box', + 'tipologia_categoria' => 'pertinenza', + 'categoria_catastale' => 'C/6', + 'is_pertinenza' => true, + 'is_condominiale' => false, + 'fallback_interno_prefix' => 'BOX', + 'sort_order' => 10, + 'meta' => json_encode(['note' => 'Default legacy mapping B -> C/6']), + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'legacy_source' => 'condomin', + 'legacy_field' => 'tipo_pr', + 'legacy_value' => 'D', + 'descrizione' => 'Magazzino / deposito da archivio condomin', + 'match_regex' => 'MAGAZZ|DEPOSITO', + 'tipo_unita' => 'magazzino_deposito', + 'tipologia_codice' => 'MAG', + 'tipologia_nome' => 'Magazzino', + 'tipologia_categoria' => 'deposito', + 'categoria_catastale' => 'C/2', + 'is_pertinenza' => false, + 'is_condominiale' => false, + 'fallback_interno_prefix' => 'MAG', + 'sort_order' => 20, + 'meta' => json_encode(['note' => 'Default legacy mapping D + magazzino/deposito -> C/2']), + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'legacy_source' => 'condomin', + 'legacy_field' => 'tipo_pr', + 'legacy_value' => null, + 'descrizione' => 'Locale condominiale generico', + 'match_regex' => 'CONDOMINIO.*MAGAZZ|MAGAZZINO.*CONDOMINIO', + 'tipo_unita' => 'magazzino_deposito', + 'tipologia_codice' => 'CONDO_MAG', + 'tipologia_nome' => 'Magazzino Condominiale', + 'tipologia_categoria' => 'condominiale', + 'categoria_catastale' => 'C/2', + 'is_pertinenza' => false, + 'is_condominiale' => true, + 'fallback_interno_prefix' => 'CMAG', + 'sort_order' => 5, + 'meta' => json_encode(['note' => 'Override per record condominiali senza interno']), + 'created_at' => $now, + 'updated_at' => $now, + ], + ]; + + foreach ($rows as $row) { + $matchRegex = $row['match_regex'] ?? null; + $query = DB::table('legacy_unita_tipo_mappings') + ->where('legacy_source', $row['legacy_source']) + ->where('legacy_field', $row['legacy_field']); + + if ($row['legacy_value'] === null) { + $query->whereNull('legacy_value'); + } else { + $query->where('legacy_value', $row['legacy_value']); + } + + $existing = $query + ->when($matchRegex === null, fn($builder) => $builder->whereNull('match_regex')) + ->when($matchRegex !== null, fn($builder) => $builder->where('match_regex', $matchRegex)) + ->exists(); + + if (! $existing) { + DB::table('legacy_unita_tipo_mappings')->insert($row); + } + } + } + + public function down(): void + { + Schema::dropIfExists('legacy_unita_tipo_mappings'); + } +};