/** * gridRenderer.js — Data-driven grid renderer for imported.php * * Reads window.gridData (array of row objects) and window.gridMeta (column defs, config). * Renders only visible rows into DOM. Propagate/edit operates on gridData array, * then re-renders visible rows. Save reads from gridData, not DOM. */ (function () { "use strict"; const PAGE_SIZE = 20; let revealedCount = PAGE_SIZE; let dropdownOptionsCache = {}; // fieldId -> [{id, text}] — used only for small lists const dropdownNameCache = {}; // "fieldId_valueId" -> label let clientData = []; // loaded from get_clienti.php let fixedFieldCache = window.fixedFieldDataCache || {}; window.fixedFieldDataCache = fixedFieldCache; const data = window.gridData || []; const meta = window.gridMeta || {}; const columns = meta.columns || []; let totalRows = data.length; // ── DOM refs ──────────────────────────────────────────────────────────── let rowContainer = null; let headerContainer = null; let topContainer = null; let statusEl = null; // ── Helpers ───────────────────────────────────────────────────────────── function esc(str) { 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)] ?? ""; } function setDetailValue(rowIndex, mappingId, value) { data[rowIndex].details[String(mappingId)] = value; data[rowIndex]._dirty = true; } function getFixedValue(rowIndex, key) { return data[rowIndex].fixedFields[key] ?? ""; } function setFixedValue(rowIndex, key, value) { data[rowIndex].fixedFields[key] = value; data[rowIndex]._dirty = true; } // ── Client data (AJAX Select2, no bulk loading) ────────────────────── function formatClientLabel(client) { return (client.Nominativo || "").trim(); } // Cache of resolved client names: id → name const clientNameCache = {}; // Build select with only the selected option function buildClientOptionsHTML(selectedId) { let html = ''; if (selectedId) { const label = clientNameCache[selectedId] || selectedId; html += ``; } return html; } // Pre-resolve all unique client IDs used in data rows, then re-render async function resolveClientNames() { const ids = new Set(); data.forEach((row) => { if (row.idclient) ids.add(String(row.idclient)); 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)); } } }); // Batch resolve via single request per 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 */ } }), ); } // Select2 AJAX config for client selects const clientSelect2Config = { placeholder: "Search client...", allowClear: true, width: "100%", minimumInputLength: 0, dropdownCssClass: "select2-dropdown-smaller", ajax: { 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, }, }; async function loadClientData() { // No longer loads all clients — AJAX Select2 handles search // Just resolve names for pre-selected values await resolveClientNames(); } // ── 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" }, }; let _pendingFixed = {}; async function loadFixedFieldOptions(fieldKey, clientId) { const config = fixedFieldApiConfig[fieldKey]; if (!config) return []; // Client-sourced fields — handled by AJAX Select2, skip preloading if (config.source === "clients") { fixedFieldCache[fieldKey] = []; return []; } const cacheKey = fieldKey + (clientId ? "_" + clientId : ""); if (fixedFieldCache[cacheKey]) return fixedFieldCache[cacheKey]; if (_pendingFixed[cacheKey]) return _pendingFixed[cacheKey]; const params = { field: config.endpoint }; if (config.dependsOn && clientId) { Object.assign(params, config.getParams(clientId)); } _pendingFixed[cacheKey] = (async () => { try { 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", }), ); fixedFieldCache[cacheKey] = results; delete _pendingFixed[cacheKey]; return results; } catch (e) { delete _pendingFixed[cacheKey]; console.error("Failed to load fixed field " + fieldKey, e); return []; } })(); return _pendingFixed[cacheKey]; } // ── Custom field dropdown data loading ───────────────────────────────── // Select2 AJAX config factory for SceltaMultipla function sceltaSelect2Config(fieldId) { return { placeholder: "Search...", allowClear: true, width: "100%", minimumInputLength: 0, ajax: { 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, }, }; } // ── Preload all data ─────────────────────────────────────────────────── async function preloadAllData() { // 1. Clients 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))); // Client-sourced fixed fields 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) => { if (row.idclient) clientIds.add(String(row.idclient)); }); const dependent = Object.keys(fixedFieldApiConfig).filter( (k) => fixedFieldApiConfig[k].dependsOn, ); for (const fieldKey of dependent) { for (const cid of clientIds) { await loadFixedFieldOptions(fieldKey, cid); } } // 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)), ), ]; if (allFieldIds.length > 0) { try { 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 || ""; }); } } } catch (e) { console.error("Failed to preload dropdown data:", e); } } console.log("[gridRenderer] preload done:", { clients: clientData.length, fixedFieldCache: Object.keys(fixedFieldCache), dropdownOptionsCache: Object.keys(dropdownOptionsCache), }); } // ── Cell rendering ───────────────────────────────────────────────────── function createCell(col, rowIndex, cellIndex) { 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; div.dataset.index = cellIndex; div.style.flex = `0 0 ${col.width}px`; const row = data[rowIndex]; switch (col.type) { 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"; let html = `${label}`; if (row.commessaweb) { html += `${esc(row.commessaweb)}`; } div.innerHTML = html; 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 || ""; 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, ); div.appendChild(sel); break; } case "detail": { const val = getDetailValue(rowIndex, col.key); div.innerHTML = createInputHTML(col, val, rowIndex); break; } case "tested_component": div.style.overflow = "visible"; div.innerHTML = `
`; break; case "awb": div.innerHTML = `` + `` + ``; break; case "tracking": div.className = "grid-cell tracking-info"; div.dataset.row = rowIndex; div.innerHTML = 'Shipment Info'; break; 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) { div.innerHTML = `File`; } else { div.innerHTML = `${esc(val)}`; } break; } } return div; } 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 v = esc(value); if (col.dataType === "SceltaMultipla") { const options = buildDropdownOptionsHTML(col.fieldId, value); return ``; } if (col.dataType === "Data") { return ``; } if (col.dataType === "INT") { return ``; } 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" : ""; 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" : ""; let opts = ''; if (value) { const label = clientNameCache[value] || value; opts += ``; } return ``; } // Select — build from cache const isApiField = !!config; const selectClass = isApiField ? "api-fixed-select" : ""; let options = ''; const cacheKey = config?.dependsOn ? col.key + "_" + (data[rowIndex].idclient || "") : col.key; const items = fixedFieldCache[cacheKey] || []; items.forEach((item) => { const sel = String(item.id) === String(value) ? " selected" : ""; options += ``; }); 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; html += ``; } return html; } // ── Row rendering ────────────────────────────────────────────────────── 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"; let html = ""; if (meta.isAdmin) { html += ``; } html += ``; html += ``; html += ``; html += ``; div.innerHTML = html; return div; } function renderRow(rowIndex) { const row = data[rowIndex]; const rowDiv = document.createElement("div"); rowDiv.className = "grid-row"; rowDiv.dataset.id = row.iddatadb; if (row._dirty) rowDiv.classList.add("row-dirty"); // Action buttons rowDiv.appendChild(renderActionButtons(rowIndex)); // All columns let cellIndex = 1; 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"); const messages = []; 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"); 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}"]`, ); } else { 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); } tooltip.textContent = err.message; } }); // Show error msg on 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(); }); btnCell2.appendChild(errorEl); } } // Restore export error indicator if present if (row._exportError) { 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(); }); btnCell.appendChild(errorEl); } } return rowDiv; } // ── Render visible rows ──────────────────────────────────────────────── function renderVisibleRows() { if (!rowContainer) return; // Refresh totalRows in case data was modified externally totalRows = data.length; if (revealedCount < totalRows) revealedCount = totalRows; // Destroy Select2 on existing rows $(rowContainer).find(".select2-hidden-accessible").select2("destroy"); rowContainer.innerHTML = ""; const end = Math.min(revealedCount, totalRows); for (let i = 0; i < end; i++) { rowContainer.appendChild(renderRow(i)); } // Init flatpickr on visible date pickers initFlatpickr(); updateStatus(); updateDirtyIndicator(); } function renderSingleRow(rowIndex) { 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"); const newRow = renderRow(rowIndex); existing.replaceWith(newRow); // Init flatpickr $(newRow) .find(".date-picker") .each(function () { flatpickr(this, { dateFormat: "Y-m-d", allowInput: true }); }); } function revealNextBatch() { if (revealedCount >= totalRows) return; const start = revealedCount; revealedCount = Math.min(revealedCount + PAGE_SIZE, totalRows); for (let i = start; i < revealedCount; i++) { rowContainer.appendChild(renderRow(i)); } initFlatpickr(); updateStatus(); } function initFlatpickr() { $(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 = ""; // Actions header 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"; h.dataset.index = idx; h.style.flex = `0 0 ${col.width}px`; h.style.position = "relative"; h.innerHTML = esc(col.label) + '
'; headerContainer.appendChild(h); idx++; }); initColumnResizers(); } function renderTopRow() { if (!topContainer) return; topContainer.innerHTML = ""; // Empty cell for actions column 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"; cell.style.flex = `0 0 ${col.width}px`; if ( col.editable === false || col.type === "static" || col.type === "tracking" ) { // Empty top cell } else if (col.type === "idclient") { 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") { const isApiField = !!fixedFieldApiConfig[col.key]; if (isApiField) { cell.innerHTML = `` + ``; } else { 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 { cell.innerHTML = `` + ``; } } topContainer.appendChild(cell); }); // Populate top row selects populateTopRowSelects(); } async function populateTopRowSelects() { // Client selects in top row — AJAX mode const clientSel = document.getElementById("clientSelect"); if (clientSel) { clientSel.innerHTML = buildClientOptionsHTML(meta.defaultIdclient); $(clientSel).select2(clientSelect2Config); } const fornitSel = document.getElementById("clienteFornitoreSelect"); if (fornitSel) { fornitSel.innerHTML = buildClientOptionsHTML(""); $(fornitSel).select2(clientSelect2Config); } // Fixed field selects in top row 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") { $(sel).select2(clientSelect2Config); return; } if (config && config.dependsOn) { // 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), ); } } 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))); } else { const items = fixedFieldCache[fieldKey] || []; sel.innerHTML = ''; 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)); }); // Flatpickr in top row topContainer.querySelectorAll(".date-picker").forEach((el) => { flatpickr(el, { dateFormat: "Y-m-d", allowInput: true }); }); } // ── Event delegation ─────────────────────────────────────────────────── function attachEvents() { if (!rowContainer) return; // Cell value changes → write to gridData 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") { data[rowIndex].mainFieldValue = value; } setDetailValue(rowIndex, colKey, value); } else if (colType === "fixed") { setFixedValue(rowIndex, colKey, value); } else if (colType === "idclient") { data[rowIndex].idclient = value; data[rowIndex]._dirty = true; } 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, ); } // Visual feedback cell.classList.add("cell-changed"); updateDirtyIndicator(); }); // Propagate buttons document.addEventListener("click", function (e) { const btn = e.target.closest(".propagate-btn"); if (!btn) return; const colIndex = parseInt(btn.dataset.colIndex); const column = btn.dataset.column; 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"); if (!cell) return; const input = cell.querySelector("select, input"); if (!input) return; 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 (label && label !== value) { clientNameCache[value] = label; // Also cache for SceltaMultipla const fieldId = input.dataset?.fieldId; 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; }); } else if (col) { if (col.type === "detail" || col.type === "main_field") { data.forEach((row) => { row.details[col.key] = value; if (col.type === "main_field") row.mainFieldValue = value; row._dirty = true; }); } } renderVisibleRows(); }); // Select2 change events (don't bubble via native addEventListener) $(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() || ""; // Cache selected label if (value) { const label = $(this).find("option:selected").text(); if (label && label !== value) clientNameCache[value] = label; } if (colType === "idclient") { data[rowIndex].idclient = value; data[rowIndex]._dirty = true; } else if (colType === "cliente_fornitore_id") { data[rowIndex].cliente_fornitore_id = value; data[rowIndex]._dirty = true; } cell.classList.add("cell-changed"); updateDirtyIndicator(); }); // Cache labels on SceltaMultipla change $(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; } }); // Select2 change on fixed field selects $(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() || ""; if (key) { data[rowIndex].fixedFields[key] = value; data[rowIndex]._dirty = true; cell.classList.add("cell-changed"); updateDirtyIndicator(); } }); // Scroll function onScroll() { if (revealedCount >= totalRows) return; const docEl = document.documentElement; 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 }); } // ── Save ─────────────────────────────────────────────────────────────── window.buildSavePayload = function (rowIndex) { const row = data[rowIndex]; const formData = new FormData(); formData.append("iddatadb", row.iddatadb); // Details for (const [mappingId, value] of Object.entries(row.details)) { formData.append(`details${mappingId}field_value`, value); } // Client 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 || {}; for (const [logicalKey, value] of Object.entries(row.fixedFields)) { const realKey = aliasMap[logicalKey] || logicalKey; formData.append(realKey, value); } return formData; }; // ── Dirty indicator ──────────────────────────────────────────────────── function updateDirtyIndicator() { 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"; } if (changedEl) { changedEl.textContent = dirtyCount > 0 ? `(${dirtyCount} rows)` : ""; } } function updateStatus() { if (!statusEl) return; const shown = Math.min(revealedCount, totalRows); if (shown >= totalRows) { statusEl.textContent = `All ${totalRows} rows loaded`; setTimeout(() => (statusEl.style.display = "none"), 2000); } else { statusEl.textContent = `Showing ${shown} of ${totalRows} rows — scroll down for more`; } } // ── 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); }); $(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`; }); // 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); }); }); } // ── Init ─────────────────────────────────────────────────────────────── async function init() { rowContainer = document.getElementById("gridRowContainer"); headerContainer = document.getElementById("gridHeaderContainer"); topContainer = document.getElementById("gridTopContainer"); if (!rowContainer) { 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;"; if (totalRows > PAGE_SIZE) { statusEl.textContent = `Loading data...`; rowContainer.parentElement.appendChild(statusEl); } // Preload all dropdown/field data await preloadAllData(); // Render renderHeaders(); renderTopRow(); renderVisibleRows(); initColumnResizers(); // Events attachEvents(); initLazySelect2(); } // Start when DOM ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } // ── Public API ───────────────────────────────────────────────────────── window.gridRenderer = { renderVisibleRows, renderSingleRow, revealNextBatch, buildSavePayload: window.buildSavePayload, getData: () => data, getMeta: () => meta, getClientData: () => clientData, getDirtyRows: () => data.filter((r) => r._dirty).map((r, i) => i), clearDirty: (rowIndex) => { data[rowIndex]._dirty = false; updateDirtyIndicator(); }, }; })();