712 lines
36 KiB
PHP
712 lines
36 KiB
PHP
@extends('admin.layouts.netgescon')
|
||
|
||
@section('title', 'Mapping Unità (Atomico)')
|
||
|
||
@section('breadcrumb')
|
||
<a href="{{ route('admin.gescon-import.index') }}">GESCON Import</a>
|
||
<span>/</span>
|
||
<span>Mapping Unità</span>
|
||
@endsection
|
||
|
||
@section('content')
|
||
<div class="content-wrapper">
|
||
<div class="px-6">
|
||
@include('components.layout.alerts')
|
||
|
||
<div class="flex items-center justify-between mb-3 flex-wrap gap-2">
|
||
<h2 class="text-lg font-semibold">Mapping Unità e Anagrafiche — Atomico</h2>
|
||
<div class="flex items-center gap-2">
|
||
<a class="netgescon-btn netgescon-btn-outline-secondary" href="{{ route('admin.gescon-import.index', array_merge(request()->query(), ['sec'=>'anagrafica','item'=>'stabili'])) }}#stabili"><i class="mdi mdi-arrow-left"></i> Torna</a>
|
||
<a class="netgescon-btn netgescon-btn-outline-primary" href="{{ route('admin.gescon-import.relations', array_merge(request()->query(), ['segment'=>'unita'])) }}"><i class="mdi mdi-vector-link"></i> Relazioni</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="netgescon-card">
|
||
<div class="netgescon-card-header flex items-center justify-between flex-wrap gap-3">
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
<label class="text-sm text-slate-600">Legacy</label>
|
||
<input id="legacy_code" class="netgescon-input" value="{{ $legacyCode }}" placeholder="es. 0024" style="padding:.35rem .6rem;" @if($legacyCode) readonly @endif>
|
||
<label class="text-sm text-slate-600">Anno</label>
|
||
<input id="filter-anno" class="netgescon-input" placeholder="{{ $firstYear ?? 'anno' }}" value="{{ $firstYear ?? data_get($unitaLastFilters,'anno') }}" style="width:7rem;padding:.35rem .6rem;" @if(!empty($firstYear)) readonly @endif>
|
||
@if(!empty($firstYear))<span class="netgescon-badge netgescon-badge-light" title="Primo anno disponibile">{{ $firstYear }}</span>@endif
|
||
<label class="text-sm text-slate-600">Scala</label>
|
||
<input id="filter-scala" class="netgescon-input" placeholder="A" value="{{ data_get($unitaLastFilters,'scala') }}" style="width:6rem;padding:.35rem .6rem;">
|
||
<button class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-secondary" onclick="window.__reloadUnitaPreview()"><i class="mdi mdi-refresh"></i> Anteprima</button>
|
||
@if(!empty($mappingSources))
|
||
<label class="text-sm text-slate-600">Segment</label>
|
||
<select id="segment-filter" class="netgescon-input" style="padding:.35rem .6rem;">
|
||
<option value="">— nessun filtro —</option>
|
||
@foreach($mappingSources as $ms)
|
||
<option value="{{ $ms['segment'] }}">{{ $ms['segment'] }}</option>
|
||
@endforeach
|
||
</select>
|
||
<button class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-secondary" onclick="window.__toggleAllLegacy()" title="Mostra/Nascondi solo campi segment">Segment only</button>
|
||
@endif
|
||
</div>
|
||
<div class="text-xs text-slate-500">Le modifiche sono salvate per codice legacy e utente.</div>
|
||
</div>
|
||
<div class="netgescon-card-body">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<!-- SX: elenco campi legacy e sample valori -->
|
||
<div class="netgescon-card p-0">
|
||
<div class="p-3 border-b flex items-center justify-between flex-wrap gap-2">
|
||
<div class="flex items-center gap-2">
|
||
<strong>Campi Legacy (tabelle staging)</strong>
|
||
<span class="text-xs text-slate-500">({{ count($legacyHeaders ?? []) }} campi)</span>
|
||
</div>
|
||
<input id="filter-legacy" class="netgescon-input netgescon-input-sm" placeholder="Filtra campi legacy..." style="max-width:16rem;">
|
||
</div>
|
||
<div class="p-0">
|
||
<div class="table-responsive" style="max-height:48rem;overflow:auto;min-height:34rem;">
|
||
<table class="table table-sm">
|
||
<thead><tr><th>Campo</th><th>Valore (anteprima)</th></tr></thead>
|
||
<tbody id="legacy-rows">
|
||
@php $firstRow = (array) (($legacyUnita[0] ?? []) ?: []); @endphp
|
||
@forelse(($legacyHeaders ?? []) as $lh)
|
||
<tr data-legacy="{{ $lh }}"><td class="text-xs w-56">{{ $lh }}</td><td class="text-xs">{{ $firstRow[$lh] ?? '—' }}</td></tr>
|
||
@empty
|
||
<tr><td colspan="2" class="text-center text-slate-500">Seleziona un legacy code e premi Anteprima</td></tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DX: mapping raggruppato come per Stabili -->
|
||
<div class="netgescon-card p-0">
|
||
<div class="p-3 border-b flex items-center justify-between flex-wrap gap-2">
|
||
<strong>Campi Target (NetGescon)</strong>
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
<label class="text-xs text-slate-500">Preset</label>
|
||
<select id="unita-preset" class="netgescon-input netgescon-input-xs">
|
||
<option value="" @selected(($unitaPresetKey ?? '')==='')>— nessuno —</option>
|
||
<option value="gescon-base" @selected(($unitaPresetKey ?? '')==='gescon-base')>GESCON: base</option>
|
||
<option value="gescon-esteso" @selected(($unitaPresetKey ?? '')==='gescon-esteso')>GESCON: esteso</option>
|
||
<option value="gescon-catasto" @selected(($unitaPresetKey ?? '')==='gescon-catasto')>GESCON: catasto</option>
|
||
</select>
|
||
<button class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="window.__applyPreset()" title="Applica preset">Applica preset</button>
|
||
<button class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="window.__applyAutoPrefill()" title="Prefill da storico/preset">Prefill</button>
|
||
<button class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="window.__duplicaSuInquilino()" title="Copia i campi CF/Nome/Cognome sul gruppo Inquilino">Duplica su Inquilino</button>
|
||
<button class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-primary" onclick="window.__saveUnitaFieldMapping()"><i class="mdi mdi-content-save"></i> Salva</button>
|
||
</div>
|
||
</div>
|
||
@php $existing = collect($unitaFieldMapping ?? [])->keyBy('legacy'); @endphp
|
||
<div class="p-3 space-y-4" id="target-groups">
|
||
<div>
|
||
<div class="font-semibold mb-2">Anagrafica Unica (Soggetto)</div>
|
||
@foreach(($targetGroups['soggetto'] ?? []) as $tf)
|
||
@php $selLegacy = optional($existing->firstWhere('target', $tf))['legacy'] ?? null; @endphp
|
||
<div class="grid grid-cols-2 gap-2 items-center mb-2">
|
||
<label class="text-xs text-slate-600">{{ $tf }}</label>
|
||
<select class="netgescon-input netgescon-input-sm w-full target-select" data-target="{{ $tf }}">
|
||
<option value="">— da associare —</option>
|
||
@foreach(($legacyHeaders ?? []) as $lh)
|
||
<option value="{{ $lh }}" @selected($selLegacy===$lh)>{{ $lh }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
<div class="border-t pt-3">
|
||
<div class="font-semibold mb-2">Unità Immobiliare</div>
|
||
@foreach(($targetGroups['unita'] ?? []) as $tf)
|
||
@php $selLegacy = optional($existing->firstWhere('target', $tf))['legacy'] ?? null; @endphp
|
||
<div class="grid grid-cols-2 gap-2 items-center mb-2">
|
||
<label class="text-xs text-slate-600">{{ $tf }}</label>
|
||
<select class="netgescon-input netgescon-input-sm w-full target-select" data-target="{{ $tf }}">
|
||
<option value="">— da associare —</option>
|
||
@foreach(($legacyHeaders ?? []) as $lh)
|
||
<option value="{{ $lh }}" @selected($selLegacy===$lh)>{{ $lh }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
<div class="border-t pt-3">
|
||
<div class="font-semibold mb-2">Relazioni principali</div>
|
||
@foreach(($targetGroups['relazioni'] ?? []) as $tf)
|
||
@php $selLegacy = optional($existing->firstWhere('target', $tf))['legacy'] ?? null; @endphp
|
||
<div class="grid grid-cols-2 gap-2 items-center mb-2">
|
||
<label class="text-xs text-slate-600">{{ $tf }}</label>
|
||
<select class="netgescon-input netgescon-input-sm w-full target-select" data-target="{{ $tf }}">
|
||
<option value="">— da associare —</option>
|
||
@foreach(($legacyHeaders ?? []) as $lh)
|
||
<option value="{{ $lh }}" @selected($selLegacy===$lh)>{{ $lh }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||
<!-- Navigatore legacy: Unità / Tabelle / Voci -->
|
||
<div class="netgescon-card p-0">
|
||
<div class="p-3 border-b flex items-center justify-between flex-wrap gap-2">
|
||
<strong>Navigazione dati legacy</strong>
|
||
<div class="text-xs text-slate-500">Primo / Prec / Pos / Succ / Ultimo</div>
|
||
</div>
|
||
<div class="p-3 space-y-3">
|
||
<div>
|
||
<div class="font-semibold mb-1">CONDOMIN (Unità)</div>
|
||
<div class="flex items-center gap-2 mb-2">
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navUnita('first')">«</button>
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navUnita('prev')">‹</button>
|
||
<input id="nav-unita-index" class="netgescon-input netgescon-input-xs" value="0" style="width:5rem;">
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navUnita('next')">›</button>
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navUnita('last')">»</button>
|
||
</div>
|
||
<pre id="nav-unita-json" class="text-xs bg-slate-50 p-2 rounded border overflow-auto" style="max-height:12rem"></pre>
|
||
</div>
|
||
<div class="border-t pt-2">
|
||
<div class="font-semibold mb-1">TABELLE (Millesimi)</div>
|
||
<div class="flex items-center gap-2 mb-2">
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navTabelle('first')">«</button>
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navTabelle('prev')">‹</button>
|
||
<input id="nav-tabelle-index" class="netgescon-input netgescon-input-xs" value="0" style="width:5rem;">
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navTabelle('next')">›</button>
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navTabelle('last')">»</button>
|
||
</div>
|
||
<pre id="nav-tabelle-json" class="text-xs bg-slate-50 p-2 rounded border overflow-auto" style="max-height:10rem"></pre>
|
||
</div>
|
||
<div class="border-t pt-2">
|
||
<div class="font-semibold mb-1">VOCI (Piano dei conti)</div>
|
||
<div class="flex items-center gap-2 mb-2">
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navVoci('first')">«</button>
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navVoci('prev')">‹</button>
|
||
<input id="nav-voci-index" class="netgescon-input netgescon-input-xs" value="0" style="width:5rem;">
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navVoci('next')">›</button>
|
||
<button class="netgescon-btn netgescon-btn-xs" onclick="window.__navVoci('last')">»</button>
|
||
</div>
|
||
<pre id="nav-voci-json" class="text-xs bg-slate-50 p-2 rounded border overflow-auto" style="max-height:10rem"></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Staging Explorer: qualsiasi tabella -->
|
||
<div class="netgescon-card p-0">
|
||
<div class="p-3 border-b flex items-center justify-between flex-wrap gap-2">
|
||
<strong>Staging explorer</strong>
|
||
<span class="text-xs text-slate-500">condomin, millesimi, voci...</span>
|
||
</div>
|
||
<div class="p-3 space-y-2">
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
<input id="stg-guess" class="netgescon-input netgescon-input-sm" placeholder="guess (es. mille, voci, condomin)" style="width:16rem">
|
||
<input id="stg-table" class="netgescon-input netgescon-input-sm" placeholder="table (opzionale)" style="width:14rem">
|
||
<button class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-secondary" onclick="window.__stagingProbe()">Sonda</button>
|
||
</div>
|
||
<div class="text-xs text-slate-500" id="stg-meta"></div>
|
||
<div class="table-responsive" style="max-height:16rem;overflow:auto;">
|
||
<table class="table table-sm">
|
||
<thead><tr id="stg-head"></tr></thead>
|
||
<tbody id="stg-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="netgescon-card mt-4">
|
||
<div class="netgescon-card-header flex items-center justify-between">
|
||
<strong>Creazione colonne (facoltativa)</strong>
|
||
<span class="text-xs text-slate-500">Operazione separata: evita inline sulla tabella</span>
|
||
</div>
|
||
<div class="netgescon-card-body flex items-center gap-2 flex-wrap">
|
||
<input id="new-col-name" class="netgescon-input netgescon-input-sm" placeholder="nome_colonna">
|
||
<select id="new-col-type" class="netgescon-input netgescon-input-sm">
|
||
<option>string</option><option>text</option><option>integer</option><option>decimal</option><option>boolean</option><option>date</option><option>datetime</option>
|
||
</select>
|
||
<label class="text-xs"><input id="new-col-null" type="checkbox" checked> nullable</label>
|
||
<button class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-primary" onclick="window.__createTargetColumn()"><i class="mdi mdi-plus"></i> Crea colonna</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="netgescon-card mt-4">
|
||
<div class="netgescon-card-header flex items-center justify-between flex-wrap gap-2">
|
||
<strong>Palazzine e Scale</strong>
|
||
<span class="text-xs text-slate-500">Raggruppa le scale in palazzine personalizzate</span>
|
||
</div>
|
||
<div class="netgescon-card-body">
|
||
<div class="text-xs text-slate-500 mb-2">Scale trovate: <span id="scale-chip-list">@forelse(($scaleList ?? []) as $s)<span class="inline-block px-2 py-0.5 bg-slate-100 rounded mr-1">{{ $s }}</span>@empty<span class="inline-block px-2 py-0.5 bg-slate-100 rounded mr-1">—</span>@endforelse</span></div>
|
||
@if(!empty($scalaSummary))
|
||
<div class="text-xxs text-slate-400 mb-2">
|
||
@foreach($scalaSummary as $item)
|
||
<span class="inline-block mr-2">Scala <strong>{{ $item['scala'] }}</strong>: {{ $item['count'] }} UI, interni {{ implode(', ', $item['interni'] ?? []) }}</span>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
<div id="palazzine-list" class="space-y-2"></div>
|
||
<div class="mt-2 flex items-center gap-2">
|
||
<input id="pal-name" class="netgescon-input netgescon-input-sm" placeholder="Nome palazzina (es. Pal. 1)">
|
||
<input id="pal-scales" class="netgescon-input netgescon-input-sm" placeholder="Scale (es. A,B,C)">
|
||
<button class="netgescon-btn netgescon-btn-sm" onclick="window.__addPalazzina()"><i class="mdi mdi-plus"></i> Aggiungi</button>
|
||
<button class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-primary" onclick="window.__savePalazzine()"><i class="mdi mdi-content-save"></i> Salva configurazione</button>
|
||
</div>
|
||
|
||
<div class="netgescon-card mt-4">
|
||
<div class="netgescon-card-header flex items-center justify-between flex-wrap gap-2">
|
||
<strong>Schema piani e adiacenze (preview)</strong>
|
||
<div class="flex items-center gap-2">
|
||
<button class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-secondary" onclick="window.__buildSchemaPreview()"><i class="mdi mdi-vector-arrange-above"></i> Costruisci schema</button>
|
||
</div>
|
||
</div>
|
||
<div class="netgescon-card-body">
|
||
<div class="text-xs text-slate-500 mb-2">Se il campo "piano" non è presente in CONDOMIN, il calcolo sopra/sotto è limitato; i lati sono stimati su interni adiacenti.</div>
|
||
<div id="schema-preview" class="space-y-3"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@if(!empty($peoplePreview))
|
||
<div class="netgescon-card mt-4">
|
||
<div class="netgescon-card-header flex items-center justify-between flex-wrap gap-2">
|
||
<strong>Anteprima anagrafiche dal file CONDOMIN</strong>
|
||
<span class="text-xs text-slate-500">I conteggi includono proprietari e inquilini distinti per scala/interno</span>
|
||
</div>
|
||
<div class="netgescon-card-body space-y-2">
|
||
@php
|
||
$owners = $peoplePreview['proprietari'] ?? [];
|
||
$tenants = $peoplePreview['inquilini'] ?? [];
|
||
@endphp
|
||
<div class="text-xs text-slate-600">Proprietari rilevati: <strong>{{ count($owners) }}</strong></div>
|
||
@if(!empty($owners))
|
||
<div class="text-xxs text-slate-500">Esempi: {{ collect($owners)->take(6)->map(fn($p) => trim(($p['cognome'] ?? '').' '.($p['nome'] ?? '')))->filter()->implode(', ') }}</div>
|
||
@endif
|
||
<div class="text-xs text-slate-600">Inquilini rilevati: <strong>{{ count($tenants) }}</strong></div>
|
||
@if(!empty($tenants))
|
||
<div class="text-xxs text-slate-500">Esempi: {{ collect($tenants)->take(6)->map(fn($p) => trim(($p['cognome'] ?? '').' '.($p['nome'] ?? '')))->filter()->implode(', ') }}</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@endif
|
||
@endsection
|
||
|
||
@section('module-scripts')
|
||
<script>
|
||
(function(){
|
||
const CFG = window.GESCON_MAPPING || { routes: {} };
|
||
// Inject backend-provided prefill mapping if any
|
||
const PREFILL = @json($prefillMapping ?? []);
|
||
window.addEventListener('DOMContentLoaded', ()=>{
|
||
// Auto apply prefill mapping once
|
||
if(Array.isArray(PREFILL) && PREFILL.length){
|
||
PREFILL.forEach(p=>{
|
||
if(!p || !p.target) return;
|
||
const sel = document.querySelector(`#target-groups .target-select[data-target="${p.target}"]`);
|
||
if(sel){
|
||
// ensure option exists; if not, create it to visualize suggestion
|
||
if(!Array.from(sel.options).some(o=>o.value===p.legacy) && p.legacy){
|
||
const opt = document.createElement('option'); opt.value=p.legacy; opt.textContent=p.legacy; sel.appendChild(opt);
|
||
}
|
||
if(p.legacy) sel.value = p.legacy;
|
||
}
|
||
});
|
||
}
|
||
// Auto fire anteprima if legacy code+anno presenti
|
||
const legacyVal = document.getElementById('legacy_code')?.value.trim();
|
||
if(legacyVal){ setTimeout(()=> window.__reloadUnitaPreview && window.__reloadUnitaPreview(), 400); }
|
||
});
|
||
const saveUrl = '{{ route('admin.gescon-import.mapping.save') }}';
|
||
const columnsUrl = '{{ route('admin.gescon-import.create-target-columns') }}';
|
||
const sampleUnitaUrl = '{{ route('admin.gescon-import.sample-unita') }}';
|
||
const sampleTabelleUrl = '{{ route('admin.gescon-import.sample-tabelle') }}';
|
||
const sampleVociUrl = '{{ route('admin.gescon-import.sample-voci') }}';
|
||
const stagingColumnsUrl = '{{ route('admin.gescon-import.staging-columns') }}';
|
||
const stagingAllColumnsUrl = '{{ route('admin.gescon-import.staging-all-columns') }}';
|
||
|
||
window.__reloadUnitaPreview = async function(){
|
||
const legacy = document.getElementById('legacy_code').value.trim();
|
||
if(!legacy){ alert('Inserisci legacy code'); return; }
|
||
const url = new URL('{{ route('admin.gescon-import.unita-year') }}', window.location.origin);
|
||
url.searchParams.set('legacy_code', legacy);
|
||
const anno = document.getElementById('filter-anno')?.value.trim();
|
||
if(anno) url.searchParams.set('anno', anno);
|
||
try{
|
||
const res = await fetch(url.toString());
|
||
const js = await res.json();
|
||
const tbody = document.getElementById('legacy-rows');
|
||
// Rebuild left table values preview using first row
|
||
const first = (js && js.rows && js.rows[0]) ? js.rows[0] : {};
|
||
Array.from(tbody.querySelectorAll('tr[data-legacy]')).forEach(tr=>{
|
||
const key = tr.getAttribute('data-legacy');
|
||
const td = tr.querySelector('td:nth-child(2)');
|
||
if(td) td.textContent = (first && first[key] != null) ? String(first[key]) : '—';
|
||
});
|
||
if(js && js.summary && Array.isArray(js.summary.scale_list)){
|
||
scaleAll = js.summary.scale_list;
|
||
renderScaleChips();
|
||
}
|
||
}catch(e){ alert('Errore anteprima'); }
|
||
};
|
||
|
||
window.__saveUnitaFieldMapping = async function(){
|
||
const legacy = document.getElementById('legacy_code').value.trim();
|
||
if(!legacy){ alert('Seleziona legacy code'); return; }
|
||
// Build mapping array legacy→target from group selects (target→legacy mapping in UI)
|
||
const m = [];
|
||
document.querySelectorAll('#target-groups .target-select').forEach(sel=>{
|
||
const target = sel.getAttribute('data-target');
|
||
const legacyKey = sel.value || null;
|
||
if(target){ m.push({ legacy: legacyKey, target: target }); }
|
||
});
|
||
const data = m;
|
||
const presetKey = document.getElementById('unita-preset')?.value || '';
|
||
try{
|
||
const res = await fetch(saveUrl, { method:'POST', headers:{'Content-Type':'application/json','X-CSRF-TOKEN':document.querySelector('meta[name="csrf-token"]').content, 'Accept':'application/json'}, body: JSON.stringify({ _mapping_only:true, _legacy_code: legacy, unita_field_mapping: JSON.stringify(data), unita_preset_key: presetKey }) });
|
||
if(res.ok){ alert('Mapping salvato'); } else { alert('Errore salvataggio'); }
|
||
}catch(e){ alert('Errore di rete'); }
|
||
};
|
||
|
||
window.__applyAutoPrefill = function(){
|
||
// Basic heuristics: map common fields if present
|
||
const pairs = {
|
||
'cognome':'cognome','nome':'nome','codice_fiscale':'codice_fiscale',
|
||
'scala':'scala','interno':'interno','millesimi_generali':'millesimi_generali','millesimi_risc':'millesimi_riscaldamento'
|
||
};
|
||
Object.entries(pairs).forEach(([legacy, target])=>{
|
||
const sel = document.querySelector(`#target-groups .target-select[data-target="${target}"]`);
|
||
if(sel){
|
||
const opt = Array.from(sel.options).find(o=>o.value===legacy);
|
||
if(opt) sel.value = legacy;
|
||
}
|
||
});
|
||
};
|
||
|
||
window.__applyPreset = function(){
|
||
const key = document.getElementById('unita-preset').value;
|
||
const presets = {
|
||
'gescon-base': {
|
||
soggetto: { 'cognome':'cognome', 'nome':'nome', 'codice_fiscale':'codice_fiscale' },
|
||
unita: { 'scala':'scala', 'interno':'interno', 'millesimi_generali':'millesimi_generali', 'millesimi_risc':'millesimi_riscaldamento' }
|
||
},
|
||
'gescon-esteso': {
|
||
soggetto: { 'cognome':'cognome', 'nome':'nome', 'codice_fiscale':'codice_fiscale', 'email':'email' },
|
||
unita: { 'scala':'scala', 'interno':'interno', 'piano':'piano', 'millesimi_generali':'millesimi_generali', 'millesimi_risc':'millesimi_riscaldamento' }
|
||
},
|
||
'gescon-catasto': {
|
||
soggetto: {},
|
||
unita: {
|
||
'sezione':'sezione',
|
||
'foglio':'foglio',
|
||
'particella':'particella',
|
||
'sub':'subalterno',
|
||
'categoria':'categoria_catastale',
|
||
'classe':'classe_catastale',
|
||
'consistenza':'consistenza',
|
||
'superficie':'superficie',
|
||
'rendita':'rendita'
|
||
}
|
||
}
|
||
};
|
||
const p = presets[key];
|
||
if(!p) return;
|
||
const assign = (group) => {
|
||
Object.entries(group||{}).forEach(([legacy,target])=>{
|
||
const sel = document.querySelector(`#target-groups .target-select[data-target="${target}"]`);
|
||
if(!sel) return;
|
||
const opt = Array.from(sel.options).find(o=>o.value===legacy);
|
||
if(opt) sel.value = legacy;
|
||
});
|
||
};
|
||
assign(p.soggetto);
|
||
assign(p.unita);
|
||
};
|
||
|
||
window.__duplicaSuInquilino = function(){
|
||
const map = {
|
||
'conduttore.cf': 'codice_fiscale',
|
||
'conduttore.nome': 'nome',
|
||
'conduttore.cognome': 'cognome',
|
||
'inquilino.cf': 'codice_fiscale',
|
||
'inquilino.nome': 'nome',
|
||
'inquilino.cognome': 'cognome'
|
||
};
|
||
Object.entries(map).forEach(([targetRel, sourceTarget])=>{
|
||
const sourceSel = document.querySelector(`#target-groups .target-select[data-target="${sourceTarget}"]`);
|
||
const destSel = document.querySelector(`#target-groups .target-select[data-target="${targetRel}"]`);
|
||
if(sourceSel && destSel){ destSel.value = sourceSel.value; }
|
||
});
|
||
};
|
||
|
||
// Legacy navigators
|
||
async function fetchSample(url, index){
|
||
const legacy = document.getElementById('legacy_code').value.trim();
|
||
const u = new URL(url, window.location.origin);
|
||
if(legacy) u.searchParams.set('legacy_code', legacy);
|
||
u.searchParams.set('index', String(index||0));
|
||
const res = await fetch(u.toString());
|
||
return res.json();
|
||
}
|
||
window.__navUnita = async function(dir){
|
||
const idxEl = document.getElementById('nav-unita-index');
|
||
let idx = parseInt(idxEl.value||'0',10) || 0;
|
||
if(dir==='first') idx = 0; else if(dir==='prev') idx = Math.max(0, idx-1); else if(dir==='next') idx = idx+1; // 'last' unknown -> just +1000 then fetch will clamp server-side if implemented
|
||
if(dir==='last') idx = idx + 1000;
|
||
const js = await fetchSample(sampleUnitaUrl, idx);
|
||
if(js && js.ok){ idxEl.value = js.index ?? idx; document.getElementById('nav-unita-json').textContent = JSON.stringify(js.data, null, 2); }
|
||
};
|
||
window.__navTabelle = async function(dir){
|
||
const idxEl = document.getElementById('nav-tabelle-index');
|
||
let idx = parseInt(idxEl.value||'0',10) || 0;
|
||
if(dir==='first') idx = 0; else if(dir==='prev') idx = Math.max(0, idx-1); else if(dir==='next') idx = idx+1; if(dir==='last') idx = idx + 1000;
|
||
const js = await fetchSample(sampleTabelleUrl, idx);
|
||
if(js && js.ok){ idxEl.value = js.index ?? idx; document.getElementById('nav-tabelle-json').textContent = JSON.stringify(js.data, null, 2); }
|
||
};
|
||
window.__navVoci = async function(dir){
|
||
const idxEl = document.getElementById('nav-voci-index');
|
||
let idx = parseInt(idxEl.value||'0',10) || 0;
|
||
if(dir==='first') idx = 0; else if(dir==='prev') idx = Math.max(0, idx-1); else if(dir==='next') idx = idx+1; if(dir==='last') idx = idx + 1000;
|
||
const js = await fetchSample(sampleVociUrl, idx);
|
||
if(js && js.ok){ idxEl.value = js.index ?? idx; document.getElementById('nav-voci-json').textContent = JSON.stringify(js.data, null, 2); }
|
||
};
|
||
|
||
// Staging explorer
|
||
window.__stagingProbe = async function(){
|
||
const legacy = document.getElementById('legacy_code').value.trim();
|
||
const guess = document.getElementById('stg-guess').value.trim();
|
||
const table = document.getElementById('stg-table').value.trim();
|
||
const u = new URL(stagingColumnsUrl, window.location.origin);
|
||
if(legacy) u.searchParams.set('legacy_code', legacy);
|
||
if(guess) u.searchParams.set('guess', guess);
|
||
if(table) u.searchParams.set('table', table);
|
||
try{
|
||
const res = await fetch(u.toString());
|
||
const js = await res.json();
|
||
const meta = document.getElementById('stg-meta');
|
||
const head = document.getElementById('stg-head');
|
||
const body = document.getElementById('stg-body');
|
||
head.innerHTML = ''; body.innerHTML = '';
|
||
if(!js || !js.ok){ meta.textContent = (js && js.error) ? ('Errore: '+js.error) : 'Nessun dato'; return; }
|
||
meta.textContent = `Conn: ${js.connection} | Tabella: ${js.table} | Count: ${js.count}`;
|
||
// Render header
|
||
(js.columns||[]).forEach(c=>{ const th = document.createElement('th'); th.className='text-xs'; th.textContent = c; head.appendChild(th); });
|
||
// Render rows
|
||
(js.rows||[]).forEach(r=>{
|
||
const tr = document.createElement('tr');
|
||
(js.columns||[]).forEach(c=>{ const td = document.createElement('td'); td.className='text-xs'; td.textContent = (r && r[c] != null) ? String(r[c]) : ''; tr.appendChild(td); });
|
||
body.appendChild(tr);
|
||
});
|
||
}catch(e){ document.getElementById('stg-meta').textContent = 'Errore richiesta staging'; }
|
||
};
|
||
|
||
// Palazzine: render, add, save
|
||
const palRoot = document.getElementById('palazzine-list');
|
||
const palInit = @json($palazzineConfig ?? []);
|
||
let scaleAll = @json($scaleList ?? []);
|
||
const scaleSummary = @json($scalaSummary ?? []);
|
||
const scaleChipRoot = document.getElementById('scale-chip-list');
|
||
|
||
function renderScaleChips(){
|
||
if(!scaleChipRoot) return;
|
||
if(!Array.isArray(scaleAll) || !scaleAll.length){
|
||
scaleChipRoot.innerHTML = '<span class="inline-block px-2 py-0.5 bg-slate-100 rounded mr-1">—</span>';
|
||
return;
|
||
}
|
||
scaleChipRoot.innerHTML = scaleAll.map(s=>`<span class="inline-block px-2 py-0.5 bg-slate-100 rounded mr-1">${s}</span>`).join('');
|
||
}
|
||
renderScaleChips();
|
||
function renderPal(){
|
||
palRoot.innerHTML = '';
|
||
if(!palInit.length){ palRoot.innerHTML = '<div class="text-xs text-slate-500">Nessuna palazzina definita</div>'; return; }
|
||
palInit.forEach((p, idx)=>{
|
||
const div = document.createElement('div');
|
||
div.className = 'flex items-center gap-2 flex-wrap';
|
||
const scales = (p.scales||[]).map(s=>`<span class="inline-block px-2 py-0.5 bg-slate-100 rounded mr-1">${s}</span>`).join('');
|
||
div.innerHTML = `<strong class="mr-2">${p.name}</strong>${scales}<button class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-danger" data-del="${idx}"><i class="mdi mdi-delete"></i></button>`;
|
||
palRoot.appendChild(div);
|
||
});
|
||
palRoot.querySelectorAll('button[data-del]').forEach(btn=>{
|
||
btn.addEventListener('click',()=>{ palInit.splice(parseInt(btn.getAttribute('data-del')),1); renderPal(); });
|
||
});
|
||
}
|
||
renderPal();
|
||
|
||
window.__addPalazzina = function(){
|
||
const name = document.getElementById('pal-name').value.trim();
|
||
const scales = (document.getElementById('pal-scales').value||'').split(',').map(s=>s.trim()).filter(Boolean);
|
||
if(!name || !scales.length){ alert('Nome e almeno una scala sono richiesti'); return; }
|
||
// Validate scales exist
|
||
const missing = scales.filter(s=>!scaleAll.includes(s));
|
||
if(missing.length){ if(!confirm('Scale non presenti nell\'anteprima: '+missing.join(', ')+'. Continuare?')) return; }
|
||
palInit.push({ name, scales });
|
||
document.getElementById('pal-name').value='';
|
||
document.getElementById('pal-scales').value='';
|
||
renderPal();
|
||
};
|
||
|
||
window.__savePalazzine = async function(){
|
||
const legacy = document.getElementById('legacy_code').value.trim();
|
||
if(!legacy){ alert('Seleziona legacy code'); return; }
|
||
try{
|
||
const res = await fetch(saveUrl, { method:'POST', headers:{'Content-Type':'application/json','X-CSRF-TOKEN':document.querySelector('meta[name="csrf-token"]').content, 'Accept':'application/json'}, body: JSON.stringify({ _mapping_only:true, _legacy_code: legacy, palazzine_config: JSON.stringify(palInit) }) });
|
||
if(res.ok){ alert('Palazzine salvate'); } else { alert('Errore salvataggio palazzine'); }
|
||
}catch(e){ alert('Errore di rete'); }
|
||
};
|
||
|
||
window.__buildSchemaPreview = async function(){
|
||
const legacy = document.getElementById('legacy_code').value.trim();
|
||
if(!legacy){ alert('Seleziona legacy code'); return; }
|
||
const url = new URL('{{ route('admin.gescon-import.unita-year') }}', window.location.origin);
|
||
url.searchParams.set('legacy_code', legacy);
|
||
const anno = document.getElementById('filter-anno')?.value.trim();
|
||
if(anno) url.searchParams.set('anno', anno);
|
||
try{
|
||
const res = await fetch(url.toString());
|
||
const js = await res.json();
|
||
const rows = Array.isArray(js.rows) ? js.rows : [];
|
||
if(js && js.summary && Array.isArray(js.summary.scale_list)){
|
||
scaleAll = js.summary.scale_list;
|
||
renderScaleChips();
|
||
}
|
||
renderSchema(rows);
|
||
}catch(e){ alert('Errore caricamento anteprima schema'); }
|
||
};
|
||
|
||
function renderSchema(rows){
|
||
const root = document.getElementById('schema-preview');
|
||
root.innerHTML = '';
|
||
if(!rows.length){ root.innerHTML = '<div class="text-xs text-slate-500">Nessun dato anteprima</div>'; return; }
|
||
const hasPiano = rows.some(r=>r.piano!==undefined && r.piano!==null && r.piano!=='');
|
||
const byScala = {};
|
||
rows.forEach(r=>{
|
||
const s = (r.scala||'').toString()||'—';
|
||
if(!byScala[s]) byScala[s]=[];
|
||
byScala[s].push(r);
|
||
});
|
||
// If palazzine defined, group scales accordingly; otherwise single group "Tutte le scale"
|
||
const pal = palInit.length ? palInit : [{ name:'Tutte le scale', scales: Object.keys(byScala) }];
|
||
pal.forEach(p=>{
|
||
const sec = document.createElement('div');
|
||
sec.innerHTML = `<div class="font-semibold mb-1">${p.name}</div>`;
|
||
// For each scala in group
|
||
(p.scales||[]).forEach(sc=>{
|
||
const list = (byScala[sc]||[]).slice();
|
||
// Derive numeric piano and interno when possible
|
||
list.forEach(x=>{ x._p = parseInt(x.piano||x.PIANO||x.Piano||0,10) || 0; x._i = parseInt(x.interno||x.INTERNO||x.Interno||0,10) || 0; });
|
||
// Sort by piano desc, interno asc
|
||
list.sort((a,b)=> (b._p-a._p) || (a._i-b._i));
|
||
const wrap = document.createElement('div');
|
||
wrap.className = 'mb-2';
|
||
wrap.innerHTML = `<div class="text-sm text-slate-600 mb-1">Scala ${sc}</div>`;
|
||
// Group by piano
|
||
const floors = {};
|
||
list.forEach(x=>{ const k = hasPiano ? x._p : 0; if(!floors[k]) floors[k]=[]; floors[k].push(x); });
|
||
const floorsOrder = Object.keys(floors).map(n=>parseInt(n,10)).sort((a,b)=>b-a);
|
||
floorsOrder.forEach(fp=>{
|
||
const row = document.createElement('div');
|
||
row.className = 'flex items-center gap-2 flex-wrap mb-1';
|
||
const label = document.createElement('div');
|
||
label.className = 'text-xs w-20 text-slate-500';
|
||
label.textContent = hasPiano ? `Piano ${fp}` : 'Piano (?)';
|
||
row.appendChild(label);
|
||
floors[fp].sort((a,b)=>a._i-b._i).forEach(u=>{
|
||
const chip = document.createElement('button');
|
||
chip.type = 'button';
|
||
chip.className = 'px-2 py-1 text-xs rounded border hover:bg-slate-50';
|
||
chip.title = `${u.cognome||''} ${u.nome||''}`.trim();
|
||
chip.textContent = `${u._i||u.interno||'?'}${u.cognome? ' - '+(u.cognome||'') : ''}`.trim();
|
||
chip.addEventListener('click', ()=> highlightNeighbors(sc, fp, u, floors));
|
||
row.appendChild(chip);
|
||
});
|
||
wrap.appendChild(row);
|
||
});
|
||
sec.appendChild(wrap);
|
||
});
|
||
root.appendChild(sec);
|
||
});
|
||
}
|
||
|
||
function highlightNeighbors(scala, piano, unit, floors){
|
||
// Reset highlights
|
||
document.querySelectorAll('#schema-preview button').forEach(b=> b.style.backgroundColor='');
|
||
// find neighbors: up/down by piano, left/right by interno +/-1 on same piano
|
||
const curButtons = Array.from(document.querySelectorAll('#schema-preview button'));
|
||
const matchChip = (pred) => curButtons.filter(btn=> pred(btn));
|
||
const isThis = (btn)=> btn.textContent.split(' - ')[0].split(' ')[0].replace(/\D/g,'') === String(unit._i||unit.interno||'');
|
||
const thisBtn = curButtons.find(isThis);
|
||
if(thisBtn) thisBtn.style.backgroundColor = '#ffe7a3';
|
||
const sameFloor = (floors[piano]||[]);
|
||
const left = sameFloor.find(u=> (u._i-1) === unit._i);
|
||
const right = sameFloor.find(u=> (u._i+1) === unit._i);
|
||
const above = (floors[piano+1]||[]).find(u=> u._i === unit._i);
|
||
const below = (floors[piano-1]||[]).find(u=> u._i === unit._i);
|
||
const mark = (u, color)=>{
|
||
if(!u) return;
|
||
const btn = curButtons.find(b=> b.textContent.startsWith(String(u._i)));
|
||
if(btn) btn.style.backgroundColor = color;
|
||
};
|
||
mark(left, '#cfe8ff');
|
||
mark(right, '#cfe8ff');
|
||
mark(above, '#d7ffd9');
|
||
mark(below, '#ffd7d7');
|
||
}
|
||
|
||
window.__createTargetColumn = async function(){
|
||
const name = document.getElementById('new-col-name').value.trim();
|
||
const type = document.getElementById('new-col-type').value.trim();
|
||
const nullable = !!document.getElementById('new-col-null').checked;
|
||
if(!name){ alert('Nome colonna richiesto'); return; }
|
||
try{
|
||
const res = await fetch(columnsUrl, { method:'POST', headers:{'Content-Type':'application/json','X-CSRF-TOKEN':document.querySelector('meta[name="csrf-token"]').content}, body: JSON.stringify({ table:'unita_immobiliari', columns:[{ name, type, nullable }] }) });
|
||
const js = await res.json().catch(()=>({}));
|
||
if(!res.ok || js.ok===false){ throw new Error(js.error||('HTTP '+res.status)); }
|
||
alert('Colonna creata (se non esisteva)');
|
||
document.getElementById('new-col-name').value='';
|
||
}catch(e){ alert('Errore: '+e.message); }
|
||
};
|
||
|
||
// Filter
|
||
const filter = document.getElementById('filter-legacy');
|
||
filter && filter.addEventListener('input', ()=>{
|
||
const q = (filter.value||'').toLowerCase();
|
||
document.querySelectorAll('#legacy-rows tr[data-legacy]').forEach(tr=>{
|
||
const k = (tr.getAttribute('data-legacy')||'').toLowerCase();
|
||
tr.style.display = !q || k.includes(q) ? '' : 'none';
|
||
});
|
||
});
|
||
|
||
// Load all staging columns and append as table.column entries for mapping choices
|
||
(async function loadAllStagingColumns(){
|
||
try{
|
||
const res = await fetch(stagingAllColumnsUrl);
|
||
const js = await res.json();
|
||
if(!js || !js.ok) return;
|
||
const extraOptions = [];
|
||
const leftTbody = document.getElementById('legacy-rows');
|
||
// Append table.column entries after the plain condomin headers
|
||
(js.tables||[]).forEach(t=>{
|
||
const name = t.name;
|
||
(t.columns||[]).forEach(c=>{
|
||
const full = `${name}.${c}`;
|
||
extraOptions.push(full);
|
||
// Don’t duplicate CONDOMIN’s base fields already shown
|
||
if(!leftTbody.querySelector(`tr[data-legacy="${full}"]`)){
|
||
const tr = document.createElement('tr');
|
||
tr.setAttribute('data-legacy', full);
|
||
tr.innerHTML = `<td class="text-xs w-56">${full}</td><td class="text-xs">—</td>`;
|
||
leftTbody.appendChild(tr);
|
||
}
|
||
});
|
||
});
|
||
// Extend each target select with table.column options (following existing plain headers)
|
||
document.querySelectorAll('#target-groups .target-select').forEach(sel=>{
|
||
// add a divider optgroup for clarity
|
||
const og = document.createElement('optgroup'); og.label = '— tabelle.staging —';
|
||
extraOptions.forEach(v=>{ const o = document.createElement('option'); o.value=v; o.textContent=v; og.appendChild(o); });
|
||
sel.appendChild(og);
|
||
});
|
||
}catch(e){ /* silent */ }
|
||
})();
|
||
})();
|
||
</script>
|
||
@endsection
|
||
|