netgescon-day0/resources/views/admin/gescon-import/unita-mapping-page.blade.php

712 lines
36 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

@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);
// Dont duplicate CONDOMINs 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