netgescon-day0/resources/views/admin/gescon-import/partials/voci-field-mapping.blade.php

965 lines
45 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

@php
$legacyCode = request()->input('legacy_code', $legacyCode ?? null);
$existingMap = collect($vociFieldMapping ?? [])
->filter(fn ($row) => !empty($row['legacy']))
->keyBy(fn ($row) => $row['legacy']);
$humanize = static function (?string $value): string {
if ($value === null) {
return '';
}
$value = str_replace(['.', '_'], ' ', $value);
$value = preg_replace('/\s+/', ' ', $value ?? '') ?? '';
return ucfirst(trim($value));
};
$importConnection = config('database.connections.gescon_import') ? 'gescon_import' : config('database.default');
$probeTables = [
'voci_spesa',
'voci',
'tab_voci',
'tab_spese',
'piano_spese',
'conti',
];
$stagingHeaders = [];
if ($importConnection) {
try {
$schema = \Illuminate\Support\Facades\Schema::connection($importConnection);
foreach ($probeTables as $table) {
if (!$schema->hasTable($table)) {
continue;
}
foreach ($schema->getColumnListing($table) as $column) {
$column = trim($column);
if ($column === '') {
continue;
}
if (!in_array($column, $stagingHeaders, true)) {
$stagingHeaders[] = $column;
}
}
}
} catch (\Throwable $th) {
$stagingHeaders = [];
}
}
$legacyFields = [];
$legacyKeyIndex = [];
$appendField = static function (array $field) use (&$legacyFields, &$legacyKeyIndex, $humanize): void {
$key = (string) ($field['key'] ?? '');
if ($key === '' || isset($legacyKeyIndex[$key])) {
return;
}
if (empty($field['label'])) {
$field['label'] = $humanize($key);
}
$legacyFields[] = [
'key' => $key,
'label' => $field['label'],
'group' => $field['group'] ?? null,
'description' => $field['description'] ?? null,
'default_target' => $field['default_target'] ?? null,
];
$legacyKeyIndex[$key] = true;
};
$preferredFields = [
[
'key' => 'cod_voce',
'label' => 'Codice voce',
'group' => 'Identificativi',
'description' => 'Codice progressivo della voce di spesa.',
'default_target' => 'cod_voce',
],
[
'key' => 'descrizione',
'label' => 'Descrizione voce',
'group' => 'Identificativi',
'description' => 'Descrizione testuale della voce.',
'default_target' => 'descrizione',
],
[
'key' => 'gruppo',
'label' => 'Gruppo',
'group' => 'Identificativi',
'description' => 'Gruppo o categoria di appartenenza.',
'default_target' => 'gruppo',
],
[
'key' => 'tipo',
'label' => 'Tipo voce',
'group' => 'Identificativi',
'description' => 'Tipologia della voce (ordinaria, straordinaria, ecc.).',
'default_target' => 'tipo',
],
[
'key' => 'tabella',
'label' => 'Tabella collegata',
'group' => 'Identificativi',
'description' => 'Tabella millesimale di riferimento, se presente.',
'default_target' => 'tabella',
],
[
'key' => 'conto_aggancio',
'label' => 'Conto di aggancio',
'group' => 'Contabilità',
'description' => 'Conto piano dei conti collegato alla voce.',
'default_target' => 'conto_aggancio',
],
[
'key' => 'sottoconto_aggancio',
'label' => 'Sottoconto di aggancio',
'group' => 'Contabilità',
'description' => 'Sottoconto legacy collegato alla voce.',
'default_target' => 'sottoconto_aggancio',
],
[
'key' => 'centro_costo',
'label' => 'Centro di costo',
'group' => 'Contabilità',
'description' => 'Centro di costo di riferimento.',
'default_target' => 'centro_costo',
],
[
'key' => 'natura2',
'label' => 'Natura 2',
'group' => 'Ripartizioni',
'description' => 'Natura secondaria legacy per riparti personalizzati.',
'default_target' => 'natura2',
],
[
'key' => 'raoa',
'label' => 'RAOA / Flag riparto',
'group' => 'Ripartizioni',
'description' => 'Indicazione legacy sulle modalità di riparto.',
'default_target' => 'raoa',
],
[
'key' => 'percentuale_proprietario',
'label' => '% Proprietario',
'group' => 'Ripartizioni',
'description' => 'Percentuale a carico del proprietario.',
'default_target' => 'percentuale_proprietario',
],
[
'key' => 'percentuale_inquilino',
'label' => '% Inquilino',
'group' => 'Ripartizioni',
'description' => 'Percentuale a carico dellinquilino.',
'default_target' => 'percentuale_inquilino',
],
[
'key' => 'importo_proprietario',
'label' => 'Importo proprietario (€)',
'group' => 'Importi',
'description' => 'Quota importo imputata al proprietario.',
'default_target' => 'importo_proprietario',
],
[
'key' => 'importo_inquilino',
'label' => 'Importo inquilino (€)',
'group' => 'Importi',
'description' => 'Quota importo imputata allinquilino.',
'default_target' => 'importo_inquilino',
],
[
'key' => 'note',
'label' => 'Note',
'group' => 'Metadati',
'description' => 'Annotazioni e commenti legacy.',
'default_target' => 'note',
],
[
'key' => 'legacy_codice',
'label' => 'Codice legacy',
'group' => 'Metadati',
'description' => 'Codice originale della voce legacy.',
'default_target' => 'legacy_codice',
],
[
'key' => 'legacy_tipo',
'label' => 'Tipo legacy',
'group' => 'Metadati',
'description' => 'Tipologia originale registrata nel gestionale legacy.',
'default_target' => 'legacy_tipo',
],
[
'key' => 'flag_attiva',
'label' => 'Flag attiva',
'group' => 'Metadati',
'description' => 'Indica se la voce risulta attiva o meno.',
'default_target' => 'flag_attiva',
],
[
'key' => 'ordinamento',
'label' => 'Ordinamento',
'group' => 'Metadati',
'description' => 'Ordine preferenziale della voce.',
'default_target' => 'ordinamento',
],
];
foreach ($preferredFields as $field) {
$appendField($field);
}
foreach ($stagingHeaders as $column) {
$appendField([
'key' => $column,
'label' => $humanize($column),
'group' => 'Colonne legacy',
'description' => null,
'default_target' => $existingMap->get($column)['target'] ?? null,
]);
}
foreach ($existingMap as $legacyKey => $row) {
$appendField([
'key' => $legacyKey,
'label' => $humanize($legacyKey),
'group' => 'Mappature esistenti',
'description' => null,
'default_target' => $row['target'] ?? $row['target_field'] ?? null,
]);
}
$legacyFieldMeta = array_map(function ($field) {
return [
'key' => $field['key'],
'label' => $field['label'] ?? $field['key'],
'group' => $field['group'] ?? null,
'description' => $field['description'] ?? null,
'default_target' => $field['default_target'] ?? null,
];
}, $legacyFields);
$preferredOrder = array_map(fn ($field) => $field['key'], $legacyFields);
$existingMappingLookup = $existingMap->mapWithKeys(fn ($row, $key) => [
$key => $row['target'] ?? $row['target_field'] ?? null,
])->all();
$targetFields = [
'cod_voce',
'descrizione',
'gruppo',
'tipo',
'note',
'tabella',
'conto_aggancio',
'sottoconto_aggancio',
'centro_costo',
'natura2',
'raoa',
'percentuale_proprietario',
'percentuale_inquilino',
'importo_proprietario',
'importo_inquilino',
'legacy_codice',
'legacy_tipo',
'flag_attiva',
'ordinamento',
];
@endphp
<script>
window.GESCON_MAPPING = window.GESCON_MAPPING || {};
window.GESCON_MAPPING.vociLegacyMeta = @json(array_values($legacyFieldMeta));
window.GESCON_MAPPING.preferredOrder = window.GESCON_MAPPING.preferredOrder || {};
window.GESCON_MAPPING.preferredOrder.voci = @json(array_values($preferredOrder));
window.GESCON_MAPPING.initialMapping = window.GESCON_MAPPING.initialMapping || {};
window.GESCON_MAPPING.initialMapping.voci = @json($existingMappingLookup);
</script>
<script>
(function(){
const CFG = window.GESCON_MAPPING || (window.GESCON_MAPPING = {});
const LEGACY = CFG.legacyCode || @json($legacyCode);
let ll;
let ml;
let fl;
let fm;
let tableLabel;
let countLabel;
let previewMeta;
let previewBox;
let previewIndexLabel;
let previewColumns = [];
let previewRows = [];
let previewIndex = 0;
let previewTableName = '—';
let previewCount = 0;
let legacyTemplate;
let mappingTemplate;
function refreshDomRefs(){
ll = document.getElementById('legacy-voci-list');
ml = document.getElementById('mapping-voci-rows');
fl = document.getElementById('filter-legacy-voci');
fm = document.getElementById('filter-mapping-voci');
tableLabel = document.getElementById('legacy-voci-table');
countLabel = document.getElementById('legacy-voci-count');
previewMeta = document.getElementById('legacy-voci-preview-meta');
previewBox = document.getElementById('legacy-voci-preview');
previewIndexLabel = document.getElementById('legacy-voci-preview-index');
legacyTemplate = document.getElementById('voci-legacy-row-template');
mappingTemplate = document.getElementById('voci-mapping-row-template');
}
refreshDomRefs();
const escapeHtml = window.escapeHtml || function(value){
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
};
const LEGACY_META = {};
const rawMeta = CFG.vociLegacyMeta || [];
if (Array.isArray(rawMeta)) {
rawMeta.forEach(item => {
if (item && item.key) {
LEGACY_META[item.key] = item;
}
});
} else if (rawMeta && typeof rawMeta === 'object') {
Object.assign(LEGACY_META, rawMeta);
}
const preferredOrder = Array.isArray(CFG.preferredOrder?.voci) ? CFG.preferredOrder.voci.slice() : Object.keys(LEGACY_META);
const orderLookup = new Map(preferredOrder.map((key, index) => [key, index]));
const initialMapping = (CFG.initialMapping && CFG.initialMapping.voci) || {};
function applyFilter(container, selector, input){
if(!container || !input) return;
const q = (input.value||'').trim().toLowerCase();
container.querySelectorAll(selector).forEach(el=>{
const txt = (el.getAttribute('data-field')||el.getAttribute('data-legacy')||el.textContent||'').toLowerCase();
el.style.display = !q || txt.includes(q) ? '' : 'none';
});
}
function getLegacyMeta(field){
if(!field) return { key: '', label: '' };
if(!Object.prototype.hasOwnProperty.call(LEGACY_META, field)){
LEGACY_META[field] = {
key: field,
label: field,
group: null,
description: null,
default_target: null,
};
}
return LEGACY_META[field];
}
function orderIndex(key){
return orderLookup.has(key) ? orderLookup.get(key) : Number.MAX_SAFE_INTEGER;
}
function sortColumns(columns){
return columns
.filter(col => typeof col === 'string' && col.trim() !== '')
.sort((a, b) => {
const ia = orderIndex(a);
const ib = orderIndex(b);
if (ia !== ib) return ia - ib;
return a.localeCompare(b);
});
}
function ensureLegacyRow(field){
if (!field || !ll) return null;
const selector = `.legacy-voce-row[data-field="${CSS.escape(field)}"]`;
let row = ll.querySelector(selector);
if (!row) {
if (legacyTemplate && legacyTemplate.content) {
row = legacyTemplate.content.firstElementChild.cloneNode(true);
} else {
row = document.createElement('div');
row.className = 'legacy-voce-row px-2 py-2 text-sm cursor-pointer hover:bg-slate-100 flex justify-between gap-3';
row.innerHTML = '<div><div class="font-semibold text-sm" data-legacy-label></div><div class="text-[11px] text-slate-500" data-legacy-key></div><div class="text-[11px] text-slate-400" data-legacy-description></div></div><div class="text-[11px] text-emerald-600" data-legacy-default></div>';
}
}
const meta = getLegacyMeta(field);
row.setAttribute('data-field', field);
row.dataset.group = meta.group || '';
const labelEl = row.querySelector('[data-legacy-label]');
if (labelEl) {
labelEl.textContent = meta.label || field;
}
const keyEl = row.querySelector('[data-legacy-key]');
if (keyEl) {
keyEl.textContent = field;
}
const desc = row.querySelector('[data-legacy-description]');
if (desc) {
if (meta.description) {
desc.textContent = meta.description;
desc.classList.remove('hidden');
} else {
desc.textContent = '';
desc.classList.add('hidden');
}
}
const def = row.querySelector('[data-legacy-default]');
if (def) {
if (meta.default_target) {
def.textContent = 'Suggerito → ' + meta.default_target;
def.classList.remove('hidden');
} else {
def.textContent = '';
def.classList.add('hidden');
}
}
return row;
}
function ensureMappingRow(field, existingValues){
if (!field || !ml) return null;
const selector = `.mapping-voce-row[data-legacy="${CSS.escape(field)}"]`;
let row = ml.querySelector(selector);
if (!row) {
if (mappingTemplate && mappingTemplate.content) {
row = mappingTemplate.content.firstElementChild.cloneNode(true);
} else {
row = document.createElement('div');
row.className = 'mapping-voce-row group border-b last:border-b-0 px-2 py-2 text-xs flex items-start gap-3';
row.innerHTML = '<div class="w-48 shrink-0"><div class="font-semibold text-xs" data-legacy-label></div><div class="text-[11px] text-slate-500" data-legacy-key></div></div><div class="flex-1"><input type="text" class="netgescon-input netgescon-input-sm w-full target-voce-input" list="voci-target-fields" data-legacy="" /><div class="mt-1 hidden inline-create-ui"><div class="flex items-center gap-2 flex-wrap"><input type="text" class="netgescon-input netgescon-input-sm new-col-name" placeholder="Nome nuovo campo" style="max-width: 16rem;"><select class="netgescon-input netgescon-input-sm new-col-type" style="max-width: 10rem;"><option value="string">string</option><option value="text">text</option><option value="integer">integer</option><option value="decimal">decimal</option><option value="boolean">boolean</option><option value="date">date</option><option value="datetime">datetime</option></select><label class="flex items-center gap-1 text-xs"><input type="checkbox" class="new-col-nullable" checked> null</label><button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-success" onclick="window.__createVoceField && window.__createVoceField(this)">Crea e associa</button><button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="window.__toggleInlineCreate && window.__toggleInlineCreate(this, false)">Annulla</button></div></div></div><div class="flex flex-col items-end gap-1"><button type="button" class="opacity-0 group-hover:opacity-100 transition text-red-500" onclick="this.closest('.mapping-voce-row').querySelector('.target-voce-input').value='';" title="Rimuovi"><i class="mdi mdi-close"></i></button><button type="button" class="text-emerald-600 hover:text-emerald-700 underline decoration-dotted text-[11px]" onclick="window.__toggleInlineCreate && window.__toggleInlineCreate(this, true)">+ Crea campo</button></div>';
}
}
const meta = getLegacyMeta(field);
row.setAttribute('data-legacy', field);
row.dataset.defaultTarget = meta.default_target || '';
const labelEl = row.querySelector('[data-legacy-label]');
if (labelEl) {
labelEl.textContent = meta.label || field;
}
const keyEl = row.querySelector('[data-legacy-key]');
if (keyEl) {
keyEl.textContent = field;
}
const input = row.querySelector('.target-voce-input');
if (input) {
input.dataset.legacy = field;
const value = existingValues[field] ?? initialMapping[field] ?? meta.default_target ?? '';
input.value = value || '';
}
return row;
}
function renderLegacyList(columns){
if (!ll) return;
if (!columns.length) {
ll.innerHTML = '<div class="text-xs text-slate-500 py-2">Nessuna colonna legacy rilevata.</div>';
return;
}
ll.innerHTML = '';
let lastGroup = null;
columns.forEach(field => {
const meta = getLegacyMeta(field);
if (meta.group && meta.group !== lastGroup) {
const heading = document.createElement('div');
heading.className = 'text-[10px] uppercase tracking-wide text-slate-400 px-2 pt-3';
heading.textContent = meta.group;
ll.appendChild(heading);
lastGroup = meta.group;
}
const row = ensureLegacyRow(field);
if (row) {
ll.appendChild(row);
}
});
}
function renderMappingRows(columns, existingValues){
if (!ml) return;
if (!columns.length) {
ml.innerHTML = '<div class="text-xs text-slate-500 py-2">Nessuna colonna legacy da associare.</div>';
return;
}
ml.innerHTML = '';
columns.forEach(field => {
const row = ensureMappingRow(field, existingValues);
if (row) {
ml.appendChild(row);
}
});
}
function collectExistingMapping(){
const existing = {};
if (!ml) return existing;
ml.querySelectorAll('.mapping-voce-row').forEach(row => {
const key = row.getAttribute('data-legacy');
const val = row.querySelector('.target-voce-input')?.value;
if (key && val && val.trim() !== '') {
existing[key] = val.trim();
}
});
return existing;
}
function getCurrentPreviewRow(){
if (!previewRows.length) return null;
if (previewIndex < 0) previewIndex = 0;
if (previewIndex >= previewRows.length) previewIndex = previewRows.length - 1;
return previewRows[previewIndex] || null;
}
function pickRowValue(row, field){
if (!row || !field) return null;
if (Array.isArray(row)) {
let idx = previewColumns.findIndex(col => col === field);
if (idx < 0) {
const lf = field.toLowerCase();
idx = previewColumns.findIndex(col => (col || '').toLowerCase() === lf);
}
return idx >= 0 ? (row[idx] ?? null) : null;
}
if (Object.prototype.hasOwnProperty.call(row, field)) {
return row[field];
}
const lowerField = field.toLowerCase();
for (const key in row) {
if (!Object.prototype.hasOwnProperty.call(row, key) || typeof key !== 'string') continue;
if (key.toLowerCase() === lowerField) {
return row[key];
}
}
return null;
}
function updateMappingPreviewValues(rowData){
if (!ml) return;
ml.querySelectorAll('.mapping-voce-row').forEach(row => {
const key = row.getAttribute('data-legacy');
const span = row.querySelector('[data-preview-value]');
if (!span) return;
const val = rowData && key ? pickRowValue(rowData, key) : null;
span.textContent = (val === null || val === undefined || val === '') ? '—' : String(val);
});
}
function refreshVociPreviewBox(){
refreshDomRefs();
if (!previewBox || !previewMeta) return;
if (tableLabel) tableLabel.textContent = previewTableName || '—';
if (countLabel) countLabel.textContent = previewCount ?? (previewRows.length || 0);
if (!previewColumns.length) {
previewMeta.textContent = 'Nessuna colonna rilevata';
previewBox.innerHTML = '<div class="text-xs text-slate-500 px-3 py-2">Non sono state trovate colonne nella staging per le voci.</div>';
if (previewIndexLabel) previewIndexLabel.textContent = 'Nessuna riga disponibile';
updateMappingPreviewValues(null);
return;
}
const rowsLabel = previewRows.length ? previewRows.length + ' righe' : 'anteprima vuota';
previewMeta.textContent = previewColumns.length + ' colonne · ' + rowsLabel;
if (!previewRows.length) {
previewBox.innerHTML = '<div class="text-xs text-slate-500 px-3 py-2">Nessuna voce trovata.</div>';
if (previewIndexLabel) previewIndexLabel.textContent = 'Nessuna riga disponibile';
updateMappingPreviewValues(null);
return;
}
if (previewIndexLabel) {
previewIndexLabel.textContent = 'Riga ' + (previewIndex + 1) + ' di ' + previewRows.length;
}
const row = getCurrentPreviewRow() || {};
const shownCols = previewColumns.slice(0, 8);
const body = shownCols.map(col => {
const val = pickRowValue(row, col);
return `
<tr>
<th class="px-2 py-1 text-[11px] uppercase text-slate-500 text-left">${escapeHtml(col)}</th>
<td class="px-2 py-1 text-[12px] text-slate-700">${escapeHtml(val ?? '')}</td>
</tr>
`; }).join('');
previewBox.innerHTML = `<table class="table-auto w-full text-xs"><tbody>${body}</tbody></table>`;
updateMappingPreviewValues(row);
}
function renderVociPreview(detail){
previewColumns = Array.isArray(detail?.columns) ? detail.columns : [];
previewRows = Array.isArray(detail?.rows) ? detail.rows : [];
previewIndex = 0;
previewTableName = detail?.table || '—';
previewCount = typeof detail?.count === 'number' ? detail.count : (previewRows.length || 0);
refreshVociPreviewBox();
}
function notifyVociColumns(detail){
try {
window.__lastVociColumnsPayload = detail;
window.dispatchEvent(new CustomEvent('gescon:vociColumnsLoaded', { detail }));
} catch (_) {/* silent */}
}
async function loadLegacyVociHeaders(){
refreshDomRefs();
try {
const route = CFG.routes && CFG.routes.stagingColumns;
if (!route) return;
const params = new URLSearchParams();
params.set('guess', 'voci');
if (LEGACY) params.set('legacy_code', LEGACY);
const res = await fetch(`${route}?${params.toString()}`);
const js = await res.json().catch(() => ({}));
if (!res.ok || js.ok === false) {
if (ll) ll.innerHTML = '<div class="text-xs text-danger py-2">Errore caricamento colonne legacy.</div>';
notifyVociColumns({ columns: [], rows: [], table: null, count: 0, legacy: LEGACY });
return;
}
const cols = Array.isArray(js.columns) ? js.columns : [];
cols.forEach(col => {
const key = typeof col === 'string' ? col : '';
if (!key) return;
if (!LEGACY_META[key]) {
LEGACY_META[key] = { key, label: key, group: null, description: null, default_target: null };
}
});
const orderedColumns = cols.length ? sortColumns(cols) : preferredOrder.slice();
const existingValues = collectExistingMapping();
renderLegacyList(orderedColumns);
renderMappingRows(orderedColumns, existingValues);
notifyVociColumns({
columns: orderedColumns,
rows: Array.isArray(js.rows) ? js.rows : [],
table: js.table || null,
count: js.count ?? null,
legacy: LEGACY,
});
renderVociPreview({
columns: cols,
rows: Array.isArray(js.rows) ? js.rows : [],
table: js.table || null,
count: js.count ?? null,
});
} catch (error) {
if (ll) ll.innerHTML = '<div class="text-xs text-danger py-2">Errore caricamento voci.</div>';
notifyVociColumns({ columns: [], rows: [], table: null, count: 0, legacy: LEGACY });
}
}
document.addEventListener('DOMContentLoaded', function(){
refreshDomRefs();
if (fl) {
fl.addEventListener('input', ()=>applyFilter(ll, '.legacy-voce-row', fl));
}
if (fm) {
fm.addEventListener('input', ()=>applyFilter(ml, '.mapping-voce-row', fm));
}
setTimeout(loadLegacyVociHeaders, 250);
if (window.__lastVociColumnsPayload) {
renderVociPreview(window.__lastVociColumnsPayload);
}
});
window.reloadVociColumns = function reloadVociColumns(){
return loadLegacyVociHeaders();
};
window.browseVociPreview = function browseVociPreview(step){
if (!previewRows.length) return;
const total = previewRows.length;
previewIndex = (previewIndex + step) % total;
if (previewIndex < 0) previewIndex = total - 1;
refreshVociPreviewBox();
};
window.addEventListener('gescon:vociColumnsLoaded', function(ev){
try { renderVociPreview(ev.detail || {}); } catch (_) { /* silent */ }
});
window.saveVociFieldMapping = async function saveVociFieldMapping(){
const rows = ml ? Array.from(ml.querySelectorAll('.mapping-voce-row')) : [];
const data = rows.map(row => {
const legacy = row.getAttribute('data-legacy');
const target = row.querySelector('.target-voce-input')?.value.trim();
return legacy ? { legacy, target: target || null } : null;
}).filter(Boolean);
try {
const csrf = CFG.csrf || document.querySelector('meta[name="csrf-token"]').content;
const legacyCodeInput = document.getElementById('import-legacy-code')
|| document.querySelector('input[name="legacy_code"]')
|| document.querySelector('input[name="_legacy_code"]');
const legacyCode = legacyCodeInput ? legacyCodeInput.value.trim() : '';
if (!legacyCode) {
alert('Prima seleziona un codice legacy stabile.');
return;
}
const resp = await fetch('{{ route('admin.gescon-import.mapping.save') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrf,
'Accept': 'application/json',
},
body: JSON.stringify({
_legacy_code: legacyCode,
voci_field_mapping: JSON.stringify(data),
_mapping_only: true,
}),
});
if (resp.ok) {
alert('Mapping Voci salvato.');
} else {
alert('Errore salvataggio mapping (' + resp.status + ').');
}
} catch (error) {
alert('Errore: ' + error.message);
}
};
window.__createVoceField = async function createVoceField(btn){
const row = btn.closest('.mapping-voce-row');
if (!row) return;
const ui = row.querySelector('.inline-create-ui');
if (!ui) return;
const name = (ui.querySelector('.new-col-name')?.value || '').trim();
const type = (ui.querySelector('.new-col-type')?.value || 'string').trim();
const nullable = !!ui.querySelector('.new-col-nullable')?.checked;
if (!name) {
alert('Inserisci un nome per il nuovo campo.');
return;
}
const route = CFG.routes && CFG.routes.createTargetColumns;
if (!route) {
alert('Endpoint creazione colonne non disponibile.');
return;
}
try {
const payload = {
table: 'voci_spesa',
columns: [{ name, type, nullable }],
};
const resp = await fetch(route, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': CFG.csrf || document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify(payload),
});
const js = await resp.json().catch(() => ({}));
if (!resp.ok || js.ok === false) {
throw new Error(js.error || ('HTTP ' + resp.status));
}
const datalist = document.getElementById('voci-target-fields');
if (datalist && !Array.from(datalist.options).some(opt => opt.value === name)) {
const option = document.createElement('option');
option.value = name;
datalist.appendChild(option);
}
const input = row.querySelector('.target-voce-input');
if (input) {
input.value = name;
}
ui.classList.add('hidden');
} catch (error) {
alert('Errore creazione campo: ' + error.message);
}
};
window.__toggleInlineCreate = function toggleInlineCreate(btn, force){
const row = btn?.closest ? btn.closest('.mapping-voce-row') : null;
if (!row) return;
const ui = row.querySelector('.inline-create-ui');
if (!ui) return;
const shouldShow = typeof force === 'boolean' ? force : ui.classList.contains('hidden');
if (shouldShow) {
ui.classList.remove('hidden');
const input = ui.querySelector('.new-col-name');
if (input) {
setTimeout(() => input.focus(), 50);
}
} else {
ui.classList.add('hidden');
}
};
})();
</script>
<div class="netgescon-card mt-6" id="voci-field-mapping-card">
<div class="netgescon-card-header flex items-center justify-between">
<h4 class="card-title mb-0 text-base">Mapping Campi Voci Spese (Legacy → Archivi)</h4>
<button type="button" class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-primary" onclick="window.saveVociFieldMapping && window.saveVociFieldMapping()">Salva Mapping</button>
</div>
<div class="netgescon-card-body p-0 overflow-visible">
<div class="flex flex-col md:flex-row">
<div class="md:w-1/2 border-r">
<div class="p-3">
<div class="flex items-center justify-between mb-2">
<h5 class="font-semibold text-sm">Campi Legacy (voci)</h5>
<input type="text" id="filter-legacy-voci" placeholder="Filtra..." class="netgescon-input netgescon-input-sm w-40" />
</div>
<div class="flex items-center gap-3 text-xs text-slate-500 mb-2">
<span>Tabella: <span class="font-semibold text-slate-700" id="legacy-voci-table">—</span></span>
<span>Righe trovate: <span class="font-semibold text-slate-700" id="legacy-voci-count">0</span></span>
</div>
<div class="max-h-none overflow-visible" id="legacy-voci-list">
@php $currentGroup = null; @endphp
@forelse($legacyFields as $field)
@if(($field['group'] ?? null) && $field['group'] !== $currentGroup)
<div class="text-[10px] uppercase tracking-wide text-slate-400 px-2 pt-3">
{{ $field['group'] }}
</div>
@php $currentGroup = $field['group']; @endphp
@endif
<div class="legacy-voce-row px-2 py-2 text-sm cursor-pointer hover:bg-slate-100 flex justify-between gap-3" data-field="{{ $field['key'] }}" data-group="{{ $field['group'] ?? '' }}">
<div>
<div class="font-semibold text-sm">{{ $field['label'] }}</div>
<div class="text-[11px] text-slate-500">{{ $field['key'] }}</div>
@if(!empty($field['description']))
<div class="text-[11px] text-slate-400">{{ $field['description'] }}</div>
@endif
</div>
@if(!empty($field['default_target']))
<div class="text-[11px] text-emerald-600 whitespace-nowrap">Suggerito → {{ $field['default_target'] }}</div>
@endif
</div>
@empty
<div class="text-xs text-slate-500 py-2">Caricamento...</div>
@endforelse
</div>
<div class="mt-4">
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
<div>
<h6 class="text-[11px] uppercase tracking-wide text-slate-500">Anteprima dati legacy</h6>
<span class="text-[11px] text-slate-500" id="legacy-voci-preview-meta">In attesa selezione…</span>
</div>
<div class="flex items-center gap-2 flex-wrap">
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="window.reloadVociColumns && window.reloadVociColumns()">
<i class="mdi mdi-refresh"></i> Sync anteprima
</button>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="window.browseVociPreview && window.browseVociPreview(-1)">Prev</button>
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="window.browseVociPreview && window.browseVociPreview(1)">Next</button>
</div>
</div>
</div>
<div class="border rounded bg-white overflow-visible max-h-none" id="legacy-voci-preview">
<div class="text-xs text-slate-500 px-3 py-2">Carica un codice legacy per vedere i primi conti/voci disponibili.</div>
</div>
<div class="text-[11px] text-slate-500 mt-1" id="legacy-voci-preview-index">—</div>
</div>
</div>
</div>
<div class="md:w-1/2">
<div class="p-3">
<div class="flex items-center justify-between mb-2">
<h5 class="font-semibold text-sm">Associazione a Campi Target</h5>
<input type="text" id="filter-mapping-voci" placeholder="Filtra mapping..." class="netgescon-input netgescon-input-sm w-44" />
</div>
<div class="max-h-none overflow-visible" id="mapping-voci-rows">
@forelse($legacyFields as $field)
@php
$legacyKey = $field['key'];
$prefill = $existingMap->get($legacyKey)['target'] ?? ($field['default_target'] ?? null);
@endphp
<div class="mapping-voce-row group border-b last:border-b-0 px-2 py-2 text-xs flex items-start gap-3" data-legacy="{{ $legacyKey }}" data-default-target="{{ $field['default_target'] ?? '' }}">
<div class="w-48 shrink-0">
<div class="font-semibold text-xs" data-legacy-label>{{ $field['label'] }}</div>
<div class="text-[11px] text-slate-500" data-legacy-key>{{ $legacyKey }}</div>
</div>
<div class="flex-1">
<input type="text" class="netgescon-input netgescon-input-sm w-full target-voce-input" placeholder="Seleziona / cerca campo target" list="voci-target-fields" value="{{ $prefill ?? '' }}" data-legacy="{{ $legacyKey }}" />
<div class="text-[11px] text-slate-500 mt-1">Anteprima → <span class="font-mono" data-preview-value>—</span></div>
<div class="mt-1 hidden inline-create-ui">
<div class="flex items-center gap-2 flex-wrap">
<input type="text" class="netgescon-input netgescon-input-sm new-col-name" placeholder="Nome nuovo campo" style="max-width: 16rem;">
<select class="netgescon-input netgescon-input-sm new-col-type" style="max-width: 10rem;">
<option value="string">string</option>
<option value="text">text</option>
<option value="integer">integer</option>
<option value="decimal">decimal</option>
<option value="boolean">boolean</option>
<option value="date">date</option>
<option value="datetime">datetime</option>
</select>
<label class="flex items-center gap-1 text-xs"><input type="checkbox" class="new-col-nullable" checked> null</label>
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-success" onclick="window.__createVoceField && window.__createVoceField(this)">Crea e associa</button>
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="window.__toggleInlineCreate && window.__toggleInlineCreate(this, false)">Annulla</button>
</div>
</div>
</div>
<div class="flex flex-col items-end gap-1">
<button type="button" class="opacity-0 group-hover:opacity-100 transition text-red-500" onclick="this.closest('.mapping-voce-row').querySelector('.target-voce-input').value='';" title="Rimuovi"><i class="mdi mdi-close"></i></button>
<button type="button" class="text-emerald-600 hover:text-emerald-700 underline decoration-dotted text-[11px]" onclick="window.__toggleInlineCreate && window.__toggleInlineCreate(this, true)">+ Crea campo</button>
</div>
</div>
@empty
<div class="text-xs text-slate-500 py-2">In attesa colonne legacy...</div>
@endforelse
<datalist id="voci-target-fields">
@foreach($targetFields as $tf)
<option value="{{ $tf }}"></option>
@endforeach
</datalist>
</div>
<div class="mt-3 text-right">
<button type="button" class="netgescon-btn netgescon-btn-sm netgescon-btn-outline-primary" onclick="window.saveVociFieldMapping && window.saveVociFieldMapping()">Salva</button>
</div>
</div>
</div>
</div>
<div class="border-t p-3 bg-slate-50">
<div class="text-xs text-slate-600">Suggerimento: lascia vuoto il campo target se non vuoi importare quel campo legacy.</div>
</div>
</div>
</div>
<template id="voci-legacy-row-template">
<div class="legacy-voce-row px-2 py-2 text-sm cursor-pointer hover:bg-slate-100 flex justify-between gap-3" data-field="">
<div>
<div class="font-semibold text-sm" data-legacy-label></div>
<div class="text-[11px] text-slate-500" data-legacy-key></div>
<div class="text-[11px] text-slate-400" data-legacy-description></div>
</div>
<div class="text-[11px] text-emerald-600" data-legacy-default></div>
</div>
</template>
<template id="voci-mapping-row-template">
<div class="mapping-voce-row group border-b last:border-b-0 px-2 py-2 text-xs flex items-start gap-3" data-legacy="">
<div class="w-48 shrink-0">
<div class="font-semibold text-xs" data-legacy-label></div>
<div class="text-[11px] text-slate-500" data-legacy-key></div>
</div>
<div class="flex-1">
<input type="text" class="netgescon-input netgescon-input-sm w-full target-voce-input" placeholder="Seleziona / cerca campo target" list="voci-target-fields" data-legacy="" />
<div class="text-[11px] text-slate-500 mt-1">Anteprima → <span class="font-mono" data-preview-value>—</span></div>
<div class="mt-1 hidden inline-create-ui">
<div class="flex items-center gap-2 flex-wrap">
<input type="text" class="netgescon-input netgescon-input-sm new-col-name" placeholder="Nome nuovo campo" style="max-width: 16rem;">
<select class="netgescon-input netgescon-input-sm new-col-type" style="max-width: 10rem;">
<option value="string">string</option>
<option value="text">text</option>
<option value="integer">integer</option>
<option value="decimal">decimal</option>
<option value="boolean">boolean</option>
<option value="date">date</option>
<option value="datetime">datetime</option>
</select>
<label class="flex items-center gap-1 text-xs"><input type="checkbox" class="new-col-nullable" checked> null</label>
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-success" onclick="window.__createVoceField && window.__createVoceField(this)">Crea e associa</button>
<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="window.__toggleInlineCreate && window.__toggleInlineCreate(this, false)">Annulla</button>
</div>
</div>
</div>
<div class="flex flex-col items-end gap-1">
<button type="button" class="opacity-0 group-hover:opacity-100 transition text-red-500" onclick="this.closest('.mapping-voce-row').querySelector('.target-voce-input').value='';" title="Rimuovi"><i class="mdi mdi-close"></i></button>
<button type="button" class="text-emerald-600 hover:text-emerald-700 underline decoration-dotted text-[11px]" onclick="window.__toggleInlineCreate && window.__toggleInlineCreate(this, true)">+ Crea campo</button>
</div>
</div>
</template>