1054 lines
73 KiB
PHP
1054 lines
73 KiB
PHP
@extends('admin.layouts.netgescon')
|
|
|
|
@section('title', 'GESCON Import • Configurazione')
|
|
|
|
@section('breadcrumb')
|
|
<a href="{{ route('admin.gescon-import.index') }}" class="text-blue-600 hover:underline">GESCON Import</a>
|
|
<i class="fas fa-chevron-right text-xs"></i>
|
|
<span>Configurazione</span>
|
|
@endsection
|
|
|
|
@section('content')
|
|
<div class="px-6">
|
|
@include('components.layout.alerts')
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div>
|
|
<h1 class="h3 mb-1"><i class="mdi mdi-cog-outline mr-2"></i>Configurazione GESCON Import</h1>
|
|
<p class="text-gray-500 mb-0">Percorsi sorgente e anteprima stabili rilevati</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<a href="{{ route('admin.gescon-import.index') }}" class="netgescon-btn netgescon-btn-outline-secondary">
|
|
<i class="mdi mdi-home-outline mr-1"></i>Sistema Import
|
|
</a>
|
|
<a href="{{ route('admin.gescon-import.config', array_merge(request()->query(), ['refresh' => 1])) }}" class="netgescon-btn netgescon-btn-outline-secondary">
|
|
<i class="mdi mdi-refresh mr-1"></i>Riscansiona
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<section class="content">
|
|
<div class="px-0">
|
|
<div class="grid md:grid-cols-12 gap-4">
|
|
<!-- Colonna sinistra: Impostazioni sorgente -->
|
|
<div class="col-span-12 lg:col-span-5">
|
|
<div class="netgescon-card">
|
|
<div class="netgescon-card-header">
|
|
<h3 class="card-title mb-0"><i class="mdi mdi-folder-outline me-2"></i> Impostazioni Sorgente</h3>
|
|
</div>
|
|
<form method="POST" action="{{ route('admin.gescon-import.config.save') }}">
|
|
@csrf
|
|
<div class="netgescon-card-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Percorso cartella sorgente</label>
|
|
<div class="flex gap-2">
|
|
<input type="text" name="gescon_source_path" id="gescon_source_path" class="netgescon-input flex-1" value="{{ $settings['gescon.source_path'] ?? '' }}" placeholder="/percorso/alla/cartella/dbc">
|
|
<button type="button" class="netgescon-btn netgescon-btn-outline-secondary" onclick="openDirPicker('gescon_source_path')"><i class="mdi mdi-folder-search-outline"></i></button>
|
|
</div>
|
|
<small class="text-gray-500">Cartella che contiene Stabili.mdb e gli altri file MDB/CSV</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Percorso file Stabili.mdb</label>
|
|
<div class="flex gap-2">
|
|
<input type="text" name="gescon_mdb_stabili" id="gescon_mdb_stabili" class="netgescon-input flex-1" value="{{ $settings['gescon.mdb_stabili'] ?? '' }}" placeholder="/percorso/Stabili.mdb">
|
|
<button type="button" class="netgescon-btn netgescon-btn-outline-secondary" onclick="openDirPicker('gescon_mdb_stabili', true)"><i class="mdi mdi-file-find-outline"></i></button>
|
|
<button type="button" class="netgescon-btn netgescon-btn-outline-dark" onclick="verifyMdbPath()"><i class="mdi mdi-clipboard-check-outline"></i></button>
|
|
</div>
|
|
<pre id="verify-mdb-result" class="mt-2 p-2 bg-gray-100 border rounded text-sm" style="display:none"></pre>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Indice master cartellestabili_e_anni.mdb</label>
|
|
<div class="flex gap-2">
|
|
<input type="text" name="gescon_mdb_index" id="gescon_mdb_index" class="netgescon-input flex-1" value="{{ $settings['gescon.mdb_index'] ?? '/mnt/gescon-archives/gescon/cartellestabili_e_anni.mdb' }}" placeholder="/mnt/gescon-archives/gescon/cartellestabili_e_anni.mdb">
|
|
<button type="button" class="netgescon-btn netgescon-btn-outline-secondary" onclick="openDirPicker('gescon_mdb_index', true)"><i class="mdi mdi-file-find-outline"></i></button>
|
|
</div>
|
|
<small class="text-gray-500">Usato per elencare stabili e anni disponibili; consigliato per anteprima e scoperta archivi</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Connessione import (facoltativa)</label>
|
|
<input type="text" name="gescon_import_conn" class="netgescon-input" value="{{ $settings['gescon.import_conn'] ?? '' }}" placeholder="gescon_import">
|
|
<small class="text-gray-500">Nome connessione DB Laravel per staging (se configurata in config/database.php)</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">[Avanzato] Connessione sorgente per mapping</label>
|
|
<input type="text" name="gescon_source_conn" class="netgescon-input" value="{{ \App\Models\UserSetting::get('gescon.source_conn', 'gescon_import') }}" placeholder="gescon_import">
|
|
<small class="text-gray-500">Per la scoperta campi legacy multi-tabella (es. proprietari/inquilini).</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">[Avanzato] Tabelle sorgente (JSON o CSV)</label>
|
|
<textarea name="gescon_source_tables" class="netgescon-input" rows="3" placeholder='["condomin","proprietari","inquilini"]'>{{ \App\Models\UserSetting::get('gescon.source_tables', '') }}</textarea>
|
|
<small class="text-gray-500">Seleziona le tabelle legacy da cui proporre i campi nella UI di mapping (Unità, Soggetti, ecc.).</small>
|
|
</div>
|
|
</div>
|
|
<div class="netgescon-card-footer flex justify-between">
|
|
<a href="{{ route('admin.gescon-import.index') }}" class="netgescon-btn netgescon-btn-secondary">
|
|
<i class="mdi mdi-arrow-left"></i> Indietro
|
|
</a>
|
|
<button type="submit" class="netgescon-btn netgescon-btn-primary">
|
|
<i class="mdi mdi-content-save"></i> Salva Impostazioni
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="alert alert-info mt-3">
|
|
<i class="mdi mdi-information-outline me-1"></i>
|
|
Assicurati che il pacchetto mdb-tools sia installato sul server per la scansione di Stabili.mdb.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Colonna destra: Archivio Migrazioni + Anteprima stabili e stato import -->
|
|
<div class="col-span-12 lg:col-span-7">
|
|
<div class="netgescon-card mb-3">
|
|
<div class="netgescon-card-header flex flex-col gap-2">
|
|
<div class="flex justify-between items-center w-full flex-wrap gap-3">
|
|
<h3 class="card-title mb-0 flex items-center gap-2"><i class="mdi mdi-database-search-outline"></i> Selezione Archivi / Tabelle / Colonne</h3>
|
|
<div class="flex items-center gap-2 flex-wrap" id="config-stabile-picker">
|
|
<label for="config_legacy_code" class="text-xs text-gray-600">Stabile legacy</label>
|
|
<select id="config_legacy_code" class="netgescon-input netgescon-input-sm" style="padding:.30rem .5rem;min-width:200px;" onchange="handleConfigLegacyChange(this.value)">
|
|
<option value="">— Seleziona stabile legacy —</option>
|
|
@foreach(($preview ?? []) as $p)
|
|
<option value="{{ $p['legacy_code'] }}">{{ $p['legacy_code'] }} — {{ $p['denominazione'] ?? 'N/D' }}</option>
|
|
@endforeach
|
|
</select>
|
|
<label for="config_year_filter" class="text-xs text-gray-600">Anno</label>
|
|
<select id="config_year_filter" class="netgescon-input netgescon-input-sm" style="padding:.30rem .5rem;min-width:90px;" onchange="handleYearFilterChange(this.value)">
|
|
<option value="">(tutti)</option>
|
|
</select>
|
|
<input type="text" id="template_stabile_code" class="netgescon-input netgescon-input-sm" placeholder="tpl codice" style="width:6.2rem;" value="{{ \App\Models\UserSetting::get('gescon.template_stabile_code','') }}" title="Codice stabile template" />
|
|
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="applyTemplateSnapshot()" title="Applica snapshot template"><i class="mdi mdi-restore"></i></button>
|
|
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-primary" onclick="saveTemplateSnapshot()" title="Salva snapshot template"><i class="mdi mdi-content-save"></i></button>
|
|
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="document.getElementById('config_legacy_code').value='';handleConfigLegacyChange('')" title="Reset stabile"><i class="mdi mdi-close"></i></button>
|
|
@php($tplSnap = \App\Models\UserSetting::get('gescon.template_stabile_sources'))
|
|
@if($tplSnap)
|
|
<span class="netgescon-badge netgescon-badge-success" title="Snapshot presente">TPL</span>
|
|
@else
|
|
<span class="netgescon-badge netgescon-badge-secondary" title="Nessun snapshot">TPL</span>
|
|
@endif
|
|
<div id="config-legacy-hint" class="text-xs text-gray-400"></div>
|
|
</div>
|
|
</div>
|
|
<small class="text-gray-500">Configurazione avanzata sorgenti MDB • Snapshot template integrato</small>
|
|
</div>
|
|
<form method="POST" action="{{ route('admin.gescon-import.config.save') }}" id="archives-form">
|
|
@csrf
|
|
<div class="netgescon-card-body grid md:grid-cols-12 gap-3">
|
|
<div class="md:col-span-4">
|
|
<h6 class="text-xs font-semibold text-gray-600 mb-1 flex items-center justify-between">Archivi MDB <button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="reloadArchives()"><i class="mdi mdi-refresh"></i></button></h6>
|
|
<div id="mdb-archives" class="border rounded p-2 h-64 overflow-auto text-sm bg-white"></div>
|
|
</div>
|
|
<div class="md:col-span-4">
|
|
<h6 class="text-xs font-semibold text-gray-600 mb-1 flex items-center justify-between">Tabelle <button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="reloadTables()"><i class="mdi mdi-refresh"></i></button></h6>
|
|
<div id="mdb-tables" class="border rounded p-2 h-64 overflow-auto text-sm bg-white"><div class="text-gray-400">Seleziona un archivio…</div></div>
|
|
</div>
|
|
<div class="md:col-span-4">
|
|
<h6 class="text-xs font-semibold text-gray-600 mb-1 flex items-center justify-between">Colonne <button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="reloadColumns()"><i class="mdi mdi-refresh"></i></button></h6>
|
|
<div id="mdb-columns" class="border rounded p-2 h-64 overflow-auto text-sm bg-white"><div class="text-gray-400">Seleziona una tabella…</div></div>
|
|
</div>
|
|
<div class="md:col-span-12 mt-2 flex justify-between items-center">
|
|
<div class="text-xs text-gray-500">Le selezioni verranno salvate e riutilizzate nelle UI di mapping (Unità, Soggetti, Tabelle, Voci)</div>
|
|
<button type="submit" class="netgescon-btn netgescon-btn-sm netgescon-btn-primary"><i class="mdi mdi-content-save"></i> Salva selezione</button>
|
|
</div>
|
|
<div class="md:col-span-12 mt-3">
|
|
<details class="border rounded group" open>
|
|
<summary class="cursor-pointer px-3 py-2 flex items-center justify-between bg-gray-50 border-b">
|
|
<span class="text-xs font-semibold text-gray-700 flex items-center gap-2"><i class="mdi mdi-format-list-bulleted"></i> Segment Mapping & Viste</span>
|
|
<span class="text-[10px] text-gray-500">clicca per espandere/ridurre</span>
|
|
</summary>
|
|
<div class="p-3 space-y-2">
|
|
<div class="text-[11px] text-gray-600">Viste mapping disponibili: <span class="netgescon-badge netgescon-badge-light">Unità</span> <span class="netgescon-badge netgescon-badge-light">Soggetti</span> <span class="netgescon-badge netgescon-badge-light">Tabelle</span> <span class="netgescon-badge netgescon-badge-light">Voci</span></div>
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<label class="text-[11px] text-gray-600">Filtro tipo</label>
|
|
<select id="segment-type-filter" class="netgescon-input netgescon-input-sm" style="padding:.25rem .4rem;">
|
|
<option value="">(tutti)</option>
|
|
<option value="anagrafica">anagrafica</option>
|
|
<option value="stabile">stabile</option>
|
|
<option value="unita">unita</option>
|
|
<option value="soggetti">soggetti</option>
|
|
<option value="tabelle">tabelle</option>
|
|
<option value="voci">voci</option>
|
|
<option value="contabile">contabile</option>
|
|
<option value="generico">generico</option>
|
|
</select>
|
|
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="resetSegmentFilter()"><i class="mdi mdi-filter-off"></i></button>
|
|
</div>
|
|
<div class="overflow-auto border rounded" style="max-height: 220px;" id="segment-table-wrapper">
|
|
<table class="table table-sm mb-0 align-middle">
|
|
<thead class="table-light text-xs">
|
|
<tr>
|
|
<th style="width:140px;">Segment</th>
|
|
<th style="width:110px;">Tipo</th>
|
|
<th style="width:220px;">Tabelle (multi)</th>
|
|
<th>Colonne (multi)</th>
|
|
<th style="width:100px;" title="Archivio/Entità target dove confluiranno i dati">Target</th>
|
|
<th style="width:40px;">Del</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="segment-mapping-rows"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<input type="text" id="new-seg-name" class="netgescon-input netgescon-input-sm" placeholder="segment (es. unita)">
|
|
<select id="new-seg-type" class="netgescon-input netgescon-input-sm" style="width:8rem;">
|
|
<option value="">tipo</option>
|
|
<option value="anagrafica">anagrafica</option>
|
|
<option value="stabile">stabile</option>
|
|
<option value="unita">unita</option>
|
|
<option value="soggetti">soggetti</option>
|
|
<option value="tabelle">tabelle</option>
|
|
<option value="voci">voci</option>
|
|
<option value="contabile">contabile</option>
|
|
<option value="generico">generico</option>
|
|
</select>
|
|
<input type="text" id="new-seg-target" class="netgescon-input netgescon-input-sm" style="width:8rem;" placeholder="target (es. stabili)">
|
|
<button type="button" class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-secondary" onclick="addSegmentRow()"><i class="mdi mdi-plus"></i> Aggiungi</button>
|
|
</div>
|
|
<div class="text-[10px] text-gray-500">I segment definiscono quali tabelle/colonne condivise appaiono nelle maschere di mapping; archivi costanti non vanno ripetuti per stabile.</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<div class="md:col-span-12 mt-3">
|
|
<details class="border rounded group" open>
|
|
<summary class="cursor-pointer px-3 py-2 flex items-center justify-between bg-gray-50 border-b">
|
|
<span class="text-xs font-semibold text-gray-700 flex items-center gap-2"><i class="mdi mdi-view-dashboard-outline"></i> Mapping Universale (Sintesi)</span>
|
|
<span class="text-[10px] text-gray-500">overview segmenti</span>
|
|
</summary>
|
|
<div class="p-3 space-y-2" id="universal-mapping-summary">
|
|
<div class="text-[11px] text-gray-600">Panoramica segmenti selezionati: completamento tabelle/colonne e target.</div>
|
|
<div class="overflow-auto border rounded" style="max-height:220px;">
|
|
<table class="table table-sm mb-0 align-middle text-xs">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Segment</th>
|
|
<th>Tipo</th>
|
|
<th>Target</th>
|
|
<th>#Tab</th>
|
|
<th>#Col</th>
|
|
<th>Stato</th>
|
|
<th style="width:80px;">Azioni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="universal-summary-rows"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="buildUniversalSummary()"><i class="mdi mdi-refresh"></i> Aggiorna</button>
|
|
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-primary" onclick="openUniversalTest()"><i class="mdi mdi-launch"></i> Test maschera</button>
|
|
</div>
|
|
<div class="text-[10px] text-gray-500">La maschera universale (in sviluppo) permetterà di mappare qualsiasi sorgente (MDB/CSV/URL) verso archivi target definiti dai segment.</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" name="gescon_source_path" value="{{ $settings['gescon.source_path'] ?? '' }}">
|
|
<input type="hidden" name="gescon_mdb_stabili" value="{{ $settings['gescon.mdb_stabili'] ?? '' }}">
|
|
<input type="hidden" name="gescon_mdb_index" value="{{ $settings['gescon.mdb_index'] ?? '' }}">
|
|
<input type="hidden" name="gescon_import_conn" value="{{ $settings['gescon.import_conn'] ?? '' }}">
|
|
<div id="hidden-selected-archives"></div>
|
|
<div id="hidden-selected-tables"></div>
|
|
<div id="hidden-selected-columns"></div>
|
|
<div id="hidden-mapping-sources"></div>
|
|
</form>
|
|
</div>
|
|
<div class="netgescon-card mb-3">
|
|
<div class="netgescon-card-header flex justify-between items-center">
|
|
<h3 class="card-title mb-0"><i class="mdi mdi-database-import-outline me-2"></i> Archivio Migrazioni</h3>
|
|
<div class="text-sm text-gray-500">
|
|
@if(isset($archive))
|
|
<span class="me-3">Sorgente: {!! $archive['source_registered'] ? '<span class="netgescon-badge netgescon-badge-success">registrata</span>' : '<span class="netgescon-badge netgescon-badge-secondary">non registrata</span>' !!}</span>
|
|
<span>Record Stabili: <span class="netgescon-badge netgescon-badge-primary">{{ $archive['dataset_records'] }}</span></span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<div class="netgescon-card-body">
|
|
<p class="text-gray-500 mb-2">Acquisisci gli stabili da Stabili.mdb nell'archivio locale per lavorare in modo affidabile e ripetibile. L'anteprima userà l'archivio, con fallback a file se vuoto.</p>
|
|
<div class="flex gap-2">
|
|
<form method="POST" action="{{ route('admin.gescon-import.archive.register-stabili') }}">
|
|
@csrf
|
|
<button type="submit" class="netgescon-btn netgescon-btn-outline-secondary"><i class="mdi mdi-database-plus-outline me-1"></i> Registra sorgente</button>
|
|
</form>
|
|
<form method="POST" action="{{ route('admin.gescon-import.archive.ingest-stabili') }}" onsubmit="document.getElementById('ingest_mdb_path').value = document.getElementById('gescon_mdb_stabili').value; return confirm('Avviare acquisizione Stabili da MDB nell\'archivio?');">
|
|
@csrf
|
|
<input type="hidden" id="ingest_mdb_path" name="mdb_path" value="{{ $settings['gescon.mdb_stabili'] ?? '' }}">
|
|
<button type="submit" class="netgescon-btn netgescon-btn-outline-primary"><i class="mdi mdi-download-outline me-1"></i> Acquisisci Stabili</button>
|
|
</form>
|
|
<a href="{{ route('admin.gescon-import.archive.status') }}" target="_blank" class="netgescon-btn netgescon-btn-outline-dark"><i class="mdi mdi-monitor-eye me-1"></i> Stato</a>
|
|
<button type="button" class="netgescon-btn netgescon-btn-outline-secondary" onclick="scanMdbTables()"><i class="mdi mdi-table-search me-1"></i> Scansiona tabelle MDB</button>
|
|
</div>
|
|
@if(isset($hasMdbTools) && !$hasMdbTools)
|
|
<div class="alert alert-warning mt-3 mb-0 py-2"><i class="mdi mdi-alert-outline me-1"></i> mdb-tools non rilevato: l'acquisizione da MDB non sarà possibile. Usa comunque il fallback per directory.</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<div class="netgescon-card">
|
|
<div class="netgescon-card-header flex justify-between items-center">
|
|
<h3 class="card-title mb-0"><i class="mdi mdi-home-group me-2"></i> Stabili rilevati (anteprima)</h3>
|
|
<div class="flex gap-2 items-center">
|
|
@if(isset($hasMdbTools) && !$hasMdbTools)
|
|
<span class="text-xs text-amber-600">mdbtools non rilevato</span>
|
|
@endif
|
|
<a href="{{ route('admin.gescon-import.config', array_merge(request()->query(), ['refresh' => 1])) }}" class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-secondary">Riscansiona</a>
|
|
</div>
|
|
</div>
|
|
<div class="netgescon-card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width: 120px;">Codice</th>
|
|
<th>Denominazione</th>
|
|
<th class="text-center" style="width: 160px;">Archivi</th>
|
|
<th style="width: 360px;">Azioni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse($preview as $item)
|
|
<tr>
|
|
<td><span class="netgescon-badge">{{ $item['legacy_code'] }}</span></td>
|
|
<td>{{ $item['denominazione'] ?? 'N/D' }}</td>
|
|
<td class="text-center whitespace-nowrap">
|
|
@if(!empty($item['has_generale']))
|
|
<span class="netgescon-badge netgescon-badge-success">GEN</span>
|
|
@else
|
|
<span class="netgescon-badge netgescon-badge-secondary">GEN</span>
|
|
@endif
|
|
@php($yc = (int)($item['years_count'] ?? 0))
|
|
@if($yc>0)
|
|
<span class="netgescon-badge netgescon-badge-primary">ANNI {{ $yc }}</span>
|
|
@else
|
|
<span class="netgescon-badge netgescon-badge-secondary">ANNI 0</span>
|
|
@endif
|
|
</td>
|
|
<td class="p-1">
|
|
<div class="flex items-center gap-2 flex-wrap actions-inline" style="--gap:0.35rem;">
|
|
<form method="POST" action="{{ route('admin.gescon-import.execute') }}" class="inline-flex">
|
|
@csrf
|
|
<input type="hidden" name="tipo_import" value="all">
|
|
<input type="hidden" name="legacy_code" value="{{ $item['legacy_code'] }}">
|
|
<input type="hidden" name="legacy_denominazione" value="{{ $item['denominazione'] ?? '' }}">
|
|
<input type="hidden" name="path" value="{{ $settings['gescon.source_path'] ?? '' }}">
|
|
<input type="hidden" name="mdb_path" value="{{ $settings['gescon.mdb_stabili'] ?? '' }}">
|
|
<button type="submit" name="test_mode" value="1" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-primary" title="Dry-run">
|
|
<i class="mdi mdi-flask-outline"></i>
|
|
</button>
|
|
</form>
|
|
<form method="POST" action="{{ route('admin.gescon-import.execute') }}" onsubmit="return confirm('Procedere con la sincronizzazione di {{ $item['legacy_code'] }}?');" class="inline-flex items-center gap-1">
|
|
@csrf
|
|
<input type="hidden" name="tipo_import" value="all">
|
|
<input type="hidden" name="legacy_code" value="{{ $item['legacy_code'] }}">
|
|
<input type="hidden" name="legacy_denominazione" value="{{ $item['denominazione'] ?? '' }}">
|
|
<input type="hidden" name="path" value="{{ $settings['gescon.source_path'] ?? '' }}">
|
|
<input type="hidden" name="mdb_path" value="{{ $settings['gescon.mdb_stabili'] ?? '' }}">
|
|
<label class="text-[10px] text-gray-500 flex items-center gap-1"><input type="checkbox" name="force_mapping" value="1"> <span>Forza</span></label>
|
|
<button type="submit" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-success" title="Sincronizza">
|
|
<i class="mdi mdi-database-import"></i>
|
|
</button>
|
|
</form>
|
|
<a class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" href="{{ route('admin.gescon-import.index', ['legacy_code' => $item['legacy_code']]) }}#mapping" title="Mappa campi">
|
|
<i class="mdi mdi-link-variant"></i>
|
|
</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="4" class="text-center text-gray-500 py-3">Nessuno stabile trovato: verifica il percorso o installa mdb-tools.</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="netgescon-card mt-3">
|
|
<div class="netgescon-card-header">
|
|
<h3 class="card-title mb-0"><i class="mdi mdi-check-circle-outline me-2"></i> Stato import recente</h3>
|
|
</div>
|
|
<div class="netgescon-card-body p-0">
|
|
@php($mappings = \App\Models\GesconImportMapping::where('user_id', auth()->id())->orderByDesc('updated_at')->limit(10)->get())
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Legacy</th>
|
|
<th>Denominazione</th>
|
|
<th>Stato</th>
|
|
<th>Dry-run</th>
|
|
<th>Sync</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse($mappings as $m)
|
|
<tr>
|
|
<td><code>{{ $m->legacy_code }}</code></td>
|
|
<td>{{ $m->legacy_denominazione ?? '—' }}</td>
|
|
<td>
|
|
@switch($m->status)
|
|
@case('dry-run')
|
|
<span class="netgescon-badge netgescon-badge-info">Dry-run</span>
|
|
@break
|
|
@case('synced')
|
|
<span class="netgescon-badge netgescon-badge-success">Sincronizzato</span>
|
|
@break
|
|
@case('error')
|
|
<span class="netgescon-badge netgescon-badge-danger">Errore</span>
|
|
@break
|
|
@default
|
|
<span class="netgescon-badge netgescon-badge-secondary">In attesa</span>
|
|
@endswitch
|
|
</td>
|
|
<td>{{ optional($m->last_dry_run_at)->format('d/m/Y H:i') ?? '—' }}</td>
|
|
<td>{{ optional($m->last_sync_at)->format('d/m/Y H:i') ?? '—' }}</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="5" class="text-center text-gray-500 py-3">Nessuna esecuzione recente</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
@endsection
|
|
|
|
@push('styles')
|
|
<style>
|
|
.dirpicker-modal { position: fixed; inset: 0; background: rgba(0,0,0,.35); display: none; align-items: center; justify-content: center; z-index: 1050; }
|
|
.dirpicker-dialog { width: min(720px, 96vw); max-height: 80vh; background: #fff; border-radius: .5rem; box-shadow: 0 10px 30px rgba(0,0,0,.2); display: flex; flex-direction: column; }
|
|
.dirpicker-header { padding: .75rem 1rem; border-bottom: 1px solid #e5e7eb; display: flex; align-items: center; gap: .5rem; }
|
|
.dirpicker-body { padding: .5rem 1rem; overflow: auto; }
|
|
.dirpicker-footer { padding: .75rem 1rem; border-top: 1px solid #e5e7eb; display: flex; justify-content: space-between; }
|
|
.dir-entry { display: flex; align-items: center; gap: .5rem; padding: .35rem .25rem; border-radius: .25rem; cursor: pointer; }
|
|
.dir-entry:hover { background: #f3f4f6; }
|
|
.dir-entry .type { width: 22px; text-align: center; }
|
|
/* inline actions */
|
|
.actions-inline form { display:inline-flex; }
|
|
.actions-inline .netgescon-btn { padding:.25rem .4rem; }
|
|
.actions-inline label { margin:0; }
|
|
/* Universal modal */
|
|
.universal-modal { position: fixed; inset: 0; background: rgba(0,0,0,.45); display:none; align-items:center; justify-content:center; z-index: 1060; }
|
|
.universal-dialog { width: min(1100px, 98vw); max-height: 86vh; background:#fff; border-radius:.5rem; box-shadow: 0 15px 40px rgba(0,0,0,.25); display:flex; flex-direction:column; }
|
|
.universal-header { padding:.6rem .9rem; border-bottom:1px solid #e5e7eb; display:flex; gap:.5rem; align-items:center; justify-content:space-between; }
|
|
.universal-body { padding:.6rem .9rem; overflow:auto; }
|
|
.universal-footer { padding:.6rem .9rem; border-top:1px solid #e5e7eb; display:flex; justify-content:space-between; align-items:center; }
|
|
.mapper-table th, .mapper-table td { vertical-align: middle; }
|
|
.mapper-table .legacy { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; }
|
|
.chip { display:inline-block; padding:.1rem .35rem; border-radius:999px; background:#eef2ff; color:#3730a3; font-size:10px; }
|
|
</style>
|
|
@endpush
|
|
|
|
@push('scripts')
|
|
<script>
|
|
let dirTargetInput = null;
|
|
let dirAllowFiles = false;
|
|
function openDirPicker(targetId, allowFiles=false){
|
|
dirTargetInput = document.getElementById(targetId);
|
|
dirAllowFiles = !!allowFiles;
|
|
const modal = document.getElementById('dirpicker');
|
|
modal.style.display = 'flex';
|
|
const start = dirTargetInput && dirTargetInput.value ? dirTargetInput.value : '{{ $settings['gescon.source_path'] ?? '/mnt/gescon-archives/gescon' }}';
|
|
loadDir(start);
|
|
}
|
|
function closeDirPicker(){ document.getElementById('dirpicker').style.display='none'; }
|
|
async function loadDir(path){
|
|
const list = document.getElementById('dir-list');
|
|
list.innerHTML = '<div class="text-gray-500 p-2">Caricamento...</div>';
|
|
try {
|
|
const url = new URL("{{ route('admin.gescon-import.browse') }}", window.location.origin);
|
|
url.searchParams.set('path', path);
|
|
const res = await fetch(url.toString(), { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
|
const data = await res.json();
|
|
if (data.error) { list.innerHTML = '<div class="text-danger p-2">' + data.error + '</div>'; return; }
|
|
document.getElementById('dir-path').value = data.path;
|
|
renderEntries(data);
|
|
} catch (e) { list.innerHTML = '<div class="text-danger p-2">Errore caricamento</div>'; }
|
|
}
|
|
function renderEntries(data){
|
|
const list = document.getElementById('dir-list');
|
|
list.innerHTML = '';
|
|
const up = document.createElement('div');
|
|
up.className = 'dir-entry';
|
|
up.innerHTML = '<span class="type">⬆️</span><span class="name">..</span>';
|
|
up.onclick = () => {
|
|
const cur = data.path;
|
|
const parent = cur.replace(/\/$/, '').split('/').slice(0, -1).join('/') || '/';
|
|
loadDir(parent);
|
|
};
|
|
list.appendChild(up);
|
|
(data.entries||[]).forEach(e => {
|
|
const row = document.createElement('div');
|
|
row.className = 'dir-entry';
|
|
row.innerHTML = '<span class="type">' + (e.is_dir ? '📁' : '📄') + '</span><span class="name">' + e.name + '</span>';
|
|
row.onclick = () => {
|
|
if (e.is_dir) loadDir(e.path); else if (dirAllowFiles) selectPath(e.path);
|
|
};
|
|
list.appendChild(row);
|
|
});
|
|
}
|
|
function selectPath(p){ if (dirTargetInput) dirTargetInput.value = p; closeDirPicker(); }
|
|
|
|
async function verifyMdbPath(){
|
|
const el = document.getElementById('gescon_mdb_stabili');
|
|
const out = document.getElementById('verify-mdb-result');
|
|
out.style.display='block';
|
|
out.textContent = 'Verifica in corso…';
|
|
try{
|
|
const res = await fetch("{{ route('admin.gescon-import.archive.verify-mdb') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
|
body: JSON.stringify({ mdb_path: el.value })
|
|
});
|
|
const data = await res.json();
|
|
out.textContent = JSON.stringify(data, null, 2);
|
|
}catch(e){ out.textContent = 'Errore: ' + e; }
|
|
}
|
|
|
|
function handleConfigLegacyChange(val){
|
|
try { localStorage.setItem('gescon_config_legacy_code', val||''); } catch(_){ }
|
|
const hint = document.getElementById('config-legacy-hint');
|
|
if(hint){ hint.textContent = val ? ('Selezionato: '+val) : ''; }
|
|
// Rebuild year list (best-effort: derive from archives already caricati)
|
|
setTimeout(populateYearFilter, 50);
|
|
applyArchiveFilter();
|
|
}
|
|
function handleYearFilterChange(v){ try { localStorage.setItem('gescon_config_year_filter', v||''); } catch(_){ } applyArchiveFilter(); }
|
|
function populateYearFilter(){
|
|
const sel = document.getElementById('config_year_filter'); if(!sel) return;
|
|
const prev = sel.value || ( ()=>{ try { return localStorage.getItem('gescon_config_year_filter')||''; } catch(_){ return ''; } } )();
|
|
const years = new Set();
|
|
const yearRegex = /(19|20)\d{2}/g; // cattura 1900-2099 ovunque
|
|
document.querySelectorAll('#mdb-archives label').forEach(l=>{
|
|
const path = l.querySelector('input')?.getAttribute('data-path')||'';
|
|
const found = path.match(yearRegex);
|
|
if(found){ found.forEach(y=>{ const yi=parseInt(y,10); if(yi>=1990 && yi<=2099) years.add(String(yi)); }); }
|
|
});
|
|
const arr = Array.from(years).sort();
|
|
sel.innerHTML = '<option value="">(tutti)</option>' + arr.map(y=>`<option value="${y}">${y}</option>`).join('');
|
|
if(prev && sel.querySelector('option[value="'+prev+'"]')){ sel.value=prev; }
|
|
}
|
|
(function(){
|
|
document.addEventListener('DOMContentLoaded', function(){
|
|
const sel = document.getElementById('config_legacy_code');
|
|
if(sel){
|
|
let saved=null; try { saved = localStorage.getItem('gescon_config_legacy_code'); } catch(_){ }
|
|
if(saved && sel.querySelector('option[value="'+saved+'"]')){ sel.value=saved; handleConfigLegacyChange(saved); }
|
|
}
|
|
});
|
|
})();
|
|
|
|
async function scanMdbTables(){
|
|
const mdb = document.getElementById('gescon_mdb_stabili').value;
|
|
const res = await fetch("{{ route('admin.gescon-import.archive.scan-tables') }}", {
|
|
method: 'POST', headers: { 'Content-Type':'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }, body: JSON.stringify({ mdb_path: mdb })
|
|
});
|
|
const data = await res.json();
|
|
if (data.error) { alert('Errore: ' + data.error); return; }
|
|
const tables = data.tables || [];
|
|
if (!tables.length) { alert('Nessuna tabella trovata'); return; }
|
|
const preSel = ['Stabili','FE','Fornitori','Soggetti','generale_stabile','singolo_anno'];
|
|
const pick = prompt('Tabelle trovate (separa con virgola). Suggerite: ' + preSel.join(', '), preSel.filter(t=>tables.includes(t)).join(', '));
|
|
if (!pick) return;
|
|
const chosen = pick.split(',').map(s=>s.trim()).filter(Boolean);
|
|
const res2 = await fetch("{{ route('admin.gescon-import.archive.ingest-tables') }}", {
|
|
method: 'POST', headers: { 'Content-Type':'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }, body: JSON.stringify({ mdb_path: mdb, tables: chosen })
|
|
});
|
|
const data2 = await res2.json();
|
|
if (!data2.ok) { alert('Import fallito: ' + (data2.error || '')); return; }
|
|
alert('Import OK: ' + data2.result.map(r=>r.table+':'+r.rows).join(' | '));
|
|
}
|
|
|
|
// ================== Nuova UI selezione archivi/tabelle/colonne ==================
|
|
const selState = {
|
|
archives: @json(\App\Models\UserSetting::get('gescon.selected_archives', [])),
|
|
tables: @json(\App\Models\UserSetting::get('gescon.selected_tables', [])),
|
|
columns: @json(\App\Models\UserSetting::get('gescon.selected_columns', [])),
|
|
currentArchive: null,
|
|
currentTable: null,
|
|
mappingSources: @json($mappingSources ?? []),
|
|
};
|
|
|
|
function renderSelectedHidden(){
|
|
const ha = document.getElementById('hidden-selected-archives');
|
|
const ht = document.getElementById('hidden-selected-tables');
|
|
const hc = document.getElementById('hidden-selected-columns');
|
|
ha.innerHTML = selState.archives.map(a=>`<input type="hidden" name="selected_archives[]" value="${a.replace(/&/g,'&')}">`).join('');
|
|
ht.innerHTML = selState.tables.map(t=>`<input type="hidden" name="selected_tables[]" value="${t.replace(/&/g,'&')}">`).join('');
|
|
hc.innerHTML = selState.columns.map(c=>`<input type="hidden" name="selected_columns[]" value="${c.replace(/&/g,'&')}">`).join('');
|
|
// mapping sources hidden
|
|
const hms = document.getElementById('hidden-mapping-sources');
|
|
hms.innerHTML = '';
|
|
selState.mappingSources.forEach((row,idx)=>{
|
|
const seg = (row.segment||'').replace(/"/g,'"');
|
|
(row.tables||[]).forEach(t=>{ hms.innerHTML += `<input type="hidden" name="mapping_sources[${idx}][tables][]" value="${t.replace(/&/g,'&')}">`; });
|
|
(row.columns||[]).forEach(c=>{ hms.innerHTML += `<input type="hidden" name="mapping_sources[${idx}][columns][]" value="${c.replace(/&/g,'&')}">`; });
|
|
hms.innerHTML += `<input type="hidden" name="mapping_sources[${idx}][segment]" value="${seg}">`;
|
|
if(row.type) hms.innerHTML += `<input type="hidden" name="mapping_sources[${idx}][type]" value="${row.type.replace(/&/g,'&')}">`;
|
|
if(row.target) hms.innerHTML += `<input type="hidden" name="mapping_sources[${idx}][target]" value="${row.target.replace(/&/g,'&')}">`;
|
|
});
|
|
}
|
|
|
|
function toggleIn(arr, value){
|
|
const i = arr.indexOf(value); if (i>=0) arr.splice(i,1); else arr.push(value); return arr;
|
|
}
|
|
|
|
async function reloadArchives(){
|
|
const box = document.getElementById('mdb-archives');
|
|
box.innerHTML = '<div class="text-gray-500">Caricamento…</div>';
|
|
try {
|
|
const res = await fetch(@json(route('admin.gescon-import.mdb-scan')));
|
|
const j = await res.json();
|
|
if(!j.ok){ box.innerHTML = '<div class="text-danger">Errore: '+(j.error||'')+'</div>'; return; }
|
|
box.innerHTML = '';
|
|
j.archives.forEach(a=>{
|
|
const id = a.path;
|
|
const checked = selState.archives.includes(id) ? 'checked' : '';
|
|
const div = document.createElement('label');
|
|
div.className = 'flex items-center gap-2 border-b py-1 cursor-pointer hover:bg-gray-50';
|
|
div.innerHTML = `<input type="checkbox" data-path="${id}" ${checked}> <span class="flex-1 truncate" title="${id}">${a.name}</span><span class="text-xs text-gray-400">${(a.size/1024/1024).toFixed(1)} MB</span>`;
|
|
div.querySelector('input').addEventListener('change', e=>{ toggleIn(selState.archives, id); renderSelectedHidden(); });
|
|
div.addEventListener('click', e=>{ if(e.target.tagName!=='INPUT'){ div.querySelector('input').click(); } selState.currentArchive=id; reloadTables(); });
|
|
box.appendChild(div);
|
|
});
|
|
// Apply filtering (hide noise) unless toggled
|
|
applyArchiveFilter();
|
|
}catch(e){ box.innerHTML='<div class="text-danger">Errore</div>'; }
|
|
renderSelectedHidden();
|
|
}
|
|
|
|
// ===== Archive Filtering Logic =====
|
|
let showAllArchives = false;
|
|
function applyArchiveFilter(){
|
|
const legacySel = (document.getElementById('config_legacy_code')?.value || '').trim();
|
|
const yearFilter = document.getElementById('config_year_filter')?.value || '';
|
|
const noisePatterns = [/^Elenchi_per_lettere/i, /^Automat_gescon/i, /^Db_vuoto/i, /^Documenti/i, /^El_Tipo_Detraz/i, /^Elenco_copie/i, /^Elenco_per_assemblee/i, /^Frasi/i, /^Log\.mdb$/i, /^Posta_gen/i, /^Ravv/i, /^Semafori/i, /^Temporanei/i, /^temp_dov/i, /^codici_comuni/i];
|
|
const preferKeep = [/^Stabili/i, /^STABILI/i, /^Singolo_anno/i, /^singolo_stabile/i, /^generale_stabile/i, /^parti_comuni/i];
|
|
const labels = document.querySelectorAll('#mdb-archives label');
|
|
let keptGenerale = []; // track generale_stabile variants to keep best one
|
|
labels.forEach(l=>{
|
|
const name = l.querySelector('span.flex-1')?.textContent.trim() || '';
|
|
l.dataset._archName = name;
|
|
if(/^generale_stabile/i.test(name)) keptGenerale.push(l);
|
|
});
|
|
// Choose largest generale_stabile variant only
|
|
if(keptGenerale.length>1){
|
|
keptGenerale.sort((a,b)=>{ const sa=parseFloat(a.querySelector('span.text-xs.text-gray-400')?.textContent)||0; const sb=parseFloat(b.querySelector('span.text-xs.text-gray-400')?.textContent)||0; return sb-sa; });
|
|
const keep = keptGenerale[0];
|
|
keptGenerale.slice(1).forEach(l=> l.dataset._dup="1");
|
|
keep.dataset._primary="1";
|
|
}
|
|
labels.forEach(l=>{
|
|
const nm = l.dataset._archName||'';
|
|
if(showAllArchives){ l.style.display=''; return; }
|
|
// Hide duplicates of generale_stabile
|
|
if(l.dataset._dup==='1'){ l.style.display='none'; return; }
|
|
const path = l.querySelector('input')?.getAttribute('data-path')||'';
|
|
// Year filter first: if set and path doesn't contain year, hide
|
|
if(yearFilter){ if(!new RegExp(yearFilter).test(path)){ l.style.display='none'; return; } }
|
|
// Keep if preferKeep matches
|
|
if(preferKeep.some(rx=>rx.test(nm))){ l.style.display=''; return; }
|
|
// If legacy code picked, allow archives whose path contains that code (folder name pattern) best effort
|
|
if(legacySel && path.includes(legacySel)){
|
|
// If year filter active, also verify
|
|
if(yearFilter){ if(new RegExp(yearFilter).test(path)) l.style.display=''; else { l.style.display='none'; } }
|
|
else l.style.display='';
|
|
return;
|
|
}
|
|
// Year filter alone (per casi generici): mostra solo archivi con l'anno
|
|
if(yearFilter){ if(!new RegExp(yearFilter).test(path)){ l.style.display='none'; return; } }
|
|
// Noise? hide
|
|
if(noisePatterns.some(rx=>rx.test(nm))){ l.style.display='none'; return; }
|
|
// Default hide for others
|
|
l.style.display='none';
|
|
});
|
|
injectArchiveToggle();
|
|
populateYearFilter();
|
|
}
|
|
function injectArchiveToggle(){
|
|
if(document.getElementById('archive-filter-toggle')) return;
|
|
const header = document.querySelector('#mdb-archives')?.previousElementSibling;
|
|
if(!header) return;
|
|
const btn = document.createElement('button');
|
|
btn.type='button';
|
|
btn.id='archive-filter-toggle';
|
|
btn.className='netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary';
|
|
btn.innerHTML = showAllArchives ? 'Filtro archivi: ON' : 'Mostra tutti';
|
|
btn.title = 'Toggle elenco completo archivi MDB';
|
|
btn.addEventListener('click', ()=>{ showAllArchives = !showAllArchives; applyArchiveFilter(); btn.innerHTML = showAllArchives? 'Filtro archivi: ON' : 'Mostra tutti'; });
|
|
header.appendChild(btn);
|
|
}
|
|
|
|
async function reloadTables(){
|
|
const box = document.getElementById('mdb-tables');
|
|
if(!selState.currentArchive){ box.innerHTML = '<div class="text-gray-400">Seleziona un archivio…</div>'; return; }
|
|
box.innerHTML = '<div class="text-gray-500">Caricamento…</div>';
|
|
try {
|
|
const url = new URL(@json(route('admin.gescon-import.mdb-tables')), window.location.origin);
|
|
url.searchParams.set('path', selState.currentArchive);
|
|
const res = await fetch(url.toString());
|
|
const j = await res.json();
|
|
if(!j.ok){ box.innerHTML = '<div class="text-danger">Errore: '+(j.error||'')+'</div>'; return; }
|
|
box.innerHTML='';
|
|
j.tables.forEach(t=>{
|
|
const key = selState.currentArchive+'::'+t;
|
|
const checked = selState.tables.includes(key)?'checked':'';
|
|
const div = document.createElement('label');
|
|
div.className='flex items-center gap-2 border-b py-1 cursor-pointer hover:bg-gray-50';
|
|
div.innerHTML = `<input type="checkbox" data-table="${t}" ${checked}> <span class="flex-1 truncate" title="${t}">${t}</span>`;
|
|
div.querySelector('input').addEventListener('change', e=>{ toggleIn(selState.tables, key); renderSelectedHidden(); });
|
|
div.addEventListener('click', e=>{ if(e.target.tagName!=='INPUT'){ div.querySelector('input').click(); } selState.currentTable=t; reloadColumns(); });
|
|
box.appendChild(div);
|
|
});
|
|
}catch(e){ box.innerHTML='<div class="text-danger">Errore</div>'; }
|
|
renderSelectedHidden();
|
|
}
|
|
|
|
async function reloadColumns(){
|
|
const box = document.getElementById('mdb-columns');
|
|
if(!selState.currentArchive || !selState.currentTable){ box.innerHTML = '<div class="text-gray-400">Seleziona una tabella…</div>'; return; }
|
|
box.innerHTML = '<div class="text-gray-500">Caricamento…</div>';
|
|
try {
|
|
const url = new URL(@json(route('admin.gescon-import.mdb-columns')), window.location.origin);
|
|
url.searchParams.set('path', selState.currentArchive);
|
|
url.searchParams.set('table', selState.currentTable);
|
|
const res = await fetch(url.toString());
|
|
const j = await res.json();
|
|
if(!j.ok){ box.innerHTML = '<div class="text-danger">Errore: '+(j.error||'')+'</div>'; return; }
|
|
box.innerHTML='';
|
|
j.columns.forEach(c=>{
|
|
const key = selState.currentArchive+'::'+selState.currentTable+'::'+c;
|
|
const checked = selState.columns.includes(key)?'checked':'';
|
|
const div = document.createElement('label');
|
|
div.className='flex items-center gap-2 border-b py-1 cursor-pointer hover:bg-gray-50';
|
|
div.innerHTML = `<input type="checkbox" data-col="${c}" ${checked}> <span class="flex-1 truncate" title="${c}">${c}</span>`;
|
|
div.querySelector('input').addEventListener('change', e=>{ toggleIn(selState.columns, key); renderSelectedHidden(); });
|
|
box.appendChild(div);
|
|
});
|
|
}catch(e){ box.innerHTML='<div class="text-danger">Errore</div>'; }
|
|
renderSelectedHidden();
|
|
// Auto-suggest related tables after columns load
|
|
try { suggestRelatedTablesFromColumns(); } catch(_){ }
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', ()=>{
|
|
renderSelectedHidden();
|
|
setTimeout(()=>reloadArchives(), 150);
|
|
rebuildSegmentRows();
|
|
// Default 'stabile' segment if missing
|
|
if(!selState.mappingSources.find(s=>s.segment==='stabile')){
|
|
selState.mappingSources.push({ segment:'stabile', type:'stabile', target:'stabili', tables:[], columns:[] });
|
|
rebuildSegmentRows();
|
|
}
|
|
setTimeout(populateYearFilter, 400);
|
|
buildUniversalSummary();
|
|
});
|
|
|
|
// ===== Segment mapping table logic =====
|
|
function rebuildSegmentRows(){
|
|
const body = document.getElementById('segment-mapping-rows');
|
|
if(!body) return;
|
|
body.innerHTML='';
|
|
selState.mappingSources.forEach((row,idx)=>{
|
|
const tr = document.createElement('tr');
|
|
const type = row.type||''; const target = row.target||'';
|
|
tr.dataset.segType = type;
|
|
tr.innerHTML = `
|
|
<td><code>${row.segment}</code></td>
|
|
<td><input type="text" value="${type}" class="netgescon-input netgescon-input-xs w-full" onchange="updateSegMeta(${idx},'type',this.value)"></td>
|
|
<td class="align-top">
|
|
<select multiple data-idx="${idx}" data-kind="tables" class="form-select form-select-sm min-w-[180px] segment-tables-select"></select>
|
|
</td>
|
|
<td class="align-top">
|
|
<select multiple data-idx="${idx}" data-kind="columns" class="form-select form-select-sm w-full segment-columns-select"></select>
|
|
</td>
|
|
<td><input type="text" value="${target}" class="netgescon-input netgescon-input-xs w-full" placeholder="stabili/unita/..." onchange="updateSegMeta(${idx},'target',this.value)"></td>
|
|
<td class="text-center"><button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-danger" title="Elimina" onclick="deleteSegment(${idx})"><i class="mdi mdi-close"></i></button></td>`;
|
|
body.appendChild(tr);
|
|
});
|
|
// Populate selects
|
|
const allTables = selState.tables.map(t=>t.split('::').slice(-1)[0]);
|
|
const tableOptions = [...new Set(allTables)].sort();
|
|
const allCols = selState.columns.map(c=>c.split('::').slice(-1)[0]);
|
|
const colOptions = [...new Set(allCols)].sort();
|
|
body.querySelectorAll('.segment-tables-select').forEach(sel=>{
|
|
const idx = +sel.dataset.idx;
|
|
const cur = selState.mappingSources[idx].tables || [];
|
|
tableOptions.forEach(o=>{
|
|
const opt = document.createElement('option'); opt.value = o; opt.textContent = o; if(cur.includes(o)) opt.selected=true; sel.appendChild(opt);
|
|
});
|
|
sel.addEventListener('change', ()=>{ selState.mappingSources[idx].tables = Array.from(sel.selectedOptions).map(o=>o.value); renderSelectedHidden(); });
|
|
});
|
|
body.querySelectorAll('.segment-columns-select').forEach(sel=>{
|
|
const idx = +sel.dataset.idx;
|
|
const cur = selState.mappingSources[idx].columns || [];
|
|
colOptions.forEach(o=>{
|
|
const opt = document.createElement('option'); opt.value = o; opt.textContent = o; if(cur.includes(o)) opt.selected=true; sel.appendChild(opt);
|
|
});
|
|
sel.addEventListener('change', ()=>{ selState.mappingSources[idx].columns = Array.from(sel.selectedOptions).map(o=>o.value); renderSelectedHidden(); });
|
|
});
|
|
renderSelectedHidden();
|
|
}
|
|
function addSegmentRow(){
|
|
const inp = document.getElementById('new-seg-name');
|
|
const typeSel = document.getElementById('new-seg-type');
|
|
const tgtInp = document.getElementById('new-seg-target');
|
|
const name = (inp.value||'').trim();
|
|
if(!name) return; if(selState.mappingSources.find(r=>r.segment===name)) { alert('Segment esistente'); return; }
|
|
selState.mappingSources.push({ segment: name, type: (typeSel.value||''), target: (tgtInp.value||''), tables: [], columns: [] });
|
|
inp.value=''; tgtInp.value=''; typeSel.value=''; rebuildSegmentRows();
|
|
}
|
|
function updateSegMeta(idx,key,val){ if(!selState.mappingSources[idx]) return; selState.mappingSources[idx][key]=val; renderSelectedHidden(); }
|
|
function deleteSegment(idx){ selState.mappingSources.splice(idx,1); rebuildSegmentRows(); }
|
|
|
|
// Segment filter by type
|
|
document.addEventListener('DOMContentLoaded', ()=>{
|
|
const sf = document.getElementById('segment-type-filter');
|
|
if(sf){ sf.addEventListener('change', ()=>applySegmentTypeFilter(sf.value)); }
|
|
});
|
|
function applySegmentTypeFilter(v){
|
|
document.querySelectorAll('#segment-mapping-rows tr').forEach(tr=>{
|
|
if(!v) { tr.style.display=''; return; }
|
|
tr.style.display = (tr.dataset.segType===v) ? '' : 'none';
|
|
});
|
|
}
|
|
function resetSegmentFilter(){ const sf=document.getElementById('segment-type-filter'); if(sf){ sf.value=''; applySegmentTypeFilter(''); } }
|
|
|
|
// Auto-add related tables based on selected columns naming patterns (id_, fk_, codice_)
|
|
function suggestRelatedTablesFromColumns(){
|
|
const patternCols = selState.columns.map(c=>c.split('::').slice(-1)[0]);
|
|
const related = new Set();
|
|
patternCols.forEach(c=>{
|
|
if(/^id_(.+)/i.test(c)){ related.add(c.replace(/^id_/i,'').toLowerCase()); }
|
|
else if(/(.+)_id$/i.test(c)){ related.add(c.replace(/_id$/i,'').toLowerCase()); }
|
|
else if(/^fk_(.+)/i.test(c)){ related.add(c.replace(/^fk_/i,'').toLowerCase()); }
|
|
});
|
|
if(!related.size) return;
|
|
// For each segment lacking those tables, offer to append
|
|
const missing = [];
|
|
related.forEach(rt=>{
|
|
const has = selState.mappingSources.some(s=> (s.tables||[]).includes(rt));
|
|
if(!has) missing.push(rt);
|
|
});
|
|
if(missing.length && confirm('Aggiungere tabelle correlate ai segment?\n'+missing.join(', '))){
|
|
// Add to a generic segment or create one
|
|
let generic = selState.mappingSources.find(s=>s.segment==='relazioni');
|
|
if(!generic){ generic = { segment:'relazioni', type:'generico', target:'', tables:[], columns:[] }; selState.mappingSources.push(generic); }
|
|
missing.forEach(m=>{ if(!generic.tables.includes(m)) generic.tables.push(m); });
|
|
rebuildSegmentRows();
|
|
}
|
|
}
|
|
// ===== Universal Mapping Summary =====
|
|
function buildUniversalSummary(){
|
|
const body = document.getElementById('universal-summary-rows'); if(!body) return;
|
|
body.innerHTML='';
|
|
(selState.mappingSources||[]).forEach(ms=>{
|
|
const tabs = (ms.tables||[]).length; const cols=(ms.columns||[]).length; const tgt = ms.target||'';
|
|
let stato='';
|
|
if(!tabs && !cols) stato='<span class="netgescon-badge netgescon-badge-secondary">vuoto</span>'; else if(tgt && tabs && cols) stato='<span class="netgescon-badge netgescon-badge-success">ok</span>'; else stato='<span class="netgescon-badge netgescon-badge-warning">parziale</span>';
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `<td><code>${ms.segment}</code></td><td>${ms.type||''}</td><td>${tgt||'—'}</td><td>${tabs}</td><td>${cols}</td><td>${stato}</td><td><button type='button' class='netgescon-btn netgescon-btn-2xs netgescon-btn-outline-secondary' onclick='focusSegment("${ms.segment}")'>Focus</button></td>`;
|
|
body.appendChild(tr);
|
|
});
|
|
}
|
|
function focusSegment(seg){
|
|
// Scroll to segment table and filter by segment name (temporary highlight)
|
|
const rows = document.querySelectorAll('#segment-mapping-rows tr');
|
|
rows.forEach(r=>{ const code = r.querySelector('td code')?.textContent||''; r.style.outline = (code===seg)?'2px solid #2563eb':'none'; });
|
|
document.getElementById('segment-mapping-rows')?.scrollIntoView({behavior:'smooth', block:'center'});
|
|
}
|
|
function openUniversalTest(){
|
|
try{
|
|
const modal = document.getElementById('universal-modal');
|
|
const title = document.getElementById('universal-title');
|
|
const legacy = (document.getElementById('config_legacy_code')?.value||'').trim();
|
|
const year = (document.getElementById('config_year_filter')?.value||'').trim();
|
|
if(title) title.textContent = `Maschera Universale Import — ${legacy||'—'} ${year?('• '+year):''}`;
|
|
buildUniversalRows();
|
|
if(modal) modal.style.display='flex';
|
|
}catch(e){ alert('Errore apertura maschera: '+e); }
|
|
}
|
|
function closeUniversal(){ const modal = document.getElementById('universal-modal'); if(modal) modal.style.display='none'; }
|
|
function buildUniversalRows(){
|
|
const body = document.getElementById('universal-rows'); if(!body) return; body.innerHTML='';
|
|
const srcSet = new Set(selState.tables.map(t=>t));
|
|
const srcList = Array.from(srcSet).map(k=>{
|
|
const parts = k.split('::'); const arch = parts[0]||''; const tab = parts[1]||parts.slice(-1)[0];
|
|
const label = `${arch.split('/').slice(-1)[0]} → ${tab}`;
|
|
return { key:k, label };
|
|
});
|
|
const targets = {
|
|
'unita': ['codice_unita','scala','piano','interno','categoria_catastale','classe_catastale','millesimi_generali','millesimi_riscaldamento','superficie','note'],
|
|
'soggetto': ['cognome','nome','codice_fiscale','email','telefono','indirizzo','civico','citta','provincia'],
|
|
'proprietario': ['cf','nome','cognome'],
|
|
'inquilino': ['cf','nome','cognome'],
|
|
'catasto': ['sezione','foglio','particella','subalterno','rendita','consistenza']
|
|
};
|
|
const parsedCols = selState.columns.map(k=>{ const p=k.split('::'); return { key:k, arch:p[0], table:p[1], col:p[2]||p.slice(-1)[0] }; });
|
|
parsedCols.forEach((it, idx)=>{
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td class="legacy"><span class="chip">${it.table}</span> ${it.col}</td>
|
|
<td>
|
|
<select class="form-select form-select-sm" data-row="${idx}" data-kind="source">
|
|
${srcList.map(s=>`<option value="${s.key}" ${s.key.startsWith(it.arch+'::'+it.table)?'selected':''}>${s.label}</option>`).join('')}
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<div class="flex gap-1 items-center">
|
|
<select class="form-select form-select-sm w-auto" data-row="${idx}" data-kind="tgroup">
|
|
${Object.keys(targets).map(g=>`<option value="${g}">${g}</option>`).join('')}
|
|
</select>
|
|
<select class="form-select form-select-sm flex-1" data-row="${idx}" data-kind="target">
|
|
${targets['unita'].map(f=>`<option value="unita.${f}">unita.${f}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
</td>
|
|
<td class="text-center"><button type="button" class="netgescon-btn netgescon-btn-2xs netgescon-btn-outline-danger" onclick="this.closest('tr').remove()"><i class="mdi mdi-delete"></i></button></td>
|
|
`;
|
|
body.appendChild(tr);
|
|
});
|
|
body.querySelectorAll('select[data-kind="tgroup"]').forEach(sel=>{
|
|
sel.addEventListener('change', ()=>{
|
|
const grp = sel.value; const row = sel.closest('tr'); const tgtSel = row.querySelector('select[data-kind="target"]');
|
|
const opts = (targets[grp]||[]).map(f=>`<option value="${grp}.${f}">${grp}.${f}</option>`).join('');
|
|
tgtSel.innerHTML = opts;
|
|
});
|
|
});
|
|
}
|
|
function saveUniversalPreset(){
|
|
const legacy = (document.getElementById('config_legacy_code')?.value||'').trim();
|
|
const year = (document.getElementById('config_year_filter')?.value||'').trim();
|
|
const name = prompt('Nome preset import (es. banca_unicredit_0021):'); if(!name) return;
|
|
const rows = Array.from(document.querySelectorAll('#universal-rows tr'));
|
|
const mappings = rows.map(r=>{
|
|
const legacyEl = r.querySelector('.legacy');
|
|
const table = (legacyEl.querySelector('.chip')?.textContent||'').trim();
|
|
const txt = legacyEl.textContent.replace(table,'').trim();
|
|
const col = txt.trim();
|
|
const source = r.querySelector('select[data-kind="source"]')?.value||'';
|
|
const target = r.querySelector('select[data-kind="target"]')?.value||'';
|
|
return { legacy: `${table}.${col}`.trim(), source, target };
|
|
}).filter(m=>m.target);
|
|
const preset = { name, stable: legacy||null, year: year||null, createdAt: new Date().toISOString(), mappings };
|
|
// Persist server-side via route
|
|
fetch(@json(route('admin.gescon-import.presets.save')), {
|
|
method: 'POST',
|
|
headers: { 'Content-Type':'application/json', 'X-CSRF-TOKEN':'{{ csrf_token() }}' },
|
|
body: JSON.stringify({ name, preset })
|
|
}).then(r=>r.json()).then(j=>{
|
|
if(!j.ok){ alert('Errore salvataggio preset: '+(j.error||'')); return; }
|
|
alert('Preset salvato su server.');
|
|
}).catch(e=> alert('Errore rete preset: '+e));
|
|
}
|
|
// Re-run suggestions on submit (best-effort)
|
|
document.getElementById('archives-form')?.addEventListener('submit', ()=>{ try { suggestRelatedTablesFromColumns(); } catch(_){ } });
|
|
|
|
// ===== Template Stabile Snapshot Functions =====
|
|
async function saveTemplateSnapshot(){
|
|
// Force hidden inputs refresh
|
|
renderSelectedHidden();
|
|
const form = document.getElementById('archives-form');
|
|
const fd = new FormData(form);
|
|
// Ensure template code present
|
|
let code = (document.getElementById('template_stabile_code')?.value||'').trim();
|
|
if(!code){
|
|
code = prompt('Codice stabile template (es. 0021):','');
|
|
if(!code) return;
|
|
document.getElementById('template_stabile_code').value = code;
|
|
fd.set('template_stabile_code', code);
|
|
}
|
|
fd.append('force_snapshot','1');
|
|
const btns = form.querySelectorAll('button'); btns.forEach(b=>b.disabled=true);
|
|
try{
|
|
const res = await fetch(form.action, { method:'POST', headers:{'X-CSRF-TOKEN':'{{ csrf_token() }}'}, body:fd });
|
|
if(!res.ok){ alert('Errore salvataggio snapshot'); }
|
|
else { location.reload(); }
|
|
}catch(e){ alert('Errore rete snapshot'); }
|
|
finally { btns.forEach(b=>b.disabled=false); }
|
|
}
|
|
async function applyTemplateSnapshot(){
|
|
try {
|
|
const snap = @json(\App\Models\UserSetting::get('gescon.template_stabile_sources'));
|
|
if(!snap){ alert('Nessuno snapshot template salvato.'); return; }
|
|
selState.archives = Array.isArray(snap.archives)? [...snap.archives]:[];
|
|
selState.tables = Array.isArray(snap.tables)? [...snap.tables]:[];
|
|
selState.columns = Array.isArray(snap.columns)? [...snap.columns]:[];
|
|
selState.mappingSources = Array.isArray(snap.mapping_sources)? [...snap.mapping_sources]:[];
|
|
// Reset current selection contexts
|
|
selState.currentArchive=null; selState.currentTable=null;
|
|
renderSelectedHidden();
|
|
reloadArchives();
|
|
rebuildSegmentRows();
|
|
alert('Snapshot applicato alle selezioni correnti (non ancora salvato). Premere Salva selezione.');
|
|
try { suggestRelatedTablesFromColumns(); } catch(_){ }
|
|
}catch(e){ alert('Errore applicazione snapshot'); }
|
|
}
|
|
</script>
|
|
|
|
<!-- Universal Import Modal -->
|
|
<div class="universal-modal" id="universal-modal" role="dialog" aria-modal="true" aria-label="Maschera Universale Import">
|
|
<div class="universal-dialog">
|
|
<div class="universal-header">
|
|
<div class="flex items-center gap-2">
|
|
<strong id="universal-title">Maschera Universale Import</strong>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-primary" onclick="saveUniversalPreset()"><i class="mdi mdi-content-save"></i> Salva preset</button>
|
|
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="closeUniversal()"><i class="mdi mdi-close"></i></button>
|
|
</div>
|
|
</div>
|
|
<div class="universal-body">
|
|
<div class="mb-2 text-xs text-gray-600">Per ogni campo legacy scegli la sorgente (archivio→tabella) e il target applicativo.</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover mapper-table">
|
|
<thead class="table-light text-xs">
|
|
<tr>
|
|
<th style="width: 38%;">Legacy (tabella.colonna)</th>
|
|
<th style="width: 32%;">Sorgente (archivio → tabella)</th>
|
|
<th>Target</th>
|
|
<th style="width: 44px;">Del</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="universal-rows"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="universal-footer">
|
|
<div class="text-[11px] text-gray-500">I preset vengono salvati sul server per questo utente e saranno riutilizzabili.</div>
|
|
<div class="flex items-center gap-2">
|
|
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="buildUniversalRows()"><i class="mdi mdi-refresh"></i> Ricostruisci</button>
|
|
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-primary" onclick="saveUniversalPreset()"><i class="mdi mdi-content-save"></i> Salva preset</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="dirpicker" class="dirpicker-modal" role="dialog" aria-modal="true" aria-label="Selettore directory">
|
|
<div class="dirpicker-dialog">
|
|
<div class="dirpicker-header">
|
|
<strong><i class="mdi mdi-folder-outline me-1"></i> Seleziona percorso</strong>
|
|
<input id="dir-path" type="text" class="form-control ms-2" onkeydown="if(event.key==='Enter'){ event.preventDefault(); loadDir(this.value); }">
|
|
<button type="button" class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-secondary ms-2" onclick="loadDir(document.getElementById('dir-path').value)">Vai</button>
|
|
<button type="button" class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-dark ms-auto" onclick="closeDirPicker()">Chiudi</button>
|
|
</div>
|
|
<div id="dir-list" class="dirpicker-body"></div>
|
|
<div class="dirpicker-footer">
|
|
<small class="text-gray-500">Radici consentite: /home, /var/www, /mnt</small>
|
|
<div>
|
|
<button type="button" class="netgescon-btn netgescon-btn-primary" onclick="selectPath(document.getElementById('dir-path').value)">Seleziona percorso corrente</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endpush
|
|
|
|
@if(request()->query('debug')==='1')
|
|
<div class="p-2 mb-2 text-xs bg-amber-50 border border-amber-300 rounded">
|
|
View path: <code>{{ __FILE__ }}</code>
|
|
</div>
|
|
@endif
|