*/ public array $stabiliOptions = []; /** @var array */ public array $anniOptions = []; public static function canAccess(): bool { if (! config('netgescon.legacy_import_enabled', true)) { return false; } $user = Auth::user(); return ModuleVisibility::canAccessGesconInternal($user, ['super-admin', 'admin', 'amministratore']) && $user instanceof User && $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, 'stabile_denominazione' => '', 'stabile_indirizzo' => '', 'stabile_cap' => '', 'stabile_citta' => '', 'stabile_provincia' => '', 'stabile_codice_fiscale' => '', 'stabile_banca_principale' => '', 'stabile_iban_principale' => '', 'gestione_setup_enabled' => true, 'gestione_anno_iniziale' => $defaultAnno ?: (int) date('Y'), 'gestione_mostra_riscaldamento' => false, 'gestione_mesi_ordinaria' => [], 'gestione_mesi_riscaldamento' => [], 'gestione_replica_tutti_anni' => true, ]); if (is_string($defaultStabileCode) && $defaultStabileCode !== '') { $this->refreshAnniOptionsForStabile($defaultStabileCode, $defaultPathRoot); $this->hydrateImportSetupState($defaultStabileCode, $defaultPathRoot, $activeStabile); } } 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; } $root = '/mnt/gescon-archives/gescon'; $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('Gestioni da caricare (ultima aperta -> ritroso)') ->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 solo le gestioni che vuoi importare, partendo dall ultima disponibile e andando a ritroso. 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('Gestioni da lasciare aperte') ->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('Normalmente qui resta selezionata l ultima gestione aperta. 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.'), \Filament\Schemas\Components\Section::make('Seleziona dati da importare / sincronizzare') ->description('Seleziona i moduli da importare e clicca sul pulsante in basso per avviare l\'importazione dei soli dati selezionati.') ->columns(3) ->schema([ 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 di spesa') ->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 (Movimenti)') ->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)'), Checkbox::make('update_only') ->label('Solo aggiornamento incrementale (non cancella i record esistenti)') ->default(true), \Filament\Schemas\Components\Actions::make([ \Filament\Actions\Action::make('importa_selezionati_btn') ->label('Avvia Sincronizzazione / Importazione Dati Selezionati') ->icon('heroicon-m-arrow-down-tray') ->color('success') ->requiresConfirmation() ->action(function (\App\Services\GesconImport\EssentialImportService $service) { $this->eseguiImportSelezionato($service); }) ])->columnSpanFull() ]), ]); } 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(); $selectedCode = (string) (($this->getImportFormState())['stabile_code'] ?? ''); if ($selectedCode !== '') { $this->hydrateImportSetupState($selectedCode, (string) (($this->getImportFormState())['path_root'] ?? '/mnt/gescon-archives/gescon')); } } public function caricaSetupDaArchivio(): void { $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; } $legacyRow = $this->resolveLegacyStabileRow($code, $path); $stabile = $this->resolveSelectedStabileByCode($code); if ($legacyRow === null && ! $stabile instanceof Stabile) { Notification::make() ->title('Nessun dato disponibile') ->body('Non ho trovato dati legacy o locali per lo stabile selezionato.') ->danger() ->send(); return; } $this->hydrateImportSetupState($code, $path, $stabile, $legacyRow); Notification::make() ->title('Setup caricato') ->body('Dati stabile, banca e gestione caricati nella stessa schermata.') ->success() ->send(); } public function salvaSetupStabileImportazione(): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $amm = $this->resolveImportAmministratore($user, $this->getImportFormState()); if (! $amm instanceof Amministratore) { Notification::make()->title('Nessun amministratore disponibile')->danger()->send(); return; } try { $stabile = $this->upsertSelectedStabileFromImportState($amm); $this->normalizeImportedFinancialSetup($stabile); if ((bool) (($this->getImportFormState())['gestione_setup_enabled'] ?? true)) { $this->applyGestioneSetup($stabile); } } catch (\Throwable $e) { Notification::make() ->title('Salvataggio setup fallito') ->body($e->getMessage()) ->danger() ->send(); return; } StabileContext::setActiveStabileId($user, (int) $stabile->id); $this->hydrateImportSetupState($stabile->codice_stabile, (string) (($this->getImportFormState())['path_root'] ?? '/mnt/gescon-archives/gescon'), $stabile); Notification::make() ->title('Setup stabile salvato') ->body('Lo stabile viene aggiornato in-place dalla stessa schermata, senza creare duplicati.') ->success() ->send(); } public function salvaSetupGestioneImportazione(): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $amm = $this->resolveImportAmministratore($user, $this->getImportFormState()); if (! $amm instanceof Amministratore) { Notification::make()->title('Nessun amministratore disponibile')->danger()->send(); return; } try { $stabile = $this->upsertSelectedStabileFromImportState($amm); $count = $this->applyGestioneSetup($stabile); } catch (\Throwable $e) { Notification::make() ->title('Salvataggio gestione fallito') ->body($e->getMessage()) ->danger() ->send(); return; } $this->hydrateImportSetupState($stabile->codice_stabile, (string) (($this->getImportFormState())['path_root'] ?? '/mnt/gescon-archives/gescon'), $stabile); Notification::make() ->title('Gestione aggiornata') ->body('Gestioni create o aggiornate: ' . $count) ->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 = $this->resolveImportAmministratore($user, $state); if (! $amm) { Notification::make() ->title('Nessun amministratore disponibile') ->body('Serve almeno un record in tabella amministratori.') ->danger() ->send(); return; } try { $stabile = $this->upsertSelectedStabileFromImportState($amm); $this->normalizeImportedFinancialSetup($stabile); } catch (\Throwable $e) { Notification::make() ->title('Creazione o aggiornamento stabile fallito') ->body($e->getMessage()) ->danger() ->send(); return; } StabileContext::setActiveStabileId($user, (int) $stabile->id); $this->hydrateImportSetupState($code, (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'), $stabile); Notification::make() ->title('Stabile creato o aggiornato') ->body('I dati anagrafici, la banca principale e la cassa CON vengono riallineati direttamente da questa schermata.') ->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; } $amm = $this->resolveImportAmministratore($user, $state); if (! $amm) { Notification::make()->title('Nessun amministratore disponibile')->danger()->send(); return; } try { $stabile = $this->upsertSelectedStabileFromImportState($amm); } catch (\Throwable $e) { Notification::make() ->title('Preparazione stabile fallita') ->body($e->getMessage()) ->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), 'update_only' => (bool) ($state['update_only'] ?? true), ]; 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; // Sincronizzazione automatica Fornitori collegata try { $fornMdb = (string) ($state['fornitori_mdb'] ?? ''); if ($fornMdb === '') { $fornMdb = rtrim($options['path'], '/') . '/dbc/Fornitori.mdb'; } if (is_file($fornMdb)) { Artisan::call('gescon:auto-sync-fornitori', [ '--fornitori-mdb' => $fornMdb, '--path-root' => $options['path'], '--apply' => true, ]); } } catch (\Throwable $e) { // Silently skip or log } } 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 { $legacyYearArg = (string) ($options['anno'] ?? ''); if ((! empty($options['with_banche']) || ! empty($options['with_operazioni']) || ! empty($options['with_incassi'])) && ! empty($options['path']) && ! empty($code)) { foreach ($this->resolveLegacySingoloMdbPaths($code, (string) $options['path'], $legacyYearArg !== '' ? $legacyYearArg : null) as $mdbPath => $legacyDir) { Artisan::call('gescon:load-mdb', [ '--mdb' => $mdbPath, '--table' => 'anagr_casse', '--stabile' => $code, '--legacy-year' => $legacyDir, '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); } } if (! empty($options['with_operazioni']) && is_string($singoloMdb)) { Artisan::call('gescon:load-mdb', [ '--mdb' => $singoloMdb, '--table' => 's_cassa', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); Artisan::call('gescon:load-mdb', [ '--mdb' => $singoloMdb, '--table' => 'operazioni', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); Artisan::call('gescon:load-mdb', [ '--mdb' => $singoloMdb, '--table' => 'voc_spe', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); Artisan::call('gescon:load-mdb', [ '--mdb' => $singoloMdb, '--table' => 'tabelle', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--refresh-slice' => 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'] ?? ''), '--refresh-slice' => 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'] ?? ''), '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); Artisan::call('gescon:load-mdb', [ '--mdb' => $generaleMdb, '--table' => 'emes_det', '--stabile' => $code, '--legacy-year' => (string) ($options['anno'] ?? ''), '--refresh-slice' => 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; } if (! empty($options['update_only'])) { $params['--update-only'] = true; } Artisan::call('gescon:import-full', $params); $outputs[] = trim((string) Artisan::output()); } if (empty($options['dry'])) { $stabile->refresh(); $this->normalizeImportedFinancialSetup($stabile); $outputs[] = 'Normalizzazione cassa CON + banca principale completata'; if (! empty($options['with_gestioni'])) { $gestioniUpdated = $this->applyGestioneSetup($stabile); $outputs[] = 'Gestioni create o aggiornate: ' . $gestioniUpdated; } } $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(); if ($exit === 0 && ! $dryRun) { $stabile = $this->resolveSelectedStabileByCode($code); if ($stabile instanceof Stabile) { $this->normalizeImportedFinancialSetup($stabile); if ((bool) ($state['gestione_setup_enabled'] ?? true)) { $this->applyGestioneSetup($stabile); } $this->hydrateImportSetupState($code, $path, $stabile); } } } 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; } krsort($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($keys, 0, 2); } $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 = [$selectedYears[0]]; } $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 = isset($selectedYears[0]) ? [$selectedYears[0]] : []; } 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 resolveImportAmministratore(User $user, array $state): ?Amministratore { $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; } return $amm ?: Amministratore::query()->orderBy('id')->first(); } private function hydrateImportSetupState(string $code, string $path, ?Stabile $stabile = null, ?array $legacyRow = null): void { $normalizedCode = str_pad(trim($code), 4, '0', STR_PAD_LEFT); $stabile ??= $this->resolveSelectedStabileByCode($normalizedCode); $legacyRow ??= $this->resolveLegacyStabileRow($normalizedCode, $path); $ordMonths = $this->normalizeMonthValues( $stabile?->rate_ordinarie_mesi ?? $this->extractLegacyRateMonths($legacyRow ?? [], 'ord_rata_') ); $risMonths = $this->normalizeMonthValues( $stabile?->rate_riscaldamento_mesi ?? $this->extractLegacyRateMonths($legacyRow ?? [], 'ris_rata_') ); $config = $stabile?->getOperationalConfiguration() ?? []; $this->data = array_merge($this->data ?? [], [ 'stabile_denominazione' => (string) ($stabile?->denominazione ?: ($this->firstNonEmpty($legacyRow ?? [], ['denominazione']) ?? '')), 'stabile_indirizzo' => (string) ($stabile?->indirizzo ?: ($this->firstNonEmpty($legacyRow ?? [], ['indirizzo']) ?? '')), 'stabile_cap' => (string) ($stabile?->cap ?: ($this->firstNonEmpty($legacyRow ?? [], ['cap']) ?? '')), 'stabile_citta' => (string) ($stabile?->citta ?: ($this->firstNonEmpty($legacyRow ?? [], ['citta']) ?? '')), 'stabile_provincia' => (string) ($stabile?->provincia ?: ($this->firstNonEmpty($legacyRow ?? [], ['provincia']) ?? '')), 'stabile_codice_fiscale' => (string) ($stabile?->codice_fiscale ?: ($this->firstNonEmpty($legacyRow ?? [], ['codice_fiscale_cond', 'codice_fiscale', 'cf']) ?? '')), 'stabile_banca_principale' => (string) ($stabile?->banca_principale ?: ($this->firstNonEmpty($legacyRow ?? [], ['denominazione_banca', 'banca', 'banca_principale']) ?? '')), 'stabile_iban_principale' => (string) ($stabile?->iban_principale ?: ($this->normalizeIbanValue($this->firstNonEmpty($legacyRow ?? [], ['iban_principale', 'iban_banca', 'iban'])) ?? '')), 'gestione_anno_iniziale' => $this->resolveDefaultGestioneYear($stabile), 'gestione_mostra_riscaldamento' => (bool) ($config['mostra_riscaldamento'] ?? ($risMonths !== [])), 'gestione_mesi_ordinaria' => $ordMonths, 'gestione_mesi_riscaldamento' => $risMonths, ]); } private function upsertSelectedStabileFromImportState(Amministratore $amm): Stabile { $state = $this->getImportFormState(); $code = trim((string) ($state['stabile_code'] ?? '')); if ($code === '') { throw new \RuntimeException('Seleziona uno stabile prima di salvare il setup.'); } $code = str_pad($code, 4, '0', STR_PAD_LEFT); $legacyRow = $this->resolveLegacyStabileRow($code, (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon')); $stabile = $this->resolveSelectedStabileByCode($code, (int) $amm->id) ?? new Stabile(); $denominazione = trim((string) ($state['stabile_denominazione'] ?? '')); if ($denominazione === '') { $denominazione = trim((string) ($this->firstNonEmpty($legacyRow ?? [], ['denominazione']) ?? '')); } if ($denominazione === '') { $label = $this->stabiliOptions[$code] ?? $code; $denominazione = trim((string) preg_replace('/^' . preg_quote($code, '/') . '\s+—\s+/u', '', $label)); } if ($denominazione === '' || $denominazione === $code) { $denominazione = 'Stabile ' . $code; } $ordMonths = $this->normalizeMonthValues((array) ($state['gestione_mesi_ordinaria'] ?? $this->extractLegacyRateMonths($legacyRow ?? [], 'ord_rata_'))); $risMonths = $this->normalizeMonthValues((array) ($state['gestione_mesi_riscaldamento'] ?? $this->extractLegacyRateMonths($legacyRow ?? [], 'ris_rata_'))); $config = $stabile->getOperationalConfiguration(); $config['mostra_riscaldamento'] = (bool) ($state['gestione_mostra_riscaldamento'] ?? false); $stabile->fill([ 'amministratore_id' => (int) $amm->id, 'codice_stabile' => $code, 'denominazione' => $denominazione, 'indirizzo' => trim((string) ($state['stabile_indirizzo'] ?? ($this->firstNonEmpty($legacyRow ?? [], ['indirizzo']) ?? ''))), 'cap' => trim((string) ($state['stabile_cap'] ?? ($this->firstNonEmpty($legacyRow ?? [], ['cap']) ?? ''))), 'citta' => trim((string) ($state['stabile_citta'] ?? ($this->firstNonEmpty($legacyRow ?? [], ['citta']) ?? ''))), 'provincia' => trim((string) ($state['stabile_provincia'] ?? ($this->firstNonEmpty($legacyRow ?? [], ['provincia']) ?? ''))), 'codice_fiscale' => trim((string) ($state['stabile_codice_fiscale'] ?? ($this->firstNonEmpty($legacyRow ?? [], ['codice_fiscale_cond', 'codice_fiscale', 'cf']) ?? ''))), 'banca_principale' => trim((string) ($state['stabile_banca_principale'] ?? ($this->firstNonEmpty($legacyRow ?? [], ['denominazione_banca', 'banca', 'banca_principale']) ?? ''))), 'iban_principale' => $this->normalizeIbanValue($state['stabile_iban_principale'] ?? ($this->firstNonEmpty($legacyRow ?? [], ['iban_principale', 'iban_banca', 'iban']) ?? null)), 'iban_condominio' => $this->normalizeIbanValue($state['stabile_iban_principale'] ?? ($this->firstNonEmpty($legacyRow ?? [], ['iban_principale', 'iban_banca', 'iban']) ?? null)), 'rate_ordinarie_mesi' => $ordMonths, 'rate_riscaldamento_mesi' => $config['mostra_riscaldamento'] ? $risMonths : [], 'configurazione_avanzata' => $config, 'stato' => 'attivo', 'attivo' => true, ]); $stabile->save(); $stabile->syncRubricaContatto(); return $stabile->fresh(); } private function applyGestioneSetup(Stabile $stabile): int { $state = $this->getImportFormState(); $replicaTuttiAnni = (bool) ($state['gestione_replica_tutti_anni'] ?? true); $mostraRiscald = (bool) ($state['gestione_mostra_riscaldamento'] ?? false); $ordMonths = $this->normalizeMonthValues((array) ($state['gestione_mesi_ordinaria'] ?? [])); $risMonths = $this->normalizeMonthValues((array) ($state['gestione_mesi_riscaldamento'] ?? [])); $years = $this->resolveGestioneTargetYears($stabile, $replicaTuttiAnni, (int) ($state['gestione_anno_iniziale'] ?? date('Y'))); $updated = 0; foreach ($years as $year) { $this->upsertGestioneRecord($stabile, $year, 'ordinaria', $ordMonths, true); $updated++; if ($mostraRiscald && $risMonths !== []) { $this->upsertGestioneRecord($stabile, $year, 'riscaldamento', $risMonths, true); $updated++; continue; } $existingHeating = GestioneContabile::query() ->where('stabile_id', $stabile->id) ->where('anno_gestione', $year) ->where('tipo_gestione', 'riscaldamento') ->first(); if ($existingHeating instanceof GestioneContabile) { $existingHeating->mesi_rate_riscaldamento = []; $existingHeating->gestione_attiva = false; $existingHeating->save(); } } $config = $stabile->getOperationalConfiguration(); $config['mostra_riscaldamento'] = $mostraRiscald; $stabile->configurazione_avanzata = $config; $stabile->rate_ordinarie_mesi = $ordMonths; $stabile->rate_riscaldamento_mesi = $mostraRiscald ? $risMonths : []; $stabile->save(); return $updated; } private function upsertGestioneRecord(Stabile $stabile, int $year, string $tipo, array $months, bool $active): void { $gestione = GestioneContabile::query()->firstOrNew([ 'tenant_id' => 'default', 'stabile_id' => $stabile->id, 'anno_gestione' => $year, 'tipo_gestione' => $tipo, ]); $gestione->denominazione = ucfirst($tipo) . ' ' . $year; $gestione->data_inizio = sprintf('%04d-01-01', $year); $gestione->data_fine = sprintf('%04d-12-31', $year); $gestione->stato = 'aperta'; $gestione->protocollo_prefix = strtoupper(substr($tipo, 0, 1)) . $year; $gestione->ultimo_protocollo = (int) ($gestione->ultimo_protocollo ?? 0); $gestione->gestione_attiva = $active; $gestione->codice_archivio_legacy = $stabile->codice_stabile; if ($tipo === 'ordinaria') { $gestione->mesi_rate_ordinaria = $months; } else { $gestione->mesi_rate_riscaldamento = $months; } $gestione->save(); } private function resolveGestioneTargetYears(Stabile $stabile, bool $replicaTuttiAnni, int $fallbackYear): array { $years = [$fallbackYear > 0 ? $fallbackYear : (int) date('Y')]; if ($replicaTuttiAnni) { $years = array_merge( $years, array_map('intval', array_keys($this->anniOptions)), GestioneContabile::query()->where('stabile_id', $stabile->id)->pluck('anno_gestione')->map(fn($value) => (int) $value)->all(), array_map('intval', (array) (($this->getImportFormState())['align_years_selected'] ?? [])), ); } $years = array_values(array_unique(array_filter($years, fn(int $value): bool => $value > 0))); sort($years); return $years; } private function normalizeImportedFinancialSetup(Stabile $stabile): void { try { $this->syncFinancialSetupFromLegacyAnagrCasse($stabile); } catch (\Throwable) { // Non bloccare l'import per la normalizzazione dei dati bancari. } } private function syncFinancialSetupFromLegacyAnagrCasse(Stabile $stabile): void { $state = $this->getImportFormState(); $rows = $this->resolveLegacyAnagrCasseRows($stabile->codice_stabile, (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon')); if ($rows === []) { return; } $legacyStabile = $this->resolveLegacyStabileRow($stabile->codice_stabile, (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon')); $primaryIban = $this->normalizeIbanValue($stabile->iban_principale ?: ($legacyStabile['iban_principale'] ?? null)); $primaryBank = trim((string) ($stabile->banca_principale ?: ($legacyStabile['denominazione_banca'] ?? ''))); $seenCodes = []; $markedPrimary = DatiBancari::query()->where('stabile_id', $stabile->id)->where('is_nostro_conto', true)->exists(); foreach ($rows as $row) { $codCassa = strtoupper(trim((string) ($row['codice'] ?? ($row['cod_cassa'] ?? '')))); if ($codCassa === '' || isset($seenCodes[$codCassa])) { continue; } $seenCodes[$codCassa] = true; $descrizione = trim((string) ($row['descrizione'] ?? $codCassa)); $iban = $this->normalizeIbanValue($row['iban'] ?? ($row['iban_banca'] ?? null)); if ($iban === null && $codCassa !== 'CON') { $iban = $primaryIban; } if ($codCassa !== 'CON' && $primaryBank !== '') { $descrizione = $primaryBank; } if ($iban === null) { $existsCassa = DB::table('casse')->where('stabile_id', $stabile->id)->where('cod_cassa', $codCassa)->first(); $payloadCassa = [ 'tenant_id' => null, 'stabile_id' => $stabile->id, 'cod_cassa' => $codCassa, 'descrizione' => $descrizione, 'tipo' => $codCassa === 'CON' ? 'cassa_contanti' : 'altro', 'iban' => null, 'intestazione_conto' => trim((string) ($stabile->denominazione ?: ('Stabile ' . $stabile->codice_stabile))), 'attiva' => true, 'meta' => json_encode(['source' => 'Anagr_casse', 'legacy' => $row]), 'updated_at' => now(), ]; if (! $existsCassa) { $payloadCassa['created_at'] = now(); DB::table('casse')->insert($payloadCassa); } else { DB::table('casse')->where('id', $existsCassa->id)->update($payloadCassa); } continue; } DB::table('casse') ->where('stabile_id', $stabile->id) ->where('cod_cassa', $codCassa) ->delete(); $account = DatiBancari::query() ->where('stabile_id', $stabile->id) ->where(function ($query) use ($codCassa, $iban): void { $query->where('legacy_cod_cassa', $codCassa) ->orWhere('iban', $iban); }) ->orderByDesc('id') ->first(); if (! $account instanceof DatiBancari) { $account = new DatiBancari(); $account->stabile_id = $stabile->id; } $shouldMarkPrimary = ! $markedPrimary || ($primaryIban !== null && $iban === $primaryIban); $account->tipo_conto = 'corrente'; $account->denominazione_banca = $descrizione; $account->numero_conto = $codCassa; $account->iban = $iban; $account->intestazione_conto = trim((string) ($stabile->denominazione ?: ('Stabile ' . $stabile->codice_stabile))); $account->stato_conto = 'attivo'; $account->is_nostro_conto = $shouldMarkPrimary; $account->legacy_cod_cassa = $codCassa; $account->save(); if ($shouldMarkPrimary) { $markedPrimary = true; } } DB::table('casse') ->where('stabile_id', $stabile->id) ->whereNotIn('cod_cassa', array_keys($seenCodes)) ->whereIn('cod_cassa', ['CDL']) ->delete(); } private function resolveLegacyStabileRow(string $code, string $path): ?array { $normalizedCode = str_pad(trim($code), 4, '0', STR_PAD_LEFT); try { $row = DB::connection('gescon_import') ->table('stabili') ->where('cod_stabile', $normalizedCode) ->first(); if ($row) { return array_change_key_case((array) $row, CASE_LOWER); } } catch (\Throwable) { // fallback MDB diretto } $state = $this->getImportFormState(); $mdb = trim((string) ($state['stabili_mdb'] ?? '')); if ($mdb === '') { $mdb = rtrim($path, '/') . '/dbc/Stabili.mdb'; } if (! is_file($mdb) || ! is_readable($mdb)) { return null; } foreach ($this->mdbExportToRows($mdb, ['Stabili', 'stabili', 'Condomin', 'condomin']) as $row) { $rowCode = $this->firstNonEmpty($row, ['cod_stabile', 'codice_stabile', 'stabile', 'codice', 'cod']); if ($rowCode === null) { continue; } if (str_pad(trim((string) $rowCode), 4, '0', STR_PAD_LEFT) === $normalizedCode) { return $row; } } return null; } /** @return array> */ private function resolveLegacyAnagrCasseRows(string $code, string $path): array { $normalizedCode = str_pad(trim($code), 4, '0', STR_PAD_LEFT); $rows = []; try { if (Schema::connection('gescon_import')->hasTable('anag_casse')) { $rows = DB::connection('gescon_import') ->table('anag_casse') ->where('cod_stabile', $normalizedCode) ->orderBy('legacy_year') ->get() ->map(fn($row): array=> array_change_key_case((array) $row, CASE_LOWER)) ->all(); } } catch (\Throwable) { $rows = []; } if ($rows === []) { foreach ($this->resolveLegacySingoloMdbPaths($normalizedCode, $path) as $mdbPath => $legacyDir) { foreach ($this->mdbExportToRows($mdbPath, ['Anagr_casse', 'anagr_casse', 'ANAGR_CASSE']) as $row) { $row['legacy_year'] = $legacyDir; $rows[] = $row; } } } $dedup = []; foreach ($rows as $row) { $codeKey = strtoupper(trim((string) ($row['codice'] ?? ($row['cod_cassa'] ?? '')))); if ($codeKey === '') { continue; } if (! isset($dedup[$codeKey])) { $dedup[$codeKey] = $row; continue; } $current = $dedup[$codeKey]; $currentScore = $this->scoreLegacyAnagrCasseRow($current); $candidateScore = $this->scoreLegacyAnagrCasseRow($row); if ($candidateScore >= $currentScore) { $dedup[$codeKey] = array_merge($current, array_filter($row, fn($value) => $value !== null && $value !== '')); } } return array_values($dedup); } /** @return array */ private function resolveLegacySingoloMdbPaths(string $code, string $path, ?string $onlyLegacyDir = null): array { $stableDir = rtrim($path, '/') . '/' . str_pad(trim($code), 4, '0', STR_PAD_LEFT); if (! is_dir($stableDir)) { return []; } $paths = []; foreach ((array) glob($stableDir . '/*/singolo_anno.mdb') as $mdbPath) { $legacyDir = basename(dirname($mdbPath)); if ($onlyLegacyDir !== null && trim($onlyLegacyDir) !== '' && $legacyDir !== trim($onlyLegacyDir)) { continue; } $paths[$mdbPath] = $legacyDir; } ksort($paths); return $paths; } private function scoreLegacyAnagrCasseRow(array $row): int { $score = 0; foreach (['descrizione', 'iban', 'iban_banca', 'codice', 'cod_cassa'] as $field) { $value = trim((string) ($row[$field] ?? '')); if ($value !== '') { $score++; } } return $score; } private function resolveDefaultGestioneYear(?Stabile $stabile): int { $state = $this->getImportFormState(); $anno = (int) ($state['gestione_anno_iniziale'] ?? 0); if ($anno > 0) { return $anno; } $selected = array_values(array_filter(array_map('intval', (array) ($state['align_years_selected'] ?? [])))); if ($selected !== []) { sort($selected); return (int) end($selected); } if ($this->anniOptions !== []) { $years = array_map('intval', array_keys($this->anniOptions)); sort($years); return (int) end($years); } if ($stabile instanceof Stabile) { $gestioneYear = (int) GestioneContabile::query()->where('stabile_id', $stabile->id)->max('anno_gestione'); if ($gestioneYear > 0) { return $gestioneYear; } } return (int) date('Y'); } private function extractLegacyRateMonths(array $legacyRow, string $prefix): array { $months = []; foreach (range(1, 12) as $month) { $key = strtolower($prefix . $month); $value = $legacyRow[$key] ?? null; if ($value === null) { continue; } $normalized = strtolower(trim((string) $value)); if ($normalized !== '' && ! in_array($normalized, ['0', 'false', 'no', 'off', 'n'], true)) { $months[] = $month; } } return $months; } private function normalizeMonthValues(mixed $values): array { if (! is_array($values)) { $values = (array) $values; } $months = array_values(array_unique(array_filter(array_map( static fn($value): int => (int) $value, $values, ), static fn(int $value): bool => $value >= 1 && $value <= 12))); sort($months); return $months; } private function normalizeIbanValue(mixed $value): ?string { $normalized = strtoupper(preg_replace('/\s+/', '', trim((string) ($value ?? '')))); return $normalized !== '' ? $normalized : null; } private function resolveSelectedStabileByCode(?string $code, ?int $amministratoreId = null): ?Stabile { $normalized = trim((string) $code); if ($normalized === '') { return null; } if ($amministratoreId === null) { $state = $this->getImportFormState(); $amministratoreId = (int) ($state['amministratore_id'] ?? 0); } $trimmed = ltrim($normalized, '0'); return Stabile::query() ->when($amministratoreId > 0, fn($query) => $query->where('amministratore_id', $amministratoreId)) ->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; } }