*/ public array $stabiliOptions = []; /** @var array */ public array $anniOptions = []; public static function canAccess(): bool { $user = Auth::user(); return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore']) && $user->can('gescon-import.execute'); } public function mount(): void { $user = Auth::user(); $defaultPathRoot = '/mnt/gescon-archives/gescon'; $defaultStabileCode = null; $defaultAnno = null; $activeStabile = $user instanceof User ? StabileContext::getActiveStabile($user) : null; if ($activeStabile) { $rawCode = $activeStabile->codice_stabile ?? null; if (is_string($rawCode) && $rawCode !== '') { $defaultStabileCode = str_pad(trim($rawCode), 4, '0', STR_PAD_LEFT); $label = $defaultStabileCode . ' — ' . trim((string) ($activeStabile->denominazione ?? '')); $this->stabiliOptions = [$defaultStabileCode => trim($label, ' —')]; try { $defaultAnno = DB::connection('gescon_import') ->table('anni_gestione') ->where('cod_stabile', $defaultStabileCode) ->max('anno'); } catch (\Throwable) { $defaultAnno = null; } } } $defaultAmministratoreId = null; if ($user instanceof User && $user->amministratore) { $defaultAmministratoreId = (int) $user->amministratore->id; } elseif ($user instanceof User && $user->hasAnyRole(['super-admin', 'admin'])) { $defaultAmministratoreId = (int) (Amministratore::query()->orderBy('id')->value('id') ?? 0); } $this->getSchema('form')?->fill([ 'source_mode' => 'linux_server', 'path_root' => $defaultPathRoot, 'stabili_mdb' => rtrim($defaultPathRoot, '/') . '/dbc/Stabili.mdb', 'fornitori_mdb' => rtrim($defaultPathRoot, '/') . '/dbc/Fornitori.mdb', 'amministratore_id' => $defaultAmministratoreId ?: null, 'stabile_code' => $defaultStabileCode, 'anno' => $defaultAnno, 'align_years_selected' => ['2024', '2025'], 'align_water_years_selected' => ['2024'], 'align_open_years_selected' => ['2024', '2025'], 'align_mark_obsolete_years' => false, 'align_with_f24' => true, 'with_unita' => true, 'with_soggetti' => true, 'with_diritti' => true, 'with_millesimi' => true, 'with_voci' => true, 'with_banche' => true, 'with_gestioni' => false, 'with_assemblee' => false, 'with_operazioni' => false, 'with_rate' => false, 'with_incassi' => false, 'with_giroconti' => false, 'with_straord' => false, 'with_addebiti' => false, 'with_detrazioni' => false, 'sync_staging' => true, 'dry_run' => false, ]); if (is_string($defaultStabileCode) && $defaultStabileCode !== '') { $this->refreshAnniOptionsForStabile($defaultStabileCode, $defaultPathRoot); } } private function extractGesconZipToTemp(string $zipPath): ?string { if (! is_file($zipPath) || ! is_readable($zipPath)) { return null; } if (! class_exists(\ZipArchive::class)) { throw new \RuntimeException('Estrazione ZIP non disponibile: estensione PHP zip mancante.'); } $base = storage_path('app/temp/gescon-ui-extracted'); if (! is_dir($base)) { @mkdir($base, 0775, true); } $target = $base . '/' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)); @mkdir($target, 0775, true); $zip = new \ZipArchive(); if ($zip->open($zipPath) !== true) { return null; } $zip->extractTo($target); $zip->close(); // Se lo zip contiene una sola cartella root, entra automaticamente. $entries = array_values(array_filter(scandir($target) ?: [], fn($n) => ! in_array($n, ['.', '..'], true))); if (count($entries) === 1) { $only = $target . '/' . $entries[0]; if (is_dir($only)) { return $only; } } return $target; } public function form(Schema $schema): Schema { return $schema ->statePath('data') ->components([ Select::make('source_mode') ->label('Modalità caricamento') ->options([ 'linux_server' => 'Linux / Server (path su disco)', 'windows_upload' => 'Windows / PC (carica file e cartelle)', ]) ->required() ->dehydrated(), TextInput::make('path_root') ->label('Percorso archivio GESCON (root)') ->helperText('Esempio: /mnt/gescon-archives/gescon. Dentro: //singolo_anno.mdb e dbc/Stabili.mdb') ->required() ->visible(fn($get): bool => (string) $get('source_mode') !== 'windows_upload'), FileUpload::make('stabili_mdb_upload') ->label('Seleziona Stabili.mdb (da PC)') ->helperText('Opzionale: carica il file da Windows/Linux. Verrà salvato sul server e usato per l\'import.') ->disk('local') ->directory('temp/gescon-ui') ->preserveFilenames() ->acceptedFileTypes([ '.mdb', 'application/octet-stream', ]) ->visible(fn($get): bool => (string) $get('source_mode') === 'windows_upload') ->afterStateUpdated(function (Set $set, ?string $state): void { if (! is_string($state) || $state === '') { return; } $set('stabili_mdb', storage_path('app/' . $state)); // In modalità Windows il root può essere uno zip estratto; non forzare qui. // In modalità Linux il root viene gestito dal path manuale/amministratore. }), TextInput::make('stabili_mdb') ->label('Percorso file Stabili.mdb') ->helperText('Esempio: /mnt/gescon-archives/gescon/dbc/Stabili.mdb') ->required() ->visible(fn($get): bool => (string) $get('source_mode') !== 'windows_upload') ->afterStateUpdated(function (Set $set, ?string $state): void { if (! is_string($state) || trim($state) === '') { return; } $path = trim($state); $normalized = str_replace('\\', '/', $path); if (str_ends_with($normalized, '/dbc/Stabili.mdb')) { $set('path_root', dirname(dirname($path))); } elseif (str_ends_with($normalized, '/Stabili.mdb')) { $set('path_root', dirname($path)); } }), FileUpload::make('fornitori_mdb_upload') ->label('Seleziona Fornitori.mdb (da PC)') ->helperText('Opzionale: carica Fornitori.mdb; se non caricato verrà cercato in /dbc/Fornitori.mdb dentro il root.') ->disk('local') ->directory('temp/gescon-ui') ->preserveFilenames() ->acceptedFileTypes([ '.mdb', 'application/octet-stream', ]) ->visible(fn($get): bool => (string) $get('source_mode') === 'windows_upload') ->afterStateUpdated(function (Set $set, ?string $state): void { if (! is_string($state) || $state === '') { return; } $set('fornitori_mdb', storage_path('app/' . $state)); }), FileUpload::make('gescon_zip_upload') ->label('Carica archivio GESCON (ZIP)') ->helperText('Consigliato: zip della cartella GESCON del condominio/amministratore (con dbc/ e le cartelle stabili). Dopo upload verrà estratto su server e usato come root temporaneo.') ->disk('local') ->directory('temp/gescon-ui') ->preserveFilenames() ->acceptedFileTypes([ '.zip', 'application/zip', 'application/x-zip-compressed', 'application/octet-stream', ]) ->visible(fn($get): bool => (string) $get('source_mode') === 'windows_upload') ->afterStateUpdated(function (Set $set, ?string $state): void { if (! is_string($state) || $state === '') { return; } $zipPath = storage_path('app/' . $state); $root = $this->extractGesconZipToTemp($zipPath); if (! is_string($root) || $root === '') { return; } // Normalizza slashes: i metodi a valle lavorano con path reali $set('path_root', $root); // Se nello zip è presente dbc/Stabili.mdb o Stabili.mdb, auto-aggancia. $cand1 = rtrim($root, '/') . '/dbc/Stabili.mdb'; $cand2 = rtrim($root, '/') . '/Stabili.mdb'; if (is_file($cand1)) { $set('stabili_mdb', $cand1); } elseif (is_file($cand2)) { $set('stabili_mdb', $cand2); } $candF1 = rtrim($root, '/') . '/dbc/Fornitori.mdb'; $candF2 = rtrim($root, '/') . '/Fornitori.mdb'; if (is_file($candF1)) { $set('fornitori_mdb', $candF1); } elseif (is_file($candF2)) { $set('fornitori_mdb', $candF2); } }), TextInput::make('fornitori_mdb') ->label('Percorso file Fornitori.mdb (auto)') ->helperText('Se vuoto, verrà usato /dbc/Fornitori.mdb') ->maxLength(600) ->dehydrated(), Select::make('amministratore_id') ->label('Carica per amministratore') ->helperText('Seleziona il proprietario (tenant) dello stabile/import. Visibile solo a super-admin/admin.') ->options(function (): array { return Amministratore::query() ->orderBy('id') ->get() ->mapWithKeys(function (Amministratore $a): array { $label = trim((string) ($a->denominazione_studio ?: $a->nome_completo)); if ($label === '') { $label = 'Amministratore #' . $a->id; } $code = (string) ($a->codice_univoco ?: $a->codice_amministratore ?: ''); $suffix = $code !== '' ? (' [' . $code . ']') : ''; return [$a->id => $label . $suffix . ' (ID ' . $a->id . ')']; }) ->all(); }) ->searchable() ->required() ->afterStateUpdated(function (Set $set, $get, ?string $state): void { if ((string) $get('source_mode') === 'windows_upload') { // In modalità Windows prima si caricano i file/cartelle su temp. // L'eventuale "salvataggio definitivo" su /mnt verrà gestito da un'azione dedicata. return; } $ammId = (int) ($state ?? 0); if ($ammId <= 0) { return; } $amm = Amministratore::query()->find($ammId); if (! $amm) { return; } $code = trim((string) ($amm->codice_univoco ?: $amm->codice_amministratore ?: '')); if ($code === '') { $code = 'ID' . $amm->id; } $safe = preg_replace('/[^A-Za-z0-9_-]+/', '_', $code); $safe = trim((string) $safe, '_'); if ($safe === '') { $safe = 'ID' . $amm->id; } $root = '/mnt/gescon-archives/gescon/' . $safe; $set('path_root', $root); $set('stabili_mdb', $root . '/dbc/Stabili.mdb'); $set('fornitori_mdb', $root . '/dbc/Fornitori.mdb'); }) ->visible(function (): bool { $user = Auth::user(); return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin']); }), Select::make('stabile_code') ->label('Stabile da importare') ->options(fn(): array=> $this->stabiliOptions) ->searchable() ->reactive() ->afterStateUpdated(function (Set $set, $get, ?string $state): void { $root = (string) ($get('path_root') ?? '/mnt/gescon-archives/gescon'); $this->refreshAnniOptionsForStabile((string) ($state ?? ''), $root, $set, $get); }) ->helperText('Prima premi “Carica elenco stabili da Stabili.mdb”, poi seleziona lo stabile.'), TextInput::make('anno') ->label('Anno gestione (opzionale)') ->helperText('Inserisci un anno (es. 2025) o "all" per tutte le cartelle. Serve per trovare //singolo_anno.mdb quando lo staging non è già caricato.') ->dehydrated(), Select::make('align_years_selected') ->label('Anni contabili target (comando unico)') ->options(fn(): array=> $this->anniOptions) ->multiple() ->searchable() ->live() ->dehydrateStateUsing(fn($state): array=> $this->normalizeSelectedOptionValues((array) ($state ?? []), $this->anniOptions)) ->validationMessages(['in' => 'Seleziona anni validi dall elenco aggiornato.']) ->helperText('Seleziona uno o piu anni. Le directory legacy vengono mappate automaticamente da tabella anni (nome_dir).') ->dehydrated(), Select::make('align_water_years_selected') ->label('Anni acqua target') ->options(fn(): array=> $this->anniOptions) ->multiple() ->searchable() ->live() ->dehydrateStateUsing(fn($state): array=> $this->normalizeSelectedOptionValues((array) ($state ?? []), $this->anniOptions)) ->validationMessages(['in' => 'Seleziona anni acqua validi dall elenco aggiornato.']) ->helperText('Per ACQUA (acqua_gen/acqua_dett/acqua_fatture).') ->dehydrated(), Select::make('align_open_years_selected') ->label('Anni da tenere aperti') ->options(fn(): array=> $this->anniOptions) ->multiple() ->searchable() ->live() ->dehydrateStateUsing(fn($state): array=> $this->normalizeSelectedOptionValues((array) ($state ?? []), $this->anniOptions)) ->validationMessages(['in' => 'Seleziona anni aperti validi dall elenco aggiornato.']) ->helperText('Gli anni fuori da questa selezione vengono chiusi secondo le regole di import.') ->dehydrated(), Checkbox::make('align_mark_obsolete_years') ->label('Marca subito come obsoleti/chiusi gli anni importati') ->helperText('Utile per import storico a fini archivio.'), Checkbox::make('align_with_f24') ->label('Importa anche modello F24 da generale_stabile.mdb') ->helperText('Legge F24.cod_tributo_1..6, Rateiz_1..6, Anno_1..6 e importi correlati.'), Checkbox::make('with_unita')->label('Unità'), Checkbox::make('with_soggetti')->label('Anagrafiche (soggetti)'), Checkbox::make('with_diritti')->label('Diritti'), Checkbox::make('with_millesimi')->label('Millesimi'), Checkbox::make('with_voci') ->label('Voci') ->reactive() ->afterStateUpdated(function (Set $set, $state): void { if ($state) { $set('with_millesimi', true); } }), Checkbox::make('with_banche')->label('Banche / casse'), Checkbox::make('with_gestioni')->label('Gestioni (anni)'), Checkbox::make('with_assemblee')->label('Assemblee legacy'), Checkbox::make('with_operazioni') ->label('Operazioni') ->reactive() ->afterStateUpdated(function (Set $set, $state): void { if ($state) { $set('with_voci', true); $set('with_millesimi', true); $set('with_banche', true); $set('with_unita', true); $set('with_soggetti', true); $set('with_diritti', true); } }), Checkbox::make('with_rate')->label('Rate'), Checkbox::make('with_incassi') ->label('Incassi') ->reactive() ->afterStateUpdated(function (Set $set, $state): void { if ($state) { $set('with_rate', true); $set('with_banche', true); } }), Checkbox::make('with_giroconti')->label('Giroconti'), Checkbox::make('with_straord')->label('Straordinari'), Checkbox::make('with_addebiti')->label('Addebiti'), Checkbox::make('with_detrazioni')->label('Detrazioni fiscali'), Checkbox::make('sync_staging')->label('Aggiorna staging prima di importare'), Checkbox::make('dry_run')->label('Dry-run (non scrive nel DB)'), ]); } public function loadStabili(): void { $state = $this->getImportFormState(); $mdb = (string) ($state['stabili_mdb'] ?? ''); if ($mdb === '' || ! is_file($mdb) || ! is_readable($mdb)) { Notification::make() ->title('Stabili.mdb non disponibile') ->body('Verifica percorso e che il mount sia attivo: ' . $mdb) ->danger() ->send(); return; } $rows = $this->mdbExportToRows($mdb, ['Stabili', 'stabili', 'Condomin', 'condomin']); if (empty($rows)) { Notification::make() ->title('Nessun record letto da Stabili.mdb') ->body('Controlla che mdbtools sia installato e che il file sia valido.') ->danger() ->send(); return; } $opts = []; foreach ($rows as $r) { $code = $this->firstNonEmpty($r, ['cod_stabile', 'codice_stabile', 'stabile', 'codice', 'cod']); if (! $code) { continue; } $code = str_pad(trim((string) $code), 4, '0', STR_PAD_LEFT); $denom = $this->firstNonEmpty($r, ['denominazione', 'descrizione', 'desc', 'nome', 'ragione_sociale']); $label = $code . ($denom ? (' — ' . trim((string) $denom)) : ''); $opts[$code] = $label; } if (empty($opts)) { Notification::make() ->title('Elenco stabili vuoto') ->danger() ->send(); return; } ksort($opts); $this->stabiliOptions = $opts; $state = $this->getImportFormState(); if (empty($state['stabile_code'])) { $active = Auth::user() instanceof User ? StabileContext::getActiveStabile(Auth::user()) : null; $activeCode = $active?->codice_stabile ?? null; if (is_string($activeCode) && $activeCode !== '') { $activeCode = str_pad(trim($activeCode), 4, '0', STR_PAD_LEFT); if (isset($opts[$activeCode])) { $state['stabile_code'] = $activeCode; $this->getSchema('form')?->fill($state); } } } $state = $this->getImportFormState(); $this->refreshAnniOptionsForStabile( (string) ($state['stabile_code'] ?? ''), (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon') ); Notification::make() ->title('Elenco stabili caricato') ->body('Trovati: ' . count($opts)) ->success() ->send(); } public function creaStabileDaMdb(): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code === '') { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $state = $this->getImportFormState(); $amm = null; if ($user->hasAnyRole(['super-admin', 'admin'])) { $ammId = (int) ($state['amministratore_id'] ?? 0); $amm = $ammId > 0 ? Amministratore::query()->find($ammId) : null; } else { $amm = $user->amministratore; } if (! $amm) { $amm = Amministratore::query()->orderBy('id')->first(); } if (! $amm) { Notification::make() ->title('Nessun amministratore disponibile') ->body('Serve almeno un record in tabella amministratori.') ->danger() ->send(); return; } $stabile = Stabile::query()->where('codice_stabile', $code)->first(); if (! $stabile) { $label = $this->stabiliOptions[$code] ?? $code; $denom = trim((string) preg_replace('/^' . preg_quote($code, '/') . '\s+—\s+/u', '', $label)); if ($denom === '' || $denom === $code) { $denom = 'Stabile ' . $code; } $stabile = Stabile::query()->create([ 'amministratore_id' => $amm->id, 'codice_stabile' => $code, 'denominazione' => $denom, 'stato' => 'attivo', 'attivo' => true, ]); Notification::make() ->title('Stabile creato') ->body('Codice: ' . $code) ->success() ->send(); } else { // Se un admin/super-admin ha selezionato un amministratore diverso, riallinea per evitare "sparizioni" da filtri tenant. if ($user->hasAnyRole(['super-admin', 'admin']) && (int) $stabile->amministratore_id !== (int) $amm->id) { $stabile->amministratore_id = (int) $amm->id; $stabile->save(); Notification::make() ->title('Stabile riassegnato') ->body('Ora lo stabile è assegnato all’amministratore selezionato.') ->warning() ->send(); } Notification::make() ->title('Stabile già presente') ->body('Userò lo stabile esistente: ' . $code) ->success() ->send(); } StabileContext::setActiveStabileId($user, (int) $stabile->id); Notification::make() ->title('Stabile impostato come attivo') ->body('Ora puoi eseguire l\'import selezionato o proseguire da Elenco stabili.') ->success() ->send(); } public function applicaEnrichmentStabile(): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); $path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'); if ($code === '') { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } try { $res = (new StabileEnrichmentService())->enrich($code, $path); } catch (\Throwable $e) { Notification::make() ->title('Enrichment fallito') ->body($e->getMessage()) ->danger() ->send(); return; } $body = 'Stabile: ' . $code . ' | catasto=' . (! empty($res['catasto']) ? 'OK' : '-') . ' | rate=' . (! empty($res['rate_flags']) ? 'OK' : '-') . ' | banca/posta=' . (! empty($res['banca_posta']) ? 'OK' : '-'); Notification::make() ->title('Enrichment completato') ->body($body) ->success() ->send(); } public function aggiornaAnagraficheDaCondomin(): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code === '') { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $dryRun = (bool) ($state['dry_run'] ?? false); $params = [ '--stabile' => $code, '--limit' => 20000, '--only' => 'both', '--link-ruolo' => true, '--include-comproprietari' => true, ]; if (! $dryRun) { $params['--apply'] = true; } Artisan::call('gescon:auto-sync-anagrafiche', $params); $out = trim((string) Artisan::output()); $lines = $out === '' ? [] : preg_split('/\r\n|\r|\n/', $out); $tail = $lines ? implode("\n", array_slice($lines, -8)) : 'Comando eseguito.'; Notification::make() ->title($dryRun ? 'Dry-run anagrafiche (condomin) completato' : 'Sync anagrafiche (condomin) completato') ->body($tail) ->success() ->send(); } public function risincronizzaRelazioniImportate(): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code === '') { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $stabile = $this->resolveSelectedStabileByCode($code); if (! $stabile instanceof Stabile) { Notification::make()->title('Stabile locale non trovato')->body('Prima crea o apri lo stabile selezionato.')->danger()->send(); return; } $result = $this->runRelationSyncSequence($code, (int) $stabile->id, (bool) ($state['dry_run'] ?? false)); if (! $result['ok']) { Notification::make() ->title('Risincronizzazione relazioni fallita') ->body((string) ($result['message'] ?? 'Errore non specificato.')) ->danger() ->send(); return; } Notification::make() ->title((bool) ($state['dry_run'] ?? false) ? 'Dry-run relazioni completato' : 'Risincronizzazione relazioni completata') ->body((string) ($result['message'] ?? 'Operazione completata')) ->success() ->send(); } public function aggiornaFornitoriGescon(): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $state = $this->getImportFormState(); $path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'); $amm = null; if ($user->hasAnyRole(['super-admin', 'admin'])) { $ammId = (int) ($state['amministratore_id'] ?? 0); $amm = $ammId > 0 ? Amministratore::query()->find($ammId) : null; } else { $amm = $user->amministratore; } if (! $amm) { $amm = Amministratore::query()->orderBy('id')->first(); } if (! $amm) { Notification::make()->title('Nessun amministratore disponibile')->danger()->send(); return; } $mdb = (string) ($state['fornitori_mdb'] ?? ''); if ($mdb === '') { $mdb = rtrim($path, '/') . '/dbc/Fornitori.mdb'; } if (! is_file($mdb)) { Notification::make() ->title('Fornitori.mdb non trovato') ->body('Percorso atteso: ' . $mdb) ->danger() ->send(); return; } try { $exit = Artisan::call('gescon:sync-fornitori-legacy', [ 'amministratore_id' => (string) $amm->id, '--mdb' => $mdb, ]); $out = trim((string) Artisan::output()); } catch (\Throwable $e) { Notification::make()->title('Aggiornamento fornitori fallito')->body($e->getMessage())->danger()->send(); return; } if ($exit !== 0) { $snippet = $out !== '' ? $out : ''; $snippet = $snippet !== '' ? mb_substr($snippet, 0, 800) : ''; Notification::make() ->title('Aggiornamento fornitori: errore') ->body('Exit sync=' . $exit . ($snippet !== '' ? "\n\n" . $snippet : '')) ->danger() ->send(); return; } Notification::make() ->title('Fornitori aggiornati') ->body('Amministratore: ' . ($amm->codice_univoco ?: ('ID ' . $amm->id))) ->success() ->send(); } public function refreshImportTagLegacyFornitoriGlobale(): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $state = $this->getImportFormState(); $path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'); $dryRun = (bool) ($state['dry_run'] ?? false); $amm = null; if ($user->hasAnyRole(['super-admin', 'admin'])) { $ammId = (int) ($state['amministratore_id'] ?? 0); $amm = $ammId > 0 ? Amministratore::query()->find($ammId) : null; } else { $amm = $user->amministratore; } if (! $amm) { $amm = Amministratore::query()->orderBy('id')->first(); } if (! $amm) { Notification::make()->title('Nessun amministratore disponibile')->danger()->send(); return; } $mdb = (string) ($state['fornitori_mdb'] ?? ''); if ($mdb === '') { $mdb = rtrim($path, '/') . '/dbc/Fornitori.mdb'; } if (! is_file($mdb)) { Notification::make() ->title('Fornitori.mdb non trovato') ->body('Percorso atteso: ' . $mdb) ->danger() ->send(); return; } try { $exit = Artisan::call('gescon:sync-fornitori-legacy', [ 'amministratore_id' => (string) $amm->id, '--mdb' => $mdb, '--dry-run' => $dryRun, ]); $out = trim((string) Artisan::output()); } catch (\Throwable $e) { Notification::make() ->title('Refresh + import TAG fallito') ->body($e->getMessage()) ->danger() ->send(); return; } if ($exit !== 0) { $snippet = $out !== '' ? $out : ''; $snippet = $snippet !== '' ? mb_substr($snippet, 0, 800) : ''; Notification::make() ->title('Refresh + import TAG: errore') ->body('Exit sync=' . $exit . ($snippet !== '' ? "\n\n" . $snippet : '')) ->danger() ->send(); return; } Notification::make() ->title($dryRun ? 'Dry-run TAG globale completato' : 'Refresh + import TAG globale completato') ->body('Amministratore: ' . ($amm->codice_univoco ?: ('ID ' . $amm->id))) ->success() ->send(); } public function applicaEnrichmentTuttiStabili(): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $state = $this->getImportFormState(); $path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'); $amm = null; if ($user->hasAnyRole(['super-admin', 'admin'])) { $ammId = (int) ($state['amministratore_id'] ?? 0); $amm = $ammId > 0 ? Amministratore::query()->find($ammId) : null; } else { $amm = $user->amministratore; } if (! $amm) { $amm = Amministratore::query()->orderBy('id')->first(); } if (! $amm) { Notification::make()->title('Nessun amministratore disponibile')->danger()->send(); return; } try { $exit = Artisan::call('gescon:enrich-stabili', [ '--path' => $path, '--amministratore-id' => (string) $amm->id, ]); $out = trim((string) Artisan::output()); } catch (\Throwable $e) { Notification::make()->title('Enrichment massivo fallito')->body($e->getMessage())->danger()->send(); return; } if ($exit !== 0) { $snippet = $out !== '' ? mb_substr($out, 0, 800) : ''; Notification::make()->title('Enrichment massivo: errore')->body($snippet)->danger()->send(); return; } $lastLine = $out !== '' ? trim((string) collect(preg_split('/\R/', $out) ?: [])->last()): ''; Notification::make() ->title('Enrichment massivo completato') ->body($lastLine !== '' ? $lastLine : 'Operazione completata') ->success() ->send(); } public function eseguiImportSelezionato(EssentialImportService $service): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code === '') { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $stabile = Stabile::query()->where('codice_stabile', $code)->first(); if (! $stabile) { Notification::make() ->title('Stabile non creato') ->body('Prima crea lo stabile dal pulsante dedicato.') ->danger() ->send(); return; } $amm = null; if ($user->hasAnyRole(['super-admin', 'admin'])) { $ammId = (int) ($state['amministratore_id'] ?? 0); $amm = $ammId > 0 ? Amministratore::query()->find($ammId) : null; } else { $amm = $user->amministratore; } if (! $amm) { $amm = Amministratore::query()->orderBy('id')->first(); } if (! $amm) { Notification::make()->title('Nessun amministratore disponibile')->danger()->send(); return; } $options = [ 'path' => (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'), 'anno' => isset($state['anno']) && trim((string) $state['anno']) !== '' ? trim((string) $state['anno']) : null, 'dry' => (bool) ($state['dry_run'] ?? false), 'sync_staging' => (bool) ($state['sync_staging'] ?? false), 'with_unita' => (bool) ($state['with_unita'] ?? false), 'with_soggetti' => (bool) ($state['with_soggetti'] ?? false), 'with_diritti' => (bool) ($state['with_diritti'] ?? false), 'with_millesimi' => (bool) ($state['with_millesimi'] ?? false), 'with_voci' => (bool) ($state['with_voci'] ?? false), 'with_banche' => (bool) ($state['with_banche'] ?? false), 'with_gestioni' => (bool) ($state['with_gestioni'] ?? false), 'with_assemblee' => (bool) ($state['with_assemblee'] ?? false), 'with_operazioni' => (bool) ($state['with_operazioni'] ?? false), 'with_rate' => (bool) ($state['with_rate'] ?? false), 'with_incassi' => (bool) ($state['with_incassi'] ?? false), 'with_giroconti' => (bool) ($state['with_giroconti'] ?? false), 'with_straord' => (bool) ($state['with_straord'] ?? false), 'with_addebiti' => (bool) ($state['with_addebiti'] ?? false), 'with_detrazioni' => (bool) ($state['with_detrazioni'] ?? false), ]; if (! empty($options['with_operazioni'])) { $options['with_voci'] = true; $options['with_millesimi'] = true; $options['with_banche'] = true; $options['with_unita'] = true; $options['with_soggetti'] = true; $options['with_diritti'] = true; } if (! empty($options['with_rate'])) { $options['with_unita'] = true; $options['with_soggetti'] = true; $options['with_diritti'] = true; } if (! empty($options['with_incassi'])) { $options['with_rate'] = true; $options['with_banche'] = true; } if (! empty($options['with_voci'])) { $options['with_millesimi'] = true; } $order = [ 'unita', 'soggetti', 'diritti', 'millesimi', 'voci', 'banche', 'assemblee', 'operazioni', 'rate', 'incassi', 'giroconti', 'straord', 'addebiti', 'detrazioni', ]; $steps = []; foreach ($order as $step) { $flag = 'with_' . $step; if (! empty($options[$flag])) { $steps[] = $step; } } if (empty($steps) && ! empty($options['with_gestioni'])) { $steps = []; } if (empty($steps) && empty($options['with_gestioni'])) { Notification::make() ->title('Nessun dato selezionato') ->danger() ->send(); return; } $outputs = []; if (! empty($options['with_operazioni'])) { try { $exit = Artisan::call('gescon:sync-fornitori-legacy', [ 'amministratore_id' => (string) $amm->id, '--mdb' => (string) ($state['fornitori_mdb'] ?? rtrim($options['path'], '/') . '/dbc/Fornitori.mdb'), ]); $outputs[] = trim((string) Artisan::output()); if ($exit !== 0) { Notification::make() ->title('Aggiornamento fornitori: errore') ->body('Exit sync=' . $exit) ->danger() ->send(); return; } } catch (\Throwable $e) { Notification::make() ->title('Aggiornamento fornitori fallito') ->body($e->getMessage()) ->danger() ->send(); return; } } if (! empty($options['sync_staging'])) { $singoloMdb = null; if (! empty($options['path']) && ! empty($code)) { $annoFolder = $options['anno'] ?: '0001'; $singoloMdb = rtrim($options['path'], '/') . '/' . $code . '/' . $annoFolder . '/singolo_anno.mdb'; } $generaleMdb = ! empty($options['path']) && ! empty($code) ? rtrim($options['path'], '/') . '/' . $code . '/generale_stabile.mdb' : null; try { if (! empty($options['with_operazioni']) && is_string($singoloMdb)) { Artisan::call('gescon:load-mdb', [ '--mdb' => $singoloMdb, '--table' => 'operazioni', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--truncate' => true, ]); $outputs[] = trim((string) Artisan::output()); Artisan::call('gescon:load-mdb', [ '--mdb' => $singoloMdb, '--table' => 'voc_spe', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--truncate' => true, ]); $outputs[] = trim((string) Artisan::output()); Artisan::call('gescon:load-mdb', [ '--mdb' => $singoloMdb, '--table' => 'tabelle', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--truncate' => true, ]); $outputs[] = trim((string) Artisan::output()); Artisan::call('gescon:load-mdb', [ '--mdb' => $singoloMdb, '--table' => 'anagr_casse', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--truncate' => true, ]); $outputs[] = trim((string) Artisan::output()); } if (! empty($options['with_incassi']) && is_string($singoloMdb)) { Artisan::call('gescon:load-mdb', [ '--mdb' => $singoloMdb, '--table' => 'incassi', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--truncate' => true, ]); $outputs[] = trim((string) Artisan::output()); } if (! empty($options['with_rate']) && is_string($generaleMdb)) { Artisan::call('gescon:load-mdb', [ '--mdb' => $generaleMdb, '--table' => 'emes_gen', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--truncate' => true, ]); $outputs[] = trim((string) Artisan::output()); Artisan::call('gescon:load-mdb', [ '--mdb' => $generaleMdb, '--table' => 'emes_det', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--truncate' => true, ]); $outputs[] = trim((string) Artisan::output()); } } catch (\Throwable $e) { Notification::make() ->title('Aggiornamento staging fallito') ->body($e->getMessage()) ->danger() ->send(); return; } } foreach ($steps as $step) { $params = [ '--stabile' => $code, '--solo' => $step, '--path' => $options['path'], ]; if (! empty($options['anno'])) { $params['--anno'] = $options['anno']; } if (! empty($options['dry'])) { $params['--dry-run'] = true; } Artisan::call('gescon:import-full', $params); $outputs[] = trim((string) Artisan::output()); } if (! empty($options['with_gestioni']) && empty($options['dry'])) { $gestParams = [ '--stabile' => $code, '--root' => $options['path'], ]; Artisan::call('gescon:import-gestioni', $gestParams); $outputs[] = trim((string) Artisan::output()); } $shouldSyncRelations = ! empty($options['with_unita']) || ! empty($options['with_soggetti']) || ! empty($options['with_diritti']); if ($shouldSyncRelations) { $relationSync = $this->runRelationSyncSequence($code, (int) $stabile->id, (bool) $options['dry']); if (! $relationSync['ok']) { Notification::make() ->title('Import completato con errore nel re-sync relazioni') ->body((string) ($relationSync['message'] ?? 'Errore non specificato.')) ->danger() ->send(); return; } foreach ((array) ($relationSync['outputs'] ?? []) as $syncOutput) { if (is_string($syncOutput) && trim($syncOutput) !== '') { $outputs[] = trim($syncOutput); } } } $tail = collect($outputs) ->filter() ->map(fn(string $o) => trim((string) collect(preg_split('/\R/', $o) ?: [])->last())) ->filter() ->implode("\n"); Notification::make() ->title('Importazione completata') ->body($tail !== '' ? $tail : 'Operazione completata') ->success() ->send(); } public function eseguiImportAllineamentoCompleto(): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code === '') { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $path = trim((string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon')); $years = implode(',', array_values(array_filter(array_map( fn($v) => trim((string) $v), (array) ($state['align_years_selected'] ?? ['2024', '2025']) )))); $waterYears = implode(',', array_values(array_filter(array_map( fn($v) => trim((string) $v), (array) ($state['align_water_years_selected'] ?? ['2024']) )))); $openYears = implode(',', array_values(array_filter(array_map( fn($v) => trim((string) $v), (array) ($state['align_open_years_selected'] ?? ['2024', '2025']) )))); $dryRun = (bool) ($state['dry_run'] ?? false); $markObsoleteYears = (bool) ($state['align_mark_obsolete_years'] ?? false); $withF24 = (bool) ($state['align_with_f24'] ?? true); if ($path === '') { Notification::make()->title('Percorso archivio mancante')->danger()->send(); return; } if ($years === '') { Notification::make()->title('Anni contabili mancanti')->danger()->send(); return; } $params = [ '--stabile' => [$code], '--path' => $path, '--years' => $years, '--water-years' => $waterYears !== '' ? $waterYears : '2024', '--open-years' => $openYears !== '' ? $openYears : $years, ]; if ($markObsoleteYears) { $params['--mark-obsolete-years'] = true; } if (! $withF24) { $params['--skip-f24'] = true; } if ($dryRun) { $params['--dry-run'] = true; } try { $exit = Artisan::call('gescon:import-align', $params); $out = trim((string) Artisan::output()); } catch (\Throwable $e) { Notification::make() ->title('Import + allineamento fallito') ->body($e->getMessage()) ->danger() ->send(); return; } $lines = $out === '' ? [] : preg_split('/\R/', $out); $tail = $lines ? implode("\n", array_slice($lines, -10)) : 'Comando eseguito.'; Notification::make() ->title($exit === 0 ? 'Import + allineamento completato' : 'Import + allineamento con errori') ->body($tail) ->{$exit === 0 ? 'success' : 'danger'}() ->send(); } public function caricaAnniDaGenerale(): void { $state = $this->getImportFormState(); $code = (string) ($state['stabile_code'] ?? ''); $path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'); $loaded = $this->refreshAnniOptionsForStabile($code, $path); if ($loaded <= 0) { Notification::make() ->title('Nessun anno trovato in generale_stabile.mdb') ->body('Verifica percorso archivio e stabile selezionato.') ->danger() ->send(); return; } Notification::make() ->title('Anni caricati da generale_stabile.mdb') ->body('Anni disponibili: ' . $loaded) ->success() ->send(); } /** @return array */ public function getCommandPreviewData(): array { $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code !== '') { $code = str_pad($code, 4, '0', STR_PAD_LEFT); } $path = trim((string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon')); $annoDir = trim((string) ($state['anno'] ?? '')); $years = implode(',', array_values(array_filter(array_map( fn($value) => trim((string) $value), (array) ($state['align_years_selected'] ?? []) )))); $waterYears = implode(',', array_values(array_filter(array_map( fn($value) => trim((string) $value), (array) ($state['align_water_years_selected'] ?? []) )))); $openYears = implode(',', array_values(array_filter(array_map( fn($value) => trim((string) $value), (array) ($state['align_open_years_selected'] ?? []) )))); $adminId = (int) ($state['amministratore_id'] ?? 0); $stabile = $this->resolveSelectedStabileByCode($code); $stabileId = $stabile instanceof Stabile ? (int) $stabile->id : null; $fornitoriMdb = trim((string) ($state['fornitori_mdb'] ?? '')); if ($fornitoriMdb === '' && $path !== '') { $fornitoriMdb = rtrim($path, '/') . '/dbc/Fornitori.mdb'; } $stabiliMdb = trim((string) ($state['stabili_mdb'] ?? '')); if ($stabiliMdb === '' && $path !== '') { $stabiliMdb = rtrim($path, '/') . '/dbc/Stabili.mdb'; } $generaleMdb = ($path !== '' && $code !== '') ? rtrim($path, '/') . '/' . $code . '/generale_stabile.mdb' : ''; $singoloMdb = ($path !== '' && $code !== '' && $annoDir !== '') ? rtrim($path, '/') . '/' . $code . '/' . $annoDir . '/singolo_anno.mdb' : ''; return [ 'source_mode' => (string) ($state['source_mode'] ?? 'linux_server'), 'stabile_code' => $code, 'stabile_id' => $stabileId, 'amministratore_id' => $adminId > 0 ? $adminId : null, 'anno_dir' => $annoDir, 'years_csv' => $years, 'water_years_csv' => $waterYears, 'open_years_csv' => $openYears, 'paths' => [ 'root' => $path, 'stabili_mdb' => $stabiliMdb, 'fornitori_mdb' => $fornitoriMdb, 'generale_mdb' => $generaleMdb, 'singolo_mdb' => $singoloMdb, ], 'commands' => [ 'load_stabili' => $stabiliMdb !== '' ? 'php artisan import:gescon-fornitori ' . max(1, $adminId) . ' --mdb="' . $fornitoriMdb . '" --preview --limit=5' : null, 'fornitori_sync' => ($adminId > 0 && $fornitoriMdb !== '') ? 'php artisan gescon:sync-fornitori-legacy ' . $adminId . ' --mdb="' . $fornitoriMdb . '"' : null, 'allineamento' => ($code !== '' && $path !== '' && $years !== '') ? 'php artisan gescon:import-align --stabile=' . $code . ' --path="' . $path . '" --years=' . $years . ' --water-years=' . ($waterYears !== '' ? $waterYears : $years) . ' --open-years=' . ($openYears !== '' ? $openYears : $years) : null, 'unita' => ($code !== '' && $path !== '' && $annoDir !== '') ? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=unita --anno=' . $annoDir . ' --path="' . $path . '"' : null, 'soggetti' => ($code !== '' && $path !== '' && $annoDir !== '') ? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=soggetti --anno=' . $annoDir . ' --path="' . $path . '"' : null, 'diritti' => ($code !== '' && $path !== '' && $annoDir !== '') ? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=diritti --anno=' . $annoDir . ' --path="' . $path . '"' : null, 'anagrafiche_sync' => ($code !== '') ? 'php artisan gescon:auto-sync-anagrafiche --stabile=' . $code . ' --only=both --link-ruolo --include-comproprietari --apply' : null, 'ruoli_sync' => ($stabileId !== null) ? 'php artisan gescon:sync-rubrica-ruoli --stabile=' . $stabileId . ' --apply' : null, 'repair_contacts' => ($code !== '') ? 'php artisan gescon:repair-nominativi-contacts --stabile=' . $code . ' --apply' : null, ], ]; } private function refreshAnniOptionsForStabile(string $code, string $root, ?Set $set = null, mixed $get = null): int { $code = trim($code); if ($code === '') { $this->anniOptions = []; return 0; } $code = str_pad($code, 4, '0', STR_PAD_LEFT); $root = trim($root); if ($root === '') { $this->anniOptions = []; return 0; } $mdb = rtrim($root, '/') . '/' . $code . '/generale_stabile.mdb'; if (! is_file($mdb) || ! is_readable($mdb)) { $this->anniOptions = []; return 0; } $rows = $this->mdbExportToRows($mdb, ['anni', 'Anni', 'ANNI']); if (empty($rows)) { $this->anniOptions = []; return 0; } $opts = []; foreach ($rows as $r) { $yearRaw = $this->firstNonEmpty($r, ['anno_o', 'anno_r', 'anno']); $year = null; if (is_scalar($yearRaw) && preg_match('/(19|20)\d{2}/', (string) $yearRaw, $m)) { $year = $m[0]; } if ($year === null) { continue; } $dirRaw = $this->firstNonEmpty($r, ['nome_dir', 'dir', 'cartella', 'id_anno']); $dir = trim((string) ($dirRaw ?? '')); if ($dir === '' && is_numeric((string) ($r['id_anno'] ?? ''))) { $dir = str_pad((string) ((int) $r['id_anno']), 4, '0', STR_PAD_LEFT); } $opts[(string) $year] = $dir !== '' ? ((string) $year . ' (dir ' . $dir . ')') : (string) $year; } if (empty($opts)) { $this->anniOptions = []; return 0; } ksort($opts); $this->anniOptions = $opts; $state = $this->getImportFormState(); $keys = array_keys($opts); $selectedYears = array_values(array_filter(array_map( fn($v) => trim((string) $v), (array) ($state['align_years_selected'] ?? []) ), fn($v) => in_array($v, $keys, true))); if (empty($selectedYears)) { $selectedYears = array_slice(array_reverse($keys), 0, 2); sort($selectedYears); } $selectedWater = array_values(array_filter(array_map( fn($v) => trim((string) $v), (array) ($state['align_water_years_selected'] ?? []) ), fn($v) => in_array($v, $keys, true))); if (empty($selectedWater) && ! empty($selectedYears)) { $selectedWater = [end($selectedYears)]; } $selectedOpen = array_values(array_filter(array_map( fn($v) => trim((string) $v), (array) ($state['align_open_years_selected'] ?? []) ), fn($v) => in_array($v, $keys, true))); if (empty($selectedOpen)) { $selectedOpen = $selectedYears; } if ($set !== null && is_callable($get)) { $set('align_years_selected', $selectedYears); $set('align_water_years_selected', $selectedWater); $set('align_open_years_selected', $selectedOpen); } else { $state['align_years_selected'] = $selectedYears; $state['align_water_years_selected'] = $selectedWater; $state['align_open_years_selected'] = $selectedOpen; $this->getSchema('form')?->fill($state); } return count($opts); } private function resolveSelectedStabileByCode(?string $code): ?Stabile { $normalized = trim((string) $code); if ($normalized === '') { return null; } $trimmed = ltrim($normalized, '0'); return Stabile::query() ->where(function ($query) use ($normalized, $trimmed): void { $query->where('codice_stabile', $normalized); if ($trimmed !== '' && $trimmed !== $normalized) { $query->orWhere('codice_stabile', $trimmed); } }) ->first(); } /** @return array{ok:bool,message:string,outputs:array} */ private function runRelationSyncSequence(string $code, int $stabileId, bool $dryRun): array { $outputs = []; $statuses = []; try { $syncParams = [ '--stabile' => $code, '--limit' => 20000, '--only' => 'both', '--link-ruolo' => true, '--include-comproprietari' => true, ]; if (! $dryRun) { $syncParams['--apply'] = true; } $statuses[] = Artisan::call('gescon:auto-sync-anagrafiche', $syncParams); $outputs[] = trim((string) Artisan::output()); $ruoliParams = [ '--stabile' => (string) $stabileId, '--limit' => 5000, ]; if (! $dryRun) { $ruoliParams['--apply'] = true; } $statuses[] = Artisan::call('gescon:sync-rubrica-ruoli', $ruoliParams); $outputs[] = trim((string) Artisan::output()); $repairParams = [ '--stabile' => $code, '--limit' => 5000, ]; if (! $dryRun) { $repairParams['--apply'] = true; } $statuses[] = Artisan::call('gescon:repair-nominativi-contacts', $repairParams); $outputs[] = trim((string) Artisan::output()); } catch (\Throwable $e) { return [ 'ok' => false, 'message' => $e->getMessage(), 'outputs' => $outputs, ]; } $tail = collect($outputs) ->filter(fn($value) => is_string($value) && trim($value) !== '') ->map(fn(string $output) => trim((string) collect(preg_split('/\R/', $output) ?: [])->last())) ->filter(fn($value) => is_string($value) && $value !== '') ->implode("\n"); return [ 'ok' => collect($statuses)->every(fn($status) => (int) $status === 0), 'message' => $tail !== '' ? $tail : 'Operazione completata.', 'outputs' => $outputs, ]; } /** @return array */ private function getImportFormState(): array { $state = is_array($this->data) ? $this->data : []; $state['align_years_selected'] = $this->normalizeSelectedOptionValues( (array) ($state['align_years_selected'] ?? []), $this->anniOptions, ); $state['align_water_years_selected'] = $this->normalizeSelectedOptionValues( (array) ($state['align_water_years_selected'] ?? []), $this->anniOptions, ); $state['align_open_years_selected'] = $this->normalizeSelectedOptionValues( (array) ($state['align_open_years_selected'] ?? []), $this->anniOptions, ); return $state; } /** * @param array $values * @param array $options * @return array */ private function normalizeSelectedOptionValues(array $values, array $options): array { $normalized = array_values(array_filter(array_map( static fn($value): string => trim((string) $value), $values, ), static fn(string $value): bool => $value !== '')); if ($options === []) { return $normalized; } $allowed = array_map('strval', array_keys($options)); return array_values(array_filter($normalized, static fn(string $value): bool => in_array($value, $allowed, true))); } private function firstNonEmpty(array $row, array $keys): mixed { foreach ($keys as $k) { $lk = strtolower(trim((string) $k)); if (array_key_exists($lk, $row) && trim((string) $row[$lk]) !== '') { return $row[$lk]; } if (array_key_exists($k, $row) && trim((string) $row[$k]) !== '') { return $row[$k]; } } return null; } /** * Esporta una tabella MDB in array di righe associative (headers normalizzati lower-case). */ private function mdbExportToRows(string $mdbPath, array $tableCandidates): array { $binTables = trim((string) @shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables'; $binExport = trim((string) @shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export'; if (! is_file($mdbPath) || ! is_readable($mdbPath)) { return []; } $tables = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: ''; $table = null; if ($tables) { $names = array_filter(preg_split('/\r?\n/', trim($tables))); foreach ($tableCandidates as $cand) { foreach ($names as $n) { if (strcasecmp($n, $cand) === 0) { $table = $n; break 2; } } } if (! $table) { foreach ($tableCandidates as $cand) { foreach ($names as $n) { if (stripos($n, $cand) !== false) { $table = $n; break 2; } } } } } if (! $table) { return []; } $tmp = tempnam(sys_get_temp_dir(), 'mdbcsv_'); $cmd = sprintf( '%s -D %s -d %s -q %s %s %s > %s 2>/dev/null', escapeshellarg($binExport), escapeshellarg('%Y-%m-%d'), escapeshellarg('|'), escapeshellarg('"'), escapeshellarg($mdbPath), escapeshellarg($table), 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); $rows = []; while (($cols = fgetcsv($fh, 0, '|')) !== false) { if ($cols === [null] || $cols === false) { continue; } $cols = array_map(fn($v) => is_string($v) ? trim($v) : $v, $cols); $cnt = count($headers); if (count($cols) < $cnt) { $cols = array_pad($cols, $cnt, null); } if (count($cols) > $cnt) { $cols = array_slice($cols, 0, $cnt); } $row = @array_combine($headers, $cols) ?: []; if (! empty($row)) { $rows[] = $row; } } fclose($fh); @unlink($tmp); return $rows; } }