1280 lines
53 KiB
JavaScript
1280 lines
53 KiB
JavaScript
/**
|
|
* 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 = '<option value="">Select a client...</option>';
|
|
if (selectedId) {
|
|
const label = clientNameCache[selectedId] || selectedId;
|
|
html += `<option value="${esc(String(selectedId))}" selected>${esc(String(label))}</option>`;
|
|
}
|
|
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 = `<span class="status-badge status-${esc(st)}">${label}</span>`;
|
|
if (row.commessaweb) {
|
|
html += `<span class="commessaweb-code" style="display:block; font-size:0.75em; color:#555; margin-top:2px;">${esc(row.commessaweb)}</span>`;
|
|
}
|
|
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 = `<div style="display:flex; align-items:center; gap:4px; width:100%; height:100%;">
|
|
<input type="text" class="cell-input manual-input tested-component-input" value="${esc(row.tested_component || "")}" style="flex:1; min-width:0; height:28px;">
|
|
<button type="button" class="add-part-btn btn btn-sm btn-primary" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" style="display:inline-flex; align-items:center; justify-content:center; min-width:28px; width:28px; height:28px; padding:0; font-size:12px; flex-shrink:0; text-align:center;">
|
|
<i class="fas fa-plus" style="margin:0; padding:0;"></i>
|
|
</button>
|
|
</div>`;
|
|
break;
|
|
|
|
case "awb":
|
|
div.innerHTML =
|
|
`<select class="carrier-select"><option value="tnt-it">TNT Italy</option><option value="dhl">DHL</option><option value="gls">GLS</option><option value="sda">SDA</option><option value="ups">UPS</option></select>` +
|
|
`<input type="text" class="cell-input awb-input" placeholder="AWB Number">` +
|
|
`<button type="button" class="go-btn" data-row="${rowIndex}"><i class="fas fa-play"></i></button>`;
|
|
break;
|
|
|
|
case "tracking":
|
|
div.className = "grid-cell tracking-info";
|
|
div.dataset.row = rowIndex;
|
|
div.innerHTML =
|
|
'<span class="tracking-result">Shipment Info</span>';
|
|
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 = `<a href="imported_trf/${esc(val)}" target="_blank">File</a>`;
|
|
} else {
|
|
div.innerHTML = `<span>${esc(val)}</span>`;
|
|
}
|
|
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 `<select class="cell-input dropdown-select searchable-dropdown ${cls}${reqCls}" data-field-id="${col.fieldId}"${req}>${options}</select>`;
|
|
}
|
|
if (col.dataType === "Data") {
|
|
return `<input type="text" class="cell-input date-picker ${cls}${reqCls}" value="${v}"${req}>`;
|
|
}
|
|
if (col.dataType === "INT") {
|
|
return `<input type="number" class="cell-input ${cls}${reqCls}" value="${v}"${req}>`;
|
|
}
|
|
if (
|
|
col.autoValue === "import_time" ||
|
|
(meta.timeLabels || []).includes(col.label)
|
|
) {
|
|
return `<input type="time" class="cell-input ${cls}${reqCls}" value="${v}"${req}>`;
|
|
}
|
|
return `<input type="text" class="cell-input ${cls}${reqCls}" value="${v}"${req}>`;
|
|
}
|
|
|
|
function createFixedFieldHTML(col, value, rowIndex) {
|
|
if (col.dataType === "DATE") {
|
|
const reqCls = col.isRequired ? " required-input" : "";
|
|
const req = col.isRequired ? " required" : "";
|
|
return `<input type="text" class="cell-input date-picker manual-input${reqCls} fixed-input" data-fixed-key="${col.key}" value="${esc(value)}"${req}>`;
|
|
}
|
|
|
|
// 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 = '<option value="">Select...</option>';
|
|
if (value) {
|
|
const label = clientNameCache[value] || value;
|
|
opts += `<option value="${esc(String(value))}" selected>${esc(String(label))}</option>`;
|
|
}
|
|
return `<select class="cell-input manual-input fixed-input searchable-client api-fixed-select${reqCls}" data-fixed-key="${col.key}" data-current-value="${esc(value)}"${req}>${opts}</select>`;
|
|
}
|
|
|
|
// Select — build from cache
|
|
const isApiField = !!config;
|
|
const selectClass = isApiField ? "api-fixed-select" : "";
|
|
let options = '<option value="">Seleziona...</option>';
|
|
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 += `<option value="${item.id}"${sel}>${esc(item.text)}</option>`;
|
|
});
|
|
|
|
const reqCls = col.isRequired ? " required-input" : "";
|
|
const req = col.isRequired ? " required" : "";
|
|
return `<select class="cell-input manual-input fixed-input ${selectClass}${reqCls}" data-fixed-key="${col.key}" data-current-value="${esc(value)}"${req}>${options}</select>`;
|
|
}
|
|
|
|
function buildDropdownOptionsHTML(fieldId, selectedValue) {
|
|
let html = '<option value="">Seleziona...</option>';
|
|
if (selectedValue) {
|
|
const label =
|
|
dropdownNameCache[fieldId + "_" + selectedValue] ||
|
|
selectedValue;
|
|
html += `<option value="${esc(String(selectedValue))}" selected>${esc(String(label))}</option>`;
|
|
}
|
|
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 += `<button type="button" class="export-lims-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="${isExported ? "Already exported" : "Export to LIMS"}" style="background:${isExported ? "#ccc" : "#eb0b0b"}; color:white; border:none; border-radius:5px; cursor:${isExported ? "not-allowed" : "pointer"}; ${isExported ? "opacity:0.5;" : ""}" ${isExported ? "disabled" : ""}><i class="fas fa-upload"></i></button>`;
|
|
}
|
|
html += `<button type="button" class="save-btn action-btn" data-row="${rowIndex}" title="Save" style="background:#28a745; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-save"></i></button>`;
|
|
html += `<button type="button" class="photos-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Photos" style="background:#007bff; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-camera"></i></button>`;
|
|
html += `<button type="button" class="parts-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Parts" style="background:#ffc107; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-puzzle-piece"></i></button>`;
|
|
html += `<button type="button" class="delete-btn action-btn" data-row="${rowIndex}" data-iddatadb="${row.iddatadb}" title="Delete" style="background:#dc3545; color:white; border:none; border-radius:5px; cursor:pointer;"><i class="fas fa-trash"></i></button>`;
|
|
|
|
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("<br>");
|
|
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, "<br>");
|
|
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) + '<div class="resizer"></div>';
|
|
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 =
|
|
`<select class="custom-field dropdown-select client-select searchable-client" data-column="idclient" id="clientSelect"><option value="">Select a client...</option></select>` +
|
|
`<button type="button" class="propagate-btn" data-column="idclient" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
|
|
} else if (col.type === "cliente_fornitore_id") {
|
|
cell.innerHTML =
|
|
`<select class="custom-field dropdown-select client-select searchable-client" data-column="cliente_fornitore_id" id="clienteFornitoreSelect"><option value="">Select a supplier...</option></select>` +
|
|
`<button type="button" class="propagate-btn" data-column="cliente_fornitore_id" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
|
|
} else if (col.type === "fixed" && col.dataType === "DATE") {
|
|
cell.innerHTML =
|
|
`<input type="text" class="custom-field date-picker manual-input fixed-top" data-column="fixed_${col.key}" data-fixed-key="${col.key}" value="">` +
|
|
`<button type="button" class="propagate-btn" data-column="fixed_${col.key}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
|
|
} else if (col.type === "fixed") {
|
|
const isApiField = !!fixedFieldApiConfig[col.key];
|
|
if (isApiField) {
|
|
cell.innerHTML =
|
|
`<select class="custom-field dropdown-select api-fixed-select fixed-top" data-column="fixed_${col.key}" data-fixed-key="${col.key}"><option value="">Seleziona...</option></select>` +
|
|
`<button type="button" class="propagate-btn" data-column="fixed_${col.key}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
|
|
} else {
|
|
cell.innerHTML =
|
|
`<input type="number" class="custom-field manual-input fixed-top" data-column="fixed_${col.key}" data-fixed-key="${col.key}" value="">` +
|
|
`<button type="button" class="propagate-btn" data-column="fixed_${col.key}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
|
|
}
|
|
} else if (col.type === "detail" || col.type === "main_field") {
|
|
if (col.dataType === "SceltaMultipla") {
|
|
cell.innerHTML =
|
|
`<select class="custom-field dropdown-select" data-column="col_${colIdx}" data-field-id="${col.fieldId}"><option value="">Seleziona...</option></select>` +
|
|
`<button type="button" class="propagate-btn" data-column="col_${colIdx}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
|
|
} else if (col.dataType === "Data") {
|
|
cell.innerHTML =
|
|
`<input type="text" class="custom-field date-picker" data-column="col_${colIdx}" value="">` +
|
|
`<button type="button" class="propagate-btn" data-column="col_${colIdx}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
|
|
} else {
|
|
cell.innerHTML =
|
|
`<input type="text" class="custom-field" data-column="col_${colIdx}" value="">` +
|
|
`<button type="button" class="propagate-btn" data-column="col_${colIdx}" data-col-index="${colIdx}"><i class="fas fa-arrow-down"></i></button>`;
|
|
}
|
|
}
|
|
|
|
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 = '<option value="">Seleziona...</option>';
|
|
[...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 = '<option value="">Seleziona...</option>';
|
|
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,
|
|
);
|
|
} else if (colType === "tested_component") {
|
|
data[rowIndex].tested_component = value;
|
|
data[rowIndex]._dirty = true;
|
|
}
|
|
|
|
// Visual feedback
|
|
cell.classList.add("cell-changed");
|
|
updateDirtyIndicator();
|
|
});
|
|
|
|
rowContainer.addEventListener("input", function (e) {
|
|
const cell = e.target.closest(".grid-cell");
|
|
if (!cell || !cell.dataset.row) return;
|
|
|
|
const rowIndex = parseInt(cell.dataset.row, 10);
|
|
const colType = cell.dataset.colType;
|
|
|
|
if (colType === "tested_component") {
|
|
data[rowIndex].tested_component = e.target.value;
|
|
data[rowIndex]._dirty = true;
|
|
cell.classList.add("cell-changed");
|
|
updateDirtyIndicator();
|
|
}
|
|
});
|
|
|
|
// Persist tested_component before clicking +
|
|
document.addEventListener("mousedown", function (e) {
|
|
const btn = e.target.closest(".add-part-btn");
|
|
if (!btn) return;
|
|
|
|
const rowIndex = parseInt(btn.dataset.row, 10);
|
|
if (isNaN(rowIndex) || !data[rowIndex]) return;
|
|
|
|
const rowEl = btn.closest(".grid-row");
|
|
if (!rowEl) return;
|
|
|
|
const testedInput = rowEl.querySelector(".tested-component-input");
|
|
if (!testedInput) return;
|
|
|
|
data[rowIndex].tested_component = testedInput.value || "";
|
|
data[rowIndex]._dirty = true;
|
|
|
|
const cell = testedInput.closest(".grid-cell");
|
|
if (cell) 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 || "");
|
|
formData.append("tested_component", row.tested_component || "");
|
|
|
|
// 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();
|
|
},
|
|
};
|
|
})();
|