netgescon-day0/app/Http/Controllers/Admin/GesconImportController.php

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