diff --git a/public/userarea/gridRenderer.js b/public/userarea/gridRenderer.js index 502c21d..57ce0e0 100644 --- a/public/userarea/gridRenderer.js +++ b/public/userarea/gridRenderer.js @@ -6,7 +6,7 @@ * then re-renders visible rows. Save reads from gridData, not DOM. */ (function () { - 'use strict'; + "use strict"; const PAGE_SIZE = 20; let revealedCount = PAGE_SIZE; @@ -30,14 +30,14 @@ // ── Helpers ───────────────────────────────────────────────────────────── function esc(str) { - if (str === null || str === undefined) return ''; - const d = document.createElement('div'); + if (str === null || str === undefined) return ""; + const d = document.createElement("div"); d.textContent = String(str); return d.innerHTML; } function getDetailValue(rowIndex, mappingId) { - return data[rowIndex].details[String(mappingId)] ?? ''; + return data[rowIndex].details[String(mappingId)] ?? ""; } function setDetailValue(rowIndex, mappingId, value) { @@ -46,7 +46,7 @@ } function getFixedValue(rowIndex, key) { - return data[rowIndex].fixedFields[key] ?? ''; + return data[rowIndex].fixedFields[key] ?? ""; } function setFixedValue(rowIndex, key, value) { @@ -57,7 +57,7 @@ // ── Client data (AJAX Select2, no bulk loading) ────────────────────── function formatClientLabel(client) { - return (client.Nominativo || '').trim(); + return (client.Nominativo || "").trim(); } // Cache of resolved client names: id → name @@ -76,47 +76,59 @@ // Pre-resolve all unique client IDs used in data rows, then re-render async function resolveClientNames() { const ids = new Set(); - data.forEach(row => { + data.forEach((row) => { if (row.idclient) ids.add(String(row.idclient)); - if (row.cliente_fornitore_id) ids.add(String(row.cliente_fornitore_id)); + if (row.cliente_fornitore_id) + ids.add(String(row.cliente_fornitore_id)); // Fixed fields that are client-sourced if (row.fixedFields) { for (const [key, val] of Object.entries(row.fixedFields)) { const cfg = fixedFieldApiConfig[key]; - if (cfg && cfg.source === 'clients' && val) ids.add(String(val)); + if (cfg && cfg.source === "clients" && val) + ids.add(String(val)); } } }); // Batch resolve via single request per ID - const unresolvedIds = [...ids].filter(id => !clientNameCache[id]); + const unresolvedIds = [...ids].filter((id) => !clientNameCache[id]); if (unresolvedIds.length === 0) return; - await Promise.all(unresolvedIds.map(async (id) => { - try { - const resp = await fetch('search_clienti.php?id=' + encodeURIComponent(id)); - const json = await resp.json(); - const item = (json.results || [])[0]; - if (item) clientNameCache[id] = item.text; - } catch (e) { /* ignore */ } - })); + await Promise.all( + unresolvedIds.map(async (id) => { + try { + const resp = await fetch( + "search_clienti.php?id=" + encodeURIComponent(id), + ); + const json = await resp.json(); + const item = (json.results || [])[0]; + if (item) clientNameCache[id] = item.text; + } catch (e) { + /* ignore */ + } + }), + ); } // Select2 AJAX config for client selects const clientSelect2Config = { - placeholder: 'Search client...', + placeholder: "Search client...", allowClear: true, - width: '100%', + width: "100%", minimumInputLength: 0, - dropdownCssClass: 'select2-dropdown-smaller', + dropdownCssClass: "select2-dropdown-smaller", ajax: { - url: 'search_clienti.php', - dataType: 'json', + url: "search_clienti.php", + dataType: "json", delay: 150, - data: function(params) { return { q: params.term || '', limit: 20 }; }, - processResults: function(data) { return { results: data.results || [] }; }, - cache: true - } + data: function (params) { + return { q: params.term || "", limit: 20 }; + }, + processResults: function (data) { + return { results: data.results || [] }; + }, + cache: true, + }, }; async function loadClientData() { @@ -128,12 +140,30 @@ // ── Fixed field data loading ─────────────────────────────────────────── const fixedFieldApiConfig = { - MoltiplicatorePrezzo: { endpoint: 'MoltiplicatorePrezzo', idKey: 'IdMoltiplicatorePrezzo', textKey: 'Descrizione' }, - AnagraficaCertestObject: { endpoint: 'AnagraficaCertestObject', idKey: 'IdAnagrafica', textKey: 'NomeAnagrafica' }, - AnagraficaCertestService: { endpoint: 'AnagraficaCertestService', idKey: 'IdAnagrafica', textKey: 'NomeAnagrafica' }, - ClienteResponsabile: { endpoint: 'ClienteResponsabile', idKey: 'IdClienteResponsabile', textKey: 'Nominativo', dependsOn: 'idclient', getParams: (cid) => ({ id_cliente: cid }) }, - ClienteFornitore: { source: 'clients' }, - ClienteAnalisi: { source: 'clients' }, + MoltiplicatorePrezzo: { + endpoint: "MoltiplicatorePrezzo", + idKey: "IdMoltiplicatorePrezzo", + textKey: "Descrizione", + }, + AnagraficaCertestObject: { + endpoint: "AnagraficaCertestObject", + idKey: "IdAnagrafica", + textKey: "NomeAnagrafica", + }, + AnagraficaCertestService: { + endpoint: "AnagraficaCertestService", + idKey: "IdAnagrafica", + textKey: "NomeAnagrafica", + }, + ClienteResponsabile: { + endpoint: "ClienteResponsabile", + idKey: "IdClienteResponsabile", + textKey: "Nominativo", + dependsOn: "idclient", + getParams: (cid) => ({ id_cliente: cid }), + }, + ClienteFornitore: { source: "clients" }, + ClienteAnalisi: { source: "clients" }, }; let _pendingFixed = {}; @@ -143,12 +173,12 @@ if (!config) return []; // Client-sourced fields — handled by AJAX Select2, skip preloading - if (config.source === 'clients') { + if (config.source === "clients") { fixedFieldCache[fieldKey] = []; return []; } - const cacheKey = fieldKey + (clientId ? '_' + clientId : ''); + const cacheKey = fieldKey + (clientId ? "_" + clientId : ""); if (fixedFieldCache[cacheKey]) return fixedFieldCache[cacheKey]; if (_pendingFixed[cacheKey]) return _pendingFixed[cacheKey]; @@ -159,19 +189,32 @@ _pendingFixed[cacheKey] = (async () => { try { - const resp = await fetch('get_fixed_field_data.php?' + new URLSearchParams(params)); + const resp = await fetch( + "get_fixed_field_data.php?" + new URLSearchParams(params), + ); const json = await resp.json(); - let items = (fieldKey === 'ClienteResponsabile') ? (json.Responsabili || []) : (json.value || json.d?.results || json || []); - const results = items.map(item => ({ - id: item[config.idKey], - text: (item.Codice ? item.Codice + ' - ' : '') + (item[config.textKey] || '') - })).sort((a, b) => String(a.text).localeCompare(String(b.text), 'it', { sensitivity: 'base' })); + let items = + fieldKey === "ClienteResponsabile" + ? json.Responsabili || [] + : json.value || json.d?.results || json || []; + const results = items + .map((item) => ({ + id: item[config.idKey], + text: + (item.Codice ? item.Codice + " - " : "") + + (item[config.textKey] || ""), + })) + .sort((a, b) => + String(a.text).localeCompare(String(b.text), "it", { + sensitivity: "base", + }), + ); fixedFieldCache[cacheKey] = results; delete _pendingFixed[cacheKey]; return results; } catch (e) { delete _pendingFixed[cacheKey]; - console.error('Failed to load fixed field ' + fieldKey, e); + console.error("Failed to load fixed field " + fieldKey, e); return []; } })(); @@ -181,22 +224,29 @@ // ── Custom field dropdown data loading ───────────────────────────────── - // Select2 AJAX config factory for SceltaMultipla function sceltaSelect2Config(fieldId) { return { - placeholder: 'Search...', + placeholder: "Search...", allowClear: true, - width: '100%', + width: "100%", minimumInputLength: 0, ajax: { - url: 'search_customfield_values.php', - dataType: 'json', + url: "search_customfield_values.php", + dataType: "json", delay: 150, - data: function(params) { return { field_id: fieldId, q: params.term || '', limit: 10 }; }, - processResults: function(data) { return { results: data.results || [] }; }, - cache: true - } + data: function (params) { + return { + field_id: fieldId, + q: params.term || "", + limit: 10, + }; + }, + processResults: function (data) { + return { results: data.results || [] }; + }, + cache: true, + }, }; } @@ -207,19 +257,27 @@ await loadClientData(); // 2. Non-dependent fixed fields - const nonDependent = Object.keys(fixedFieldApiConfig).filter(k => !fixedFieldApiConfig[k].dependsOn && !fixedFieldApiConfig[k].source); - await Promise.all(nonDependent.map(k => loadFixedFieldOptions(k))); + const nonDependent = Object.keys(fixedFieldApiConfig).filter( + (k) => + !fixedFieldApiConfig[k].dependsOn && + !fixedFieldApiConfig[k].source, + ); + await Promise.all(nonDependent.map((k) => loadFixedFieldOptions(k))); // Client-sourced fixed fields - const clientSourced = Object.keys(fixedFieldApiConfig).filter(k => fixedFieldApiConfig[k].source === 'clients'); - await Promise.all(clientSourced.map(k => loadFixedFieldOptions(k))); + const clientSourced = Object.keys(fixedFieldApiConfig).filter( + (k) => fixedFieldApiConfig[k].source === "clients", + ); + await Promise.all(clientSourced.map((k) => loadFixedFieldOptions(k))); // 3. Dependent fixed fields — collect unique clientIds const clientIds = new Set(); - data.forEach(row => { + data.forEach((row) => { if (row.idclient) clientIds.add(String(row.idclient)); }); - const dependent = Object.keys(fixedFieldApiConfig).filter(k => fixedFieldApiConfig[k].dependsOn); + const dependent = Object.keys(fixedFieldApiConfig).filter( + (k) => fixedFieldApiConfig[k].dependsOn, + ); for (const fieldKey of dependent) { for (const cid of clientIds) { await loadFixedFieldOptions(fieldKey, cid); @@ -227,28 +285,41 @@ } // 4. Warm server cache + resolve SceltaMultipla names in one request - const allFieldIds = [...new Set( - columns.filter(c => (c.type === 'detail' || c.type === 'main_field') && c.dataType === 'SceltaMultipla' && c.fieldId) - .map(c => String(c.fieldId)) - )]; + const allFieldIds = [ + ...new Set( + columns + .filter( + (c) => + (c.type === "detail" || c.type === "main_field") && + c.dataType === "SceltaMultipla" && + c.fieldId, + ) + .map((c) => String(c.fieldId)), + ), + ]; if (allFieldIds.length > 0) { try { - const resp = await fetch('get_customfield_values.php?field_ids=' + allFieldIds.join(',')); + const resp = await fetch( + "get_customfield_values.php?field_ids=" + + allFieldIds.join(","), + ); const json = await resp.json(); const entries = json.data ? json.data : json; for (const [fid, values] of Object.entries(entries)) { if (Array.isArray(values)) { - values.forEach(v => { - dropdownNameCache[fid + '_' + v.IdCustomFieldsValue] = v.Valore || ''; + values.forEach((v) => { + dropdownNameCache[ + fid + "_" + v.IdCustomFieldsValue + ] = v.Valore || ""; }); } } } catch (e) { - console.error('Failed to preload dropdown data:', e); + console.error("Failed to preload dropdown data:", e); } } - console.log('[gridRenderer] preload done:', { + console.log("[gridRenderer] preload done:", { clients: clientData.length, fixedFieldCache: Object.keys(fixedFieldCache), dropdownOptionsCache: Object.keys(dropdownOptionsCache), @@ -258,8 +329,8 @@ // ── Cell rendering ───────────────────────────────────────────────────── function createCell(col, rowIndex, cellIndex) { - const div = document.createElement('div'); - div.className = 'grid-cell editable-cell'; + const div = document.createElement("div"); + div.className = "grid-cell editable-cell"; div.dataset.col = col.key; div.dataset.colType = col.type; div.dataset.row = rowIndex; @@ -269,73 +340,88 @@ const row = data[rowIndex]; switch (col.type) { - case 'main_field': - div.innerHTML = createInputHTML(col, row.mainFieldValue || '', rowIndex); + case "main_field": + div.innerHTML = createInputHTML( + col, + row.mainFieldValue || "", + rowIndex, + ); break; - case 'status': { - const st = row.status || 'i'; - const label = st === 'i' ? 'Imported' : (st === 'P' ? 'In Progress' : 'To LIMS'); + case "status": { + const st = row.status || "i"; + const label = + st === "i" + ? "Imported" + : st === "P" + ? "In Progress" + : "To LIMS"; let html = `${label}`; if (row.commessaweb) { html += `${esc(row.commessaweb)}`; } div.innerHTML = html; - div.classList.remove('editable-cell'); + div.classList.remove("editable-cell"); break; } - case 'idclient': { - const sel = document.createElement('select'); - sel.className = 'cell-input dropdown-select client-select searchable-client'; - sel.dataset.currentValue = row.idclient || ''; + case "idclient": { + const sel = document.createElement("select"); + sel.className = + "cell-input dropdown-select client-select searchable-client"; + sel.dataset.currentValue = row.idclient || ""; sel.innerHTML = buildClientOptionsHTML(row.idclient); div.appendChild(sel); break; } - case 'cliente_fornitore_id': { - const sel = document.createElement('select'); - sel.className = 'cell-input dropdown-select client-select searchable-client fornitore-select'; - sel.dataset.currentValue = row.cliente_fornitore_id || ''; - sel.innerHTML = buildClientOptionsHTML(row.cliente_fornitore_id); + case "cliente_fornitore_id": { + const sel = document.createElement("select"); + sel.className = + "cell-input dropdown-select client-select searchable-client fornitore-select"; + sel.dataset.currentValue = row.cliente_fornitore_id || ""; + sel.innerHTML = buildClientOptionsHTML( + row.cliente_fornitore_id, + ); div.appendChild(sel); break; } - case 'detail': { + case "detail": { const val = getDetailValue(rowIndex, col.key); div.innerHTML = createInputHTML(col, val, rowIndex); break; } - case 'tested_component': - div.style.overflow = 'visible'; + case "tested_component": + div.style.overflow = "visible"; div.innerHTML = `
`; break; - case 'awb': - div.innerHTML = `` + + case "awb": + div.innerHTML = + `` + `` + ``; break; - case 'tracking': - div.className = 'grid-cell tracking-info'; + case "tracking": + div.className = "grid-cell tracking-info"; div.dataset.row = rowIndex; - div.innerHTML = 'Shipment Info'; + div.innerHTML = + 'Shipment Info'; break; - case 'fixed': { + case "fixed": { const val = getFixedValue(rowIndex, col.key); div.innerHTML = createFixedFieldHTML(col, val, rowIndex); break; } - case 'static': { - const val = row[col.key] || ''; - div.classList.remove('editable-cell'); - if (col.key === 'filename_import' && val) { + case "static": { + const val = row[col.key] || ""; + div.classList.remove("editable-cell"); + if (col.key === "filename_import" && val) { div.innerHTML = `File`; } else { div.innerHTML = `${esc(val)}`; @@ -348,39 +434,42 @@ } function createInputHTML(col, value, rowIndex) { - const cls = col.isManual ? 'manual-input' : 'auto-input'; - const reqCls = col.isRequired ? ' required-input' : ''; - const req = col.isRequired ? ' required' : ''; + const cls = col.isManual ? "manual-input" : "auto-input"; + const reqCls = col.isRequired ? " required-input" : ""; + const req = col.isRequired ? " required" : ""; const v = esc(value); - if (col.dataType === 'SceltaMultipla') { + if (col.dataType === "SceltaMultipla") { const options = buildDropdownOptionsHTML(col.fieldId, value); return ``; } - if (col.dataType === 'Data') { + if (col.dataType === "Data") { return ``; } - if (col.dataType === 'INT') { + if (col.dataType === "INT") { return ``; } - if (col.autoValue === 'import_time' || (meta.timeLabels || []).includes(col.label)) { + if ( + col.autoValue === "import_time" || + (meta.timeLabels || []).includes(col.label) + ) { return ``; } return ``; } function createFixedFieldHTML(col, value, rowIndex) { - if (col.dataType === 'DATE') { - const reqCls = col.isRequired ? ' required-input' : ''; - const req = col.isRequired ? ' required' : ''; + if (col.dataType === "DATE") { + const reqCls = col.isRequired ? " required-input" : ""; + const req = col.isRequired ? " required" : ""; return ``; } // Client-sourced fields → AJAX Select2 (like idclient) const config = fixedFieldApiConfig[col.key]; - if (config && config.source === 'clients') { - const reqCls = col.isRequired ? ' required-input' : ''; - const req = col.isRequired ? ' required' : ''; + if (config && config.source === "clients") { + const reqCls = col.isRequired ? " required-input" : ""; + const req = col.isRequired ? " required" : ""; let opts = ''; if (value) { const label = clientNameCache[value] || value; @@ -391,26 +480,28 @@ // Select — build from cache const isApiField = !!config; - const selectClass = isApiField ? 'api-fixed-select' : ''; + const selectClass = isApiField ? "api-fixed-select" : ""; let options = ''; const cacheKey = config?.dependsOn - ? col.key + '_' + (data[rowIndex].idclient || '') + ? col.key + "_" + (data[rowIndex].idclient || "") : col.key; const items = fixedFieldCache[cacheKey] || []; - items.forEach(item => { - const sel = String(item.id) === String(value) ? ' selected' : ''; + items.forEach((item) => { + const sel = String(item.id) === String(value) ? " selected" : ""; options += ``; }); - const reqCls = col.isRequired ? ' required-input' : ''; - const req = col.isRequired ? ' required' : ''; + const reqCls = col.isRequired ? " required-input" : ""; + const req = col.isRequired ? " required" : ""; return ``; } function buildDropdownOptionsHTML(fieldId, selectedValue) { let html = ''; if (selectedValue) { - const label = dropdownNameCache[fieldId + '_' + selectedValue] || selectedValue; + const label = + dropdownNameCache[fieldId + "_" + selectedValue] || + selectedValue; html += ``; } return html; @@ -420,15 +511,15 @@ function renderActionButtons(rowIndex) { const row = data[rowIndex]; - const isExported = row.status === 'l'; - const div = document.createElement('div'); - div.className = 'grid-cell button-cell'; - div.style.flex = '0 0 auto'; - div.style.position = 'relative'; + const isExported = row.status === "l"; + const div = document.createElement("div"); + div.className = "grid-cell button-cell"; + div.style.flex = "0 0 auto"; + div.style.position = "relative"; - let html = ''; + let html = ""; if (meta.isAdmin) { - html += ``; + html += ``; } html += ``; html += ``; @@ -441,57 +532,78 @@ function renderRow(rowIndex) { const row = data[rowIndex]; - const rowDiv = document.createElement('div'); - rowDiv.className = 'grid-row'; + const rowDiv = document.createElement("div"); + rowDiv.className = "grid-row"; rowDiv.dataset.id = row.iddatadb; - if (row._dirty) rowDiv.classList.add('row-dirty'); + if (row._dirty) rowDiv.classList.add("row-dirty"); // Action buttons rowDiv.appendChild(renderActionButtons(rowIndex)); // All columns let cellIndex = 1; - columns.forEach(col => { + columns.forEach((col) => { rowDiv.appendChild(createCell(col, rowIndex, cellIndex)); cellIndex++; }); // Restore validation errors if present if (row._validationErrors && row._validationErrors.length > 0) { - rowDiv.classList.add('validation-row-error'); + rowDiv.classList.add("validation-row-error"); const messages = []; - row._validationErrors.forEach(err => { + row._validationErrors.forEach((err) => { messages.push(err.message); if (!err.field) return; let cell = null; - if (err.field.startsWith('field_label:')) { - const label = err.field.substring('field_label:'.length); - const headers = document.querySelectorAll('.grid-header'); + if (err.field.startsWith("field_label:")) { + const label = err.field.substring("field_label:".length); + const headers = document.querySelectorAll(".grid-header"); let targetIndex = null; - headers.forEach(h => { if (h.textContent.trim() === label) targetIndex = h.getAttribute('data-index'); }); - if (targetIndex) cell = rowDiv.querySelector(`.grid-cell[data-index="${targetIndex}"]`); + headers.forEach((h) => { + if (h.textContent.trim() === label) + targetIndex = h.getAttribute("data-index"); + }); + if (targetIndex) + cell = rowDiv.querySelector( + `.grid-cell[data-index="${targetIndex}"]`, + ); } else { - cell = rowDiv.querySelector(`.grid-cell[data-col="${err.field}"]`); + cell = rowDiv.querySelector( + `.grid-cell[data-col="${err.field}"]`, + ); } if (cell) { - cell.classList.add('validation-error'); - cell.querySelectorAll('input, select').forEach(el => el.classList.add('input-validation-error')); - let tooltip = cell.querySelector('.validation-tooltip'); - if (!tooltip) { tooltip = document.createElement('div'); tooltip.className = 'validation-tooltip'; cell.appendChild(tooltip); } + cell.classList.add("validation-error"); + cell.querySelectorAll("input, select").forEach((el) => + el.classList.add("input-validation-error"), + ); + let tooltip = cell.querySelector(".validation-tooltip"); + if (!tooltip) { + tooltip = document.createElement("div"); + tooltip.className = "validation-tooltip"; + cell.appendChild(tooltip); + } tooltip.textContent = err.message; } }); // Show error msg on button cell - const btnCell2 = rowDiv.querySelector('.button-cell'); + const btnCell2 = rowDiv.querySelector(".button-cell"); if (btnCell2) { - const errorEl = document.createElement('div'); - errorEl.className = 'batch-error-msg'; - errorEl.textContent = 'Warning — click for details'; - errorEl.addEventListener('click', () => { - document.getElementById('exportResponseMessage').innerHTML = messages.join('
'); - document.getElementById('exportResponseModalLabel').textContent = 'Validation Error (id: ' + row.iddatadb + ')'; - new bootstrap.Modal(document.getElementById('exportResponseModal'), { keyboard: false }).show(); + const errorEl = document.createElement("div"); + errorEl.className = "batch-error-msg"; + errorEl.textContent = "Warning — click for details"; + errorEl.addEventListener("click", () => { + document.getElementById("exportResponseMessage").innerHTML = + messages.join("
"); + document.getElementById( + "exportResponseModalLabel", + ).textContent = + "Validation Error (id: " + row.iddatadb + ")"; + new bootstrap.Modal( + document.getElementById("exportResponseModal"), + { keyboard: false }, + ).show(); }); btnCell2.appendChild(errorEl); } @@ -499,16 +611,22 @@ // Restore export error indicator if present if (row._exportError) { - rowDiv.classList.add('batch-row-error'); - const btnCell = rowDiv.querySelector('.button-cell'); + rowDiv.classList.add("batch-row-error"); + const btnCell = rowDiv.querySelector(".button-cell"); if (btnCell) { - const errorEl = document.createElement('div'); - errorEl.className = 'batch-error-msg'; - errorEl.textContent = 'Warning — click for details'; - errorEl.addEventListener('click', () => { - document.getElementById('exportResponseMessage').innerHTML = row._exportError.replace(/\n/g, '
'); - document.getElementById('exportResponseModalLabel').textContent = 'Error (id: ' + row.iddatadb + ')'; - new bootstrap.Modal(document.getElementById('exportResponseModal'), { keyboard: false }).show(); + const errorEl = document.createElement("div"); + errorEl.className = "batch-error-msg"; + errorEl.textContent = "Warning — click for details"; + errorEl.addEventListener("click", () => { + document.getElementById("exportResponseMessage").innerHTML = + row._exportError.replace(/\n/g, "
"); + document.getElementById( + "exportResponseModalLabel", + ).textContent = "Error (id: " + row.iddatadb + ")"; + new bootstrap.Modal( + document.getElementById("exportResponseModal"), + { keyboard: false }, + ).show(); }); btnCell.appendChild(errorEl); } @@ -527,9 +645,9 @@ if (revealedCount < totalRows) revealedCount = totalRows; // Destroy Select2 on existing rows - $(rowContainer).find('.select2-hidden-accessible').select2('destroy'); + $(rowContainer).find(".select2-hidden-accessible").select2("destroy"); - rowContainer.innerHTML = ''; + rowContainer.innerHTML = ""; const end = Math.min(revealedCount, totalRows); for (let i = 0; i < end; i++) { rowContainer.appendChild(renderRow(i)); @@ -543,19 +661,23 @@ } function renderSingleRow(rowIndex) { - const existing = rowContainer.querySelector(`.grid-row[data-id="${data[rowIndex].iddatadb}"]`); + const existing = rowContainer.querySelector( + `.grid-row[data-id="${data[rowIndex].iddatadb}"]`, + ); if (!existing) return; // Destroy Select2 before removing - $(existing).find('.select2-hidden-accessible').select2('destroy'); + $(existing).find(".select2-hidden-accessible").select2("destroy"); const newRow = renderRow(rowIndex); existing.replaceWith(newRow); // Init flatpickr - $(newRow).find('.date-picker').each(function () { - flatpickr(this, { dateFormat: 'Y-m-d', allowInput: true }); - }); + $(newRow) + .find(".date-picker") + .each(function () { + flatpickr(this, { dateFormat: "Y-m-d", allowInput: true }); + }); } function revealNextBatch() { @@ -570,80 +692,96 @@ } function initFlatpickr() { - $(rowContainer).find('.date-picker:not(.flatpickr-input)').each(function () { - flatpickr(this, { dateFormat: 'Y-m-d', allowInput: true }); - }); + $(rowContainer) + .find(".date-picker:not(.flatpickr-input)") + .each(function () { + flatpickr(this, { dateFormat: "Y-m-d", allowInput: true }); + }); } // ── Headers & Propagate row ──────────────────────────────────────────── function renderHeaders() { if (!headerContainer) return; - headerContainer.innerHTML = ''; + headerContainer.innerHTML = ""; // Actions header - const actH = document.createElement('div'); - actH.className = 'grid-header button-header'; - actH.style.flex = '0 0 380px'; - actH.textContent = 'Actions'; + const actH = document.createElement("div"); + actH.className = "grid-header button-header"; + actH.style.flex = "0 0 380px"; + actH.textContent = "Actions"; headerContainer.appendChild(actH); let idx = 1; - columns.forEach(col => { - const h = document.createElement('div'); - h.className = 'grid-header'; + columns.forEach((col) => { + const h = document.createElement("div"); + h.className = "grid-header"; h.dataset.index = idx; h.style.flex = `0 0 ${col.width}px`; - h.style.position = 'relative'; + h.style.position = "relative"; h.innerHTML = esc(col.label) + '
'; headerContainer.appendChild(h); idx++; }); + + initColumnResizers(); } function renderTopRow() { if (!topContainer) return; - topContainer.innerHTML = ''; + topContainer.innerHTML = ""; // Empty cell for actions column - const empty = document.createElement('div'); - empty.className = 'grid-cell save-all-cell'; + const empty = document.createElement("div"); + empty.className = "grid-cell save-all-cell"; topContainer.appendChild(empty); columns.forEach((col, colIdx) => { - const cell = document.createElement('div'); - cell.className = 'grid-cell grid-top-cell'; + const cell = document.createElement("div"); + cell.className = "grid-cell grid-top-cell"; cell.style.flex = `0 0 ${col.width}px`; - if (col.editable === false || col.type === 'static' || col.type === 'tracking') { + if ( + col.editable === false || + col.type === "static" || + col.type === "tracking" + ) { // Empty top cell - } else if (col.type === 'idclient') { - cell.innerHTML = `` + + } else if (col.type === "idclient") { + cell.innerHTML = + `` + ``; - } else if (col.type === 'cliente_fornitore_id') { - cell.innerHTML = `` + + } else if (col.type === "cliente_fornitore_id") { + cell.innerHTML = + `` + ``; - } else if (col.type === 'fixed' && col.dataType === 'DATE') { - cell.innerHTML = `` + + } else if (col.type === "fixed" && col.dataType === "DATE") { + cell.innerHTML = + `` + ``; - } else if (col.type === 'fixed') { + } else if (col.type === "fixed") { const isApiField = !!fixedFieldApiConfig[col.key]; if (isApiField) { - cell.innerHTML = `` + + cell.innerHTML = + `` + ``; } else { - cell.innerHTML = `` + + cell.innerHTML = + `` + ``; } - } else if (col.type === 'detail' || col.type === 'main_field') { - if (col.dataType === 'SceltaMultipla') { - cell.innerHTML = `` + + } else if (col.type === "detail" || col.type === "main_field") { + if (col.dataType === "SceltaMultipla") { + cell.innerHTML = + `` + ``; - } else if (col.dataType === 'Data') { - cell.innerHTML = `` + + } else if (col.dataType === "Data") { + cell.innerHTML = + `` + ``; } else { - cell.innerHTML = `` + + cell.innerHTML = + `` + ``; } } @@ -657,24 +795,24 @@ async function populateTopRowSelects() { // Client selects in top row — AJAX mode - const clientSel = document.getElementById('clientSelect'); + const clientSel = document.getElementById("clientSelect"); if (clientSel) { clientSel.innerHTML = buildClientOptionsHTML(meta.defaultIdclient); $(clientSel).select2(clientSelect2Config); } - const fornitSel = document.getElementById('clienteFornitoreSelect'); + const fornitSel = document.getElementById("clienteFornitoreSelect"); if (fornitSel) { - fornitSel.innerHTML = buildClientOptionsHTML(''); + fornitSel.innerHTML = buildClientOptionsHTML(""); $(fornitSel).select2(clientSelect2Config); } // Fixed field selects in top row - topContainer.querySelectorAll('.api-fixed-select').forEach(sel => { + topContainer.querySelectorAll(".api-fixed-select").forEach((sel) => { const fieldKey = sel.dataset.fixedKey; const config = fixedFieldApiConfig[fieldKey]; // Client-sourced → init as AJAX Select2 - if (config && config.source === 'clients') { + if (config && config.source === "clients") { $(sel).select2(clientSelect2Config); return; } @@ -683,30 +821,40 @@ // For dependent fields: merge all cached values across all clientIds const allItems = new Map(); for (const [key, items] of Object.entries(fixedFieldCache)) { - if (key.startsWith(fieldKey + '_')) { - items.forEach(item => allItems.set(String(item.id), item)); + if (key.startsWith(fieldKey + "_")) { + items.forEach((item) => + allItems.set(String(item.id), item), + ); } } sel.innerHTML = ''; [...allItems.values()] - .sort((a, b) => String(a.text).localeCompare(String(b.text), 'it', { sensitivity: 'base' })) - .forEach(item => sel.add(new Option(item.text, item.id))); + .sort((a, b) => + String(a.text).localeCompare(String(b.text), "it", { + sensitivity: "base", + }), + ) + .forEach((item) => sel.add(new Option(item.text, item.id))); } else { const items = fixedFieldCache[fieldKey] || []; sel.innerHTML = ''; - items.forEach(item => sel.add(new Option(item.text, item.id))); + items.forEach((item) => + sel.add(new Option(item.text, item.id)), + ); } }); // Custom field dropdowns in top row — AJAX Select2 - topContainer.querySelectorAll('.dropdown-select[data-field-id]').forEach(sel => { - const fieldId = sel.dataset.fieldId; - if (fieldId) $(sel).select2(sceltaSelect2Config(fieldId)); - }); + topContainer + .querySelectorAll(".dropdown-select[data-field-id]") + .forEach((sel) => { + const fieldId = sel.dataset.fieldId; + if (fieldId) $(sel).select2(sceltaSelect2Config(fieldId)); + }); // Flatpickr in top row - topContainer.querySelectorAll('.date-picker').forEach(el => { - flatpickr(el, { dateFormat: 'Y-m-d', allowInput: true }); + topContainer.querySelectorAll(".date-picker").forEach((el) => { + flatpickr(el, { dateFormat: "Y-m-d", allowInput: true }); }); } @@ -716,38 +864,42 @@ if (!rowContainer) return; // Cell value changes → write to gridData - rowContainer.addEventListener('change', function (e) { - const cell = e.target.closest('.grid-cell'); + rowContainer.addEventListener("change", function (e) { + const cell = e.target.closest(".grid-cell"); if (!cell || !cell.dataset.row) return; const rowIndex = parseInt(cell.dataset.row); const colType = cell.dataset.colType; const colKey = cell.dataset.col; const value = e.target.value; - if (colType === 'detail' || colType === 'main_field') { - if (colType === 'main_field') { + if (colType === "detail" || colType === "main_field") { + if (colType === "main_field") { data[rowIndex].mainFieldValue = value; } setDetailValue(rowIndex, colKey, value); - } else if (colType === 'fixed') { + } else if (colType === "fixed") { setFixedValue(rowIndex, colKey, value); - } else if (colType === 'idclient') { + } else if (colType === "idclient") { data[rowIndex].idclient = value; data[rowIndex]._dirty = true; - } else if (colType === 'cliente_fornitore_id') { + } else if (colType === "cliente_fornitore_id") { data[rowIndex].cliente_fornitore_id = value; data[rowIndex]._dirty = true; - console.log('[gridRenderer] cliente_fornitore_id changed:', rowIndex, value); + console.log( + "[gridRenderer] cliente_fornitore_id changed:", + rowIndex, + value, + ); } // Visual feedback - cell.classList.add('cell-changed'); + cell.classList.add("cell-changed"); updateDirtyIndicator(); }); // Propagate buttons - document.addEventListener('click', function (e) { - const btn = e.target.closest('.propagate-btn'); + document.addEventListener("click", function (e) { + const btn = e.target.closest(".propagate-btn"); if (!btn) return; const colIndex = parseInt(btn.dataset.colIndex); @@ -755,37 +907,51 @@ if (isNaN(colIndex) && !column) return; // Get value from the input/select in the same cell - const cell = btn.closest('.grid-cell') || btn.closest('.grid-top-cell'); + const cell = + btn.closest(".grid-cell") || btn.closest(".grid-top-cell"); if (!cell) return; - const input = cell.querySelector('select, input'); + const input = cell.querySelector("select, input"); if (!input) return; - const value = $(input).hasClass('select2-hidden-accessible') ? $(input).val() : input.value; + const value = $(input).hasClass("select2-hidden-accessible") + ? $(input).val() + : input.value; // Cache Select2 label so re-render shows name not ID - if (value && $(input).hasClass('select2-hidden-accessible')) { - const label = $(input).find('option:selected').text(); + if (value && $(input).hasClass("select2-hidden-accessible")) { + const label = $(input).find("option:selected").text(); if (label && label !== value) { clientNameCache[value] = label; // Also cache for SceltaMultipla const fieldId = input.dataset?.fieldId; - if (fieldId) dropdownNameCache[fieldId + '_' + value] = label; + if (fieldId) + dropdownNameCache[fieldId + "_" + value] = label; } } const col = columns[colIndex] || null; - if (column === 'idclient') { - data.forEach(row => { row.idclient = value; row._dirty = true; }); - } else if (column === 'cliente_fornitore_id') { - data.forEach(row => { row.cliente_fornitore_id = value; row._dirty = true; }); - } else if (column && column.startsWith('fixed_')) { - const fixedKey = column.replace('fixed_', ''); - data.forEach(row => { row.fixedFields[fixedKey] = value; row._dirty = true; }); + if (column === "idclient") { + data.forEach((row) => { + row.idclient = value; + row._dirty = true; + }); + } else if (column === "cliente_fornitore_id") { + data.forEach((row) => { + row.cliente_fornitore_id = value; + row._dirty = true; + }); + } else if (column && column.startsWith("fixed_")) { + const fixedKey = column.replace("fixed_", ""); + data.forEach((row) => { + row.fixedFields[fixedKey] = value; + row._dirty = true; + }); } else if (col) { - if (col.type === 'detail' || col.type === 'main_field') { - data.forEach(row => { + if (col.type === "detail" || col.type === "main_field") { + data.forEach((row) => { row.details[col.key] = value; - if (col.type === 'main_field') row.mainFieldValue = value; + if (col.type === "main_field") + row.mainFieldValue = value; row._dirty = true; }); } @@ -795,52 +961,53 @@ }); // Select2 change events (don't bubble via native addEventListener) - $(rowContainer).on('change', '.searchable-client', function () { - const cell = this.closest('.grid-cell'); + $(rowContainer).on("change", ".searchable-client", function () { + const cell = this.closest(".grid-cell"); if (!cell || !cell.dataset.row) return; const rowIndex = parseInt(cell.dataset.row); const colType = cell.dataset.colType; - const value = $(this).val() || ''; + const value = $(this).val() || ""; // Cache selected label if (value) { - const label = $(this).find('option:selected').text(); + const label = $(this).find("option:selected").text(); if (label && label !== value) clientNameCache[value] = label; } - if (colType === 'idclient') { + if (colType === "idclient") { data[rowIndex].idclient = value; data[rowIndex]._dirty = true; - } else if (colType === 'cliente_fornitore_id') { + } else if (colType === "cliente_fornitore_id") { data[rowIndex].cliente_fornitore_id = value; data[rowIndex]._dirty = true; } - cell.classList.add('cell-changed'); + cell.classList.add("cell-changed"); updateDirtyIndicator(); }); // Cache labels on SceltaMultipla change - $(rowContainer).on('change', '.searchable-dropdown', function () { + $(rowContainer).on("change", ".searchable-dropdown", function () { const val = $(this).val(); const fieldId = this.dataset.fieldId; if (val && fieldId) { - const label = $(this).find('option:selected').text(); - if (label && label !== val) dropdownNameCache[fieldId + '_' + val] = label; + const label = $(this).find("option:selected").text(); + if (label && label !== val) + dropdownNameCache[fieldId + "_" + val] = label; } }); // Select2 change on fixed field selects - $(rowContainer).on('change', '.api-fixed-select', function () { - const cell = this.closest('.grid-cell'); + $(rowContainer).on("change", ".api-fixed-select", function () { + const cell = this.closest(".grid-cell"); if (!cell || !cell.dataset.row) return; const rowIndex = parseInt(cell.dataset.row); const key = this.dataset.fixedKey || cell.dataset.col; - const value = $(this).val() || ''; + const value = $(this).val() || ""; if (key) { data[rowIndex].fixedFields[key] = value; data[rowIndex]._dirty = true; - cell.classList.add('cell-changed'); + cell.classList.add("cell-changed"); updateDirtyIndicator(); } }); @@ -849,13 +1016,18 @@ function onScroll() { if (revealedCount >= totalRows) return; const docEl = document.documentElement; - const scrollBottom = Math.max(docEl.scrollTop + docEl.clientHeight, document.body.scrollTop + window.innerHeight); + const scrollBottom = Math.max( + docEl.scrollTop + docEl.clientHeight, + document.body.scrollTop + window.innerHeight, + ); if (scrollBottom >= docEl.scrollHeight - 300) { revealNextBatch(); } } - window.addEventListener('scroll', onScroll, { passive: true }); - document.querySelector('.page-wrapper')?.addEventListener('scroll', onScroll, { passive: true }); + window.addEventListener("scroll", onScroll, { passive: true }); + document + .querySelector(".page-wrapper") + ?.addEventListener("scroll", onScroll, { passive: true }); } // ── Save ─────────────────────────────────────────────────────────────── @@ -863,7 +1035,7 @@ window.buildSavePayload = function (rowIndex) { const row = data[rowIndex]; const formData = new FormData(); - formData.append('iddatadb', row.iddatadb); + formData.append("iddatadb", row.iddatadb); // Details for (const [mappingId, value] of Object.entries(row.details)) { @@ -871,8 +1043,8 @@ } // Client - if (row.idclient) formData.append('idclient', row.idclient); - formData.append('cliente_fornitore_id', row.cliente_fornitore_id || ''); + if (row.idclient) formData.append("idclient", row.idclient); + formData.append("cliente_fornitore_id", row.cliente_fornitore_id || ""); // Fixed fields → real column names const aliasMap = meta.fixedAliasMap || {}; @@ -887,14 +1059,15 @@ // ── Dirty indicator ──────────────────────────────────────────────────── function updateDirtyIndicator() { - const dirtyCount = data.filter(r => r._dirty).length; - const indicator = document.getElementById('unsavedChanges'); - const changedEl = document.getElementById('changedRows'); + const dirtyCount = data.filter((r) => r._dirty).length; + const indicator = document.getElementById("unsavedChanges"); + const changedEl = document.getElementById("changedRows"); if (indicator) { - indicator.style.display = dirtyCount > 0 ? '' : 'none'; + indicator.style.display = dirtyCount > 0 ? "" : "none"; } if (changedEl) { - changedEl.textContent = dirtyCount > 0 ? `(${dirtyCount} rows)` : ''; + changedEl.textContent = + dirtyCount > 0 ? `(${dirtyCount} rows)` : ""; } } @@ -903,7 +1076,7 @@ const shown = Math.min(revealedCount, totalRows); if (shown >= totalRows) { statusEl.textContent = `All ${totalRows} rows loaded`; - setTimeout(() => statusEl.style.display = 'none', 2000); + setTimeout(() => (statusEl.style.display = "none"), 2000); } else { statusEl.textContent = `Showing ${shown} of ${totalRows} rows — scroll down for more`; } @@ -912,13 +1085,89 @@ // ── Lazy Select2 for row selects ─────────────────────────────────────── function initLazySelect2() { - $(document).on('mouseenter', '.grid-row .grid-cell', function () { - $(this).find('.searchable-client:not(.select2-hidden-accessible)').each(function () { - $(this).select2(clientSelect2Config); + $(document).on("mouseenter", ".grid-row .grid-cell", function () { + $(this) + .find(".searchable-client:not(.select2-hidden-accessible)") + .each(function () { + $(this).select2(clientSelect2Config); + }); + $(this) + .find(".searchable-dropdown:not(.select2-hidden-accessible)") + .each(function () { + const fieldId = this.dataset.fieldId; + if (fieldId) $(this).select2(sceltaSelect2Config(fieldId)); + }); + }); + } + + // ── Column resize ────────────────────────────────────────────────────── + + function initColumnResizers() { + const resizers = document.querySelectorAll(".resizer"); + + let currentResizer = null; + let startX = 0; + let startWidth = 0; + let columnIndex = null; + + function resize(e) { + if (!currentResizer || !columnIndex) return; + + const deltaX = e.pageX - startX; + const newWidth = Math.max(80, startWidth + deltaX); + + const header = document.querySelector( + `.grid-header[data-index="${columnIndex}"]`, + ); + if (header) { + header.style.flex = `0 0 ${newWidth}px`; + } + + const topCell = document.querySelector( + `.grid-top .grid-cell:nth-child(${parseInt(columnIndex, 10) + 1})`, + ); + if (topCell) { + topCell.style.flex = `0 0 ${newWidth}px`; + } + + const cells = document.querySelectorAll( + `.grid-row .grid-cell[data-index="${columnIndex}"]`, + ); + cells.forEach((cell) => { + cell.style.flex = `0 0 ${newWidth}px`; }); - $(this).find('.searchable-dropdown:not(.select2-hidden-accessible)').each(function () { - const fieldId = this.dataset.fieldId; - if (fieldId) $(this).select2(sceltaSelect2Config(fieldId)); + + // aggiorna anche la width nel meta così i rerender la mantengono + const colPos = parseInt(columnIndex, 10) - 1; + if (columns[colPos]) { + columns[colPos].width = newWidth; + } + } + + function stopResize() { + if (currentResizer) { + document.removeEventListener("mousemove", resize); + document.removeEventListener("mouseup", stopResize); + currentResizer = null; + columnIndex = null; + } + } + + resizers.forEach((resizer) => { + resizer.addEventListener("mousedown", function (e) { + e.preventDefault(); + e.stopPropagation(); + + currentResizer = this; + const header = this.closest(".grid-header"); + if (!header) return; + + columnIndex = header.getAttribute("data-index"); + startX = e.pageX; + startWidth = header.offsetWidth; + + document.addEventListener("mousemove", resize); + document.addEventListener("mouseup", stopResize); }); }); } @@ -926,18 +1175,19 @@ // ── Init ─────────────────────────────────────────────────────────────── async function init() { - rowContainer = document.getElementById('gridRowContainer'); - headerContainer = document.getElementById('gridHeaderContainer'); - topContainer = document.getElementById('gridTopContainer'); + rowContainer = document.getElementById("gridRowContainer"); + headerContainer = document.getElementById("gridHeaderContainer"); + topContainer = document.getElementById("gridTopContainer"); if (!rowContainer) { - console.error('gridRenderer: #gridRowContainer not found'); + console.error("gridRenderer: #gridRowContainer not found"); return; } // Status bar - statusEl = document.createElement('div'); - statusEl.style.cssText = 'text-align:center; padding:8px; color:#666; font-size:12px;'; + statusEl = document.createElement("div"); + statusEl.style.cssText = + "text-align:center; padding:8px; color:#666; font-size:12px;"; if (totalRows > PAGE_SIZE) { statusEl.textContent = `Loading data...`; rowContainer.parentElement.appendChild(statusEl); @@ -950,6 +1200,7 @@ renderHeaders(); renderTopRow(); renderVisibleRows(); + initColumnResizers(); // Events attachEvents(); @@ -957,8 +1208,8 @@ } // Start when DOM ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); } else { init(); } @@ -972,8 +1223,10 @@ getData: () => data, getMeta: () => meta, getClientData: () => clientData, - getDirtyRows: () => data.filter(r => r._dirty).map((r, i) => i), - clearDirty: (rowIndex) => { data[rowIndex]._dirty = false; updateDirtyIndicator(); }, + getDirtyRows: () => data.filter((r) => r._dirty).map((r, i) => i), + clearDirty: (rowIndex) => { + data[rowIndex]._dirty = false; + updateDirtyIndicator(); + }, }; - })(); diff --git a/public/userarea/imported.php b/public/userarea/imported.php index 77223e6..d7f50aa 100644 --- a/public/userarea/imported.php +++ b/public/userarea/imported.php @@ -342,8 +342,8 @@ $gridMeta = [ ?> @@ -797,7 +797,7 @@ window.gridMeta = .modal-content { + #photosModal>.modal-content { background-color: #fff; margin: 5% auto; padding: 20px; @@ -892,9 +892,17 @@ window.gridMeta = " class="btn btn-warning me-2">Imported (i) To LIMS (l) - - - - - ( records) - + + + + + ( records) + - -
-
- Rows per page -
- $lim])); - ?> - - + ?> +
+
+ Rows per page +
+ $lim])); + ?> + + +
-
- 1): ?> -
- of - - - 1): ?> - 1 - 2): ?>... - - - - ... - + 1): ?> +
+ of + + + 1): ?> + 1 + 2): ?>... + + + + ... + + + + +
- -
- -
@@ -1208,6 +1246,7 @@ window.gridMeta =
+
@@ -1218,7 +1257,12 @@ window.gridMeta =
- + @@ -1231,6 +1275,7 @@ window.gridMeta = +