middleware('auth'); // Accesso ristretto: solo super-admin e amministratore possono usare l'area Import GESCON $this->middleware('role:super-admin|amministratore'); // Visibilità area import solo se l'utente ha il permesso esplicito $this->middleware('permission:gescon-import.view'); // Esecuzione task di import richiede permesso dedicato $this->middleware('permission:gescon-import.execute')->only(['executeImport']); // Service per accesso archivi legacy/staging $this->legacy = $legacy; $this->archiveSync = $archiveSync; } /** * Dashboard principale per visualizzare i dati importati da GESCON */ public function index(\Illuminate\Http\Request $request) { $amministratore = $this->getAmministratoreCorrente(); if (!$amministratore) { return redirect()->route('admin.dashboard') ->with('error', 'Nessun amministratore associato all\'utente corrente.'); } $canSwitchAmministratore = $this->canSwitchAmministratore(); $amministratoriDisponibili = $canSwitchAmministratore ? Amministratore::orderBy('denominazione_studio') ->orderBy('nome') ->orderBy('cognome') ->get(['id', 'nome', 'cognome', 'denominazione_studio', 'codice_amministratore']) : Amministratore::where('id', $amministratore->id) ->get(['id', 'nome', 'cognome', 'denominazione_studio', 'codice_amministratore']); $selectedAmministratoreId = $amministratore->id; // Statistiche generali $stats = $this->getStatisticheImportate($amministratore->id); // Dati recenti per le cards $datiRecenti = $this->getDatiRecenti($amministratore->id); // Dati per tab Stabili (mostra tutti dello stesso amministratore; il tag GESCON è informativo, non filtrante) $stabiliQuery = Stabile::where('amministratore_id', $amministratore->id); $stabili = (clone $stabiliQuery) ->orderBy('denominazione') ->paginate(15, ['*'], 'stabili_page'); $province = (clone $stabiliQuery) ->whereNotNull('provincia') ->distinct() ->pluck('provincia') ->filter() ->sort() ->values() ->toArray(); // Dati per tab Soggetti (solo soggetti collegati a stabili dell'amministratore) $soggetti = Soggetto::with(['unitaImmobiliari.stabile']) ->whereHas('unitaImmobiliari.stabile', function ($q) use ($amministratore) { $q->where('amministratore_id', $amministratore->id); }) ->orderBy('cognome') ->orderBy('nome') ->paginate(20, ['*'], 'soggetti_page'); $stabiliList = Stabile::where('amministratore_id', $amministratore->id) ->orderBy('denominazione') ->get(['id', 'denominazione']); // Dati per tab Unità e Fornitori (solo quelli dello stesso amministratore) $unita = UnitaImmobiliare::with('stabile') ->whereHas('stabile', function ($q) use ($amministratore) { $q->where('amministratore_id', $amministratore->id); }) ->latest() ->paginate(20, ['*'], 'unita_page'); $fornitori = Fornitore::where('amministratore_id', $amministratore->id) ->orderBy('ragione_sociale') ->paginate(20, ['*'], 'fornitori_page'); // Anteprima legacy (Archivio migrazioni -> MDB -> directory) $hasMdbTools = trim((string)@shell_exec('command -v mdb-export')) !== ''; // Base degli archivi montati (directory per-stabile e sottocartelle per anno) $legacyBasePath = UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon'); // MDB generali nella sottocartella "dbc" $mdbPath = UserSetting::get('gescon.mdb_stabili', '/mnt/gescon-archives/gescon/dbc/Stabili.mdb'); $mdbIndexPath = UserSetting::get('gescon.mdb_index', '/mnt/gescon-archives/gescon/cartellestabili_e_anni.mdb'); $dataset = MigrationDataset::where('key', 'stabili')->orderByDesc('updated_at')->first(); $cacheKey = 'gescon_legacy_preview:' . md5(($legacyBasePath ?: '') . '|' . ($mdbPath ?: '') . '|' . ($dataset?->id ?? 0) . '|' . ($dataset?->records_count ?? 0)); if ($request->boolean('refresh')) { Cache::forget($cacheKey); } $legacyPreview = Cache::remember($cacheKey, now()->addMinutes(5), function () use ($dataset, $mdbPath, $hasMdbTools, $legacyBasePath, $mdbIndexPath) { try { $preview = []; // 1) Archivio migrazioni (se presente) if ($dataset && $dataset->records_count > 0) { $records = MigrationRecord::where('dataset_id', $dataset->id)->limit(5000)->get(); $map = []; foreach ($records as $rec) { $data = (array)($rec->data ?? []); $legacy = $rec->legacy_key ?: ($data['nome_directory'] ?? ($data['internet_cod_stab'] ?? ($data['cod_stabile'] ?? null))); if (!$legacy) continue; $denom = $data['denominazione'] ?? ($data['fe_denominazione'] ?? null); $map[(string)$legacy] = [ 'legacy_code' => trim((string)$legacy), 'denominazione' => $denom ? trim((string)$denom) : null, ]; } $preview = array_values($map); } // 2) Indice master cartellestabili_e_anni.mdb (preferibile per anni/cartelle) if (empty($preview) && $hasMdbTools && $mdbIndexPath && is_file($mdbIndexPath) && is_readable($mdbIndexPath)) { $preview = $this->legacy->scanStabiliFromMasterIndex($mdbIndexPath, $legacyBasePath); } // 3) MDB Stabili diretto if (empty($preview) && $mdbPath && is_file($mdbPath) && $hasMdbTools) { $preview = $this->legacy->scanStabiliFromMdb($mdbPath); } // 4) Fallback directory if (empty($preview) && is_dir($legacyBasePath)) { $preview = $this->legacy->scanLegacyCodesFromDir($legacyBasePath); } if (!empty($preview)) { $preview = $this->legacy->enrichLegacyWithArchives($preview, $legacyBasePath); } return $preview; } catch (\Throwable $e) { return []; } }); // Mapping/sync attivi per suddividere legacy in "collegati" vs "da collegare" $userId = auth()->id(); $mappedLegacyCodes = \App\Models\GesconImportMapping::where('user_id', $userId) ->where('legacy_code', '!=', '*') ->pluck('legacy_code') ->filter() ->values() ->toArray(); $syncByLegacy = \App\Models\GesconImportMapping::where('user_id', $userId) ->where('legacy_code', '!=', '*') ->get(['legacy_code', 'meta']) ->mapWithKeys(function ($m) { $meta = is_array($m->meta) ? $m->meta : []; return [$m->legacy_code => (bool)($meta['sync_enabled'] ?? false)]; }) ->toArray(); // Nuovi amministratori: se non ci sono dati importati, invia alla Config solo se ha il permesso specifico. $onboarding = ($stats['stabili_totali'] ?? 0) == 0 && ($stats['stabili_gescon'] ?? 0) == 0; if ($onboarding && !$canSwitchAmministratore) { if (auth()->user() && auth()->user()->can('gescon-import.config')) { return redirect()->route('admin.gescon-import.config'); } // Altrimenti mostra una card guida senza link attivo alla Config return view('admin.gescon-import.index', compact( 'stats', 'datiRecenti', 'amministratore', 'stabili', 'province', 'soggetti', 'stabiliList', 'unita', 'fornitori', 'legacyPreview', 'hasMdbTools', 'legacyBasePath', 'mdbPath', 'mappedLegacyCodes', 'syncByLegacy', 'legacyCode', 'legacyUnita', 'unitaFieldMapping', 'canSwitchAmministratore', 'amministratoriDisponibili', 'selectedAmministratoreId', 'onboarding' )); } // Optional: legacy Unità preview (for mapping tab) when a legacy code is selected $legacyCode = $request->query('legacy_code'); $legacyUnita = []; try { if ($legacyCode && Schema::connection('gescon_import')->hasTable('condomin')) { $legacyUnita = DB::connection('gescon_import') ->table('condomin') ->where('cod_stabile', $legacyCode) ->select(['cod_stabile', 'scala', 'interno', 'cognome', 'nome', 'cod_cond']) ->limit(500) ->get(); } } catch (\Throwable $e) { $legacyUnita = []; } // Recupera mapping Unità esistente (field mapping) per il legacy selezionato $unitaFieldMapping = []; if ($legacyCode) { try { $mapRec = GesconImportMapping::where('user_id', auth()->id()) ->where('legacy_code', $legacyCode) ->first(); if ($mapRec) { $unitaFieldMapping = (array) data_get($mapRec, 'meta.unita_field_mapping', []); } } catch (\Throwable $e) { $unitaFieldMapping = []; } } return view('admin.gescon-import.index', compact( 'stats', 'datiRecenti', 'amministratore', 'stabili', 'province', 'soggetti', 'stabiliList', 'unita', 'fornitori', 'legacyPreview', 'hasMdbTools', 'legacyBasePath', 'mdbPath', 'mappedLegacyCodes', 'syncByLegacy', 'canSwitchAmministratore', 'amministratoriDisponibili', 'selectedAmministratoreId', 'onboarding' )); } /** * Visualizza lista stabili importati da GESCON */ public function stabili(Request $request) { $amministratore = $this->getAmministratoreCorrente(); $query = Stabile::where('amministratore_id', $amministratore->id) ->where('note', 'like', '%GESCON%'); // Filtri if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { $q->where('denominazione', 'like', "%{$search}%") ->orWhere('codice_stabile', 'like', "%{$search}%") ->orWhere('indirizzo', 'like', "%{$search}%"); }); } if ($request->filled('citta')) { $query->where('citta', 'like', "%{$request->citta}%"); } if ($request->filled('provincia')) { $query->where('provincia', $request->provincia); } $stabili = $query->orderBy('denominazione')->paginate(20); // Statistiche per questa vista $stats = [ 'totale' => $query->count(), 'attivi' => $query->where('attivo', true)->count(), 'province' => $query->distinct('provincia')->count('provincia'), ]; } 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'] : []; $candidates = [ $target['codice_cassa'] ?? null, $mapping['target_cod_cassa'] ?? null, $mapping['target_cod_cassa_select'] ?? null, $mapping['codice_legacy'] ?? null, $legacy['cod_cassa'] ?? null, $mapping['legacy_cod_cassa'] ?? null, $mapping['codice_cassa'] ?? null, ]; foreach ($candidates as $candidate) { if (is_string($candidate) && trim($candidate) !== '') { return strtoupper(trim($candidate)); } } return null; } private function resolveMappingLegacyField(array $mapping, string $field): ?string { $legacy = isset($mapping['legacy']) && is_array($mapping['legacy']) ? $mapping['legacy'] : []; $candidates = [ $legacy[$field] ?? null, $mapping['legacy_' . $field] ?? null, ]; foreach ($candidates as $candidate) { if (is_string($candidate) && trim($candidate) !== '') { return trim($candidate); } } return null; } /** * Dettaglio singolo stabile importato */ public function stabileShow($id) { $amministratore = $this->getAmministratoreCorrente(); $stabile = Stabile::where('amministratore_id', $amministratore->id) ->where('id', $id) ->firstOrFail(); // Condomini collegati a questo stabile (tramite campo note o altra logica) $condomini = Soggetto::where('note', 'like', "%{$stabile->codice_stabile}%") ->orWhere('note', 'like', "%GESCON%") ->paginate(50); // Fornitori per questo amministratore $fornitori = Fornitore::where('amministratore_id', $amministratore->id) ->latest() ->take(10) ->get(); return view('admin.gescon-import.stabile-show', compact('stabile', 'condomini', 'fornitori', 'amministratore')); } /** * Lista condomini (soggetti) importati da GESCON */ public function condomini(Request $request) { $amministratore = $this->getAmministratoreCorrente(); $query = Soggetto::query(); // Filtri if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { $q->where('nome', 'like', "%{$search}%") ->orWhere('cognome', 'like', "%{$search}%") ->orWhere('ragione_sociale', 'like', "%{$search}%") ->orWhere('codice_fiscale', 'like', "%{$search}%"); }); } if ($request->filled('tipo')) { $query->where('tipo', $request->tipo); } if ($request->filled('citta')) { $query->where('citta', 'like', "%{$request->citta}%"); } $condomini = $query->orderBy('cognome') ->orderBy('nome') ->paginate(50); // Statistiche $stats = [ 'totale' => $query->count(), 'proprietari' => Soggetto::where('tipo', 'proprietario')->count(), 'inquilini' => Soggetto::where('tipo', 'inquilino')->count(), ]; return view('admin.gescon-import.condomini', compact('condomini', 'stats', 'amministratore')); } /** * Lista fornitori importati da GESCON */ public function fornitori(Request $request) { $amministratore = $this->getAmministratoreCorrente(); $query = Fornitore::where('amministratore_id', $amministratore->id); // Filtri if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { $q->where('ragione_sociale', 'like', "%{$search}%") ->orWhere('codice_fiscale', 'like', "%{$search}%") ->orWhere('partita_iva', 'like', "%{$search}%"); }); } if ($request->filled('citta')) { $query->where('citta', 'like', "%{$request->citta}%"); } if ($request->filled('provincia')) { $query->where('provincia', $request->provincia); } $fornitori = $query->orderBy('ragione_sociale')->paginate(50); // Statistiche $stats = [ 'totale' => $query->count(), 'con_piva' => $query->whereNotNull('partita_iva')->count(), 'province' => $query->distinct('provincia')->count('provincia'), ]; return view('admin.gescon-import.fornitori', compact('fornitori', 'stats', 'amministratore')); } /** * Reports e statistiche GESCON */ public function reports() { $amministratore = $this->getAmministratoreCorrente(); // Report dettagliato per amministratore $report = [ 'stabili' => [ 'totale' => Stabile::where('amministratore_id', $amministratore->id) ->where('note', 'like', '%GESCON%')->count(), 'per_provincia' => Stabile::where('amministratore_id', $amministratore->id) ->where('note', 'like', '%GESCON%') ->select('provincia', DB::raw('count(*) as total')) ->groupBy('provincia') ->get(), ], 'fornitori' => [ 'totale' => Fornitore::where('amministratore_id', $amministratore->id)->count(), 'con_piva' => Fornitore::where('amministratore_id', $amministratore->id) ->whereNotNull('partita_iva')->count(), ], 'condomini' => [ 'totale' => Soggetto::count(), 'per_tipo' => Soggetto::select('tipo', DB::raw('count(*) as total')) ->groupBy('tipo') ->get(), ] ]; return view('admin.gescon-import.reports', compact('report', 'amministratore')); } /** * Atomic page: Unità mapping UI (standalone), with "da associare" option and optional column creation panel. */ public function unitaMappingPage(Request $request) { $legacyCode = (string) $request->query('legacy_code', ''); $amministratore = $this->getAmministratoreCorrente(); // Existing mapping $unitaFieldMapping = []; $unitaLastFilters = []; $palazzineConfig = []; $unitaPresetKey = null; try { if ($legacyCode !== '') { $mapRec = GesconImportMapping::where('user_id', auth()->id()) ->where('legacy_code', $legacyCode) ->first(); if ($mapRec) { $unitaFieldMapping = (array) data_get($mapRec, 'meta.unita_field_mapping', []); $unitaLastFilters = (array) data_get($mapRec, 'meta.unita_last_filters', []); $palazzineConfig = (array) data_get($mapRec, 'meta.palazzine_config', []); $unitaPresetKey = data_get($mapRec, 'meta.unita_preset_key'); } } } catch (\Throwable $e) { $unitaFieldMapping = []; } // Legacy sample rows from staging (condomin) $legacyUnita = []; $legacyHeaders = []; try { if ($legacyCode !== '' && Schema::connection('gescon_import')->hasTable('condomin')) { $legacyUnita = DB::connection('gescon_import') ->table('condomin') ->where('cod_stabile', $legacyCode) ->select(['cod_stabile', 'cod_cond', 'scala', 'interno', 'cognome', 'nome', 'codice_fiscale', 'millesimi_generali', 'millesimi_risc']) ->limit(20) ->get() ->map(fn($r) => (array)$r) ->toArray(); // headers try { $legacyHeaders = Schema::connection('gescon_import')->getColumnListing('condomin'); } catch (\Throwable $e) { $legacyHeaders = []; } } else { // fallback headers minimal $legacyHeaders = ['cod_stabile', 'cod_cond', 'scala', 'interno', 'cognome', 'nome', 'codice_fiscale', 'millesimi_generali', 'millesimi_risc']; } } catch (\Throwable $e) { $legacyHeaders = ['cod_stabile', 'cod_cond', 'scala', 'interno']; } // Distinct scales from preview (best-effort for UI chips) $scaleList = collect($legacyUnita)->pluck('scala')->filter()->unique()->values()->all(); // Target groups similar to Stabili mapping UX $targetGroups = [ 'soggetto' => [ 'cognome', 'nome', 'codice_fiscale', 'partita_iva', 'email', 'telefono', 'indirizzo', 'civico', 'cap', 'citta', 'provincia' ], 'unita' => [ 'codice_unita', 'scala', 'piano', 'interno', 'categoria_catastale', 'classe_catastale', 'consistenza', 'rendita', 'millesimi_generali', 'millesimi_riscaldamento', 'superficie', 'note', 'sezione', 'foglio', 'particella', 'subalterno', 'indirizzo', 'civico', 'mq_riscaldati', 'mq_non_riscaldati', 'tipo_unita', 'pertinenza_di' ], 'relazioni' => [ 'conduttore.cf', 'conduttore.nome', 'conduttore.cognome', 'inquilino.cf', 'inquilino.nome', 'inquilino.cognome' ], ]; // Flat suggestion list preserved for compatibility if needed $targetFields = array_values(array_unique(array_merge( $targetGroups['unita'], $targetGroups['soggetto'], ['codice_fiscale_proprietario', 'codice_fiscale_inquilino'] ))); // Global mapping sources (segment-level tables/columns) for filtering legacy fields $mappingSources = \App\Models\UserSetting::get('gescon.mapping_sources', []); // Derive per-stabile first available year (from gestioni or gestione_years table if exists) – best effort $firstYear = null; $availableYears = []; try { if ($legacyCode && Schema::connection('gescon_import')->hasTable('gestioni')) { $yrs = DB::connection('gescon_import')->table('gestioni') ->where('cod_stabile', $legacyCode) ->whereNotNull('anno') ->orderBy('anno') ->pluck('anno')->filter()->unique()->values()->all(); $availableYears = $yrs; if (!empty($yrs)) { $firstYear = (int) min($yrs); } } } catch (\Throwable $e) { $firstYear = null; } if ($firstYear === null && $legacyCode) { // fallback: try to guess from unita/piano data if any numeric year col exists $firstYear = (int) date('Y'); } // Prefill field mapping suggestion: build from existing mapping record or heuristic using mappingSources columns $prefillMapping = []; if ($legacyCode) { try { $mapRec = GesconImportMapping::where('user_id', auth()->id()) ->where('legacy_code', $legacyCode) ->first(); if ($mapRec) { $prefillMapping = (array) data_get($mapRec, 'meta.unita_field_mapping', []); } if (empty($prefillMapping) && is_array($mappingSources)) { // Use segment 'unita' if present to propose legacy→target quick suggestions $segUnita = collect($mappingSources)->firstWhere('segment', 'unita'); $colList = (array) ($segUnita['columns'] ?? []); $pairs = [ 'cognome' => 'cognome', 'nome' => 'nome', 'codice_fiscale' => 'codice_fiscale', 'scala' => 'scala', 'interno' => 'interno', 'millesimi_generali' => 'millesimi_generali', 'millesimi_risc' => 'millesimi_riscaldamento' ]; foreach ($pairs as $legacy => $target) { if (in_array($legacy, $colList, true)) { $prefillMapping[] = ['legacy' => $legacy, 'target' => $target]; } } } } catch (\Throwable $e) { $prefillMapping = []; } } // Provide filtered mappingSources only for selected stable & unita segment in front-end context if needed $filteredSources = []; if ($legacyCode && is_array($mappingSources)) { foreach ($mappingSources as $ms) { if (!is_array($ms)) continue; // Keep only global segments relevant to unita/stabile to reduce noise if (in_array($ms['segment'] ?? '', ['stabile', 'stabili', 'unita', 'relazioni'], true)) { $filteredSources[] = $ms; } } } return view('admin.gescon-import.unita-mapping-page', compact( 'amministratore', 'legacyCode', 'legacyUnita', 'legacyHeaders', 'targetFields', 'unitaFieldMapping', 'targetGroups', 'unitaLastFilters', 'palazzineConfig', 'scaleList', 'unitaPresetKey', 'mappingSources', 'firstYear', 'availableYears', 'prefillMapping', 'filteredSources' )); } /** * Dedicated Relations Manager page (Config-like), to manage relation rules per segment. */ public function relationsPage(Request $request) { $segment = (string) $request->query('segment', 'unita'); $legacyCode = (string) $request->query('legacy_code', ''); $amministratore = $this->getAmministratoreCorrente(); // Load existing relation rules $existingRelations = []; try { if ($legacyCode !== '') { $mapRec = GesconImportMapping::where('user_id', auth()->id()) ->where('legacy_code', $legacyCode) ->first(); $existingRelations = (array) data_get($mapRec, 'meta.relation_mapping', []); } } catch (\Throwable $e) { $existingRelations = []; } // Build legacy fields list per segment $legacyFields = []; try { if ($segment === 'unita') { if (Schema::connection('gescon_import')->hasTable('condomin')) { $legacyFields = Schema::connection('gescon_import')->getColumnListing('condomin'); } } elseif ($segment === 'stabili') { // Try to read headers from Stabili.mdb; fallback to dataset $headers = []; $hasMdb = trim((string)@shell_exec('command -v mdb-export')) !== ''; $mdbPath = \App\Models\UserSetting::get('gescon.mdb_stabili'); if ($hasMdb && $mdbPath && @is_file($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'; $tables = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: ''; $tableGuess = 'Stabili'; if ($tables) { foreach (preg_split('/\r?\n/', trim($tables)) as $t) { if ($t === '') continue; if (preg_match('/stabil/i', $t)) { $tableGuess = trim($t); break; } } } $tmp = tempnam(sys_get_temp_dir(), 'mdbcsv_'); $cmd = sprintf( '%s -D %s -d %s -q %s -R "\\n" %s %s > %s 2>/dev/null', escapeshellarg($binExport), escapeshellarg('%Y-%m-%d'), escapeshellarg('|'), escapeshellarg('"'), escapeshellarg($mdbPath), escapeshellarg($tableGuess), escapeshellarg($tmp) ); @shell_exec($cmd); if (@filesize($tmp) > 0) { $fh = fopen($tmp, 'r'); $h = fgetcsv($fh, 0, '|'); fclose($fh); @unlink($tmp); $headers = $h ? array_map(fn($x) => strtolower(trim((string)$x)), $h) : []; } else { @unlink($tmp); } } if (empty($headers)) { $ds = \App\Models\MigrationDataset::where('key', 'stabili')->orderByDesc('updated_at')->first(); if ($ds) { $rec = \App\Models\MigrationRecord::where('dataset_id', $ds->id)->orderBy('id')->first(); $headers = array_keys((array)($rec?->data ?? [])); } else { $headers = ['denominazione', 'indirizzo', 'comune', 'provincia', 'cap']; } } $legacyFields = $headers; } else { // tabelle / voci: try to guess from staging $conn = \App\Models\UserSetting::get('gescon.import_conn', 'gescon_import'); $candidates = $segment === 'tabelle' ? ['milles', 'millesimi', 'tab_millesimi'] : ['piano_conti', 'pianoconti', 'voci', 'voci_spesa']; $chosen = null; $all = []; try { $all = Schema::connection($conn)->getAllTables(); } catch (\Throwable $e) { $all = []; } $names = array_values(array_filter(array_map(function ($r) { $n = $r['name'] ?? ($r['TABLE_NAME'] ?? null); return $n ? (string)$n : null; }, $all))); foreach ($candidates as $pat) { foreach ($names as $n) { if (str_contains(strtolower($n), $pat)) { $chosen = $n; break 2; } } } if ($chosen && Schema::connection($conn)->hasTable($chosen)) { $legacyFields = Schema::connection($conn)->getColumnListing($chosen); } } } catch (\Throwable $e) { $legacyFields = $legacyFields ?: []; } return view('admin.gescon-import.relations', compact('amministratore', 'segment', 'legacyCode', 'legacyFields', 'existingRelations')); } /** * Configurazione import GESCON */ public function config(Request $request) { if (!(auth()->user() && auth()->user()->can('gescon-import.config'))) { return redirect()->route('admin.gescon-import.index')->with('error', 'Non hai i permessi per accedere alla configurazione.'); } $amministratore = $this->getAmministratoreCorrente(); // Impostazioni utente predefinite per percorso sorgente e DSN MDB $settings = [ 'gescon.source_path' => UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon'), 'gescon.mdb_stabili' => UserSetting::get('gescon.mdb_stabili', '/mnt/gescon-archives/gescon/dbc/Stabili.mdb'), 'gescon.mdb_index' => UserSetting::get('gescon.mdb_index', '/mnt/gescon-archives/gescon/cartellestabili_e_anni.mdb'), 'gescon.import_conn' => UserSetting::get('gescon.import_conn', 'gescon_import'), ]; // Nuova scansione archivi MDB disponibili (1 livello) per UI selezione archivi $mdbArchives = []; $scanBase = $settings['gescon.source_path']; try { if (is_dir($scanBase)) { $rii = scandir($scanBase); foreach ($rii as $f) { if ($f === '.' || $f === '..') continue; $full = rtrim($scanBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $f; if (is_file($full) && preg_match('/\.mdb$/i', $f)) { $mdbArchives[] = ['name' => $f, 'path' => $full, 'size' => filesize($full)]; } if (is_dir($full)) { // cerca file .mdb in cartella (solo primo livello per performance) foreach (glob($full . DIRECTORY_SEPARATOR . '*.mdb') as $m) { $mdbArchives[] = ['name' => basename($m), 'path' => $m, 'dir' => $f, 'size' => filesize($m)]; } } } } } catch (\Throwable $e) { $mdbArchives = $mdbArchives ?: []; } // Valori precedenti selezionati $selectedArchives = UserSetting::get('gescon.selected_archives', []); $hasMdbTools = trim((string)@shell_exec('command -v mdb-export')) !== ''; $legacyBasePath = $settings['gescon.source_path'] ?? ''; $mdbPath = $settings['gescon.mdb_stabili'] ?? ''; $mdbIndexPath = $settings['gescon.mdb_index'] ?? ''; $dataset = MigrationDataset::where('key', 'stabili')->orderByDesc('updated_at')->first(); $cacheKey = 'gescon_legacy_preview:' . md5(($legacyBasePath ?: '') . '|' . ($mdbPath ?: '') . '|' . ($dataset?->id ?? 0) . '|' . ($dataset?->records_count ?? 0)); if ($request->boolean('refresh')) { Cache::forget($cacheKey); } $preview = Cache::remember($cacheKey, now()->addMinutes(5), function () use ($dataset, $mdbPath, $hasMdbTools, $legacyBasePath, $mdbIndexPath) { try { $preview = []; if ($dataset && $dataset->records_count > 0) { $records = MigrationRecord::where('dataset_id', $dataset->id)->limit(5000)->get(); $map = []; foreach ($records as $rec) { $data = (array)($rec->data ?? []); $legacy = $rec->legacy_key ?: ($data['nome_directory'] ?? ($data['internet_cod_stab'] ?? ($data['cod_stabile'] ?? null))); if (!$legacy) continue; $denom = $data['denominazione'] ?? ($data['fe_denominazione'] ?? null); $map[(string)$legacy] = [ 'legacy_code' => trim((string)$legacy), 'denominazione' => $denom ? trim((string)$denom) : null, ]; } $preview = array_values($map); } if (empty($preview) && $hasMdbTools && $mdbIndexPath && is_file($mdbIndexPath) && is_readable($mdbIndexPath)) { $preview = $this->legacy->scanStabiliFromMasterIndex($mdbIndexPath, $legacyBasePath); } if (empty($preview) && $mdbPath && is_file($mdbPath) && $hasMdbTools) { $preview = $this->legacy->scanStabiliFromMdb($mdbPath); } if (empty($preview) && is_dir($legacyBasePath)) { $preview = $this->legacy->scanLegacyCodesFromDir($legacyBasePath); } if (!empty($preview)) { $preview = $this->legacy->enrichLegacyWithArchives($preview, $legacyBasePath); } return $preview; } catch (\Throwable $e) { return []; } }); $archive = [ 'source_registered' => (bool) MigrationSource::where('name', 'GESCON Stabili')->exists(), 'dataset_records' => (int) ($dataset->records_count ?? 0), ]; $mappingSources = UserSetting::get('gescon.mapping_sources', []); return view('admin.gescon-import.config', compact('amministratore', 'settings', 'preview', 'hasMdbTools', 'legacyBasePath', 'archive', 'mdbArchives', 'selectedArchives', 'mappingSources')); } /** * API: elenca archivi MDB (cache semplice in memoria richiesta) – simile alla logica inline in config(). */ public function mdbScan(Request $request) { if (!(auth()->user() && auth()->user()->can('gescon-import.config'))) { return response()->json(['ok' => false, 'error' => 'Permesso negato'], 403); } $base = UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon'); $out = []; try { if (is_dir($base)) { foreach (scandir($base) as $f) { if ($f === '.' || $f === '..') continue; $full = rtrim($base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $f; if (is_file($full) && preg_match('/\.mdb$/i', $f)) { $out[] = ['name' => $f, 'path' => $full, 'size' => @filesize($full)]; } if (is_dir($full)) { foreach (glob($full . DIRECTORY_SEPARATOR . '*.mdb') as $m) { $out[] = ['name' => basename($m), 'path' => $m, 'dir' => $f, 'size' => @filesize($m)]; } } } } } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage(), 'archives' => $out]); } return response()->json(['ok' => true, 'base' => $base, 'archives' => $out]); } /** * API: lista tabelle in un archivio MDB (usa mdb-tables). Param: path (validato deve stare sotto source_path). */ public function mdbTables(Request $request) { if (!(auth()->user() && auth()->user()->can('gescon-import.config'))) { return response()->json(['ok' => false, 'error' => 'Permesso negato'], 403); } $path = (string)$request->query('path'); $base = realpath(UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon')) ?: null; $real = realpath($path) ?: null; if (!$real || !$base || !str_starts_with($real, $base)) { return response()->json(['ok' => false, 'error' => 'Percorso non consentito']); } $tables = []; try { $bin = trim((string)@shell_exec('command -v mdb-tables')) ?: 'mdb-tables'; $out = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($bin), escapeshellarg($real))); if ($out) { foreach (preg_split('/\r?\n/', trim($out)) as $t) { if ($t !== '') $tables[] = $t; } } } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage()]); } return response()->json(['ok' => true, 'tables' => $tables]); } /** * API: colonne di una tabella MDB (legge prima riga CSV via mdb-export). */ public function mdbColumns(Request $request) { if (!(auth()->user() && auth()->user()->can('gescon-import.config'))) { return response()->json(['ok' => false, 'error' => 'Permesso negato'], 403); } $path = (string)$request->query('path'); $table = (string)$request->query('table'); if (!$path || !$table) return response()->json(['ok' => false, 'error' => 'path e table richiesti'], 422); $base = realpath(UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon')) ?: null; $real = realpath($path) ?: null; if (!$real || !$base || !str_starts_with($real, $base)) { return response()->json(['ok' => false, 'error' => 'Percorso non consentito']); } try { $binExport = trim((string)@shell_exec('command -v mdb-export')) ?: 'mdb-export'; $tmp = tempnam(sys_get_temp_dir(), 'mdbcols_'); $cmd = sprintf( '%s -D %s -d %s -q %s -R "\n" %s %s > %s 2>/dev/null', escapeshellarg($binExport), escapeshellarg('%Y-%m-%d'), escapeshellarg('|'), escapeshellarg('"'), escapeshellarg($real), escapeshellarg($table), escapeshellarg($tmp) ); @shell_exec($cmd); $cols = []; if (@filesize($tmp) > 0) { $fh = fopen($tmp, 'r'); $header = fgetcsv($fh, 0, '|'); if ($header) { $cols = array_map(fn($x) => trim((string)$x), $header); } fclose($fh); } @unlink($tmp); return response()->json(['ok' => true, 'columns' => $cols]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage()]); } } /** * Esegue l'import GESCON via web interface */ public function executeImport(Request $request) { // Sicurezza aggiuntiva: blocca se l'utente non ha il permesso di eseguire if (!auth()->user()->can('gescon-import.execute')) { return redirect()->back()->with('error', 'Non hai i permessi per eseguire l\'import.'); } $amministratore = $this->getAmministratoreCorrente(); $request->validate([ 'tipo_import' => 'required|in:stabili,fornitori,condomini,all', 'test_mode' => 'boolean', 'legacy_code' => 'sometimes|nullable|string', 'archive_source_path' => 'sometimes|nullable|string', 'refresh_archive' => 'sometimes|boolean', ]); // Guard RBAC: per default gli amministratori non possono creare nuovi stabili // Il super-admin può abilitare singolarmente: amministratori.abilita_import_stabili = 1 $isSuperAdmin = auth()->user()->hasRole('super-admin') || auth()->user()->can('gescon-import.config'); try { $logger = new ImportProgressLogger(); // Esegue pipeline reale o dry-run per uno specifico stabile o per tutti (stabili) $legacyCode = $request->input('legacy_code'); // es. 0024, 0019 // Percorsi sorgente: usa default sicuri se non configurati $path = $request->input('path') ?: UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon'); $mdbOverride = $request->input('mdb_path'); $mdbCondomin = $request->input('mdb_condomin_path'); // singolo_anno.mdb (tabella condomin) $splitMode = $request->input('split_mode'); // none|palazzine|stabili-per-scala $anno = $request->input('anno'); $importAll = $request->boolean('import_all'); // Mappa tipo_import in step singolo quando opportuno (la pagina Stabili invia tipo_import=stabili) $solo = $request->input('solo'); // priorità a parametro esplicito if (!$solo) { $tipo = $request->input('tipo_import'); $map = [ 'stabili' => 'stabili', 'condomini' => 'soggetti', // corrisponde allo step soggetti 'fornitori' => null, // non gestito in questa pipeline 'all' => null, ]; if (array_key_exists($tipo, $map)) { $solo = $map[$tipo]; } } $dry = $request->boolean('test_mode'); $archiveSourceOverride = $request->input('archive_source_path'); $refreshArchive = $request->boolean('refresh_archive'); // Applica guardie prima di avviare l'Artisan if (!$isSuperAdmin) { // Blocco import STABILI per non-super-admin $tipo = (string)$request->input('tipo_import'); if ($tipo === 'stabili' || $solo === 'stabili') { // Eccezione: consenti se il super admin ha abilitato l'import per questo amministratore if (!$amministratore->abilita_import_stabili) { return redirect()->back()->with('error', 'Import STABILI disabilitato per il tuo profilo. Chiedi al Super Admin l\'abilitazione.'); } } // Per altri import, richiedi che lo stabile esista già se è stato indicato un legacy_code if ($legacyCode) { $exists = Stabile::where('amministratore_id', $amministratore->id) ->where(function ($q) use ($legacyCode) { $q->where('codice_stabile', $legacyCode) ->orWhere('codice_interno', $legacyCode); }) ->exists(); if (!$exists) { return redirect()->back()->with('error', 'Lo stabile selezionato non è stato ancora creato. Contatta il Super Admin per l\'inizializzazione.'); } } } $params = []; if ($legacyCode) $params['--stabile'] = $legacyCode; if ($path) $params['--path'] = rtrim($path, '/'); if ($solo) $params['--solo'] = $solo; if ($dry) $params['--dry-run'] = true; if ($anno) $params['--anno'] = $anno; // Super-admin: instrada verso un amministratore specifico se fornito if ($isSuperAdmin && $request->filled('amministratore_id')) { $params['--amministratore-id'] = (int) $request->input('amministratore_id'); } else { // Fallback: onora l'amministratore target salvato nel mapping (se presente) if ($legacyCode) { try { $mapRec = GesconImportMapping::where('user_id', auth()->id()) ->where('legacy_code', $legacyCode) ->first(); $targetAdminId = (int) data_get($mapRec, 'meta.target_admin_id'); if ($targetAdminId) { // Consenti se il target coincide con l'amministratore corrente o se l'utente ha permesso config $currentAdmin = $amministratore?->id ?? null; if ($isSuperAdmin || ($currentAdmin && $currentAdmin === $targetAdminId)) { $params['--amministratore-id'] = $targetAdminId; } } } catch (\Throwable $e) { /* ignore */ } } } if (!isset($params['--amministratore-id']) && !$isSuperAdmin && $amministratore) { $params['--amministratore-id'] = $amministratore->id; } // Gestione percorso MDB di Stabili: usa override, altrimenti impostazione utente, e passalo al comando if ($mdbOverride && is_string($mdbOverride)) { UserSetting::set('gescon.mdb_stabili', $mdbOverride); } // MDB Stabili: preferisci override, altrimenti impostazione utente con default; se non presente ma esiste $path/Stabili.mdb, usa quello $mdbPath = $mdbOverride ?: UserSetting::get('gescon.mdb_stabili', '/mnt/gescon-archives/gescon/dbc/Stabili.mdb'); // Se il percorso non è leggibile, prova una copia server-friendly in storage if ((!$mdbOverride) && (!is_string($mdbPath) || $mdbPath === '' || !is_readable($mdbPath))) { $storageFallback = '/var/www/netgescon/storage/gescon/Stabili.mdb'; if (is_file($storageFallback) && is_readable($storageFallback)) { $mdbPath = $storageFallback; } } if (!$mdbOverride && (!is_string($mdbPath) || $mdbPath === '' || !is_file($mdbPath))) { $guess = rtrim((string)$path, '/') . '/Stabili.mdb'; if (is_file($guess)) { $mdbPath = $guess; } } if ($mdbPath && is_string($mdbPath)) { $params['--mdb'] = $mdbPath; } // MDB specifico per tabella condomin (singolo_anno.mdb) if ($mdbCondomin && is_string($mdbCondomin)) { $params['--mdb-condomin'] = $mdbCondomin; } if (is_string($splitMode) && in_array($splitMode, ['none', 'palazzine', 'stabili-per-scala'], true)) { $params['--split-mode'] = $splitMode; } // In dry-run evita ricreare viste QA e limita i record per reattività if ($dry) { $params['--no-views'] = true; if (!$request->filled('limit')) { $params['--limit'] = 200; // default veloce } } // Opzione opzionale per forzare l'override dei campi mappati (es. CF) if ($request->boolean('force_mapping')) { $params['--force-mapping'] = true; } // Preflight minimo: per import STABILI richiede almeno una sorgente valida (MDB o staging) if (($solo === 'stabili' || $request->input('tipo_import') === 'stabili') && !$importAll) { $hasMdb = $mdbPath && is_file($mdbPath); $hasStaging = false; try { $hasStaging = ( (Schema::connection('gescon_import')->hasTable('stabili')) || (Schema::connection('gescon_import')->hasTable('condomin')) ); } catch (\Throwable $e) { $hasStaging = false; } if (!$hasMdb && !$hasStaging) { $hint = 'Sorgente non configurata: imposta il percorso di Stabili.mdb in Impostazioni oppure carica la tabella di staging (condomin/stabili).'; if ($mdbPath) { $hint .= ' Percorso risolto: ' . $mdbPath; if (is_string($mdbPath) && str_starts_with($mdbPath, '/home/')) { $hint .= ' (attenzione ai permessi: i file sotto /home potrebbero non essere leggibili dal server web)'; } } return redirect()->back()->with('error', $hint); } } // Inizio logging $taskKey = $logger->begin('import-full', [ 'legacy_code' => $legacyCode, 'solo' => $solo, 'dry' => $dry, 'anno' => $anno, 'path' => $path, ]); $logger->checkpoint('preflight', ['params' => $params]); // Caso: import massivo di tutti gli stabili dalla UI if ($importAll && ($solo === 'stabili' || $request->input('tipo_import') === 'stabili')) { if (!$isSuperAdmin) { return redirect()->back()->with('error', 'Solo il Super Admin può eseguire l\'import massivo degli stabili.'); } $hasMdbTools = trim((string)@shell_exec('command -v mdb-export')) !== ''; $done = 0; $errors = 0; $codes = []; $totCreated = 0; $totUpdated = 0; $mdbPath = $mdbOverride ?: UserSetting::get('gescon.mdb_stabili', '/mnt/gescon-archives/gescon/dbc/Stabili.mdb'); if (!is_file($mdbPath)) { $guess = rtrim((string)$path, '/') . '/Stabili.mdb'; if (is_file($guess)) { $mdbPath = $guess; } } if ($hasMdbTools && $mdbPath && is_file($mdbPath)) { $items = $this->scanStabiliFromMdb($mdbPath); $codes = array_values(array_unique(array_map(fn($it) => (string)($it['legacy_code'] ?? ''), $items))); } if (empty($codes)) { // Fallback a sorgente directory $base = UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon'); if ($base && is_dir($base)) { $items = $this->scanLegacyCodesFromDir($base); $codes = array_values(array_unique(array_map(fn($it) => (string)($it['legacy_code'] ?? ''), $items))); } } foreach ($codes as $code) { if ($code === '') continue; try { $pp = $params; $pp['--stabile'] = $code; Artisan::call('gescon:import-full', $pp); $out = (string) Artisan::output(); $logger->checkpoint('import.stabile.done', ['code' => $code]); $logger->stdout("\n===== IMPORT {$code} =====\n" . $out . "\n"); $counts = $this->parseStabiliCountsFromOutput($out); $totCreated += $counts['created'] ?? 0; $totUpdated += $counts['updated'] ?? 0; $done++; } catch (\Throwable $e) { $errors++; $logger->error('import.stabile.error', ['code' => $code, 'error' => $e->getMessage()]); // Continua con gli altri } } $logger->finish(['mode' => 'bulk', 'done' => $done, 'errors' => $errors, 'created' => $totCreated, 'updated' => $totUpdated]); return redirect()->back() ->with('success', "Import stabili completato: {$done} eseguiti" . ($errors ? ", errori: {$errors}" : '')) ->with('import_summary', [ 'mode' => 'bulk', 'done' => $done, 'errors' => $errors, 'created' => $totCreated, 'updated' => $totUpdated, ]); } Artisan::call('gescon:import-full', $params); $output = (string) Artisan::output(); $logger->stdout($output); $counts = $this->parseStabiliCountsFromOutput($output); $archiveMeta = null; if (!$dry && $legacyCode && $amministratore) { $archiveBase = is_string($archiveSourceOverride) ? trim($archiveSourceOverride) : null; if ($archiveBase === '') { $archiveBase = null; } if (!$archiveBase && $path) { $archiveBase = $path; } try { $archiveMeta = $this->archiveSync->sync( $amministratore, $legacyCode, $archiveBase, $refreshArchive ); $logger->checkpoint('post.archive', [ 'legacy_code' => $legacyCode, 'source_base' => $archiveBase, 'target' => $archiveMeta['target'] ?? null, ]); } catch (\Throwable $e) { $archiveMeta = [ 'legacy_code' => $legacyCode, 'error' => $e->getMessage(), ]; $logger->error('post.archive.error', ['error' => $e->getMessage()]); } } // Post-import: crea/aggiorna gestioni per anno a partire dagli archivi legacy (generale_stabile/anni) try { if (!empty($legacyCode) && !$dry) { $gestOpt = [ '--stabile' => $legacyCode, '--root' => rtrim((string)($path ?: '/mnt/gescon-archives/gescon'), '/'), ]; if ($request->boolean('force_update_gestioni')) { $gestOpt['--force-update-months'] = true; } Artisan::call('gescon:import-gestioni', $gestOpt); $logger->checkpoint('post.gestioni'); // Non bloccare in caso di errori: esegui in best-effort } } catch (\Throwable $e) { $logger->error('post.gestioni.error', ['error' => $e->getMessage()]); // Log silenzioso o aggiungere a summary in futuro } // Post-import: se abbiamo importato solo gli STABILI per uno specifico legacy, esegui anche lo step BANCHE try { if (!$dry && !empty($legacyCode)) { $didOnlyStabili = ($solo === 'stabili') || ($request->input('tipo_import') === 'stabili'); if ($didOnlyStabili) { $pp = []; if ($legacyCode) $pp['--stabile'] = $legacyCode; if ($path) $pp['--path'] = rtrim($path, '/'); $pp['--solo'] = 'banche'; Artisan::call('gescon:import-full', $pp); $logger->checkpoint('post.banche'); } } } catch (\Throwable $e) { $logger->error('post.banche.error', ['error' => $e->getMessage()]); // Ignora in caso di failure; verrà ripetuto in import completi } // Aggiorna mapping record if ($legacyCode) { $mapping = GesconImportMapping::firstOrCreate( ['user_id' => auth()->id(), 'legacy_code' => $legacyCode], ['source_path' => $path, 'status' => 'pending'] ); $mapping->status = $dry ? 'dry-run' : 'synced'; $mapping->legacy_denominazione = $request->input('legacy_denominazione'); $mapping->last_dry_run_at = $dry ? now() : $mapping->last_dry_run_at; $mapping->last_sync_at = $dry ? $mapping->last_sync_at : now(); $meta = is_array($mapping->meta) ? $mapping->meta : []; $meta['source_path'] = $path; if ($archiveMeta !== null) { $meta['archive'] = array_merge( array_filter([ 'source_base' => $archiveBase, 'refresh_requested' => $refreshArchive, ], fn($value) => $value !== null && $value !== ''), $archiveMeta ); } if ($amministratore) { $meta['target_admin_id'] = $amministratore->id; } if (!empty($legacyCode)) { $meta['legacy_code'] = $legacyCode; } $mapping->meta = $meta; $mapping->save(); } $logger->finish(['mode' => 'single', 'legacy_code' => $legacyCode, 'counts' => $counts]); return redirect()->back() ->with('success', 'Import GESCON avviato con successo.') ->with('import_summary', [ 'mode' => 'single', 'legacy_code' => $legacyCode, 'created' => $counts['created'] ?? null, 'updated' => $counts['updated'] ?? null, ]); } catch (\Exception $e) { if (isset($logger)) { $logger->error('fatal', ['error' => $e->getMessage()]); } return redirect()->back() ->with('error', 'Errore durante l\'import: ' . $e->getMessage()); } } /** * Estrae i conteggi dallo stdout del comando artisan gescon:import-full */ private function parseStabiliCountsFromOutput(string $out): array { $created = null; $updated = null; // Formato tipico: "stabili creati: 0, aggiornati: 2" if (preg_match('/stabili[^\n]*creati\s*:\s*(\d+)[^\n]*aggiornati\s*:\s*(\d+)/i', $out, $m)) { $created = (int)($m[1] ?? 0); $updated = (int)($m[2] ?? 0); } elseif (preg_match('/creati\s*:\s*(\d+).*aggiornati\s*:\s*(\d+)/i', $out, $m)) { $created = (int)($m[1] ?? 0); $updated = (int)($m[2] ?? 0); } return ['created' => $created ?? 0, 'updated' => $updated ?? 0]; } /** * Restituisce un record di esempio dall'archivio legacy per anteprima mapping. */ public function sampleLegacy(Request $request) { $legacy = $request->query('legacy_code'); $index = (int)$request->query('index', 0); $index = max(0, $index); try { // Preferisci direttamente Stabili.mdb per il mapping (richiesta utente); fallback a dataset se assente $hasMdb = trim((string)@shell_exec('command -v mdb-export')) !== ''; $mdbPath = \App\Models\UserSetting::get('gescon.mdb_stabili'); if (!$mdbPath || !@is_file($mdbPath) || !@is_readable($mdbPath)) { $storageFallback = '/var/www/netgescon/storage/gescon/Stabili.mdb'; if (@is_file($storageFallback) && @is_readable($storageFallback)) { $mdbPath = $storageFallback; } } if ($hasMdb && $mdbPath && is_file($mdbPath)) { $row = $this->sampleStabileFromMdb($mdbPath, $legacy, $index); if (!empty($row)) { $legacyKey = $row['legacy_key'] ?? null; unset($row['legacy_key']); return response()->json(['ok' => true, 'index' => $index, 'data' => $row, 'legacy_key' => $legacyKey]); } } // Fallback: archivio dataset (se presente) $ds = MigrationDataset::where('key', 'stabili')->orderByDesc('updated_at')->first(); if ($ds) { $rec = MigrationRecord::where('dataset_id', $ds->id) ->when($legacy, function ($q) use ($legacy) { $q->where('legacy_key', $legacy); }) ->orderBy('id') ->skip($index) ->take(1) ->first(); if ($rec) { return response()->json(['ok' => true, 'index' => $index, 'data' => (array)($rec->data ?? []), 'legacy_key' => $rec->legacy_key]); } } return response()->json(['ok' => false, 'error' => 'Anteprima non disponibile: nessun dataset e MDB non accessibile', 'index' => $index]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => 'Errore: ' . $e->getMessage()]); } } /** * Sample Unità Immobiliari (preview mapping) – placeholder: ritorna record mock o da staging condomin. */ public function sampleUnita(Request $request) { $legacy = $request->query('legacy_code'); $index = max(0, (int)$request->query('index', 0)); try { $rows = []; if ($legacy && \Schema::connection('gescon_import')->hasTable('condomin')) { $rows = \DB::connection('gescon_import') ->table('condomin') ->when($legacy, function ($q) use ($legacy) { $q->where('cod_stabile', $legacy); }) ->select(['cod_stabile', 'cod_cond', 'scala', 'interno', 'cognome', 'nome', 'codice_fiscale', 'millesimi_generali', 'millesimi_risc']) ->orderBy('cod_cond') ->skip($index) ->take(1) ->get()->map(fn($r) => (array)$r)->toArray(); } if (empty($rows)) { $rows[] = [ 'cod_stabile' => $legacy, 'cod_cond' => 'U' . str_pad((string)$index, 4, '0', STR_PAD_LEFT), 'scala' => 'A', 'interno' => '1', 'cognome' => 'Rossi', 'nome' => 'Mario', 'codice_fiscale' => 'RSSMRA80A01H501Z', 'millesimi_generali' => 45.67, 'millesimi_risc' => 12.34, ]; } return response()->json(['ok' => true, 'index' => $index, 'data' => $rows[0]]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage()]); } } /** * Sample Tabelle millesimali (preview) – per ora mock. */ public function sampleTabelle(Request $request) { $legacy = $request->query('legacy_code'); $index = max(0, (int)$request->query('index', 0)); try { // TODO: estrarre da eventuale staging o MDB futuro $mock = [ 'cod_stabile' => $legacy, 'tabella' => 'Generali', 'voce' => 'Scala A (app1)', 'millesimi' => 45.6700, 'nord_x10' => null, ]; return response()->json(['ok' => true, 'index' => $index, 'data' => $mock]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage()]); } } /** * Sample Voci (Piano dei Conti) – mock finché non importiamo reale. */ public function sampleVoci(Request $request) { $legacy = $request->query('legacy_code'); $index = max(0, (int)$request->query('index', 0)); try { $mock = [ 'cod_stabile' => $legacy, 'cod_voce' => 'A' . str_pad((string)$index, 3, '0', STR_PAD_LEFT), 'descrizione' => 'Spese generali condominio', 'gruppo' => 'MANUT', 'tipo' => 'ordinaria', ]; return response()->json(['ok' => true, 'index' => $index, 'data' => $mock]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage()]); } } /** * Restituisce lista anni disponibili (cartelle con singolo_anno.mdb) per un codice legacy. */ public function years(Request $request) { $legacy = (string) $request->query('legacy_code'); if (!$legacy) return response()->json(['ok' => false, 'error' => 'legacy_code richiesto'], 422); try { $years = $this->legacy->scanLegacyYears($legacy); return response()->json(['ok' => true, 'legacy_code' => $legacy, 'years' => $years]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage()]); } } /** * Ritorna i dati di una gestione per anno (ordinaria/riscaldamento + straordinarie) dal filesystem legacy. * JSON shape attesa dal frontend: * { * ok: true, * anno: 2024, * ordinaria: { anno:2024, stato:'aperta|chiusa', rate:{1:1,4:1,...} }, * riscaldamento: { anno:2024, stato:'aperta|chiusa', rate:{...} }, * straordinarie: [ { id:'STR001', descrizione:'Facciate', data_inizio:'2024-03-10', data_fine:null, rate_totali:6, rate_emesse:3 } ] * } */ public function gestioniYear(Request $request) { $legacy = (string)$request->query('legacy_code'); $anno = (int)$request->query('anno'); if (!$legacy || !$anno) { return response()->json(['ok' => false, 'error' => 'legacy_code e anno richiesti']); } $data = $this->legacy->gestioniYear($legacy, $anno); return response()->json($data); } /** * Anteprima Unità (tabella condomin) per anno: ritorna headers e qualche riga dal singolo_anno.mdb * GET params: legacy_code, anno, limit (default 20) * Response: { ok, legacy_code, anno, headers:[], rows:[{col:val,...}] } */ public function unitaYear(Request $request) { $legacy = (string)$request->query('legacy_code'); $anno = (int)$request->query('anno'); $limit = (int)($request->query('limit', 20)); if (!$legacy) { return response()->json(['ok' => false, 'error' => 'legacy_code richiesto'], 422); } $data = $this->legacy->unitaYear($legacy, $anno ?: null, $limit); return response()->json($data); } /** * Elenco casse/conti legacy (inclusa sempre la cassa CON) dal generale_stabile.mdb. */ public function casse(Request $request) { $legacy = (string)$request->query('legacy_code'); if (!$legacy) return \response()->json(['ok' => false, 'error' => 'legacy_code richiesto']); $rows = $this->legacy->casse($legacy); return response()->json(['ok' => true, 'legacy_code' => $legacy, 'casse' => $rows]); } /** * Applica il mapping (banche + gestioni) persistendo in DB per uno stabile target. */ public function applyMappingImport(Request $request) { $data = $request->validate([ 'legacy_code' => 'required|string', 'stabile_id' => 'required|integer|exists:stabili,id', 'anni' => 'sometimes|array', 'refresh_archive' => 'sometimes|boolean', 'archive_source_path' => 'sometimes|string|nullable', ]); $legacy = $data['legacy_code']; $stabileId = (int)$data['stabile_id']; $anni = $data['anni'] ?? null; $stabile = Stabile::with('amministratore')->findOrFail($stabileId); $amministratoreDest = $stabile->amministratore; $tenantId = (string) (auth()->user()->tenant_id ?? 'default'); $mapping = GesconImportMapping::where('user_id', auth()->id())->where('legacy_code', $legacy)->first(); if (!$mapping) { return response()->json(['ok' => false, 'error' => 'Mapping non trovato per legacy_code'], 404); } $meta = $mapping->meta ?? []; $archiveMeta = null; if ($legacy && $amministratoreDest) { try { $archiveMeta = $this->archiveSync->sync( $amministratoreDest, $legacy, $data['archive_source_path'] ?? null, (bool) ($data['refresh_archive'] ?? false) ); } catch (\Throwable $e) { $archiveMeta = [ 'legacy_code' => $legacy, 'error' => $e->getMessage(), ]; } } elseif ($legacy) { $archiveMeta = [ 'legacy_code' => $legacy, 'error' => 'Amministratore associato non trovato.', ]; } if ($archiveMeta) { $meta['archive'] = [ 'legacy_code' => $archiveMeta['legacy_code'] ?? $legacy, 'source' => $archiveMeta['source'] ?? null, 'target' => $archiveMeta['target'] ?? null, 'size_bytes' => $archiveMeta['size_bytes'] ?? null, 'files_count' => $archiveMeta['files_count'] ?? null, 'directories_count' => $archiveMeta['directories_count'] ?? null, 'latest_file_at' => $archiveMeta['latest_file_at'] ?? null, 'synced_at' => now()->toDateTimeString(), 'error' => $archiveMeta['error'] ?? null, ]; $mapping->meta = $meta; $mapping->save(); } $bancheMapping = $meta['banche_mapping'] ?? []; $defaultBank = isset($meta['stabile_banca_predefinita']) ? strtoupper((string)$meta['stabile_banca_predefinita']) : null; // Assicura che CON e CCB (se presente nell'archivio legacy) siano sempre presenti in mapping logico per DatiBancari $legacyCasse = []; try { $legacyCasse = $this->legacy->casse($legacy); } catch (\Throwable $e) { /* ignore */ } $legacyCodici = collect($legacyCasse)->pluck('codice')->map(fn($c) => strtoupper($c))->unique(); // Forza CON se non mappato if (!$legacyCodici->contains('CON')) { $legacyCodici->push('CON'); } // NON creiamo CCB se non esiste proprio; se esiste aggiungiamo if ($legacyCodici->contains('CCB') && !collect($bancheMapping)->pluck('codice_legacy')->map('strtoupper')->contains('CCB')) { $bancheMapping[] = [ 'codice_legacy' => 'CCB', 'tipo_conto' => 'bancario', 'iban' => null, 'swift' => null, 'descrizione_legacy' => 'CONTO CORRENTE BANCARIO' ]; } if (!collect($bancheMapping)->pluck('codice_legacy')->map('strtoupper')->contains('CON')) { $bancheMapping[] = [ 'codice_legacy' => 'CON', 'tipo_conto' => 'contanti', 'iban' => null, 'swift' => null, 'descrizione_legacy' => 'CASSA CONTANTI' ]; } // 1) Import Conti Bancari (mapping manuale) $logger = new ImportProgressLogger(); $logger->begin('apply-mapping', ['legacy_code' => $legacy, 'stabile_id' => $stabileId]); if ($archiveMeta) { $logger->checkpoint('archive.sync', [ 'legacy_code' => $legacy, 'target' => $archiveMeta['target'] ?? null, 'source' => $archiveMeta['source'] ?? null, 'error' => $archiveMeta['error'] ?? null, ]); } $contiCreated = 0; $contiUpdated = 0; $contiSkipped = 0; // Se definita banca predefinita, azzera il flag principale su tutti i conti dello stabile if ($defaultBank) { try { \App\Models\DatiBancari::where('stabile_id', $stabileId)->update(['is_nostro_conto' => false]); } catch (\Throwable $e) { /* ignore */ } } foreach ((array)$bancheMapping as $bm) { if (!is_array($bm)) { $contiSkipped++; continue; } $cod = $this->resolveMappingCodiceCassa($bm); if (!$cod) { $raw = $bm['codice_legacy'] ?? null; $cod = $raw ? strtoupper(trim((string)$raw)) : null; } if (!$cod) { $contiSkipped++; continue; } $targetArr = isset($bm['target']) && is_array($bm['target']) ? $bm['target'] : []; $isMain = ($defaultBank && $cod === $defaultBank) || !empty($bm['is_nostro_conto']) || !empty($bm['target_is_nostro_conto']) || !empty($targetArr['is_nostro_conto'] ?? null); $tipo = $bm['tipo_conto'] ?? 'bancario'; $iban = $bm['iban'] ?? ($bm['legacy_iban'] ?? null); if (is_string($iban)) { $iban = trim($iban) !== '' ? $iban : null; } $swift = $bm['swift'] ?? ($bm['legacy_swift'] ?? null); $desc = $bm['descrizione_legacy'] ?? ($bm['legacy_descrizione'] ?? ($bm['legacy_nome_banca'] ?? $cod)); $existing = \App\Models\ContoBancario::where('tenant_id', $tenantId)->where('codice', $cod)->first(); if ($existing) { $existing->update([ 'descrizione' => $desc, 'tipo_conto' => $tipo, 'iban' => $iban, 'swift' => $swift, 'attivo' => true, ]); $contiUpdated++; } else { \App\Models\ContoBancario::create([ 'tenant_id' => $tenantId, 'gestione_id' => null, 'id_cassa_gescon' => null, 'codice' => $cod, 'descrizione' => $desc, 'saldo_iniziale' => 0, 'tipo_conto' => $tipo, 'banca' => null, 'numero_conto' => null, 'iban' => $iban, 'swift' => $swift, 'saldo_corrente' => 0, 'attivo' => true, 'metadati_gescon' => $bm, ]); $contiCreated++; } $logger->checkpoint('banche.upsert', ['codice' => $cod, 'is_main' => (bool)$isMain]); // Upsert DatiBancari collegato allo stabile (visibilità nel dettaglio stabile) try { $db = \App\Models\DatiBancari::where('stabile_id', $stabileId) ->where(function ($q) use ($cod) { $q->where('legacy_cod_cassa', $cod)->orWhere('numero_conto', $cod); }) ->first(); $dbcPayload = [ 'stabile_id' => $stabileId, 'tipo_conto' => $tipo === 'contanti' ? 'cassa' : 'corrente', 'denominazione_banca' => $desc, 'numero_conto' => $cod, 'iban' => $iban, 'legacy_cod_cassa' => $cod, 'abi' => null, 'cab' => null, 'bic_swift' => $swift, 'intestazione_conto' => $desc, 'stato_conto' => 'attivo', 'is_nostro_conto' => (bool) $isMain, ]; if ($db) { $db->update($dbcPayload); } else { \App\Models\DatiBancari::create($dbcPayload); } } catch (\Throwable $e) { /* ignore individual failure */ } } // Seleziona comunque come principale il conto esistente con codice uguale alla banca predefinita, anche se non era nel loop if ($defaultBank) { try { \App\Models\DatiBancari::where('stabile_id', $stabileId) ->where(function ($q) use ($defaultBank) { $q->whereRaw('UPPER(COALESCE(legacy_cod_cassa, "")) = ?', [$defaultBank]) ->orWhereRaw('UPPER(COALESCE(numero_conto, "")) = ?', [$defaultBank]); }) ->update(['is_nostro_conto' => true]); } catch (\Throwable $e) { /* ignore */ } } // 2) Import Gestioni (scan anni + parse) $yearsAvailable = $anni ?: $this->legacy->scanLegacyYears($legacy); $gestioniCreated = 0; $gestioniUpdated = 0; $straOrdCreated = 0; foreach ($yearsAvailable as $anno) { $parsed = $this->legacy->gestioniYear($legacy, (int)$anno); if (!$parsed['ok']) continue; $paths = $parsed['meta'] ?? []; // Ordinaria if (!empty($parsed['ordinaria'])) { $res = $this->upsertGestione($tenantId, $stabileId, $parsed['ordinaria'], 'ordinaria', $paths); if ($res) $gestioniCreated += $res; else $gestioniUpdated++; $logger->checkpoint('gestioni.ordinaria', ['anno' => (int)$anno, 'created' => (bool)$res]); } // Riscaldamento if (!empty($parsed['riscaldamento'])) { $res = $this->upsertGestione($tenantId, $stabileId, $parsed['riscaldamento'], 'riscaldamento', $paths); if ($res) $gestioniCreated += $res; else $gestioniUpdated++; $logger->checkpoint('gestioni.riscaldamento', ['anno' => (int)$anno, 'created' => (bool)$res]); } // Straordinarie → ciascuna gestione separata $n = 1; foreach (($parsed['straordinarie'] ?? []) as $s) { $gestData = [ 'anno' => (int)$anno, 'stato' => 'aperta', 'rate' => $s['rate_totali'] ?? [], 'numero_straordinaria' => $n, 'piano' => [ 'id_legacy' => $s['id'] ?? null, 'descrizione' => $s['descrizione'] ?? null, 'rate_emesse' => $s['rate_emesse'] ?? null, 'rate_totali' => $s['rate_totali'] ?? null, ] ]; $res = $this->upsertGestione($tenantId, $stabileId, $gestData, 'straordinaria', $paths, $n); if ($res) { $straOrdCreated += $res; } else { $gestioniUpdated++; } $logger->checkpoint('gestioni.straordinaria', ['anno' => (int)$anno, 'numero' => $n, 'created' => (bool)$res]); $n++; } } $logger->finish([ 'conti' => ['created' => $contiCreated, 'updated' => $contiUpdated, 'skipped' => $contiSkipped], 'gestioni' => ['created' => $gestioniCreated + $straOrdCreated, 'updated' => $gestioniUpdated], 'anni_processati' => $yearsAvailable, ]); return response()->json([ 'ok' => true, 'legacy_code' => $legacy, 'stabile_id' => $stabileId, 'archivio' => $archiveMeta, 'conti' => ['created' => $contiCreated, 'updated' => $contiUpdated, 'skipped' => $contiSkipped], 'gestioni' => ['created' => $gestioniCreated + $straOrdCreated, 'updated' => $gestioniUpdated], 'anni_processati' => $yearsAvailable, ]); } /** * Diagnostica rapida sullo staging: connessione, tabelle presenti e sample righe per un legacy_code. */ public function stagingDiagnostics(Request $request) { $legacy = (string) $request->query('legacy_code', ''); $conn = UserSetting::get('gescon.import_conn', 'gescon_import'); $out = [ 'connection' => $conn, 'legacy_code' => $legacy ?: null, 'tables' => [], 'samples' => [], ]; try { $tables = ['condomin', 'Anag_casse', 'anag_casse', 'stabili']; foreach ($tables as $t) { $has = Schema::connection($conn)->hasTable($t); $out['tables'][$t] = $has; if ($has) { $q = DB::connection($conn)->table($t); if ($legacy && Schema::connection($conn)->hasColumn($t, 'cod_stabile')) { $q->where('cod_stabile', $legacy); } $out['samples'][$t] = $q->limit(3)->get(); } } return response()->json(['ok' => true, 'data' => $out]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage(), 'data' => $out], 500); } } /** * Ritorna le colonne e alcune righe di esempio per una tabella dello staging gescon_import. * GET: table? (opzionale, se assente usa guess), guess? (regex o frammento, es. 'mille'|'voci'), legacy_code? (filtra se presente colonna cod_stabile) * Response: { ok, connection, table, columns:[], rows:[{..}], count } */ public function stagingColumns(Request $request) { $conn = UserSetting::get('gescon.import_conn', 'gescon_import'); $table = $request->query('table'); $guess = $request->query('guess'); $legacy = $request->query('legacy_code'); try { $chosen = null; if ($table) { if (!Schema::connection($conn)->hasTable($table)) { return response()->json(['ok' => false, 'error' => "Tabella non trovata: {$table}", 'connection' => $conn], 404); } $chosen = $table; } else { // Scansione euristica: cerca per frammento in ordine di preferenza $tables = []; try { $tables = Schema::connection($conn)->getAllTables(); } catch (\Throwable $e) { $tables = []; } if (empty($tables)) { // getAllTables non sempre disponibile: fallback usando information_schema $tables = DB::connection($conn)->select("select table_name as name from information_schema.tables where table_schema=database()"); $tables = array_map(fn($r) => (array)$r, $tables); } $names = array_values(array_filter(array_map(function ($r) { $n = $r['name'] ?? ($r['TABLE_NAME'] ?? null); return $n ? (string)$n : null; }, $tables))); $order = []; if ($guess) { $g = strtolower((string)$guess); foreach ($names as $n) { if (str_contains(strtolower($n), $g)) $order[] = $n; } } // fallback comuni foreach (['condomin', 'milles', 'millesimi', 'tab_millesimi', 'tabelle', 'voci', 'pianoconti', 'piano_conti', 'conti'] as $pat) { foreach ($names as $n) { if (str_contains(strtolower($n), $pat) && !in_array($n, $order, true)) $order[] = $n; } } $chosen = $order[0] ?? null; if (!$chosen) { return response()->json(['ok' => false, 'error' => 'Nessuna tabella candidata trovata', 'connection' => $conn]); } } $cols = Schema::connection($conn)->getColumnListing($chosen); $q = DB::connection($conn)->table($chosen); if ($legacy) { if (Schema::connection($conn)->hasColumn($chosen, 'cod_stabile')) $q->where('cod_stabile', $legacy); elseif (Schema::connection($conn)->hasColumn($chosen, 'cod_condominio')) $q->where('cod_condominio', $legacy); } $rows = $q->limit(5)->get(); return response()->json([ 'ok' => true, 'connection' => $conn, 'table' => $chosen, 'columns' => array_values($cols), 'rows' => $rows, 'count' => $q->count(), ]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage(), 'connection' => $conn], 500); } } /** * List columns for an explicit staging table (advanced explorer for MDB→staging). */ public function stagingTableColumns(Request $request) { $table = trim((string) $request->query('table', '')); $conn = (string) ($request->query('connection') ?: UserSetting::get('gescon.import_conn', 'gescon_import')); if ($table === '') { return response()->json(['ok' => false, 'error' => 'Missing table'], 422); } try { if (!Schema::connection($conn)->hasTable($table)) { return response()->json(['ok' => false, 'error' => 'Table not found', 'connection' => $conn, 'table' => $table], 404); } $cols = Schema::connection($conn)->getColumnListing($table); return response()->json(['ok' => true, 'connection' => $conn, 'table' => $table, 'columns' => array_values($cols)]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage()], 500); } } /** * Aggrega tutte le colonne dalle principali tabelle dello staging, per consentire mapping oltre a CONDOMIN. * GET: connection? (default gescon_import) * Response: { ok, connection, tables: [{name, columns:[], preview:[]}] } */ public function stagingAllColumns(Request $request) { $conn = (string) ($request->query('connection') ?: UserSetting::get('gescon.import_conn', 'gescon_import')); try { // Enumera tabelle disponibili $tablesRaw = []; try { $tablesRaw = Schema::connection($conn)->getAllTables(); } catch (\Throwable $e) { $tablesRaw = DB::connection($conn)->select("select table_name as name from information_schema.tables where table_schema=database()"); $tablesRaw = array_map(fn($r) => (array)$r, $tablesRaw); } $names = array_values(array_filter(array_map(function ($r) { $n = $r['name'] ?? ($r['TABLE_NAME'] ?? null); return $n ? (string)$n : null; }, $tablesRaw))); // Focus su tabelle probabili + tutte le altre $preferred = ['condomin', 'anag_casse', 'Anag_casse', 'stabili', 'piano_conti', 'pianoconti', 'voci', 'tabelle', 'milles', 'millesimi', 'tab_millesimi']; $ordered = []; foreach ($preferred as $p) { foreach ($names as $n) { if (strtolower($n) === strtolower($p) && !in_array($n, $ordered, true)) $ordered[] = $n; } } foreach ($names as $n) { if (!in_array($n, $ordered, true)) $ordered[] = $n; } $out = []; foreach ($ordered as $t) { try { $cols = Schema::connection($conn)->getColumnListing($t); // Prendi poche righe per capire i valori $rows = DB::connection($conn)->table($t)->limit(2)->get(); $out[] = ['name' => $t, 'columns' => array_values($cols), 'preview' => $rows]; } catch (\Throwable $e) { $out[] = ['name' => $t, 'columns' => [], 'preview' => []]; } } return response()->json(['ok' => true, 'connection' => $conn, 'tables' => $out]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage(), 'connection' => $conn], 500); } } /** * Create new columns in target tables as requested by mapping UI ("crea campo nuovo"). * Input: { table: string, connection?: string, columns: [{ name, type, nullable? }] } */ public function createTargetColumns(Request $request) { $data = $request->validate([ 'table' => 'required|string', 'connection' => 'nullable|string', 'columns' => 'required|array|min:1', 'columns.*.name' => 'required|string', 'columns.*.type' => 'required|string', 'columns.*.nullable' => 'nullable|boolean', ]); $table = (string) $data['table']; $conn = (string) ($data['connection'] ?? config('database.default', 'mysql')); $cols = (array) $data['columns']; try { // Filter out existing columns to avoid errors $existing = []; try { $existing = Schema::connection($conn)->getColumnListing($table); } catch (\Throwable $e) { $existing = []; } $toAdd = array_values(array_filter($cols, function ($c) use ($existing) { $n = isset($c['name']) ? (string)$c['name'] : ''; return $n !== '' && !in_array($n, $existing, true); })); if (empty($toAdd)) { return response()->json(['ok' => true, 'skipped' => 'All columns already exist']); } Schema::connection($conn)->table($table, function ($blueprint) use ($toAdd) { foreach ($toAdd as $c) { $name = (string) $c['name']; $type = strtolower((string) $c['type']); $nullable = !empty($c['nullable']); if ($type === 'string' || $type === 'varchar') { $col = $blueprint->string($name, 255); } elseif ($type === 'text') { $col = $blueprint->text($name); } elseif ($type === 'integer' || $type === 'int') { $col = $blueprint->integer($name); } elseif ($type === 'bigint' || $type === 'biginteger') { $col = $blueprint->bigInteger($name); } elseif ($type === 'boolean' || $type === 'bool') { $col = $blueprint->boolean($name); } elseif ($type === 'decimal' || $type === 'numeric') { $col = $blueprint->decimal($name, 15, 4); } elseif ($type === 'datetime' || $type === 'timestamp') { $col = $blueprint->dateTime($name); } elseif ($type === 'date') { $col = $blueprint->date($name); } else { $col = $blueprint->text($name); } if ($nullable && method_exists($col, 'nullable')) { $col->nullable(); } } }); return response()->json(['ok' => true]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage()], 500); } } /** * Importa Unità Immobiliari (ed eventualmente Soggetti/Diritti) per uno stabile legacy. * Usa la pipeline artisan esistente (gescon:import-full) con step selezionabili. * Accetta anche un percorso esplicito a singolo_anno.mdb oppure deduce dal path configurato + anno. */ public function importUnita(Request $request) { // Permesso esecuzione import richiesto (già gestito da middleware sulla rotta) $data = $request->validate([ 'legacy_code' => 'required|string', 'steps' => 'sometimes|array', // [unita,soggetti,diritti] 'anno' => 'sometimes|integer', 'mdb_condomin_path' => 'sometimes|string|nullable', 'dry_run' => 'sometimes|boolean', 'limit' => 'sometimes|integer|min:1|max:100000', // Filtri opzionali derivati da CONDOMIN 'scala' => 'sometimes|string|nullable|max:10', 'piano_min' => 'sometimes|integer|nullable', 'piano_max' => 'sometimes|integer|nullable', 'only_pertinenze' => 'sometimes|string|nullable|in:any,cantina,box,posto_auto,soffitta,locale_tecnico' ]); $legacy = (string) $data['legacy_code']; $steps = (array) ($data['steps'] ?? ['unita']); $anno = isset($data['anno']) ? (int)$data['anno'] : null; $mdbCondomin = (string) ($data['mdb_condomin_path'] ?? ''); $dry = (bool) ($data['dry_run'] ?? true); $limit = isset($data['limit']) ? (int)$data['limit'] : null; $scala = isset($data['scala']) ? (string)$data['scala'] : null; $pianoMin = $data['piano_min'] ?? null; $pianoMax = $data['piano_max'] ?? null; $onlyPert = isset($data['only_pertinenze']) ? (string)$data['only_pertinenze'] : null; // Deduce path a singolo_anno.mdb se non fornito esplicitamente if (!$mdbCondomin && $anno) { $base = UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon'); $cand = rtrim($base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $legacy . DIRECTORY_SEPARATOR . $anno . DIRECTORY_SEPARATOR . 'singolo_anno.mdb'; if (@is_file($cand)) { $mdbCondomin = $cand; } } // Determine amministratore target similar to executeImport fallback $isSuperAdmin = auth()->user()->hasRole('super-admin') || auth()->user()->can('gescon-import.config'); $currentAdmin = $this->getAmministratoreCorrente(); $targetAdminId = null; try { $mapRec = GesconImportMapping::where('user_id', auth()->id())->where('legacy_code', $legacy)->first(); $targetAdminId = (int) data_get($mapRec, 'meta.target_admin_id'); if (!$targetAdminId) $targetAdminId = null; } catch (\Throwable $e) { $targetAdminId = null; } $logger = new ImportProgressLogger(); $logger->begin('import-unita', ['legacy_code' => $legacy, 'anno' => $anno, 'steps' => $steps, 'dry' => $dry]); $results = []; foreach ($steps as $step) { try { $params = []; if ($legacy) $params['--stabile'] = $legacy; if ($mdbCondomin) $params['--mdb-condomin'] = $mdbCondomin; if ($anno) $params['--anno'] = $anno; $params['--solo'] = $step; // unita | soggetti | diritti if ($dry) $params['--dry-run'] = true; if ($limit) $params['--limit'] = $limit; if ($scala) $params['--scala'] = $scala; if ($pianoMin !== null) $params['--piano-min'] = (string)$pianoMin; if ($pianoMax !== null) $params['--piano-max'] = (string)$pianoMax; if ($onlyPert) $params['--only-pertinenze'] = $onlyPert; // Pass amministratore id if allowed if ($targetAdminId && ($isSuperAdmin || ($currentAdmin && $currentAdmin->id === $targetAdminId))) { $params['--amministratore-id'] = $targetAdminId; } \Artisan::call('gescon:import-full', $params); $out = (string) \Illuminate\Support\Facades\Artisan::output(); $logger->checkpoint('step.done', ['step' => $step]); $logger->stdout("\n===== STEP {$step} =====\n" . $out . "\n"); $results[] = ['step' => $step, 'ok' => true, 'output' => $out]; } catch (\Throwable $e) { $logger->error('step.error', ['step' => $step, 'error' => $e->getMessage()]); $results[] = ['step' => $step, 'ok' => false, 'error' => $e->getMessage()]; } } $logger->finish(['results' => array_map(fn($r) => ['step' => $r['step'], 'ok' => $r['ok']], $results)]); // Persisti ultimi filtri usati nel mapping meta try { $mapping = GesconImportMapping::firstOrCreate( ['user_id' => auth()->id(), 'legacy_code' => $legacy], ['status' => 'pending'] ); $meta = $mapping->meta ?? []; $meta['unita_last_filters'] = [ 'anno' => $anno, 'scala' => $scala, 'piano_min' => $pianoMin, 'piano_max' => $pianoMax, 'only_pertinenze' => $onlyPert, 'dry' => $dry, 'limit' => $limit, ]; $mapping->meta = $meta; $mapping->save(); } catch (\Throwable $e) { /* ignore persistence errors */ } return response()->json([ 'ok' => !collect($results)->contains(fn($r) => $r['ok'] === false), 'legacy_code' => $legacy, 'anno' => $anno, 'mdb_condomin_path' => $mdbCondomin, 'dry_run' => $dry, 'results' => $results, ]); } /** * Salva un preset della "Maschera Universale Import" lato server nell'UserSetting dell'utente. * Input JSON: { name: string, preset: object } * Memorizza su chiave 'gescon.universal_presets' come array di voci [{name, preset, saved_at, user_id}]. */ public function saveUniversalPreset(Request $request) { if (!(auth()->user() && auth()->user()->can('gescon-import.view'))) { return response()->json(['ok' => false, 'error' => 'Permesso negato'], 403); } try { $data = $request->all(); $name = trim((string)($data['name'] ?? '')); $preset = $data['preset'] ?? null; if ($name === '' || !is_array($preset)) { return response()->json(['ok' => false, 'error' => 'Parametri non validi'], 422); } // Carica elenco esistente, append/replace per nome $list = UserSetting::get('gescon.universal_presets', []); if (!is_array($list)) { $list = []; } $entry = [ 'name' => $name, 'preset' => $preset, 'saved_at' => now()->toIso8601String(), 'user_id' => auth()->id(), ]; // Sostituisci se esiste già con lo stesso nome (case-insensitive) $replaced = false; foreach ($list as $idx => $it) { if (is_array($it) && strtolower((string)($it['name'] ?? '')) === strtolower($name)) { $list[$idx] = $entry; $replaced = true; break; } } if (!$replaced) { $list[] = $entry; } UserSetting::set('gescon.universal_presets', $list); return response()->json(['ok' => true, 'replaced' => $replaced, 'count' => count($list)]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage()], 500); } } /** * Vista stato sincronizzazione Staging: mostra per ciascun codice legacy se i dati sono presenti e aggiornati. */ public function stagingStatus(Request $request) { // Recupera elenco stabili/legacy (riusa l'anteprima già calcolata dall'index) $base = UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon'); $mdbPath = UserSetting::get('gescon.mdb_stabili', '/mnt/gescon-archives/gescon/dbc/Stabili.mdb'); $mdbIndexPath = UserSetting::get('gescon.mdb_index', '/mnt/gescon-archives/gescon/cartellestabili_e_anni.mdb'); $hasMdbTools = trim((string)@shell_exec('command -v mdb-export')) !== ''; $dataset = MigrationDataset::where('key', 'stabili')->orderByDesc('updated_at')->first(); $preview = Cache::remember('gescon_legacy_preview:staging_status', now()->addMinutes(5), function () use ($dataset, $mdbPath, $hasMdbTools, $base, $mdbIndexPath) { try { $items = []; if ($dataset && $dataset->records_count > 0) { $records = MigrationRecord::where('dataset_id', $dataset->id)->limit(10000)->get(); $map = []; foreach ($records as $rec) { $data = (array)($rec->data ?? []); $legacy = $rec->legacy_key ?: ($data['nome_directory'] ?? ($data['internet_cod_stab'] ?? ($data['cod_stabile'] ?? null))); if (!$legacy) continue; $denom = $data['denominazione'] ?? ($data['fe_denominazione'] ?? null); $map[(string)$legacy] = [ 'legacy_code' => trim((string)$legacy), 'denominazione' => $denom ? trim((string)$denom) : null, ]; } $items = array_values($map); } if (empty($items) && $hasMdbTools && $mdbIndexPath && is_file($mdbIndexPath)) { $items = $this->scanStabiliFromMasterIndex($mdbIndexPath, $base); } if (empty($items) && $mdbPath && is_file($mdbPath) && $hasMdbTools) { $items = $this->scanStabiliFromMdb($mdbPath); } if (empty($items) && is_dir($base)) { $items = $this->scanLegacyCodesFromDir($base); } return $items; } catch (\Throwable $e) { return []; } }); $overview = $this->buildStagingOverview($preview); return view('admin.gescon-import.staging-status', [ 'overview' => $overview, 'staging_connection' => $overview['connection'] ?? null, ]); } /** * Costruisce una panoramica dello stato di staging: connessione usata, presenza tabelle, e conteggi per legacy. * Ritorna un array: [connection, tables:{}, items:[{legacy_code, denominazione, staging:{condomin,casse}, legacy:{has_generale, years_count}}]] */ private function buildStagingOverview(array $legacyItems): array { $conn = UserSetting::get('gescon.import_conn', 'gescon_import'); $connCandidates = array_values(array_unique([$conn, 'gescon_import_00000000'])); $activeConn = null; $tablesPresence = []; $itemsOut = []; // Seleziona prima connessione che abbia almeno una tabella nota foreach ($connCandidates as $c) { try { $has = Schema::connection($c)->hasTable('condomin') || Schema::connection($c)->hasTable('Anag_casse') || Schema::connection($c)->hasTable('anag_casse'); if ($has) { $activeConn = $c; break; } } catch (\Throwable $e) { /* ignore */ } } $activeConn = $activeConn ?: $connCandidates[0]; foreach (['condomin', 'Anag_casse', 'anag_casse'] as $t) { try { $tablesPresence[$t] = Schema::connection($activeConn)->hasTable($t); } catch (\Throwable $e) { $tablesPresence[$t] = false; } } $base = UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon'); foreach ($legacyItems as $it) { $code = (string)($it['legacy_code'] ?? ''); if ($code === '') continue; $den = $it['denominazione'] ?? null; // Conteggi staging $cntCondomin = null; $cntCasse = null; try { $tblC = Schema::connection($activeConn)->hasTable('condomin') ? 'condomin' : (Schema::connection($activeConn)->hasTable('condomini') ? 'condomini' : null); if ($tblC) { $q = DB::connection($activeConn)->table($tblC); if (Schema::connection($activeConn)->hasColumn($tblC, 'cod_stabile')) $q->where('cod_stabile', $code); elseif (Schema::connection($activeConn)->hasColumn($tblC, 'cod_condominio')) $q->where('cod_condominio', $code); $cntCondomin = (int) $q->count(); } } catch (\Throwable $e) { $cntCondomin = null; } try { $tblB = null; foreach (['Anag_casse', 'anag_casse', 'casse', 'banche'] as $t) { if (Schema::connection($activeConn)->hasTable($t)) { $tblB = $t; break; } } if ($tblB) { $q = DB::connection($activeConn)->table($tblB); if (Schema::connection($activeConn)->hasColumn($tblB, 'cod_stabile')) $q->where('cod_stabile', $code); $cntCasse = (int) $q->count(); } } catch (\Throwable $e) { $cntCasse = null; } // Presenza archivi legacy su FS $dir = rtrim($base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $code; $hasGenerale = is_file($dir . DIRECTORY_SEPARATOR . 'generale_stabile.mdb'); $yearsCount = 0; if (is_dir($dir)) { try { foreach (scandir($dir) as $f) { if ($f === '.' || $f === '..') continue; if (!preg_match('/^\d{4}$/', $f)) continue; $sa = $dir . DIRECTORY_SEPARATOR . $f . DIRECTORY_SEPARATOR . 'singolo_anno.mdb'; if (is_file($sa)) $yearsCount++; } } catch (\Throwable $e) { /* ignore */ } } $itemsOut[] = [ 'legacy_code' => $code, 'denominazione' => $den, 'staging' => [ 'condomin_rows' => $cntCondomin, 'casse_rows' => $cntCasse, ], 'legacy' => [ 'has_generale' => $hasGenerale, 'years_count' => $yearsCount, ] ]; } // Ordina per codice naturale usort($itemsOut, fn($a, $b) => strnatcasecmp($a['legacy_code'], $b['legacy_code'])); return [ 'connection' => $activeConn, 'tables' => $tablesPresence, 'items' => $itemsOut, ]; } private function scanLegacyYears(string $legacy): array { $base = UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon'); $dir = rtrim($base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $legacy; if (!is_dir($dir)) return []; $years = []; foreach (scandir($dir) as $f) { if ($f === '.' || $f === '..') continue; if (preg_match('/^\d{4}$/', $f) && is_file($dir . DIRECTORY_SEPARATOR . $f . DIRECTORY_SEPARATOR . 'singolo_anno.mdb')) { $years[] = $f; } } sort($years, SORT_STRING); return $years; } private function parseGestioneYear(string $legacy, int $anno): array { $req = new Request(['legacy_code' => $legacy, 'anno' => $anno]); // Riusa logica gestioniYear senza HTTP overhead $resp = $this->gestioniYear($req); $data = $resp->getData(true); return $data; } private function upsertGestione(string $tenantId, int $stabileId, array $src, string $tipo, array $paths, ?int $numeroStra = null): int { $anno = (int)($src['anno'] ?? $src['anno_gestione'] ?? 0); if (!$anno) return 0; $q = \App\Models\GestioneContabile::where('tenant_id', $tenantId) ->where('stabile_id', $stabileId) ->where('anno_gestione', $anno) ->where('tipo_gestione', $tipo); if ($tipo === 'straordinaria' && $numeroStra) { $q->where('numero_straordinaria', $numeroStra); } $existing = $q->first(); $mesiOrd = null; $mesiRis = null; $mesiStra = null; if ($tipo === 'ordinaria' && !empty($src['rate'])) { $mesiOrd = array_map('intval', array_keys($src['rate'])); } if ($tipo === 'riscaldamento' && !empty($src['rate'])) { $mesiRis = array_map('intval', array_keys($src['rate'])); } if ($tipo === 'straordinaria' && !empty($src['piano']['rate_totali'])) { $mesiStra = []; } $payload = [ 'tenant_id' => $tenantId, 'stabile_id' => $stabileId, 'anno_gestione' => $anno, 'tipo_gestione' => $tipo, 'numero_straordinaria' => $tipo === 'straordinaria' ? $numeroStra : null, 'denominazione' => ucfirst($tipo) . ' ' . $anno . ($numeroStra ? ' #' . $numeroStra : ''), 'stato' => $src['stato'] ?? 'aperta', 'gestione_attiva' => true, 'mesi_rate_ordinaria' => $mesiOrd ?? [], 'mesi_rate_riscaldamento' => $mesiRis ?? [], 'mesi_rate_straordinaria' => $mesiStra ?? [], 'codice_archivio_legacy' => $paths['path_singolo_anno'] ?? null, 'piano_straordinario' => $tipo === 'straordinaria' ? ($src['piano'] ?? null) : null, ]; if ($existing) { $existing->update($payload); return 0; // updated } \App\Models\GestioneContabile::create($payload); return 1; // created } /** * Salva impostazioni base per GESCON Import (percorso e connessione) */ public function saveConfig(Request $request) { // Solo chi ha il permesso di configurazione può modificare le impostazioni globali if (!$request->boolean('_mapping_only') && !(auth()->user() && auth()->user()->can('gescon-import.config'))) { return redirect()->route('admin.gescon-import.index')->with('error', 'Non hai i permessi per modificare la configurazione.'); } // Se arriva esclusivamente il mapping, salviamo su GesconImportMapping.meta if ($request->boolean('_mapping_only')) { $request->validate([ '_legacy_code' => 'nullable|string', 'mapping' => 'array', 'preset_key' => 'nullable|string', 'preset_meta' => 'nullable|string', 'sync_enabled' => 'nullable|in:0,1' ]); $legacyCode = $request->input('_legacy_code'); $mapping = GesconImportMapping::firstOrCreate( ['user_id' => auth()->id(), 'legacy_code' => $legacyCode], ['status' => 'pending'] ); $meta = $mapping->meta ?? []; if ($request->has('mapping')) { $meta['mapping'] = $request->input('mapping', []); } // Salva anche le sorgenti selezionate (stabili/generale/singolo_anno) if ($request->has('sources')) { $meta['sources'] = array_values(array_filter((array)$request->input('sources'))); } // Flag di sincronizzazione veloce dalla lista stabili if ($request->has('sync_enabled')) { $meta['sync_enabled'] = $request->input('sync_enabled') === '1'; } // Preset selezionato e metadati (incluso NORD weighting) $presetKey = $request->input('preset_key'); if (is_string($presetKey) && $presetKey !== '') { $meta['preset_key'] = $presetKey; } $presetMetaRaw = $request->input('preset_meta'); if (is_string($presetMetaRaw) && $presetMetaRaw !== '') { try { $presetMeta = json_decode($presetMetaRaw, true, 512, JSON_THROW_ON_ERROR); if (is_array($presetMeta)) { $meta['preset_meta'] = $presetMeta; } } catch (\Throwable $e) { // ignora JSON invalido } } $mapping->meta = $meta; $mapping->save(); // Replica come mapping globale se richiesto if ($request->boolean('replicate_global')) { $global = GesconImportMapping::firstOrCreate( ['user_id' => auth()->id(), 'legacy_code' => '*'], ['status' => 'pending'] ); $gmeta = $global->meta ?? []; $gmeta['mapping'] = $meta['mapping'] ?? []; if (!empty($meta['sources'])) $gmeta['sources'] = $meta['sources']; if (!empty($meta['preset_key'])) $gmeta['preset_key'] = $meta['preset_key']; if (!empty($meta['preset_meta'])) $gmeta['preset_meta'] = $meta['preset_meta']; $global->meta = $gmeta; $global->save(); } return redirect()->route('admin.gescon-import.index', ['legacy_code' => $legacyCode]) ->with('success', 'Mapping campi salvato.'); } $data = $request->validate([ 'gescon_source_path' => 'required|string', 'gescon_mdb_stabili' => 'required|string', 'gescon_mdb_index' => 'nullable|string', 'gescon_import_conn' => 'required|string', // Stabile template di riferimento per replica selezioni 'template_stabile_code' => 'sometimes|nullable|string|max:32', // Avanzato: configurazione sorgenti per discovery mapping 'gescon_source_conn' => 'sometimes|nullable|string', 'gescon_source_tables' => 'sometimes|nullable|string', // Nuove selezioni multi-archivio / tabelle / colonne 'selected_archives' => 'sometimes|array', 'selected_archives.*' => 'string', 'selected_tables' => 'sometimes|array', 'selected_tables.*' => 'string', 'selected_columns' => 'sometimes|array', 'selected_columns.*' => 'string', // Tabella di collegamento segment->tables/columns persistente 'mapping_sources' => 'sometimes|array', 'mapping_sources.*.segment' => 'required_with:mapping_sources|string|max:100', 'mapping_sources.*.tables' => 'sometimes|array', 'mapping_sources.*.tables.*' => 'string', 'mapping_sources.*.columns' => 'sometimes|array', 'mapping_sources.*.columns.*' => 'string', ]); // Normalize paths (trim hidden spaces) and ensure we persist the full absolute path if possible $normalize = function ($p) { $p = trim((string)$p); $p = preg_replace('/[\x00-\x1F\x7F\x{00A0}\x{2000}-\x{200B}\x{202F}\x{FEFF}]/u', '', $p); return $p; }; $srcPath = $normalize($data['gescon_source_path']); $mdbPath = $normalize($data['gescon_mdb_stabili']); $mdbIndexPath = isset($data['gescon_mdb_index']) ? $normalize($data['gescon_mdb_index']) : null; $srcReal = @realpath($srcPath) ?: $srcPath; $mdbReal = @realpath($mdbPath) ?: $mdbPath; $mdbIndexReal = $mdbIndexPath ? (@realpath($mdbIndexPath) ?: $mdbIndexPath) : null; foreach ( [ 'gescon.source_path' => $srcReal, 'gescon.mdb_stabili' => $mdbReal, // Persisti l'indice se fornito, altrimenti mantieni quello esistente 'gescon.mdb_index' => $mdbIndexReal ?: UserSetting::get('gescon.mdb_index', '/mnt/gescon-archives/gescon/cartellestabili_e_anni.mdb'), 'gescon.import_conn' => $data['gescon_import_conn'], ] as $k => $v ) { UserSetting::set($k, $v); } // Opzionale: salva connessione e tabelle sorgente avanzate if (array_key_exists('gescon_source_conn', $data)) { UserSetting::set('gescon.source_conn', $data['gescon_source_conn'] ?? ''); } if (array_key_exists('gescon_source_tables', $data)) { $raw = trim((string)($data['gescon_source_tables'] ?? '')); $val = $raw; if ($raw !== '') { $decoded = null; try { $decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (\Throwable $e) { $decoded = null; } if (is_array($decoded)) { $val = json_encode($decoded, JSON_UNESCAPED_UNICODE); } } UserSetting::set('gescon.source_tables', $val); } // Persisti nuove selezioni multi-livello se presenti if ($request->has('selected_archives')) { $archives = array_values(array_filter($request->input('selected_archives', []), fn($v) => is_string($v) && $v !== '')); UserSetting::set('gescon.selected_archives', $archives); } if ($request->has('selected_tables')) { $tables = array_values(array_filter($request->input('selected_tables', []), fn($v) => is_string($v) && $v !== '')); UserSetting::set('gescon.selected_tables', $tables); } if ($request->has('selected_columns')) { $cols = array_values(array_filter($request->input('selected_columns', []), fn($v) => is_string($v) && $v !== '')); // Per le colonne possiamo salvare come struttura raggruppata table:col UserSetting::set('gescon.selected_columns', $cols); } if ($request->has('mapping_sources')) { $rawSources = $request->input('mapping_sources', []); $norm = []; foreach ($rawSources as $row) { if (!is_array($row)) continue; $seg = trim((string)($row['segment'] ?? '')); if ($seg === '') continue; $tables = array_values(array_filter(is_array($row['tables'] ?? null) ? $row['tables'] : [])); $columns = array_values(array_filter(is_array($row['columns'] ?? null) ? $row['columns'] : [])); $norm[] = ['segment' => $seg, 'tables' => $tables, 'columns' => $columns]; } UserSetting::set('gescon.mapping_sources', $norm); } // Persisti codice stabile template e uno snapshot delle selezioni correnti se richiesto if ($request->filled('template_stabile_code')) { $tpl = trim($request->input('template_stabile_code')); UserSetting::set('gescon.template_stabile_code', $tpl); // Salva snapshot per rapida replica futura $snapshot = [ 'archives' => UserSetting::get('gescon.selected_archives', []), 'tables' => UserSetting::get('gescon.selected_tables', []), 'columns' => UserSetting::get('gescon.selected_columns', []), 'mapping_sources' => UserSetting::get('gescon.mapping_sources', []), ]; UserSetting::set('gescon.template_stabile_sources', $snapshot); } return redirect()->route('admin.gescon-import.config') ->with('success', 'Impostazioni GESCON salvate.'); } /** * Salvataggio mapping dedicato (senza permesso di configurazione globale) */ public function saveMapping(Request $request) { $request->validate([ '_legacy_code' => 'nullable|string', 'mapping' => 'array', 'manual_target_values' => 'sometimes|array', 'manual_target_values.*' => 'nullable|string', 'preset_key' => 'nullable|string', 'preset_meta' => 'nullable|string', 'sync_enabled' => 'nullable|in:0,1', // Allow both JSON string and array payloads 'banche_mapping' => 'nullable', 'tabelle_mapping' => 'nullable|string', 'voci_mapping' => 'nullable|string', 'voci_field_mapping' => 'nullable|string', 'unita_mapping' => 'nullable|string', 'unita_field_mapping' => 'nullable|string', 'tabelle_field_mapping' => 'nullable|string', 'relation_mapping' => 'nullable|string', 'nord_factor' => 'nullable|integer|min:1|max:1000', 'target_admin_id' => 'sometimes|nullable|integer|exists:amministratori,id', 'stabile_banca_predefinita' => 'sometimes|nullable|string|max:50', 'palazzine_config' => 'nullable', 'unita_preset_key' => 'sometimes|nullable|string|max:100', ]); $legacyCode = $request->input('_legacy_code'); $mapping = GesconImportMapping::firstOrCreate( ['user_id' => auth()->id(), 'legacy_code' => $legacyCode], ['status' => 'pending'] ); $meta = $mapping->meta ?? []; if ($request->has('mapping')) { $meta['mapping'] = $request->input('mapping', []); } if ($request->has('manual_target_values')) { $rawManual = $request->input('manual_target_values'); if (is_array($rawManual)) { $normalizedManual = []; foreach ($rawManual as $targetKey => $value) { if (!is_string($targetKey)) { continue; } $targetKey = trim($targetKey); if ($targetKey === '') { continue; } if (is_array($value)) { $value = $value['value'] ?? null; } if ($value === null) { continue; } if (is_string($value)) { $value = trim($value); } if ($value === '') { continue; } $normalizedManual[$targetKey] = is_scalar($value) ? (string) $value : json_encode($value); } if (!empty($normalizedManual)) { $meta['manual_target_values'] = $normalizedManual; } else { unset($meta['manual_target_values']); } } } else { unset($meta['manual_target_values']); } // Relation mapping (generic cross-entity rules) if ($request->filled('relation_mapping')) { try { $rel = json_decode($request->string('relation_mapping'), true, 512, JSON_THROW_ON_ERROR); if (is_array($rel)) { // keep only safe keys per rule $norm = []; foreach ($rel as $rule) { if (!is_array($rule)) continue; $segment = isset($rule['segment']) ? (string)$rule['segment'] : ''; $legacyField = isset($rule['legacy_field']) ? (string)$rule['legacy_field'] : ''; $targetTable = isset($rule['target_table']) ? (string)$rule['target_table'] : ''; $assignTo = isset($rule['assign_to']) ? (string)$rule['assign_to'] : ''; $match = is_array($rule['match'] ?? null) ? $rule['match'] : []; if ($segment === '' || $legacyField === '' || $targetTable === '' || $assignTo === '') continue; $by = in_array(($match['by'] ?? 'field'), ['field', 'composite'], true) ? $match['by'] : 'field'; $rules = is_array($match['rules'] ?? null) ? $match['rules'] : []; $onMiss = in_array(($match['on_miss'] ?? 'log'), ['log', 'ignore', 'create'], true) ? $match['on_miss'] : 'log'; $norm[] = [ 'segment' => $segment, 'legacy_field' => $legacyField, 'target_table' => $targetTable, 'assign_to' => $assignTo, 'match' => [ 'by' => $by, 'rules' => array_values(array_map(function ($r) { return [ 'table' => isset($r['table']) ? (string)$r['table'] : null, 'field' => isset($r['field']) ? (string)$r['field'] : null, 'source' => isset($r['source']) ? (string)$r['source'] : 'legacy', ]; }, $rules)), 'on_miss' => $onMiss, ], ]; } $meta['relation_mapping'] = $norm; } } catch (\Throwable $e) { /* ignore */ } } if ($request->has('sources')) { $meta['sources'] = array_values(array_filter((array)$request->input('sources'))); } // Persisti amministratore target per questa mappatura if ($request->has('target_admin_id')) { $tid = $request->input('target_admin_id'); $meta['target_admin_id'] = $tid ? (int)$tid : null; } // Persisti banca/cassa predefinita selezionata nello step Stabili if ($request->has('stabile_banca_predefinita')) { $val = trim((string)$request->input('stabile_banca_predefinita')); $meta['stabile_banca_predefinita'] = $val !== '' ? $val : null; } // Mappatura multi-banca: accetta sia JSON string che array strutturato if ($request->has('banche_mapping')) { $raw = $request->input('banche_mapping'); $bm = []; if (is_string($raw)) { try { $decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); if (is_array($decoded)) { $bm = $decoded; } } catch (\Throwable $e) { $bm = []; } } elseif (is_array($raw)) { $bm = $raw; } if (is_array($bm)) { $norm = []; foreach ($bm as $item) { if (!is_array($item)) { continue; } $legacy = []; $target = []; if (isset($item['legacy']) && is_array($item['legacy'])) { $legacy = $item['legacy']; } if (isset($item['target']) && is_array($item['target'])) { $target = $item['target']; } // Supporta formato piatto precedente (legacy_nome_banca, nome_banca, ...) foreach ([ 'nome_banca', 'iban', 'swift', 'conto', 'intestatario', 'cod_cassa', 'descrizione', ] as $field) { $legacyKey = 'legacy_' . $field; if (!array_key_exists($field, $legacy) && array_key_exists($legacyKey, $item)) { $legacy[$field] = $item[$legacyKey]; } if (!array_key_exists($field, $target) && array_key_exists($field, $item)) { $target[$field] = $item[$field]; } } if (!array_key_exists('codice_cassa', $target) && array_key_exists('target_cod_cassa', $item)) { $target['codice_cassa'] = $item['target_cod_cassa']; } if (!array_key_exists('is_nostro_conto', $target) && array_key_exists('target_is_nostro_conto', $item)) { $target['is_nostro_conto'] = (bool) $item['target_is_nostro_conto']; } // Normalizza stringhe $legacy = array_filter(array_map(function ($value) { if (is_string($value)) { $value = trim($value); } return ($value === '' ? null : $value); }, $legacy), fn($v) => $v !== null); $target = array_filter(array_map(function ($value) { if (is_string($value)) { $value = trim($value); } return ($value === '' ? null : $value); }, $target), fn($v) => $v !== null); if (empty($legacy) && empty($target)) { continue; // riga vuota } $codiceLegacy = $item['codice_legacy'] ?? ($target['codice_cassa'] ?? null) ?? ($legacy['cod_cassa'] ?? null) ?? ($legacy['conto'] ?? null); $codiceLegacy = $codiceLegacy ? strtoupper((string) $codiceLegacy) : null; $descrLegacy = $item['descrizione_legacy'] ?? ($legacy['descrizione'] ?? null) ?? ($legacy['nome_banca'] ?? null); $entry = [ 'legacy' => $legacy ?: null, 'target' => $target ?: null, 'codice_legacy' => $codiceLegacy, 'descrizione_legacy' => $descrLegacy, 'tipo_conto' => $item['tipo_conto'] ?? ($target['tipo_conto'] ?? null), 'iban' => $item['iban'] ?? ($legacy['iban'] ?? null), 'swift' => $item['swift'] ?? ($legacy['swift'] ?? null), ]; if (isset($target['is_nostro_conto'])) { $entry['is_nostro_conto'] = (bool) $target['is_nostro_conto']; } elseif (isset($item['is_nostro_conto'])) { $entry['is_nostro_conto'] = (bool) $item['is_nostro_conto']; } // Espone campi legacy per UI (compatibilità con vecchio formato) foreach ([ 'nome_banca', 'iban', 'swift', 'conto', 'intestatario', 'cod_cassa', 'descrizione' ] as $field) { if (isset($legacy[$field]) && !isset($entry['legacy_' . $field])) { $entry['legacy_' . $field] = $legacy[$field]; } } // Salva mapping target per UI (senza sovrascrivere eventuali dati reali già forniti) foreach ([ 'nome_banca', 'iban', 'swift', 'conto', 'intestatario', 'cod_cassa' ] as $field) { $targetValue = $target[$field] ?? null; if ($targetValue !== null) { $targetKey = $field === 'cod_cassa' ? 'target_' . $field . '_select' : 'target_' . $field; $entry[$targetKey] = $targetValue; if (!array_key_exists($field, $entry) || $entry[$field] === null) { $entry[$field] = $targetValue; } } } if (isset($target['codice_cassa'])) { $entry['target_cod_cassa'] = $target['codice_cassa']; if (!array_key_exists('codice_cassa', $entry) || $entry['codice_cassa'] === null) { $entry['codice_cassa'] = $target['codice_cassa']; } } $norm[] = array_filter($entry, fn($v) => $v !== null); } $meta['banche_mapping'] = array_values($norm); } } // Tabelle millesimali + NORD factor if ($request->filled('tabelle_mapping')) { try { $tm = json_decode($request->string('tabelle_mapping'), true, 512, JSON_THROW_ON_ERROR); if (is_array($tm)) { $meta['tabelle_mapping'] = $tm; } } catch (\Throwable $e) { /* ignore */ } } if ($request->has('nord_factor')) { $nf = (int) $request->input('nord_factor'); if ($nf > 0) $meta['nord_factor'] = $nf; } // Voci (piano dei conti) if ($request->filled('voci_mapping')) { try { $vm = json_decode($request->string('voci_mapping'), true, 512, JSON_THROW_ON_ERROR); if (is_array($vm)) { $meta['voci_mapping'] = $vm; } } catch (\Throwable $e) { /* ignore */ } } // Mapping campi Voci (legacy field -> target field) if ($request->filled('voci_field_mapping')) { try { $vfm = json_decode($request->string('voci_field_mapping'), true, 512, JSON_THROW_ON_ERROR); if (is_array($vfm)) { $norm = []; foreach ($vfm as $row) { if (!is_array($row)) continue; $legacyF = isset($row['legacy']) ? trim((string)$row['legacy']) : ''; $targetF = isset($row['target']) ? trim((string)$row['target']) : ''; if ($legacyF === '') continue; $norm[] = ['legacy' => $legacyF, 'target' => $targetF !== '' ? $targetF : null]; } $meta['voci_field_mapping'] = $norm; } } catch (\Throwable $e) { /* ignore */ } } // Unità / Anagrafiche if ($request->filled('unita_mapping')) { try { $um = json_decode($request->string('unita_mapping'), true, 512, JSON_THROW_ON_ERROR); if (is_array($um)) { $meta['unita_mapping'] = $um; } } catch (\Throwable $e) { /* ignore */ } } // Mapping campi Unità (legacy field -> target field) if ($request->filled('unita_field_mapping')) { try { $ufm = json_decode($request->string('unita_field_mapping'), true, 512, JSON_THROW_ON_ERROR); if (is_array($ufm)) { // Normalizza struttura: [{legacy:"cod_cond", target:"codice_unita"}, ...] $norm = []; foreach ($ufm as $row) { if (!is_array($row)) continue; $legacyF = isset($row['legacy']) ? trim((string)$row['legacy']) : ''; $targetF = isset($row['target']) ? trim((string)$row['target']) : ''; if ($legacyF === '') continue; $norm[] = [ 'legacy' => $legacyF, 'target' => $targetF !== '' ? $targetF : null, ]; } $meta['unita_field_mapping'] = $norm; } } catch (\Throwable $e) { /* ignore */ } } // Palazzine config (JSON string or array). Shape example: [{name:'Pal.1', scales:['A','B','C']}, ...] if ($request->has('palazzine_config')) { $raw = $request->input('palazzine_config'); $cfg = []; if (is_string($raw)) { try { $dec = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); if (is_array($dec)) $cfg = $dec; } catch (\Throwable $e) { $cfg = []; } } elseif (is_array($raw)) { $cfg = $raw; } if (is_array($cfg)) { // Keep only safe keys $norm = []; foreach ($cfg as $item) { if (!is_array($item)) continue; $name = isset($item['name']) ? trim((string)$item['name']) : ''; $scales = array_values(array_filter(array_map('strval', (array)($item['scales'] ?? [])))); if ($name === '' || empty($scales)) continue; $norm[] = ['name' => $name, 'scales' => $scales]; } $meta['palazzine_config'] = $norm; } } // Optional: persist selected preset key for unità mapping if ($request->filled('unita_preset_key')) { $meta['unita_preset_key'] = (string) $request->input('unita_preset_key'); } // Mapping campi Tabelle Millesimali (legacy field -> target field) if ($request->filled('tabelle_field_mapping')) { try { $tfm = json_decode($request->string('tabelle_field_mapping'), true, 512, JSON_THROW_ON_ERROR); if (is_array($tfm)) { $norm = []; foreach ($tfm as $row) { if (!is_array($row)) continue; $legacyF = isset($row['legacy']) ? trim((string)$row['legacy']) : ''; $targetF = isset($row['target']) ? trim((string)$row['target']) : ''; if ($legacyF === '') continue; $norm[] = [ 'legacy' => $legacyF, 'target' => $targetF !== '' ? $targetF : null, ]; } $meta['tabelle_field_mapping'] = $norm; } } catch (\Throwable $e) { /* ignore */ } } if ($request->has('sync_enabled')) { $meta['sync_enabled'] = $request->input('sync_enabled') === '1'; } $presetKey = $request->input('preset_key'); if (is_string($presetKey) && $presetKey !== '') { $meta['preset_key'] = $presetKey; } $presetMetaRaw = $request->input('preset_meta'); if (is_string($presetMetaRaw) && $presetMetaRaw !== '') { try { $presetMeta = json_decode($presetMetaRaw, true, 512, JSON_THROW_ON_ERROR); if (is_array($presetMeta)) { $meta['preset_meta'] = $presetMeta; } } catch (\Throwable $e) { // ignora JSON invalido } } $mapping->meta = $meta; $mapping->save(); if ($request->boolean('replicate_global')) { $global = GesconImportMapping::firstOrCreate( ['user_id' => auth()->id(), 'legacy_code' => '*'], ['status' => 'pending'] ); $gmeta = $global->meta ?? []; $gmeta['mapping'] = $meta['mapping'] ?? []; if (!empty($meta['sources'])) $gmeta['sources'] = $meta['sources']; if (!empty($meta['preset_key'])) $gmeta['preset_key'] = $meta['preset_key']; if (!empty($meta['preset_meta'])) $gmeta['preset_meta'] = $meta['preset_meta']; if (array_key_exists('manual_target_values', $meta)) { $gmeta['manual_target_values'] = $meta['manual_target_values']; } else { unset($gmeta['manual_target_values']); } $global->meta = $gmeta; $global->save(); } return redirect()->route('admin.gescon-import.index', ['legacy_code' => $legacyCode]) ->with('success', 'Mapping campi salvato.'); } /** * Aggiornamento rapido del CF Amministratore sullo stabile corrente (usato nel mapping header) */ public function quickUpdateStabileAdminCf(Request $request, $stabileId) { $this->authorize('gescon-import.execute'); $cf = (string)($request->input('cod_fisc_amministratore', '')); try { $ok = DB::table('stabili') ->where('id', (int)$stabileId) ->update([ 'cod_fisc_amministratore' => $cf, 'updated_at' => now(), ]); return response()->json(['ok' => true, 'updated' => (bool)$ok]); } catch (\Throwable $e) { return response()->json(['ok' => false, 'error' => $e->getMessage()], 500); } } /** * Import rapido "micro" per uno stabile (solo anagrafica + mapping campi stabile.*) senza pipeline estesa. */ public function microImportStabile(Request $request) { $data = $request->validate([ 'legacy_code' => 'required|string', 'amministratore_id' => 'sometimes|integer|exists:amministratori,id' ]); $legacy = $data['legacy_code']; // Priorità: parametro esplicito > mapping target_admin_id > amministratore corrente if (isset($data['amministratore_id'])) { $amministratore = \App\Models\Amministratore::find($data['amministratore_id']); } else { $mappingRec = GesconImportMapping::where('user_id', auth()->id()) ->where('legacy_code', $legacy) ->first(); $targetAdminId = (int) data_get($mappingRec, 'meta.target_admin_id'); $amministratore = $targetAdminId ? \App\Models\Amministratore::find($targetAdminId) : null; if (!$amministratore) { $amministratore = $this->getAmministratoreCorrente(); } } if (!$amministratore) return response()->json(['ok' => false, 'error' => 'Amministratore non trovato'], 404); $mappingRec = GesconImportMapping::where('user_id', auth()->id())->where('legacy_code', $legacy)->first(); $map = (array) data_get($mappingRec, 'meta.mapping', []); $svc = new \App\Services\GesconImport\Micro\StabiliMicroImporter(); $res = $svc->import($legacy, $amministratore, $map); return response()->json(['ok' => true, 'result' => $res]); } /** * Aggiorna rapidamente il percorso di Stabili.mdb (dal picker inline) e ricarica anteprima */ public function setMdbPath(Request $request) { $request->validate(['mdb_path' => 'required|string']); $path = $request->string('mdb_path')->toString(); if (!is_file($path)) { return redirect()->back()->with('error', 'File non trovato: ' . $path); } if (!is_readable($path)) { $msg = 'Il file esiste ma non è leggibile dal server web: ' . $path; if (str_starts_with($path, '/home/')) { $msg .= ' (attenzione: percorsi in /home potrebbero non essere accessibili a www-data)'; } return redirect()->back()->with('error', $msg); } UserSetting::set('gescon.mdb_stabili', $path); return redirect()->route('admin.gescon-import.index', ['refresh' => 1, '#stabili'])->with('success', 'Percorso MDB aggiornato.'); } /** * Browser minimale lato server per selezionare directory/file su Linux (solo lettura). */ public function browse(Request $request) { $base = UserSetting::get('gescon.source_path', '/mnt/gescon-archives/gescon'); $path = $request->query('path', $base); // Hard cap: consenti solo all'interno di alcune radici note per sicurezza $allowedRoots = [ '/home', '/var/www', '/mnt', dirname($base) ]; $isAllowed = false; foreach ($allowedRoots as $root) { if (str_starts_with(realpath($path) ?: '', realpath($root) ?: '')) { $isAllowed = true; break; } } if (!$isAllowed) { return Response::json(['error' => 'Percorso non consentito'], 403); } $entries = []; try { if (is_dir($path)) { $dh = opendir($path); if ($dh) { while (($file = readdir($dh)) !== false) { if ($file === '.' || $file === '..') continue; $full = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file; $entries[] = [ 'name' => $file, 'path' => $full, 'is_dir' => is_dir($full), ]; } closedir($dh); } // Ordina directory prima, poi file usort($entries, function ($a, $b) { if ($a['is_dir'] === $b['is_dir']) return strnatcasecmp($a['name'], $b['name']); return $a['is_dir'] ? -1 : 1; }); } } catch (\Throwable $e) { return Response::json(['error' => 'Impossibile leggere la directory'], 500); } return Response::json([ 'path' => $path, 'entries' => $entries, ]); } /** * Scansiona l'MDB indice master cartellestabili_e_anni.mdb per elencare stabili e anni disponibili. * Restituisce un array di elementi: legacy_code, denominazione, years_count (e opzionale years[]). */ private function scanStabiliFromMasterIndex(string $indexMdbPath, string $legacyBasePath): 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($indexMdbPath) || !is_readable($indexMdbPath)) return []; // Individua una tabella plausibile (cartelle / stabili / anni) $tableGuess = null; $tables = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($indexMdbPath))) ?: ''; if ($tables) { foreach (preg_split('/\r?\n/', trim($tables)) as $t) { if ($t === '') continue; if (preg_match('/(cartelle|stabil|anni)/i', $t)) { $tableGuess = trim($t); break; } } } if (!$tableGuess) { // Fallback: usa la prima tabella $lines = array_filter(preg_split('/\r?\n/', trim($tables))); $tableGuess = $lines ? trim(array_values($lines)[0]) : null; } if (!$tableGuess) 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', escapeshellarg($binExport), escapeshellarg('%Y-%m-%d'), escapeshellarg('|'), escapeshellarg('"'), escapeshellarg($indexMdbPath), escapeshellarg($tableGuess), escapeshellarg($tmp) ); @shell_exec($cmd); if (!is_file($tmp) || filesize($tmp) === 0) { @unlink($tmp); return []; } $fh = fopen($tmp, 'r'); if (!$fh) { @unlink($tmp); return []; } $headers = fgetcsv($fh, 0, '|'); if (!$headers) { fclose($fh); @unlink($tmp); return []; } $headers = array_map(fn($h) => strtolower(trim((string)$h)), $headers); $byCodeYears = []; $byCodeName = []; while (($cols = fgetcsv($fh, 0, '|')) !== false) { if ($cols === [null]) continue; $cols = array_map(fn($v) => is_string($v) ? trim($v) : $v, $cols); if (count($cols) < count($headers)) $cols = array_pad($cols, count($headers), null); if (count($cols) > count($headers)) $cols = array_slice($cols, 0, count($headers)); $row = @array_combine($headers, $cols) ?: []; if (!$row) continue; // Estrapola codice, denominazione e anno da possibili colonne $code = $row['nome_directory'] ?? $row['cod_stabile'] ?? $row['internet_cod_stab'] ?? $row['codice'] ?? null; $denom = $row['denominazione'] ?? $row['fe_denominazione'] ?? null; $anno = $row['anno'] ?? $row['year'] ?? null; if (!$code) continue; $code = (string)$code; if (!isset($byCodeYears[$code])) $byCodeYears[$code] = []; if ($anno && preg_match('/^\d{4}$/', (string)$anno)) { $byCodeYears[$code][(string)$anno] = true; } if ($denom) $byCodeName[$code] = (string)$denom; } fclose($fh); @unlink($tmp); // Costruisci elementi $items = []; foreach ($byCodeYears as $code => $yearsSet) { $den = $byCodeName[$code] ?? null; $years = array_keys($yearsSet); sort($years, SORT_STRING); $items[] = [ 'legacy_code' => $code, 'denominazione' => $den, 'years_count' => count($years), 'years' => $years, ]; } // Se non sono stati letti anni, prova ad arricchire dal filesystem if (empty($items)) { $items = $this->scanLegacyCodesFromDir($legacyBasePath); $items = $this->enrichLegacyWithArchives($items, $legacyBasePath); } // Ordina per codice naturale usort($items, function ($a, $b) { return strnatcasecmp((string)($a['legacy_code'] ?? ''), (string)($b['legacy_code'] ?? '')); }); return $items; } /** * Esegue una scansione dello Stabili.mdb per mostrare elenco stabili legacy (codice e denominazione) */ private function scanStabiliFromMdb(string $mdbPath): array { // Risolvi binari mdb-tools con percorso assoluto per ambienti web (www-data) $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'; // Usa export temporaneo con delimitatore sicuro e parsing robusto $tableGuess = 'Stabili'; $tables = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: ''; if ($tables) { foreach (preg_split('/\r?\n/', trim($tables)) as $t) { if ($t === '') continue; if (preg_match('/stabil/i', $t)) { $tableGuess = trim($t); break; } } } $tmp = tempnam(sys_get_temp_dir(), 'mdbcsv_'); $cmd = sprintf( '%s -D %s -d %s -q %s -R "\\n" %s %s > %s 2>/dev/null', escapeshellarg($binExport), escapeshellarg('%Y-%m-%d'), escapeshellarg('|'), escapeshellarg('"'), escapeshellarg($mdbPath), escapeshellarg($tableGuess), escapeshellarg($tmp) ); @shell_exec($cmd); if (!is_file($tmp) || filesize($tmp) === 0) { @unlink($tmp); return []; } $fh = fopen($tmp, 'r'); if (!$fh) { @unlink($tmp); return []; } $headers = fgetcsv($fh, 0, '|'); if (!$headers) { fclose($fh); @unlink($tmp); return []; } $headers = array_map(fn($h) => strtolower(trim((string)$h)), $headers); $items = []; while (($cols = fgetcsv($fh, 0, '|')) !== false) { if ($cols === [null] || $cols === false) continue; // Allinea colonne a headers $cols = array_map(fn($v) => is_string($v) ? trim($v) : $v, $cols); $countH = count($headers); 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; $legacyCode = $row['nome_directory'] ?? ($row['internet_cod_stab'] ?? ($row['cod_stabile'] ?? null)); $denom = $row['denominazione'] ?? ($row['fe_denominazione'] ?? null); if ($legacyCode) { $items[] = [ 'legacy_code' => trim((string)$legacyCode), 'denominazione' => $denom ? trim((string)$denom) : null, ]; } } fclose($fh); @unlink($tmp); // Dedup per legacy_code $byCode = []; foreach ($items as $it) { if (!empty($it['legacy_code'])) $byCode[$it['legacy_code']] = $it; } ksort($byCode, SORT_NATURAL); return array_values($byCode); } /** * Ritorna un record di esempio da Stabili.mdb come array associativo leggibile dal mapping. * Se $legacyCode è impostato prova a cercare la riga corrispondente; altrimenti usa l'indice $index. */ private function sampleStabileFromMdb(string $mdbPath, ?string $legacyCode, int $index = 0): 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'; $tableGuess = 'Stabili'; $tables = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: ''; if ($tables) { foreach (preg_split('/\r?\n/', trim($tables)) as $t) { if ($t === '') continue; if (preg_match('/stabil/i', $t)) { $tableGuess = trim($t); break; } } } $tmp = tempnam(sys_get_temp_dir(), 'mdbcsv_'); $cmd = sprintf( '%s -D %s -d %s -q %s -R "\\n" %s %s > %s 2>/dev/null', escapeshellarg($binExport), escapeshellarg('%Y-%m-%d'), escapeshellarg('|'), escapeshellarg('"'), escapeshellarg($mdbPath), escapeshellarg($tableGuess), escapeshellarg($tmp) ); @shell_exec($cmd); if (!is_file($tmp) || filesize($tmp) === 0) { @unlink($tmp); return []; } $fh = fopen($tmp, 'r'); if (!$fh) { @unlink($tmp); return []; } $headers = fgetcsv($fh, 0, '|'); if (!$headers) { fclose($fh); @unlink($tmp); return []; } $headers = array_map(fn($h) => strtolower(trim((string)$h)), $headers); $rowData = []; $i = 0; while (($cols = fgetcsv($fh, 0, '|')) !== false) { if ($cols === [null]) continue; $cols = array_map(fn($v) => is_string($v) ? trim($v) : $v, $cols); $countH = count($headers); 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) ?: []; $legacyKey = $row['nome_directory'] ?? ($row['internet_cod_stab'] ?? ($row['cod_stabile'] ?? null)); if ($legacyCode) { if ((string)$legacyKey === (string)$legacyCode) { $rowData = $row; break; } } else { if ($i++ >= $index) { $rowData = $row; break; } } } fclose($fh); @unlink($tmp); if (empty($rowData)) return []; // Normalizza: aggiungi legacy_key e mantieni chiavi così come appaiono per il mapping $rowData['legacy_key'] = (string)($rowData['nome_directory'] ?? ($rowData['internet_cod_stab'] ?? ($rowData['cod_stabile'] ?? ''))); return $rowData; } /** * Fallback: costruisce una lista codici legacy a partire dalle directory presenti nella sorgente. */ private function scanLegacyCodesFromDir(string $basePath): array { $items = []; if (!is_dir($basePath)) return $items; try { $dh = opendir($basePath); if ($dh) { while (($file = readdir($dh)) !== false) { if ($file === '.' || $file === '..') continue; $full = rtrim($basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file; if (!is_dir($full)) continue; // Estrarre un codice numerico dalla cartella (inizio o in qualunque punto) $code = null; if (preg_match('/^(\d{2,5})/', $file, $m)) { $code = $m[1]; } elseif (preg_match('/(\d{2,5})/', $file, $m)) { $code = $m[1]; } // Se non c'è un numero nel nome, verifica la presenza di file attesi $hasGenerale = is_file($full . DIRECTORY_SEPARATOR . 'generale_stabile.mdb'); $hasYears = false; if (is_dir($full)) { try { $sub = opendir($full); if ($sub) { while (($yf = readdir($sub)) !== false) { if ($yf === '.' || $yf === '..') continue; if (!preg_match('/^\d{4}$/', $yf)) continue; $ya = $full . DIRECTORY_SEPARATOR . $yf . DIRECTORY_SEPARATOR . 'singolo_anno.mdb'; if (is_file($ya)) { $hasYears = true; break; } } closedir($sub); } } catch (\Throwable $e) { } } if ($code || $hasGenerale || $hasYears) { $items[] = [ 'legacy_code' => $code ?: $file, 'denominazione' => null, ]; } } closedir($dh); } } catch (\Throwable $e) { // ignora } // Dedup per codice e ordina naturalmente $byCode = []; foreach ($items as $it) { $c = (string)($it['legacy_code'] ?? ''); if ($c !== '') { $byCode[$c] = $it; } } uksort($byCode, 'strnatcasecmp'); return array_values($byCode); } /** * Enrich: per ogni stabile verifica se esistono i due archivi (generale_stabile.mdb e uno o più singolo_anno.mdb) * Restituisce gli stessi elementi con chiavi aggiuntive: has_generale(bool), years_count(int). */ private function enrichLegacyWithArchives(array $items, string $basePath): array { $enriched = []; foreach ($items as $it) { $code = trim((string)($it['legacy_code'] ?? '')); if ($code === '') { $enriched[] = $it; continue; } $dir = rtrim($basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $code; $hasGenerale = is_file($dir . DIRECTORY_SEPARATOR . 'generale_stabile.mdb'); $yearsCount = 0; if (is_dir($dir)) { // conta sottodirectory anno che contengono singolo_anno.mdb try { $dh = opendir($dir); if ($dh) { while (($f = readdir($dh)) !== false) { if ($f === '.' || $f === '..') continue; if (!preg_match('/^\d{4}$/', $f)) continue; $sub = $dir . DIRECTORY_SEPARATOR . $f . DIRECTORY_SEPARATOR . 'singolo_anno.mdb'; if (is_file($sub)) $yearsCount++; } closedir($dh); } } catch (\Throwable $e) { } } $it['has_generale'] = $hasGenerale; $it['years_count'] = $yearsCount; $enriched[] = $it; } return $enriched; } /** * Ottiene l'amministratore corrente dall'utente loggato */ protected function canSwitchAmministratore(): bool { $user = Auth::user(); if (!$user) { return false; } return $user->hasRole('super-admin') || $user->can('gescon-import.config'); } protected function resolveSelectedAmministratore(): ?Amministratore { if (!$this->canSwitchAmministratore()) { return null; } $request = request(); $sessionKey = 'gescon_import.selected_admin_id'; if ($request && $request->has('amministratore_id')) { $id = (int) $request->input('amministratore_id'); if ($id > 0) { $model = Amministratore::find($id); if ($model) { session([$sessionKey => $model->id]); return $model; } } session()->forget($sessionKey); return null; } $selectedId = session($sessionKey); if ($selectedId) { return Amministratore::find($selectedId); } return null; } protected function ensureAmministratoreArchive(?Amministratore $amministratore): ?Amministratore { if ($amministratore && method_exists($amministratore, 'archiveExists') && !$amministratore->archiveExists()) { if (method_exists($amministratore, 'createFolderStructure')) { $amministratore->createFolderStructure(); } } return $amministratore; } protected function getAmministratoreCorrente() { $user = Auth::user(); if (!$user) { return null; } if ($this->canSwitchAmministratore()) { $selected = $this->resolveSelectedAmministratore(); if ($selected) { return $this->ensureAmministratoreArchive($selected); } $fallback = Amministratore::orderBy('denominazione_studio') ->orderBy('nome') ->orderBy('cognome') ->first(); return $this->ensureAmministratoreArchive($fallback); } $amministratore = Amministratore::where('user_id', $user->id)->first(); if (!$amministratore && $user->hasRole('amministratore')) { // Prova a separare nome e cognome dal campo name dell'utente $parts = preg_split('/\s+/', (string)$user->name, -1, PREG_SPLIT_NO_EMPTY) ?: []; $cognome = count($parts) > 1 ? array_pop($parts) : 'N/D'; $nome = count($parts) > 0 ? implode(' ', $parts) : (string)$user->name; $amministratore = Amministratore::create([ 'user_id' => $user->id, 'nome' => $nome, 'cognome' => $cognome, ]); } return $this->ensureAmministratoreArchive($amministratore); } /** * Calcola statistiche sui dati importati */ protected function getStatisticheImportate($amministratoreId) { // ID stabili per l'amministratore $stabiliQuery = Stabile::where('amministratore_id', $amministratoreId); $stabiliIds = (clone $stabiliQuery)->pluck('id'); // Conteggi $stabiliTotali = (clone $stabiliQuery)->count(); $stabiliGescon = (clone $stabiliQuery)->where('note', 'like', '%GESCON%')->count(); $stabiliAttivi = (clone $stabiliQuery)->where('attivo', true)->count(); $unitaTotali = UnitaImmobiliare::whereIn('stabile_id', $stabiliIds)->count(); $unitaConMillesimi = UnitaImmobiliare::whereIn('stabile_id', $stabiliIds) ->where(function ($q) { $q->whereNotNull('millesimi_generali')->where('millesimi_generali', '>', 0) ->orWhereNotNull('millesimi_riscaldamento')->where('millesimi_riscaldamento', '>', 0) ->orWhereNotNull('millesimi_ascensore')->where('millesimi_ascensore', '>', 0) ->orWhereNotNull('millesimi_scale')->where('millesimi_scale', '>', 0); })->count(); $fornitoriTotali = Fornitore::where('amministratore_id', $amministratoreId)->count(); // Non abbiamo un campo "attivo" sui fornitori: consideriamo tutti attivi per ora $fornitoriAttivi = $fornitoriTotali; $condominiTotali = Soggetto::count(); $soggettiProprietari = Soggetto::where('tipo', 'proprietario')->count(); $soggettiInquilini = Soggetto::where('tipo', 'inquilino')->count(); $ultimoImport = (clone $stabiliQuery) ->where('note', 'like', '%GESCON%') ->latest('created_at') ->value('created_at'); // Struttura attesa dalla vista (nidificata) return [ 'stabili' => [ 'totali' => $stabiliGescon ?: $stabiliTotali, 'attivi' => $stabiliAttivi, ], 'unita' => [ 'totali' => $unitaTotali, 'con_millesimi' => $unitaConMillesimi, ], 'soggetti' => [ 'totali' => $condominiTotali, 'proprietari' => $soggettiProprietari, 'inquilini' => $soggettiInquilini, ], 'fornitori' => [ 'totali' => $fornitoriTotali, 'attivi' => $fornitoriAttivi, ], // Chiavi flat per retro-compatibilità con altri template 'stabili_gescon' => $stabiliGescon, 'stabili_totali' => $stabiliTotali, 'fornitori_totali' => $fornitoriTotali, 'fornitori_attivi' => $fornitoriAttivi, 'condomini_totali' => $condominiTotali, 'soggetti_proprietari' => $soggettiProprietari, 'unita_totali' => $unitaTotali, 'unita_con_millesimi' => $unitaConMillesimi, 'ultimo_import' => $ultimoImport, ]; } /** * Ottiene dati recenti per le cards */ protected function getDatiRecenti($amministratoreId) { $stabiliIds = Stabile::where('amministratore_id', $amministratoreId)->pluck('id'); return [ 'ultimi_stabili' => Stabile::where('amministratore_id', $amministratoreId) ->where('note', 'like', '%GESCON%') ->latest() ->take(5) ->get(), 'ultime_unita' => UnitaImmobiliare::whereIn('stabile_id', $stabiliIds) ->with('stabile') ->latest() ->take(10) ->get(), 'ultimi_fornitori' => Fornitore::where('amministratore_id', $amministratoreId) ->latest() ->take(5) ->get(), 'ultimi_condomini' => Soggetto::whereHas('unitaImmobiliari.stabile', function ($q) use ($amministratoreId) { $q->where('amministratore_id', $amministratoreId); }) ->latest() ->take(10) ->get(), 'ultimi_documenti' => DocumentoCollegato::whereIn('stabile_id', $stabiliIds) ->with(['stabile']) ->latest() ->take(5) ->get(), ]; } }