netgescon-day0/app/Filament/Pages/Gescon/ImportazioneArchivi.php

2387 lines
97 KiB
PHP
Executable File

<?php
namespace App\Filament\Pages\Gescon;
use App\Models\Amministratore;
use App\Models\DatiBancari;
use App\Models\GestioneContabile;
use App\Models\Stabile;
use App\Models\User;
use App\Services\GesconImport\EssentialImportService;
use App\Services\GesconImport\StabileEnrichmentService;
use App\Support\ModuleVisibility;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use UnitEnum;
class ImportazioneArchivi extends Page implements HasForms
{
use InteractsWithForms;
protected static ?string $navigationLabel = 'Importazione archivi';
protected static ?string $title = 'Importazione archivi (GESCON)';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-arrow-down-tray';
protected static UnitEnum|string|null $navigationGroup = 'GESCON';
protected static ?int $navigationSort = 5;
protected static ?string $slug = 'gescon/importazione-archivi';
protected string $view = 'filament.pages.gescon.importazione-archivi';
public ?array $data = [];
/** @var array<string, string> */
public array $stabiliOptions = [];
/** @var array<string, string> */
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: <cod_stabile>/<anno>/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 <root>/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 <stabile>/<anno>/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<string,mixed> */
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<int,array<string,mixed>> */
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<string,string> */
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<int,string>} */
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<string,mixed> */
private function getImportFormState(): array
{
$state = is_array($this->data) ? $this->data : [];
$state['align_years_selected'] = $this->normalizeSelectedOptionValues(
(array) ($state['align_years_selected'] ?? []),
$this->anniOptions,
);
$state['align_water_years_selected'] = $this->normalizeSelectedOptionValues(
(array) ($state['align_water_years_selected'] ?? []),
$this->anniOptions,
);
$state['align_open_years_selected'] = $this->normalizeSelectedOptionValues(
(array) ($state['align_open_years_selected'] ?? []),
$this->anniOptions,
);
return $state;
}
/**
* @param array<int|string,mixed> $values
* @param array<string,string> $options
* @return array<int,string>
*/
private function normalizeSelectedOptionValues(array $values, array $options): array
{
$normalized = array_values(array_filter(array_map(
static fn($value): string => trim((string) $value),
$values,
), static fn(string $value): bool => $value !== ''));
if ($options === []) {
return $normalized;
}
$allowed = array_map('strval', array_keys($options));
return array_values(array_filter($normalized, static fn(string $value): bool => in_array($value, $allowed, true)));
}
private function firstNonEmpty(array $row, array $keys): mixed
{
foreach ($keys as $k) {
$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;
}
}