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

1759 lines
69 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Filament\Pages\Gescon;
use App\Models\Amministratore;
use App\Models\Stabile;
use App\Models\User;
use App\Services\GesconImport\EssentialImportService;
use App\Services\GesconImport\StabileEnrichmentService;
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
{
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore'])
&& $user->can('gescon-import.execute');
}
public function mount(): void
{
$user = Auth::user();
$defaultPathRoot = '/mnt/gescon-archives/gescon';
$defaultStabileCode = null;
$defaultAnno = null;
$activeStabile = $user instanceof User ? StabileContext::getActiveStabile($user) : null;
if ($activeStabile) {
$rawCode = $activeStabile->codice_stabile ?? null;
if (is_string($rawCode) && $rawCode !== '') {
$defaultStabileCode = str_pad(trim($rawCode), 4, '0', STR_PAD_LEFT);
$label = $defaultStabileCode . ' — ' . trim((string) ($activeStabile->denominazione ?? ''));
$this->stabiliOptions = [$defaultStabileCode => trim($label, ' —')];
try {
$defaultAnno = DB::connection('gescon_import')
->table('anni_gestione')
->where('cod_stabile', $defaultStabileCode)
->max('anno');
} catch (\Throwable) {
$defaultAnno = null;
}
}
}
$defaultAmministratoreId = null;
if ($user instanceof User && $user->amministratore) {
$defaultAmministratoreId = (int) $user->amministratore->id;
} elseif ($user instanceof User && $user->hasAnyRole(['super-admin', 'admin'])) {
$defaultAmministratoreId = (int) (Amministratore::query()->orderBy('id')->value('id') ?? 0);
}
$this->getSchema('form')?->fill([
'source_mode' => 'linux_server',
'path_root' => $defaultPathRoot,
'stabili_mdb' => rtrim($defaultPathRoot, '/') . '/dbc/Stabili.mdb',
'fornitori_mdb' => rtrim($defaultPathRoot, '/') . '/dbc/Fornitori.mdb',
'amministratore_id' => $defaultAmministratoreId ?: null,
'stabile_code' => $defaultStabileCode,
'anno' => $defaultAnno,
'align_years_selected' => ['2024', '2025'],
'align_water_years_selected' => ['2024'],
'align_open_years_selected' => ['2024', '2025'],
'align_mark_obsolete_years' => false,
'align_with_f24' => true,
'with_unita' => true,
'with_soggetti' => true,
'with_diritti' => true,
'with_millesimi' => true,
'with_voci' => true,
'with_banche' => true,
'with_gestioni' => false,
'with_assemblee' => false,
'with_operazioni' => false,
'with_rate' => false,
'with_incassi' => false,
'with_giroconti' => false,
'with_straord' => false,
'with_addebiti' => false,
'with_detrazioni' => false,
'sync_staging' => true,
'dry_run' => false,
]);
if (is_string($defaultStabileCode) && $defaultStabileCode !== '') {
$this->refreshAnniOptionsForStabile($defaultStabileCode, $defaultPathRoot);
}
}
private function extractGesconZipToTemp(string $zipPath): ?string
{
if (! is_file($zipPath) || ! is_readable($zipPath)) {
return null;
}
if (! class_exists(\ZipArchive::class)) {
throw new \RuntimeException('Estrazione ZIP non disponibile: estensione PHP zip mancante.');
}
$base = storage_path('app/temp/gescon-ui-extracted');
if (! is_dir($base)) {
@mkdir($base, 0775, true);
}
$target = $base . '/' . date('Ymd_His') . '_' . bin2hex(random_bytes(4));
@mkdir($target, 0775, true);
$zip = new \ZipArchive();
if ($zip->open($zipPath) !== true) {
return null;
}
$zip->extractTo($target);
$zip->close();
// Se lo zip contiene una sola cartella root, entra automaticamente.
$entries = array_values(array_filter(scandir($target) ?: [], fn($n) => ! in_array($n, ['.', '..'], true)));
if (count($entries) === 1) {
$only = $target . '/' . $entries[0];
if (is_dir($only)) {
return $only;
}
}
return $target;
}
public function form(Schema $schema): Schema
{
return $schema
->statePath('data')
->components([
Select::make('source_mode')
->label('Modalità caricamento')
->options([
'linux_server' => 'Linux / Server (path su disco)',
'windows_upload' => 'Windows / PC (carica file e cartelle)',
])
->required()
->dehydrated(),
TextInput::make('path_root')
->label('Percorso archivio GESCON (root)')
->helperText('Esempio: /mnt/gescon-archives/gescon. Dentro: <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;
}
$amm = Amministratore::query()->find($ammId);
if (! $amm) {
return;
}
$code = trim((string) ($amm->codice_univoco ?: $amm->codice_amministratore ?: ''));
if ($code === '') {
$code = 'ID' . $amm->id;
}
$safe = preg_replace('/[^A-Za-z0-9_-]+/', '_', $code);
$safe = trim((string) $safe, '_');
if ($safe === '') {
$safe = 'ID' . $amm->id;
}
$root = '/mnt/gescon-archives/gescon/' . $safe;
$set('path_root', $root);
$set('stabili_mdb', $root . '/dbc/Stabili.mdb');
$set('fornitori_mdb', $root . '/dbc/Fornitori.mdb');
})
->visible(function (): bool {
$user = Auth::user();
return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin']);
}),
Select::make('stabile_code')
->label('Stabile da importare')
->options(fn(): array=> $this->stabiliOptions)
->searchable()
->reactive()
->afterStateUpdated(function (Set $set, $get, ?string $state): void {
$root = (string) ($get('path_root') ?? '/mnt/gescon-archives/gescon');
$this->refreshAnniOptionsForStabile((string) ($state ?? ''), $root, $set, $get);
})
->helperText('Prima premi “Carica elenco stabili da Stabili.mdb”, poi seleziona lo stabile.'),
TextInput::make('anno')
->label('Anno gestione (opzionale)')
->helperText('Inserisci un anno (es. 2025) o "all" per tutte le cartelle. Serve per trovare <stabile>/<anno>/singolo_anno.mdb quando lo staging non è già caricato.')
->dehydrated(),
Select::make('align_years_selected')
->label('Anni contabili target (comando unico)')
->options(fn(): array=> $this->anniOptions)
->multiple()
->searchable()
->live()
->dehydrateStateUsing(fn($state): array=> $this->normalizeSelectedOptionValues((array) ($state ?? []), $this->anniOptions))
->validationMessages(['in' => 'Seleziona anni validi dall elenco aggiornato.'])
->helperText('Seleziona uno o piu anni. Le directory legacy vengono mappate automaticamente da tabella anni (nome_dir).')
->dehydrated(),
Select::make('align_water_years_selected')
->label('Anni acqua target')
->options(fn(): array=> $this->anniOptions)
->multiple()
->searchable()
->live()
->dehydrateStateUsing(fn($state): array=> $this->normalizeSelectedOptionValues((array) ($state ?? []), $this->anniOptions))
->validationMessages(['in' => 'Seleziona anni acqua validi dall elenco aggiornato.'])
->helperText('Per ACQUA (acqua_gen/acqua_dett/acqua_fatture).')
->dehydrated(),
Select::make('align_open_years_selected')
->label('Anni da tenere aperti')
->options(fn(): array=> $this->anniOptions)
->multiple()
->searchable()
->live()
->dehydrateStateUsing(fn($state): array=> $this->normalizeSelectedOptionValues((array) ($state ?? []), $this->anniOptions))
->validationMessages(['in' => 'Seleziona anni aperti validi dall elenco aggiornato.'])
->helperText('Gli anni fuori da questa selezione vengono chiusi secondo le regole di import.')
->dehydrated(),
Checkbox::make('align_mark_obsolete_years')
->label('Marca subito come obsoleti/chiusi gli anni importati')
->helperText('Utile per import storico a fini archivio.'),
Checkbox::make('align_with_f24')
->label('Importa anche modello F24 da generale_stabile.mdb')
->helperText('Legge F24.cod_tributo_1..6, Rateiz_1..6, Anno_1..6 e importi correlati.'),
Checkbox::make('with_unita')->label('Unità'),
Checkbox::make('with_soggetti')->label('Anagrafiche (soggetti)'),
Checkbox::make('with_diritti')->label('Diritti'),
Checkbox::make('with_millesimi')->label('Millesimi'),
Checkbox::make('with_voci')
->label('Voci')
->reactive()
->afterStateUpdated(function (Set $set, $state): void {
if ($state) {
$set('with_millesimi', true);
}
}),
Checkbox::make('with_banche')->label('Banche / casse'),
Checkbox::make('with_gestioni')->label('Gestioni (anni)'),
Checkbox::make('with_assemblee')->label('Assemblee legacy'),
Checkbox::make('with_operazioni')
->label('Operazioni')
->reactive()
->afterStateUpdated(function (Set $set, $state): void {
if ($state) {
$set('with_voci', true);
$set('with_millesimi', true);
$set('with_banche', true);
$set('with_unita', true);
$set('with_soggetti', true);
$set('with_diritti', true);
}
}),
Checkbox::make('with_rate')->label('Rate'),
Checkbox::make('with_incassi')
->label('Incassi')
->reactive()
->afterStateUpdated(function (Set $set, $state): void {
if ($state) {
$set('with_rate', true);
$set('with_banche', true);
}
}),
Checkbox::make('with_giroconti')->label('Giroconti'),
Checkbox::make('with_straord')->label('Straordinari'),
Checkbox::make('with_addebiti')->label('Addebiti'),
Checkbox::make('with_detrazioni')->label('Detrazioni fiscali'),
Checkbox::make('sync_staging')->label('Aggiorna staging prima di importare'),
Checkbox::make('dry_run')->label('Dry-run (non scrive nel DB)'),
]);
}
public function loadStabili(): void
{
$state = $this->getImportFormState();
$mdb = (string) ($state['stabili_mdb'] ?? '');
if ($mdb === '' || ! is_file($mdb) || ! is_readable($mdb)) {
Notification::make()
->title('Stabili.mdb non disponibile')
->body('Verifica percorso e che il mount sia attivo: ' . $mdb)
->danger()
->send();
return;
}
$rows = $this->mdbExportToRows($mdb, ['Stabili', 'stabili', 'Condomin', 'condomin']);
if (empty($rows)) {
Notification::make()
->title('Nessun record letto da Stabili.mdb')
->body('Controlla che mdbtools sia installato e che il file sia valido.')
->danger()
->send();
return;
}
$opts = [];
foreach ($rows as $r) {
$code = $this->firstNonEmpty($r, ['cod_stabile', 'codice_stabile', 'stabile', 'codice', 'cod']);
if (! $code) {
continue;
}
$code = str_pad(trim((string) $code), 4, '0', STR_PAD_LEFT);
$denom = $this->firstNonEmpty($r, ['denominazione', 'descrizione', 'desc', 'nome', 'ragione_sociale']);
$label = $code . ($denom ? (' — ' . trim((string) $denom)) : '');
$opts[$code] = $label;
}
if (empty($opts)) {
Notification::make()
->title('Elenco stabili vuoto')
->danger()
->send();
return;
}
ksort($opts);
$this->stabiliOptions = $opts;
$state = $this->getImportFormState();
if (empty($state['stabile_code'])) {
$active = Auth::user() instanceof User ? StabileContext::getActiveStabile(Auth::user()) : null;
$activeCode = $active?->codice_stabile ?? null;
if (is_string($activeCode) && $activeCode !== '') {
$activeCode = str_pad(trim($activeCode), 4, '0', STR_PAD_LEFT);
if (isset($opts[$activeCode])) {
$state['stabile_code'] = $activeCode;
$this->getSchema('form')?->fill($state);
}
}
}
$state = $this->getImportFormState();
$this->refreshAnniOptionsForStabile(
(string) ($state['stabile_code'] ?? ''),
(string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon')
);
Notification::make()
->title('Elenco stabili caricato')
->body('Trovati: ' . count($opts))
->success()
->send();
}
public function creaStabileDaMdb(): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
$state = $this->getImportFormState();
$code = trim((string) ($state['stabile_code'] ?? ''));
if ($code === '') {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$state = $this->getImportFormState();
$amm = null;
if ($user->hasAnyRole(['super-admin', 'admin'])) {
$ammId = (int) ($state['amministratore_id'] ?? 0);
$amm = $ammId > 0 ? Amministratore::query()->find($ammId) : null;
} else {
$amm = $user->amministratore;
}
if (! $amm) {
$amm = Amministratore::query()->orderBy('id')->first();
}
if (! $amm) {
Notification::make()
->title('Nessun amministratore disponibile')
->body('Serve almeno un record in tabella amministratori.')
->danger()
->send();
return;
}
$stabile = Stabile::query()->where('codice_stabile', $code)->first();
if (! $stabile) {
$label = $this->stabiliOptions[$code] ?? $code;
$denom = trim((string) preg_replace('/^' . preg_quote($code, '/') . '\s+—\s+/u', '', $label));
if ($denom === '' || $denom === $code) {
$denom = 'Stabile ' . $code;
}
$stabile = Stabile::query()->create([
'amministratore_id' => $amm->id,
'codice_stabile' => $code,
'denominazione' => $denom,
'stato' => 'attivo',
'attivo' => true,
]);
Notification::make()
->title('Stabile creato')
->body('Codice: ' . $code)
->success()
->send();
} else {
// Se un admin/super-admin ha selezionato un amministratore diverso, riallinea per evitare "sparizioni" da filtri tenant.
if ($user->hasAnyRole(['super-admin', 'admin']) && (int) $stabile->amministratore_id !== (int) $amm->id) {
$stabile->amministratore_id = (int) $amm->id;
$stabile->save();
Notification::make()
->title('Stabile riassegnato')
->body('Ora lo stabile è assegnato allamministratore selezionato.')
->warning()
->send();
}
Notification::make()
->title('Stabile già presente')
->body('Userò lo stabile esistente: ' . $code)
->success()
->send();
}
StabileContext::setActiveStabileId($user, (int) $stabile->id);
Notification::make()
->title('Stabile impostato come attivo')
->body('Ora puoi eseguire l\'import selezionato o proseguire da Elenco stabili.')
->success()
->send();
}
public function applicaEnrichmentStabile(): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
$state = $this->getImportFormState();
$code = trim((string) ($state['stabile_code'] ?? ''));
$path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon');
if ($code === '') {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
try {
$res = (new StabileEnrichmentService())->enrich($code, $path);
} catch (\Throwable $e) {
Notification::make()
->title('Enrichment fallito')
->body($e->getMessage())
->danger()
->send();
return;
}
$body = 'Stabile: ' . $code
. ' | catasto=' . (! empty($res['catasto']) ? 'OK' : '-')
. ' | rate=' . (! empty($res['rate_flags']) ? 'OK' : '-')
. ' | banca/posta=' . (! empty($res['banca_posta']) ? 'OK' : '-');
Notification::make()
->title('Enrichment completato')
->body($body)
->success()
->send();
}
public function aggiornaAnagraficheDaCondomin(): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
$state = $this->getImportFormState();
$code = trim((string) ($state['stabile_code'] ?? ''));
if ($code === '') {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$dryRun = (bool) ($state['dry_run'] ?? false);
$params = [
'--stabile' => $code,
'--limit' => 20000,
'--only' => 'both',
'--link-ruolo' => true,
'--include-comproprietari' => true,
];
if (! $dryRun) {
$params['--apply'] = true;
}
Artisan::call('gescon:auto-sync-anagrafiche', $params);
$out = trim((string) Artisan::output());
$lines = $out === '' ? [] : preg_split('/\r\n|\r|\n/', $out);
$tail = $lines ? implode("\n", array_slice($lines, -8)) : 'Comando eseguito.';
Notification::make()
->title($dryRun ? 'Dry-run anagrafiche (condomin) completato' : 'Sync anagrafiche (condomin) completato')
->body($tail)
->success()
->send();
}
public function risincronizzaRelazioniImportate(): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
$state = $this->getImportFormState();
$code = trim((string) ($state['stabile_code'] ?? ''));
if ($code === '') {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$stabile = $this->resolveSelectedStabileByCode($code);
if (! $stabile instanceof Stabile) {
Notification::make()->title('Stabile locale non trovato')->body('Prima crea o apri lo stabile selezionato.')->danger()->send();
return;
}
$result = $this->runRelationSyncSequence($code, (int) $stabile->id, (bool) ($state['dry_run'] ?? false));
if (! $result['ok']) {
Notification::make()
->title('Risincronizzazione relazioni fallita')
->body((string) ($result['message'] ?? 'Errore non specificato.'))
->danger()
->send();
return;
}
Notification::make()
->title((bool) ($state['dry_run'] ?? false) ? 'Dry-run relazioni completato' : 'Risincronizzazione relazioni completata')
->body((string) ($result['message'] ?? 'Operazione completata'))
->success()
->send();
}
public function aggiornaFornitoriGescon(): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
$state = $this->getImportFormState();
$path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon');
$amm = null;
if ($user->hasAnyRole(['super-admin', 'admin'])) {
$ammId = (int) ($state['amministratore_id'] ?? 0);
$amm = $ammId > 0 ? Amministratore::query()->find($ammId) : null;
} else {
$amm = $user->amministratore;
}
if (! $amm) {
$amm = Amministratore::query()->orderBy('id')->first();
}
if (! $amm) {
Notification::make()->title('Nessun amministratore disponibile')->danger()->send();
return;
}
$mdb = (string) ($state['fornitori_mdb'] ?? '');
if ($mdb === '') {
$mdb = rtrim($path, '/') . '/dbc/Fornitori.mdb';
}
if (! is_file($mdb)) {
Notification::make()
->title('Fornitori.mdb non trovato')
->body('Percorso atteso: ' . $mdb)
->danger()
->send();
return;
}
try {
$exit = Artisan::call('gescon:sync-fornitori-legacy', [
'amministratore_id' => (string) $amm->id,
'--mdb' => $mdb,
]);
$out = trim((string) Artisan::output());
} catch (\Throwable $e) {
Notification::make()->title('Aggiornamento fornitori fallito')->body($e->getMessage())->danger()->send();
return;
}
if ($exit !== 0) {
$snippet = $out !== '' ? $out : '';
$snippet = $snippet !== '' ? mb_substr($snippet, 0, 800) : '';
Notification::make()
->title('Aggiornamento fornitori: errore')
->body('Exit sync=' . $exit . ($snippet !== '' ? "\n\n" . $snippet : ''))
->danger()
->send();
return;
}
Notification::make()
->title('Fornitori aggiornati')
->body('Amministratore: ' . ($amm->codice_univoco ?: ('ID ' . $amm->id)))
->success()
->send();
}
public function refreshImportTagLegacyFornitoriGlobale(): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
$state = $this->getImportFormState();
$path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon');
$dryRun = (bool) ($state['dry_run'] ?? false);
$amm = null;
if ($user->hasAnyRole(['super-admin', 'admin'])) {
$ammId = (int) ($state['amministratore_id'] ?? 0);
$amm = $ammId > 0 ? Amministratore::query()->find($ammId) : null;
} else {
$amm = $user->amministratore;
}
if (! $amm) {
$amm = Amministratore::query()->orderBy('id')->first();
}
if (! $amm) {
Notification::make()->title('Nessun amministratore disponibile')->danger()->send();
return;
}
$mdb = (string) ($state['fornitori_mdb'] ?? '');
if ($mdb === '') {
$mdb = rtrim($path, '/') . '/dbc/Fornitori.mdb';
}
if (! is_file($mdb)) {
Notification::make()
->title('Fornitori.mdb non trovato')
->body('Percorso atteso: ' . $mdb)
->danger()
->send();
return;
}
try {
$exit = Artisan::call('gescon:sync-fornitori-legacy', [
'amministratore_id' => (string) $amm->id,
'--mdb' => $mdb,
'--dry-run' => $dryRun,
]);
$out = trim((string) Artisan::output());
} catch (\Throwable $e) {
Notification::make()
->title('Refresh + import TAG fallito')
->body($e->getMessage())
->danger()
->send();
return;
}
if ($exit !== 0) {
$snippet = $out !== '' ? $out : '';
$snippet = $snippet !== '' ? mb_substr($snippet, 0, 800) : '';
Notification::make()
->title('Refresh + import TAG: errore')
->body('Exit sync=' . $exit . ($snippet !== '' ? "\n\n" . $snippet : ''))
->danger()
->send();
return;
}
Notification::make()
->title($dryRun ? 'Dry-run TAG globale completato' : 'Refresh + import TAG globale completato')
->body('Amministratore: ' . ($amm->codice_univoco ?: ('ID ' . $amm->id)))
->success()
->send();
}
public function applicaEnrichmentTuttiStabili(): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
$state = $this->getImportFormState();
$path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon');
$amm = null;
if ($user->hasAnyRole(['super-admin', 'admin'])) {
$ammId = (int) ($state['amministratore_id'] ?? 0);
$amm = $ammId > 0 ? Amministratore::query()->find($ammId) : null;
} else {
$amm = $user->amministratore;
}
if (! $amm) {
$amm = Amministratore::query()->orderBy('id')->first();
}
if (! $amm) {
Notification::make()->title('Nessun amministratore disponibile')->danger()->send();
return;
}
try {
$exit = Artisan::call('gescon:enrich-stabili', [
'--path' => $path,
'--amministratore-id' => (string) $amm->id,
]);
$out = trim((string) Artisan::output());
} catch (\Throwable $e) {
Notification::make()->title('Enrichment massivo fallito')->body($e->getMessage())->danger()->send();
return;
}
if ($exit !== 0) {
$snippet = $out !== '' ? mb_substr($out, 0, 800) : '';
Notification::make()->title('Enrichment massivo: errore')->body($snippet)->danger()->send();
return;
}
$lastLine = $out !== '' ? trim((string) collect(preg_split('/\R/', $out) ?: [])->last()): '';
Notification::make()
->title('Enrichment massivo completato')
->body($lastLine !== '' ? $lastLine : 'Operazione completata')
->success()
->send();
}
public function eseguiImportSelezionato(EssentialImportService $service): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
$state = $this->getImportFormState();
$code = trim((string) ($state['stabile_code'] ?? ''));
if ($code === '') {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$stabile = Stabile::query()->where('codice_stabile', $code)->first();
if (! $stabile) {
Notification::make()
->title('Stabile non creato')
->body('Prima crea lo stabile dal pulsante dedicato.')
->danger()
->send();
return;
}
$amm = null;
if ($user->hasAnyRole(['super-admin', 'admin'])) {
$ammId = (int) ($state['amministratore_id'] ?? 0);
$amm = $ammId > 0 ? Amministratore::query()->find($ammId) : null;
} else {
$amm = $user->amministratore;
}
if (! $amm) {
$amm = Amministratore::query()->orderBy('id')->first();
}
if (! $amm) {
Notification::make()->title('Nessun amministratore disponibile')->danger()->send();
return;
}
$options = [
'path' => (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'),
'anno' => isset($state['anno']) && trim((string) $state['anno']) !== '' ? trim((string) $state['anno']) : null,
'dry' => (bool) ($state['dry_run'] ?? false),
'sync_staging' => (bool) ($state['sync_staging'] ?? false),
'with_unita' => (bool) ($state['with_unita'] ?? false),
'with_soggetti' => (bool) ($state['with_soggetti'] ?? false),
'with_diritti' => (bool) ($state['with_diritti'] ?? false),
'with_millesimi' => (bool) ($state['with_millesimi'] ?? false),
'with_voci' => (bool) ($state['with_voci'] ?? false),
'with_banche' => (bool) ($state['with_banche'] ?? false),
'with_gestioni' => (bool) ($state['with_gestioni'] ?? false),
'with_assemblee' => (bool) ($state['with_assemblee'] ?? false),
'with_operazioni' => (bool) ($state['with_operazioni'] ?? false),
'with_rate' => (bool) ($state['with_rate'] ?? false),
'with_incassi' => (bool) ($state['with_incassi'] ?? false),
'with_giroconti' => (bool) ($state['with_giroconti'] ?? false),
'with_straord' => (bool) ($state['with_straord'] ?? false),
'with_addebiti' => (bool) ($state['with_addebiti'] ?? false),
'with_detrazioni' => (bool) ($state['with_detrazioni'] ?? false),
];
if (! empty($options['with_operazioni'])) {
$options['with_voci'] = true;
$options['with_millesimi'] = true;
$options['with_banche'] = true;
$options['with_unita'] = true;
$options['with_soggetti'] = true;
$options['with_diritti'] = true;
}
if (! empty($options['with_rate'])) {
$options['with_unita'] = true;
$options['with_soggetti'] = true;
$options['with_diritti'] = true;
}
if (! empty($options['with_incassi'])) {
$options['with_rate'] = true;
$options['with_banche'] = true;
}
if (! empty($options['with_voci'])) {
$options['with_millesimi'] = true;
}
$order = [
'unita',
'soggetti',
'diritti',
'millesimi',
'voci',
'banche',
'assemblee',
'operazioni',
'rate',
'incassi',
'giroconti',
'straord',
'addebiti',
'detrazioni',
];
$steps = [];
foreach ($order as $step) {
$flag = 'with_' . $step;
if (! empty($options[$flag])) {
$steps[] = $step;
}
}
if (empty($steps) && ! empty($options['with_gestioni'])) {
$steps = [];
}
if (empty($steps) && empty($options['with_gestioni'])) {
Notification::make()
->title('Nessun dato selezionato')
->danger()
->send();
return;
}
$outputs = [];
if (! empty($options['with_operazioni'])) {
try {
$exit = Artisan::call('gescon:sync-fornitori-legacy', [
'amministratore_id' => (string) $amm->id,
'--mdb' => (string) ($state['fornitori_mdb'] ?? rtrim($options['path'], '/') . '/dbc/Fornitori.mdb'),
]);
$outputs[] = trim((string) Artisan::output());
if ($exit !== 0) {
Notification::make()
->title('Aggiornamento fornitori: errore')
->body('Exit sync=' . $exit)
->danger()
->send();
return;
}
} catch (\Throwable $e) {
Notification::make()
->title('Aggiornamento fornitori fallito')
->body($e->getMessage())
->danger()
->send();
return;
}
}
if (! empty($options['sync_staging'])) {
$singoloMdb = null;
if (! empty($options['path']) && ! empty($code)) {
$annoFolder = $options['anno'] ?: '0001';
$singoloMdb = rtrim($options['path'], '/') . '/' . $code . '/' . $annoFolder . '/singolo_anno.mdb';
}
$generaleMdb = ! empty($options['path']) && ! empty($code)
? rtrim($options['path'], '/') . '/' . $code . '/generale_stabile.mdb'
: null;
try {
if (! empty($options['with_operazioni']) && is_string($singoloMdb)) {
Artisan::call('gescon:load-mdb', [
'--mdb' => $singoloMdb,
'--table' => 'operazioni',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--truncate' => true,
]);
$outputs[] = trim((string) Artisan::output());
Artisan::call('gescon:load-mdb', [
'--mdb' => $singoloMdb,
'--table' => 'voc_spe',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--truncate' => true,
]);
$outputs[] = trim((string) Artisan::output());
Artisan::call('gescon:load-mdb', [
'--mdb' => $singoloMdb,
'--table' => 'tabelle',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--truncate' => true,
]);
$outputs[] = trim((string) Artisan::output());
Artisan::call('gescon:load-mdb', [
'--mdb' => $singoloMdb,
'--table' => 'anagr_casse',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--truncate' => true,
]);
$outputs[] = trim((string) Artisan::output());
}
if (! empty($options['with_incassi']) && is_string($singoloMdb)) {
Artisan::call('gescon:load-mdb', [
'--mdb' => $singoloMdb,
'--table' => 'incassi',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--truncate' => true,
]);
$outputs[] = trim((string) Artisan::output());
}
if (! empty($options['with_rate']) && is_string($generaleMdb)) {
Artisan::call('gescon:load-mdb', [
'--mdb' => $generaleMdb,
'--table' => 'emes_gen',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--truncate' => true,
]);
$outputs[] = trim((string) Artisan::output());
Artisan::call('gescon:load-mdb', [
'--mdb' => $generaleMdb,
'--table' => 'emes_det',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--truncate' => true,
]);
$outputs[] = trim((string) Artisan::output());
}
} catch (\Throwable $e) {
Notification::make()
->title('Aggiornamento staging fallito')
->body($e->getMessage())
->danger()
->send();
return;
}
}
foreach ($steps as $step) {
$params = [
'--stabile' => $code,
'--solo' => $step,
'--path' => $options['path'],
];
if (! empty($options['anno'])) {
$params['--anno'] = $options['anno'];
}
if (! empty($options['dry'])) {
$params['--dry-run'] = true;
}
Artisan::call('gescon:import-full', $params);
$outputs[] = trim((string) Artisan::output());
}
if (! empty($options['with_gestioni']) && empty($options['dry'])) {
$gestParams = [
'--stabile' => $code,
'--root' => $options['path'],
];
Artisan::call('gescon:import-gestioni', $gestParams);
$outputs[] = trim((string) Artisan::output());
}
$shouldSyncRelations = ! empty($options['with_unita'])
|| ! empty($options['with_soggetti'])
|| ! empty($options['with_diritti']);
if ($shouldSyncRelations) {
$relationSync = $this->runRelationSyncSequence($code, (int) $stabile->id, (bool) $options['dry']);
if (! $relationSync['ok']) {
Notification::make()
->title('Import completato con errore nel re-sync relazioni')
->body((string) ($relationSync['message'] ?? 'Errore non specificato.'))
->danger()
->send();
return;
}
foreach ((array) ($relationSync['outputs'] ?? []) as $syncOutput) {
if (is_string($syncOutput) && trim($syncOutput) !== '') {
$outputs[] = trim($syncOutput);
}
}
}
$tail = collect($outputs)
->filter()
->map(fn(string $o) => trim((string) collect(preg_split('/\R/', $o) ?: [])->last()))
->filter()
->implode("\n");
Notification::make()
->title('Importazione completata')
->body($tail !== '' ? $tail : 'Operazione completata')
->success()
->send();
}
public function eseguiImportAllineamentoCompleto(): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
$state = $this->getImportFormState();
$code = trim((string) ($state['stabile_code'] ?? ''));
if ($code === '') {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$path = trim((string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon'));
$years = implode(',', array_values(array_filter(array_map(
fn($v) => trim((string) $v),
(array) ($state['align_years_selected'] ?? ['2024', '2025'])
))));
$waterYears = implode(',', array_values(array_filter(array_map(
fn($v) => trim((string) $v),
(array) ($state['align_water_years_selected'] ?? ['2024'])
))));
$openYears = implode(',', array_values(array_filter(array_map(
fn($v) => trim((string) $v),
(array) ($state['align_open_years_selected'] ?? ['2024', '2025'])
))));
$dryRun = (bool) ($state['dry_run'] ?? false);
$markObsoleteYears = (bool) ($state['align_mark_obsolete_years'] ?? false);
$withF24 = (bool) ($state['align_with_f24'] ?? true);
if ($path === '') {
Notification::make()->title('Percorso archivio mancante')->danger()->send();
return;
}
if ($years === '') {
Notification::make()->title('Anni contabili mancanti')->danger()->send();
return;
}
$params = [
'--stabile' => [$code],
'--path' => $path,
'--years' => $years,
'--water-years' => $waterYears !== '' ? $waterYears : '2024',
'--open-years' => $openYears !== '' ? $openYears : $years,
];
if ($markObsoleteYears) {
$params['--mark-obsolete-years'] = true;
}
if (! $withF24) {
$params['--skip-f24'] = true;
}
if ($dryRun) {
$params['--dry-run'] = true;
}
try {
$exit = Artisan::call('gescon:import-align', $params);
$out = trim((string) Artisan::output());
} catch (\Throwable $e) {
Notification::make()
->title('Import + allineamento fallito')
->body($e->getMessage())
->danger()
->send();
return;
}
$lines = $out === '' ? [] : preg_split('/\R/', $out);
$tail = $lines ? implode("\n", array_slice($lines, -10)) : 'Comando eseguito.';
Notification::make()
->title($exit === 0 ? 'Import + allineamento completato' : 'Import + allineamento con errori')
->body($tail)
->{$exit === 0 ? 'success' : 'danger'}()
->send();
}
public function caricaAnniDaGenerale(): void
{
$state = $this->getImportFormState();
$code = (string) ($state['stabile_code'] ?? '');
$path = (string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon');
$loaded = $this->refreshAnniOptionsForStabile($code, $path);
if ($loaded <= 0) {
Notification::make()
->title('Nessun anno trovato in generale_stabile.mdb')
->body('Verifica percorso archivio e stabile selezionato.')
->danger()
->send();
return;
}
Notification::make()
->title('Anni caricati da generale_stabile.mdb')
->body('Anni disponibili: ' . $loaded)
->success()
->send();
}
/** @return array<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;
}
ksort($opts);
$this->anniOptions = $opts;
$state = $this->getImportFormState();
$keys = array_keys($opts);
$selectedYears = array_values(array_filter(array_map(
fn($v) => trim((string) $v),
(array) ($state['align_years_selected'] ?? [])
), fn($v) => in_array($v, $keys, true)));
if (empty($selectedYears)) {
$selectedYears = array_slice(array_reverse($keys), 0, 2);
sort($selectedYears);
}
$selectedWater = array_values(array_filter(array_map(
fn($v) => trim((string) $v),
(array) ($state['align_water_years_selected'] ?? [])
), fn($v) => in_array($v, $keys, true)));
if (empty($selectedWater) && ! empty($selectedYears)) {
$selectedWater = [end($selectedYears)];
}
$selectedOpen = array_values(array_filter(array_map(
fn($v) => trim((string) $v),
(array) ($state['align_open_years_selected'] ?? [])
), fn($v) => in_array($v, $keys, true)));
if (empty($selectedOpen)) {
$selectedOpen = $selectedYears;
}
if ($set !== null && is_callable($get)) {
$set('align_years_selected', $selectedYears);
$set('align_water_years_selected', $selectedWater);
$set('align_open_years_selected', $selectedOpen);
} else {
$state['align_years_selected'] = $selectedYears;
$state['align_water_years_selected'] = $selectedWater;
$state['align_open_years_selected'] = $selectedOpen;
$this->getSchema('form')?->fill($state);
}
return count($opts);
}
private function resolveSelectedStabileByCode(?string $code): ?Stabile
{
$normalized = trim((string) $code);
if ($normalized === '') {
return null;
}
$trimmed = ltrim($normalized, '0');
return Stabile::query()
->where(function ($query) use ($normalized, $trimmed): void {
$query->where('codice_stabile', $normalized);
if ($trimmed !== '' && $trimmed !== $normalized) {
$query->orWhere('codice_stabile', $trimmed);
}
})
->first();
}
/** @return array{ok:bool,message:string,outputs:array<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;
}
}