965 lines
45 KiB
PHP
965 lines
45 KiB
PHP
@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 dell’inquilino.',
|
||
'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 all’inquilino.',
|
||
'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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
};
|
||
|
||
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>
|