Add legacy unit type mapping to Gescon import

This commit is contained in:
michele 2026-04-12 08:01:13 +00:00
parent 5b92dc57f8
commit ce5451fe1a
6 changed files with 606 additions and 108 deletions

View File

@ -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',
],
],
);

View File

@ -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;
}
}

View File

@ -1,5 +1,4 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
@ -20,13 +19,13 @@ class SyncGesconFornitoriLegacyCommand extends Command
public function handle(): int
{
$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');
$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;
}
}
}

View File

@ -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<string,mixed> */
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<int,string>} */
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<string,mixed> */
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<int|string,mixed> $values
* @param array<string,string> $options
* @return array<int,string>
*/
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) {

View File

@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class LegacyUnitaTipoMapping extends Model
{
protected $table = 'legacy_unita_tipo_mappings';
protected $fillable = [
'legacy_source',
'legacy_field',
'legacy_value',
'descrizione',
'match_regex',
'tipo_unita',
'tipologia_codice',
'tipologia_nome',
'tipologia_categoria',
'categoria_catastale',
'is_pertinenza',
'is_condominiale',
'fallback_interno_prefix',
'sort_order',
'meta',
];
protected $casts = [
'is_pertinenza' => 'boolean',
'is_condominiale' => 'boolean',
'sort_order' => 'integer',
'meta' => 'array',
];
}

View File

@ -0,0 +1,123 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasTable('legacy_unita_tipo_mappings')) {
Schema::create('legacy_unita_tipo_mappings', function (Blueprint $table): void {
$table->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');
}
};