diff --git a/public/userarea/add_record.php b/public/userarea/add_record.php
new file mode 100644
index 0000000..80eb1e5
--- /dev/null
+++ b/public/userarea/add_record.php
@@ -0,0 +1,61 @@
+getConnection();
+
+ // Get default idclient from template
+ $stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
+ $stmt->execute([$templateId]);
+ $template = $stmt->fetch(PDO::FETCH_ASSOC);
+ $idclient = $template['idclient'] ?? null;
+
+ // Generate import reference code
+ $importReferenceCode = date('YmdHis') . '-' . uniqid();
+
+ // Insert empty record
+ $stmt = $pdo->prepare("
+ INSERT INTO datadb (templateid, user_id, status, idclient, importreferencecode, importdate)
+ VALUES (?, ?, 'i', ?, ?, NOW())
+ ");
+ $stmt->execute([$templateId, $userId, $idclient, $importReferenceCode]);
+ $iddatadb = (int)$pdo->lastInsertId();
+
+ // Create empty import_data_details for all mappings
+ $mappingStmt = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ?");
+ $mappingStmt->execute([$templateId]);
+ $mappings = $mappingStmt->fetchAll(PDO::FETCH_COLUMN);
+
+ if (!empty($mappings)) {
+ $insertStmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, '')");
+ foreach ($mappings as $mappingId) {
+ $insertStmt->execute([$iddatadb, $mappingId]);
+ }
+ }
+
+ // Get user name
+ $userStmt = $pdo->prepare("SELECT CONCAT(first_name, ' ', last_name) AS user_name FROM auth_users WHERE id = ?");
+ $userStmt->execute([$userId]);
+ $userName = $userStmt->fetchColumn() ?: '';
+
+ echo json_encode([
+ 'success' => true,
+ 'iddatadb' => $iddatadb,
+ 'importreferencecode' => $importReferenceCode,
+ 'user_name' => $userName,
+ ]);
+} catch (Exception $e) {
+ http_response_code(500);
+ echo json_encode(['success' => false, 'message' => $e->getMessage()]);
+}
diff --git a/public/userarea/exportLims_gridData.js b/public/userarea/exportLims_gridData.js
new file mode 100644
index 0000000..3c1730a
--- /dev/null
+++ b/public/userarea/exportLims_gridData.js
@@ -0,0 +1,555 @@
+/**
+ * exportLims_gridData.js — Export to LIMS using gridData (for imported.php)
+ *
+ * Replaces export_to_lims.js for pages that use gridData instead of DOM-rendered rows.
+ * Single export + batch export (Export All) with validation.
+ */
+(function () {
+ 'use strict';
+
+ let pendingConfirmHandler = null;
+ let batchRunning = false;
+ Object.defineProperty(window, "batchRunning", { get: () => batchRunning });
+
+ let batchCancelled = false;
+ let pendingBatchConfirmHandler = null;
+
+ // ── Helpers ──────────────────────────────────────────────────────────
+
+ function cleanupBackdrop() {
+ document.querySelectorAll(".modal-backdrop").forEach(b => b.remove());
+ document.body.classList.remove("modal-open");
+ document.body.style.paddingRight = "";
+ const overlay = document.querySelector(".overlay.toggle-icon");
+ if (overlay) overlay.style.display = "none";
+ }
+
+ function getGridRow(iddatadb) {
+ return document.querySelector(`.grid-row[data-id="${iddatadb}"]`);
+ }
+
+ function getRowIndexByIddatadb(iddatadb) {
+ return (window.gridData || []).findIndex(r => String(r.iddatadb) === String(iddatadb));
+ }
+
+ // ── Validation ──────────────────────────────────────────────────────
+
+ async function validateRows(rowsToValidate) {
+ const response = await fetch("validate_export.php", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ rows: rowsToValidate }),
+ });
+ if (!response.ok) throw new Error(`Validation HTTP error: ${response.status}`);
+ return response.json();
+ }
+
+ function clearValidationErrors() {
+ // Clear from gridData
+ (window.gridData || []).forEach(row => { delete row._validationErrors; delete row._exportError; });
+
+ document.querySelectorAll(".grid-cell.validation-error").forEach(cell => {
+ cell.classList.remove("validation-error");
+ cell.querySelectorAll(".input-validation-error").forEach(el => el.classList.remove("input-validation-error"));
+ const tooltip = cell.querySelector(".validation-tooltip");
+ if (tooltip) tooltip.remove();
+ });
+ document.querySelectorAll(".grid-row.validation-row-error").forEach(row => row.classList.remove("validation-row-error"));
+ clearAllRowErrors();
+ }
+
+ function showValidationErrors(gridRow, iddatadb, errors) {
+ // Store in gridData for re-render persistence
+ const idx = getRowIndexByIddatadb(iddatadb);
+ if (idx >= 0) window.gridData[idx]._validationErrors = errors;
+
+ if (!gridRow) return;
+ gridRow.classList.add("validation-row-error");
+ const messages = [];
+
+ errors.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 = gridRow.querySelector(`.grid-cell[data-index="${targetIndex}"]`);
+ }
+ } else {
+ cell = gridRow.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;
+ }
+ });
+
+ showRowError(gridRow, iddatadb, messages.join("\n"));
+ }
+
+ // ── Send export ─────────────────────────────────────────────────────
+
+ async function sendExport(iddatadb, batchUuid) {
+ const formData = new FormData();
+ formData.append("iddatadb", iddatadb);
+ if (batchUuid) formData.append("batch_uuid", batchUuid);
+
+ const response = await fetch("export_to_lims.php", { method: "POST", body: formData });
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+ const data = await response.json();
+
+ if (data.success) {
+ // Update gridData
+ const idx = getRowIndexByIddatadb(iddatadb);
+ if (idx >= 0) {
+ window.gridData[idx].status = 'l';
+ window.gridData[idx].commessaweb = data.commessaweb;
+ }
+
+ // Update visible DOM row
+ const gridRow = getGridRow(iddatadb);
+ if (gridRow) {
+ const statusBadge = gridRow.querySelector('.grid-cell[data-col="status"] .status-badge');
+ if (statusBadge) {
+ statusBadge.classList.remove("status-i", "status-P");
+ statusBadge.classList.add("status-l");
+ statusBadge.textContent = "To LIMS";
+ }
+ const statusCell = gridRow.querySelector('.grid-cell[data-col="status"]');
+ if (statusCell && data.commessaweb) {
+ let cwSpan = statusCell.querySelector(".commessaweb-code");
+ if (!cwSpan) {
+ cwSpan = document.createElement("span");
+ cwSpan.className = "commessaweb-code";
+ cwSpan.style.cssText = "display:block; font-size:0.75em; color:#555; margin-top:2px;";
+ statusCell.appendChild(cwSpan);
+ }
+ cwSpan.textContent = data.commessaweb;
+ }
+ const exportBtn = gridRow.querySelector(".export-lims-btn");
+ if (exportBtn) {
+ exportBtn.disabled = true;
+ exportBtn.style.background = "#ccc";
+ exportBtn.style.cursor = "not-allowed";
+ exportBtn.style.opacity = "0.5";
+ }
+ }
+ }
+
+ return data;
+ }
+
+ // ── Show result modal ───────────────────────────────────────────────
+
+ function showExportResult(data) {
+ const el = document.getElementById("exportResponseModal");
+ if (!el) return;
+ const modal = new bootstrap.Modal(el, { keyboard: false });
+ const msg = document.getElementById("exportResponseMessage");
+ const label = document.getElementById("exportResponseModalLabel");
+
+ if (data.success) {
+ msg.innerHTML = `${data.message.replace(/\n/g, "
")}` +
+ `
ID CommessaWeb: ${data.idcommessaweb}` +
+ `
Codice CommessaWeb: ${data.commessaweb}` +
+ (data.totalPhotos > 0 ? `
Foto: ${data.totalPhotos}` : "");
+ label.textContent = "Esportazione Completata";
+ } else {
+ msg.textContent = `Errore: ${data.message}`;
+ label.textContent = "Errore Esportazione";
+ }
+ modal.show();
+ el.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
+ }
+
+ // ── Row UI helpers ──────────────────────────────────────────────────
+
+ function setRowExporting(row, active) {
+ if (!row) return;
+ const btnCell = row.querySelector(".button-cell");
+ if (active) {
+ row.classList.remove("batch-disabled");
+ row.classList.add("batch-exporting");
+ if (btnCell) {
+ btnCell.querySelectorAll(".action-btn").forEach(b => { b.dataset.prevDisplay = b.style.display; b.style.display = "none"; });
+ const spinner = document.createElement("span");
+ spinner.className = "batch-row-spinner";
+ spinner.innerHTML = ' Exporting...';
+ btnCell.appendChild(spinner);
+ }
+ } else {
+ row.classList.remove("batch-exporting");
+ if (btnCell) {
+ const spinner = btnCell.querySelector(".batch-row-spinner");
+ if (spinner) spinner.remove();
+ btnCell.querySelectorAll(".action-btn").forEach(b => { b.style.display = b.dataset.prevDisplay || ""; delete b.dataset.prevDisplay; });
+ }
+ }
+ }
+
+ function showRowError(row, iddatadb, message) {
+ if (!row) return;
+ row.classList.add("batch-row-error");
+ const btnCell = row.querySelector(".button-cell");
+ if (btnCell) {
+ const old = btnCell.querySelector(".batch-error-msg");
+ if (old) old.remove();
+ const errorEl = document.createElement("div");
+ errorEl.className = "batch-error-msg";
+ errorEl.textContent = "Warning — click for details";
+ errorEl.addEventListener("click", () => {
+ document.getElementById("exportResponseMessage").innerHTML = message.replace(/\n/g, "
");
+ document.getElementById("exportResponseModalLabel").textContent = "Error (id: " + iddatadb + ")";
+ new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
+ });
+ btnCell.appendChild(errorEl);
+ }
+ }
+
+ function clearAllRowErrors() {
+ document.querySelectorAll(".grid-row.batch-row-error").forEach(row => {
+ row.classList.remove("batch-row-error");
+ const msg = row.querySelector(".batch-error-msg");
+ if (msg) msg.remove();
+ });
+ }
+
+ function disableAllRowButtons() {
+ document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.add("batch-disabled"));
+ const toggle = document.querySelector(".actions-dropdown .dropdown-toggle");
+ if (toggle) { toggle.disabled = true; toggle.style.opacity = "0.5"; toggle.style.pointerEvents = "none"; }
+ }
+
+ function enableAllRowButtons() {
+ document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.remove("batch-disabled"));
+ const toggle = document.querySelector(".actions-dropdown .dropdown-toggle");
+ if (toggle) { toggle.disabled = false; toggle.style.opacity = ""; toggle.style.pointerEvents = ""; }
+ }
+
+ // ── Single row export: validate → confirm → send ────────────────────
+
+ function startExportConfirmFlow(iddatadb, rowIndex) {
+ const gridRow = getGridRow(iddatadb);
+ clearValidationErrors();
+
+ if (gridRow) {
+ setRowExporting(gridRow, true);
+ const spinner = gridRow.querySelector(".batch-row-spinner");
+ if (spinner) spinner.innerHTML = ' Validating...';
+ }
+
+ validateRows([{ iddatadb: parseInt(iddatadb), index: rowIndex }])
+ .then(validationData => {
+ if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
+
+ if (!validationData.success) {
+ showExportResult({ success: false, message: validationData.message || "Validation error" });
+ return;
+ }
+ const result = validationData.results[rowIndex];
+ if (result && !result.valid) {
+ if (gridRow) showValidationErrors(gridRow, iddatadb, result.errors);
+ return;
+ }
+ showConfirmAndExport(iddatadb, rowIndex);
+ })
+ .catch(error => {
+ if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
+ showExportResult({ success: false, message: "Validation error: " + error.message });
+ });
+ }
+
+ function showConfirmAndExport(iddatadb, rowIndex) {
+ const confirmModalElement = document.getElementById("exportConfirmModal");
+ if (!confirmModalElement) return;
+
+ const confirmModal = new bootstrap.Modal(confirmModalElement, { keyboard: false });
+ document.getElementById("exportIddatadb").textContent = iddatadb;
+ confirmModal.show();
+
+ const confirmBtn = document.getElementById("exportConfirmBtn");
+ if (!confirmBtn) { confirmModal.hide(); return; }
+
+ const confirmHandler = async () => {
+ pendingConfirmHandler = null;
+ confirmModal.hide();
+
+ const gridRow = getGridRow(iddatadb);
+ if (gridRow) setRowExporting(gridRow, true);
+
+ try {
+ const data = await sendExport(iddatadb);
+ if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
+ if (!data.success) showRowError(gridRow, iddatadb, data.message || "Unknown error");
+ showExportResult(data);
+ } catch (error) {
+ if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
+ showRowError(gridRow, iddatadb, error.message);
+ showExportResult({ success: false, message: error.message });
+ }
+ };
+
+ if (pendingConfirmHandler) confirmBtn.removeEventListener("click", pendingConfirmHandler);
+ pendingConfirmHandler = confirmHandler;
+ confirmBtn.addEventListener("click", confirmHandler, { once: true });
+ }
+
+ // ── Single row click (event delegation) ─────────────────────────────
+
+ $(document).on('click', '.export-lims-btn', function (e) {
+ e.preventDefault();
+ if (batchRunning) return;
+
+ const iddatadb = this.dataset.iddatadb;
+ const rowIndex = parseInt(this.dataset.row);
+ const gridRow = getGridRow(iddatadb);
+
+ // Check unsaved changes for this row
+ const dataRow = window.gridData?.[rowIndex];
+ if (dataRow && dataRow._dirty) {
+ const unsavedModal = new bootstrap.Modal(document.getElementById("exportUnsavedModal"), { keyboard: false });
+ unsavedModal.show();
+
+ document.getElementById("saveAndExportBtn")?.addEventListener("click", () => {
+ unsavedModal.hide();
+ // Save first, then export
+ const formData = window.buildSavePayload(rowIndex);
+ fetch('save_edited_row.php', { method: 'POST', body: formData })
+ .then(r => r.json())
+ .then(result => {
+ if (result.success) {
+ dataRow._dirty = false;
+ startExportConfirmFlow(iddatadb, rowIndex);
+ } else {
+ alert('Save failed: ' + result.message);
+ }
+ });
+ }, { once: true });
+ return;
+ }
+
+ startExportConfirmFlow(iddatadb, rowIndex);
+ });
+
+ // ── Batch export (Export All) ───────────────────────────────────────
+
+ function collectEligibleRows() {
+ // Read from gridData, not DOM
+ const eligible = [];
+ (window.gridData || []).forEach((row, index) => {
+ if (row.status !== 'l') {
+ eligible.push({ iddatadb: row.iddatadb, index, row: getGridRow(row.iddatadb) });
+ }
+ });
+ return eligible;
+ }
+
+ function hasUnsavedChanges() {
+ return (window.gridData || []).some(r => r._dirty);
+ }
+
+ async function validateAndFilter(eligibleRows) {
+ const rowsToValidate = eligibleRows.map(({ iddatadb, index }) => ({
+ iddatadb: parseInt(iddatadb),
+ index,
+ }));
+ const validationData = await validateRows(rowsToValidate);
+ if (!validationData.success) throw new Error(validationData.message || "Validation error");
+
+ const validRows = [];
+ let invalidCount = 0;
+
+ for (const item of eligibleRows) {
+ const result = validationData.results[item.index];
+ if (result && !result.valid) {
+ if (item.row) showValidationErrors(item.row, item.iddatadb, result.errors);
+ invalidCount++;
+ } else {
+ validRows.push(item);
+ }
+ }
+ return { validRows, invalidCount };
+ }
+
+ function showValidationSpinner(show) {
+ const bar = document.getElementById("batchExportBar");
+ const statusEl = document.getElementById("batchExportStatus");
+ const cancelBtn = document.getElementById("exportBatchCancelBtn");
+ if (show) {
+ if (bar) bar.style.display = "";
+ if (statusEl) statusEl.textContent = "Validating...";
+ if (cancelBtn) cancelBtn.style.display = "none";
+ } else {
+ if (bar) bar.style.display = "none";
+ if (cancelBtn) cancelBtn.style.display = "";
+ }
+ }
+
+ function showBatchConfirm(eligibleRows) {
+ clearValidationErrors();
+ showValidationSpinner(true);
+
+ validateAndFilter(eligibleRows)
+ .then(({ validRows, invalidCount }) => {
+ showValidationSpinner(false);
+ if (validRows.length === 0) {
+ document.getElementById("exportResponseMessage").innerHTML =
+ `No valid rows for export.
${invalidCount} rows with validation errors.`;
+ document.getElementById("exportResponseModalLabel").textContent = "Validation Failed";
+ new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
+ return;
+ }
+
+ const confirmModal = new bootstrap.Modal(document.getElementById("exportBatchConfirmModal"), { keyboard: false });
+ let countText = String(validRows.length);
+ if (invalidCount > 0) countText += ` (${invalidCount} excluded due to errors)`;
+ document.getElementById("exportBatchCount").textContent = countText;
+ confirmModal.show();
+
+ const confirmBtn = document.getElementById("exportBatchConfirmBtn");
+ if (pendingBatchConfirmHandler) confirmBtn.removeEventListener("click", pendingBatchConfirmHandler);
+ pendingBatchConfirmHandler = () => {
+ pendingBatchConfirmHandler = null;
+ confirmModal.hide();
+ startBatchExport(validRows);
+ };
+ confirmBtn.addEventListener("click", pendingBatchConfirmHandler, { once: true });
+ })
+ .catch(error => {
+ showValidationSpinner(false);
+ document.getElementById("exportResponseMessage").textContent = "Validation error: " + error.message;
+ document.getElementById("exportResponseModalLabel").textContent = "Validation Error";
+ new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
+ });
+ }
+
+ $(document).on('click', '.export-all-lims-btn', function (e) {
+ e.preventDefault();
+ if (batchRunning) return;
+
+ if (hasUnsavedChanges()) {
+ const unsavedModal = new bootstrap.Modal(document.getElementById("exportBatchUnsavedModal"), { keyboard: false });
+ unsavedModal.show();
+ document.getElementById("batchSaveAndExportBtn")?.addEventListener("click", () => {
+ unsavedModal.hide();
+ // Trigger save all first — listen for completion
+ alert("Please Save All first, then Export All.");
+ }, { once: true });
+ return;
+ }
+
+ const eligibleRows = collectEligibleRows();
+ if (eligibleRows.length === 0) {
+ document.getElementById("exportResponseMessage").textContent = "All rows already exported to LIMS.";
+ document.getElementById("exportResponseModalLabel").textContent = "Export All";
+ new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
+ return;
+ }
+ showBatchConfirm(eligibleRows);
+ });
+
+ function startBatchExport(eligibleRows) {
+ batchCancelled = false;
+ batchRunning = true;
+ const batchUuid = crypto.randomUUID();
+ const total = eligibleRows.length;
+ let processed = 0, succeeded = 0, failed = 0;
+
+ disableAllRowButtons();
+
+ const bar = document.getElementById("batchExportBar");
+ const statusEl = document.getElementById("batchExportStatus");
+ const cancelBtn = document.getElementById("exportBatchCancelBtn");
+ if (bar) bar.style.display = "";
+ if (cancelBtn) cancelBtn.disabled = false;
+ if (statusEl) statusEl.textContent = `Exporting 0 / ${total}...`;
+
+ cancelBtn?.addEventListener("click", () => {
+ batchCancelled = true;
+ if (statusEl) statusEl.textContent = "Cancelling... (waiting for current row)";
+ if (cancelBtn) cancelBtn.disabled = true;
+ }, { once: true });
+
+ (async () => {
+ for (let i = 0; i < eligibleRows.length; i++) {
+ if (batchCancelled) break;
+
+ const { iddatadb, row } = eligibleRows[i];
+ if (statusEl) statusEl.textContent = `Exporting ${processed + 1} / ${total} (id: ${iddatadb})...`;
+
+ const gridRow = row || getGridRow(iddatadb);
+ if (gridRow) setRowExporting(gridRow, true);
+
+ try {
+ const data = await sendExport(iddatadb, batchUuid);
+ processed++;
+ if (data.success) {
+ succeeded++;
+ } else {
+ failed++;
+ const errIdx = getRowIndexByIddatadb(iddatadb);
+ if (errIdx >= 0) window.gridData[errIdx]._exportError = data.message || "Unknown error";
+ if (gridRow) showRowError(gridRow, iddatadb, data.message || "Unknown error");
+ }
+ } catch (error) {
+ processed++;
+ failed++;
+ const errIdx = getRowIndexByIddatadb(iddatadb);
+ if (errIdx >= 0) window.gridData[errIdx]._exportError = error.message;
+ if (gridRow) showRowError(gridRow, iddatadb, error.message);
+ }
+
+ if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); }
+ }
+
+ batchRunning = false;
+ enableAllRowButtons();
+ if (bar) bar.style.display = "none";
+
+ // Re-render to reflect status changes, then restore error indicators
+ const gr = window.gridRenderer;
+ if (gr) gr.renderVisibleRows();
+
+ // Restore error state from gridData._exportError
+ (window.gridData || []).forEach((row, idx) => {
+ if (row._exportError) {
+ const gridRow = getGridRow(row.iddatadb);
+ if (gridRow) showRowError(gridRow, row.iddatadb, row._exportError);
+ }
+ });
+
+ const msgEl = document.getElementById("exportResponseMessage");
+ const labelEl = document.getElementById("exportResponseModalLabel");
+
+ if (batchCancelled) {
+ labelEl.textContent = "Export All — Cancelled";
+ msgEl.innerHTML = `Exported: ${succeeded}
Errors: ${failed}
Not processed: ${total - processed}`;
+ } else if (failed === 0) {
+ labelEl.textContent = "Export All — Complete";
+ msgEl.innerHTML = `All ${succeeded} rows exported successfully.`;
+ } else {
+ labelEl.textContent = "Export All — Completed with errors";
+ msgEl.innerHTML = `Exported: ${succeeded}
Errors: ${failed}`;
+ }
+
+ const modalEl = document.getElementById("exportResponseModal");
+ new bootstrap.Modal(modalEl, { keyboard: false }).show();
+ modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
+ })();
+ }
+})();
diff --git a/public/userarea/gridRenderer.js b/public/userarea/gridRenderer.js
new file mode 100644
index 0000000..4416509
--- /dev/null
+++ b/public/userarea/gridRenderer.js
@@ -0,0 +1,894 @@
+/**
+ * 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}]
+ 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 || [];
+ const 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 loading ────────────────────────────────────────────────
+
+ function formatClientLabel(client) {
+ const nome = client.Nominativo || '';
+ const id = client.IdCliente || '';
+ const code = (client.CodiceCliente || '').toString().trim();
+ const suffix = (code.split('_')[1] || '').trim();
+ const short = suffix || (code ? code.charAt(0) : '--');
+ return `${nome.trim()} - ${short} (ID: ${id})`;
+ }
+
+ function buildClientOptionsHTML(selectedId) {
+ let html = '';
+ clientData.forEach(c => {
+ const id = c.IdCliente || '';
+ const sel = String(id) === String(selectedId) ? ' selected' : '';
+ html += ``;
+ });
+ return html;
+ }
+
+ async function loadClientData() {
+ if (clientData.length > 0) return;
+ try {
+ const resp = await fetch('get_clienti.php');
+ const json = await resp.json();
+ clientData = json.value || [];
+ } catch (e) {
+ console.error('Failed to load clients:', e);
+ }
+ }
+
+ // ── 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
+ if (config.source === 'clients') {
+ await loadClientData();
+ const results = clientData.map(c => ({ id: c.IdCliente, text: formatClientLabel(c) }));
+ results.sort((a, b) => String(a.text).localeCompare(String(b.text), 'it', { sensitivity: 'base' }));
+ fixedFieldCache[fieldKey] = results;
+ return results;
+ }
+
+ 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 ─────────────────────────────────
+
+ async function loadDropdownOptions(fieldIds) {
+ const missing = fieldIds.filter(id => !dropdownOptionsCache[id]);
+ if (missing.length > 0) {
+ try {
+ const resp = await fetch('get_customfield_values.php?field_ids=' + missing.join(','));
+ const json = await resp.json();
+ // API returns { fieldId: [values] } directly (no success/data wrapper)
+ const entries = json.data ? json.data : json;
+ for (const [fid, values] of Object.entries(entries)) {
+ if (Array.isArray(values)) {
+ const sorted = values.sort((a, b) =>
+ String(a.Valore || '').localeCompare(String(b.Valore || ''), 'it', { sensitivity: 'base' })
+ );
+ dropdownOptionsCache[fid] = sorted;
+ }
+ }
+ } catch (e) {
+ console.error('Failed to load dropdown options:', e);
+ }
+ }
+ }
+
+ // ── 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. Custom field dropdowns
+ const fieldIds = columns
+ .filter(c => c.type === 'detail' && c.dataType === 'SceltaMultipla' && c.fieldId)
+ .map(c => String(c.fieldId));
+ const uniqueIds = [...new Set(fieldIds)];
+ if (uniqueIds.length > 0) {
+ await loadDropdownOptions(uniqueIds);
+ }
+
+ 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 ``;
+ }
+
+ // Select — build from cache
+ const isApiField = !!fixedFieldApiConfig[col.key];
+ const selectClass = isApiField ? 'api-fixed-select' : '';
+ let options = '';
+ const cacheKey = fixedFieldApiConfig[col.key]?.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 = '';
+ const items = dropdownOptionsCache[fieldId] || [];
+ items.forEach(item => {
+ const sel = String(item.IdCustomFieldsValue) === String(selectedValue) ? ' selected' : '';
+ 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;
+
+ // 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++;
+ });
+ }
+
+ 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
+ const clientSel = document.getElementById('clientSelect');
+ if (clientSel) {
+ clientSel.innerHTML = buildClientOptionsHTML(meta.defaultIdclient);
+ $(clientSel).select2({ placeholder: 'Select a client...', allowClear: true, width: '100%', minimumInputLength: 1 });
+ }
+ const fornitSel = document.getElementById('clienteFornitoreSelect');
+ if (fornitSel) {
+ fornitSel.innerHTML = buildClientOptionsHTML('');
+ $(fornitSel).select2({ placeholder: 'Select a supplier...', allowClear: true, width: '100%', minimumInputLength: 1 });
+ }
+
+ // Fixed field selects in top row
+ topContainer.querySelectorAll('.api-fixed-select').forEach(sel => {
+ const fieldKey = sel.dataset.fixedKey;
+ const config = fixedFieldApiConfig[fieldKey];
+
+ 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
+ topContainer.querySelectorAll('.dropdown-select[data-field-id]').forEach(sel => {
+ const fieldId = sel.dataset.fieldId;
+ const items = dropdownOptionsCache[fieldId] || [];
+ sel.innerHTML = '';
+ items.forEach(item => {
+ sel.add(new Option(item.Valore, item.IdCustomFieldsValue));
+ });
+ });
+
+ // 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;
+
+ 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() || '';
+
+ 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();
+ });
+
+ // 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({
+ placeholder: 'Select a client...',
+ allowClear: true,
+ width: '100%',
+ dropdownCssClass: 'select2-dropdown-smaller',
+ minimumInputLength: 1
+ });
+ });
+ $(this).find('select[data-field-id]:not(.select2-hidden-accessible)').each(function () {
+ if ((this.options || []).length > 12) {
+ $(this).select2({ placeholder: 'Seleziona...', allowClear: true, width: '100%' });
+ }
+ });
+ });
+ }
+
+ // ── 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();
+
+ // 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(); },
+ };
+
+})();
diff --git a/public/userarea/import_edit2.php b/public/userarea/import_edit2.php
index 0409654..490e41e 100644
--- a/public/userarea/import_edit2.php
+++ b/public/userarea/import_edit2.php
@@ -340,6 +340,7 @@ function fixedDefaultValue(array $f): string
align-items: center;
padding: 0;
border-bottom: 1px solid #dee2e6;
+ min-width: fit-content;
}
.grid-row:last-child {
@@ -890,9 +891,8 @@ function fixedDefaultValue(array $f): string
-
+
-
+
@@ -55,7 +55,7 @@
|
-
+
|
@@ -63,9 +63,9 @@
-
-
-
+
+
+
|
@@ -188,6 +188,14 @@
z-index: 1055 !important
}
+ /* Fix: app.css sets .btn i { margin-top:-1em; margin-bottom:-1em; font-size:1.3rem } which collapses small icon buttons */
+ #partsModal .btn i {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ font-size: 0.8rem !important;
+ margin-right: 2px !important;
+ }
+
/* Tabelle */
#partsTable tr {
display: table-row !important
diff --git a/public/userarea/modals_gridData.js b/public/userarea/modals_gridData.js
new file mode 100644
index 0000000..08e7d94
--- /dev/null
+++ b/public/userarea/modals_gridData.js
@@ -0,0 +1,208 @@
+/**
+ * modals_gridData.js — Photos, Parts, Tested Component handlers for gridData pages
+ */
+(function () {
+ 'use strict';
+
+ // ── Photos — use photos.js loadPopupContent (exported to window) ──
+ $(document).on('click', '.photos-btn', function () {
+ const iddatadb = $(this).data('iddatadb') || null;
+ const idquotations = $(this).data('idquotations') || null;
+ const modal = document.getElementById('photosModal');
+ if (!modal) return;
+
+ modal.style.display = 'block';
+
+ if (typeof window.loadPopupContent === 'function') {
+ window.loadPopupContent(iddatadb, idquotations);
+ }
+ });
+
+ // Close photos modal
+ $(document).on('click', '.close-btn', function () {
+ const modal = document.getElementById('photosModal');
+ if (modal) modal.style.display = 'none';
+ });
+
+ // Close on backdrop click
+ $(document).on('click', '#photosModal', function (e) {
+ if (e.target === this) {
+ this.style.display = 'none';
+ }
+ });
+
+ // ── Parts (matching import_edit2.php behavior) ─────────────────────
+ $(document).on('click', '.parts-btn', function () {
+ const iddatadb = $(this).data('iddatadb') || null;
+ const idquotations = $(this).data('idquotations') || null;
+
+ $.ajax({
+ url: 'modal_partsTable.php',
+ method: 'GET',
+ data: { iddatadb: iddatadb },
+ success: function (response) {
+ $('#partsModalContainer').html(response);
+ const modalElement = document.getElementById('partsModal');
+ if (!modalElement) return;
+
+ $("#trfHeader").text(iddatadb || idquotations || '');
+ $("#partsModal").data("iddatadb", iddatadb).data("idquotations", idquotations);
+
+ let modal = bootstrap.Modal.getInstance(modalElement);
+ if (!modal) modal = new bootstrap.Modal(modalElement, { backdrop: true, keyboard: true, focus: true });
+ modal.show();
+
+ if (typeof window.loadParts === 'function') {
+ window.loadParts(iddatadb, idquotations);
+ }
+ },
+ error: function (xhr, status, error) {
+ console.error('Error loading parts:', error);
+ }
+ });
+ });
+
+ $(document).on('hidden.bs.modal', '#partsModal', function () {
+ const modalElement = document.getElementById('partsModal');
+ if (modalElement) {
+ const modal = bootstrap.Modal.getInstance(modalElement);
+ if (modal) modal.dispose();
+ }
+ $('#partsModalContainer').empty();
+ $('.modal-backdrop').remove();
+ $('body').removeClass('modal-open').css('padding-right', '');
+ });
+
+ // ── Tested Component quick add ───────────────────────────────────────
+ $(document).on('click', '.add-part-btn', async function () {
+ const iddatadb = $(this).data('iddatadb') || null;
+ const rowIndex = parseInt($(this).data('row'));
+ const row = window.gridData?.[rowIndex];
+ const id = iddatadb || (row ? row.iddatadb : null);
+ if (!id) return;
+
+ const $cell = $(this).closest('.grid-cell, div');
+ const $input = $cell.find('input');
+ const raw = ($input.val() || '').trim();
+ const parts = raw.split('|').map(s => s.trim()).filter(s => s.length > 0);
+ const uniqueParts = [...new Set(parts)];
+
+ if (!uniqueParts.length) {
+ alert('Insert a description first.');
+ $input.focus();
+ return;
+ }
+
+ try {
+ for (const p of uniqueParts) {
+ const formData = new FormData();
+ formData.append('iddatadb', id);
+ formData.append('part_description', p);
+ await fetch('add_part_quick.php', { method: 'POST', body: formData });
+ }
+ alert(`Added ${uniqueParts.length} part(s).`);
+ $input.val('');
+ } catch (e) {
+ alert('Error: ' + e.message);
+ }
+ });
+
+ // ── Delete row ───────────────────────────────────────────────────────
+ let deleteIddatadb = null;
+ let deleteRowIndex = null;
+
+ $(document).on('click', '.delete-btn', function () {
+ deleteIddatadb = $(this).data('iddatadb');
+ deleteRowIndex = parseInt($(this).data('row'));
+ const modalEl = document.getElementById('deleteConfirmModal');
+ if (modalEl) {
+ document.getElementById('deleteIddatadbText').textContent = deleteIddatadb;
+ new bootstrap.Modal(modalEl).show();
+ }
+ });
+
+ $(document).on('click', '#deleteConfirmBtn', async function () {
+ const modalEl = document.getElementById('deleteConfirmModal');
+ const modal = bootstrap.Modal.getInstance(modalEl);
+ if (modal) modal.hide();
+
+ if (!deleteIddatadb) return;
+
+ try {
+ const resp = await fetch('delete_record.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id: deleteIddatadb })
+ });
+ const result = await resp.json();
+
+ if (result.success) {
+ // Remove from gridData
+ const idx = window.gridData.findIndex(r => r.iddatadb === deleteIddatadb);
+ if (idx >= 0) window.gridData.splice(idx, 1);
+
+ // Re-render
+ const gr = window.gridRenderer;
+ if (gr) gr.renderVisibleRows();
+ } else {
+ alert('Error: ' + result.message);
+ }
+ } catch (e) {
+ alert('Error: ' + e.message);
+ }
+
+ deleteIddatadb = null;
+ deleteRowIndex = null;
+ });
+
+ // ── Add new row ──────────────────────────────────────────────────────
+ $(document).on('click', '#addRowBtn', async function () {
+ const btn = this;
+ btn.disabled = true;
+
+ try {
+ const templateId = window.gridMeta?.templateId;
+ if (!templateId) { alert('Template ID missing'); return; }
+
+ const resp = await fetch('add_record.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ template_id: templateId })
+ });
+ const result = await resp.json();
+
+ if (result.success && result.iddatadb) {
+ // Build new row object
+ const newRow = {
+ iddatadb: result.iddatadb,
+ status: 'i',
+ idclient: window.gridMeta?.defaultIdclient || '',
+ cliente_fornitore_id: null,
+ commessaweb: null,
+ user_name: result.user_name || '',
+ importreferencecode: result.importreferencecode || '',
+ filename_import: '',
+ importdate: new Date().toISOString().slice(0, 19).replace('T', ' '),
+ fixedFields: {},
+ details: {},
+ mainFieldValue: '',
+ _dirty: true,
+ };
+
+ // Add to beginning of gridData
+ window.gridData.unshift(newRow);
+
+ // Re-render
+ const gr = window.gridRenderer;
+ if (gr) gr.renderVisibleRows();
+ } else {
+ alert('Error: ' + (result.message || 'Unknown error'));
+ }
+ } catch (e) {
+ alert('Error: ' + e.message);
+ } finally {
+ btn.disabled = false;
+ }
+ });
+
+})();
diff --git a/public/userarea/partsTable.js b/public/userarea/partsTable.js
index c11e2df..9d5ac7d 100644
--- a/public/userarea/partsTable.js
+++ b/public/userarea/partsTable.js
@@ -941,7 +941,7 @@ $(document).ready(function () {
|
-
+
|
@@ -950,9 +950,9 @@ $(document).ready(function () {
-
-
-
+
+
+
|
`;
$("#partsTableBody").append(newRow);
@@ -1438,7 +1438,7 @@ $(document).ready(function () {
|
-
+
|
@@ -1447,9 +1447,9 @@ $(document).ready(function () {
-
-
-
+
+
+
|
`;
$("#partsTableBody").append(newRow);
diff --git a/public/userarea/photos.js b/public/userarea/photos.js
index 12c73e7..f41f73e 100644
--- a/public/userarea/photos.js
+++ b/public/userarea/photos.js
@@ -1,5 +1,5 @@
document.addEventListener("DOMContentLoaded", function () {
- // Funzione per caricare il contenuto del popup
+ // Funzione per caricare il contenuto del popup (exported for external use)
async function loadPopupContent(iddatadb, idquotations) {
const popupContent = document.getElementById("popupContent");
if (!popupContent) {
@@ -1218,4 +1218,7 @@ document.addEventListener("DOMContentLoaded", function () {
closeBtn,
});
}
+
+ // Export for external use (gridData pages)
+ window.loadPopupContent = loadPopupContent;
});
diff --git a/public/userarea/saveAll.js b/public/userarea/saveAll.js
new file mode 100644
index 0000000..eee5dba
--- /dev/null
+++ b/public/userarea/saveAll.js
@@ -0,0 +1,128 @@
+/**
+ * saveAll.js — Save All functionality using gridData
+ */
+(function() {
+ 'use strict';
+
+ let saveAllRunning = false;
+
+ function isBusy() {
+ return saveAllRunning || window.batchRunning;
+ }
+
+ // ── Save single row ──────────────────────────────────────────────────
+ $(document).on('click', '.save-btn', async function() {
+ const btn = this;
+ const rowIndex = parseInt(btn.dataset.row);
+ const row = window.gridData?.[rowIndex];
+ if (!row) return;
+
+ const origHtml = btn.innerHTML;
+ btn.innerHTML = '';
+ btn.disabled = true;
+
+ try {
+ const formData = window.buildSavePayload(rowIndex);
+ const resp = await fetch('save_edited_row.php', { method: 'POST', body: formData });
+ const result = await resp.json();
+
+ if (result.success) {
+ row._dirty = false;
+ // Flash success on row without re-rendering (preserves Select2 state)
+ const gridRow = document.querySelector(`.grid-row[data-id="${row.iddatadb}"]`);
+ if (gridRow) {
+ gridRow.classList.remove('row-dirty');
+ gridRow.querySelectorAll('.grid-cell').forEach(cell => {
+ cell.classList.remove('cell-changed');
+ cell.classList.add('flash-success');
+ });
+ setTimeout(() => gridRow.querySelectorAll('.flash-success').forEach(c => c.classList.remove('flash-success')), 500);
+ }
+ const toastEl = document.getElementById('saveSuccessToast');
+ if (toastEl) bootstrap.Toast.getOrCreateInstance(toastEl).show();
+ } else {
+ alert('Errore: ' + result.message);
+ }
+ } catch (e) {
+ alert('Errore: ' + e.message);
+ } finally {
+ btn.innerHTML = origHtml;
+ btn.disabled = false;
+ }
+ });
+
+ // ── Save All ─────────────────────────────────────────────────────────
+ $(document).on('click', '.save-all-btn', function(e) {
+ e.preventDefault();
+ if (isBusy()) return;
+ const modalEl = document.getElementById('saveAllConfirmModal');
+ if (!modalEl) return;
+ new bootstrap.Modal(modalEl, { keyboard: false }).show();
+ });
+
+ $(document).on('click', '#saveAllConfirmBtn', async function() {
+ const confirmModal = bootstrap.Modal.getInstance(document.getElementById('saveAllConfirmModal'));
+ if (confirmModal) confirmModal.hide();
+ saveAllRunning = true;
+
+ const bar = document.getElementById('batchExportBar');
+ const statusEl = document.getElementById('batchExportStatus');
+ const cancelBtn = document.getElementById('exportBatchCancelBtn');
+ if (bar) bar.style.display = '';
+ if (cancelBtn) cancelBtn.style.display = 'none';
+ if (statusEl) statusEl.textContent = 'Saving...';
+
+ const data = window.gridData || [];
+ const dirtyRows = data.map((r, i) => r._dirty ? i : -1).filter(i => i >= 0);
+
+ if (dirtyRows.length === 0) {
+ saveAllRunning = false;
+ if (bar) bar.style.display = 'none';
+ const msgEl = document.getElementById('saveAllResultMessage');
+ if (msgEl) msgEl.textContent = 'No changes to save.';
+ new bootstrap.Modal(document.getElementById('saveAllResultModal')).show();
+ return;
+ }
+
+ let success = 0, fail = 0;
+
+ for (const idx of dirtyRows) {
+ if (statusEl) statusEl.textContent = `Saving ${success + fail + 1} / ${dirtyRows.length}...`;
+ try {
+ const formData = window.buildSavePayload(idx);
+ const resp = await fetch('save_edited_row.php', { method: 'POST', body: formData });
+ const result = await resp.json();
+ if (result.success) {
+ data[idx]._dirty = false;
+ success++;
+ } else {
+ fail++;
+ }
+ } catch (e) {
+ fail++;
+ }
+ }
+
+ saveAllRunning = false;
+ if (bar) bar.style.display = 'none';
+
+ const gr = window.gridRenderer;
+ if (gr) gr.renderVisibleRows();
+
+ const msg = `Saved: ${success}` + (fail > 0 ? `, Errors: ${fail}` : '');
+ const msgEl = document.getElementById('saveAllResultMessage');
+ if (msgEl) msgEl.textContent = msg;
+ new bootstrap.Modal(document.getElementById('saveAllResultModal')).show();
+ });
+
+ // ── beforeunload ─────────────────────────────────────────────────────
+ window.addEventListener('beforeunload', function(e) {
+ if (window.gridData && window.gridData.some(r => r._dirty)) {
+ e.preventDefault();
+ e.returnValue = '';
+ }
+ });
+
+ // Public
+ window.saveAllRunning = () => saveAllRunning;
+})();
diff --git a/public/userarea/tolims.php b/public/userarea/tolims.php
new file mode 100644
index 0000000..a254f33
--- /dev/null
+++ b/public/userarea/tolims.php
@@ -0,0 +1,1363 @@
+getConnection();
+
+// Recupera tutti i mapping dal template, includendo is_visible_import
+$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, is_visible_import, auto_value
+FROM template_mapping
+WHERE template_id = ?");
+$stmt->execute([$template_id]);
+$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+$timeLabels = [
+ 'Sample Arrival Time:',
+ 'Sample Unlock Time:',
+ 'Login Time:',
+ 'Presa in carico, Orario:',
+];
+
+// Returns the auto value for current session (import) based on mapping.auto_value
+function getImportAutoValue(array $mapping): string
+{
+ $auto = $mapping['auto_value'] ?? 'none';
+
+ if ($auto === 'import_date') {
+ return date('Y-m-d');
+ }
+
+ if ($auto === 'import_time') {
+ // HTML expects HH:MM (seconds optional)
+ return date('H:i');
+ }
+
+ return '';
+}
+
+if (empty($allMappings)) {
+ header("Location: import_xls.php?id=$template_id&status=error&message=" . urlencode("Nessun mapping trovato per il template"));
+ exit;
+}
+
+// Trova il campo main_field
+$mainFieldMapping = null;
+foreach ($allMappings as $mapping) {
+ if ($mapping['main_field'] == 1 && $mapping['is_visible_import'] == 1) {
+ $mainFieldMapping = $mapping;
+ break;
+ }
+}
+
+// Recupera l'idclient di default dal template (se presente)
+$template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
+$template_stmt->execute([$template_id]);
+$template = $template_stmt->fetch(PDO::FETCH_ASSOC);
+$default_idclient = $template['idclient'] ?? null;
+
+// Fetch records with status='l' (exported to LIMS) for this template
+$userFilter = $show_all_users ? '' : 'AND d.user_id = ?';
+$stmt = $pdo->prepare("
+ SELECT d.*, CONCAT(u.first_name, ' ', u.last_name) AS user_name
+ FROM datadb d
+ LEFT JOIN auth_users u ON d.user_id = u.id
+ WHERE d.templateid = ? AND d.status = 'l' {$userFilter}
+ ORDER BY d.iddatadb DESC
+");
+$params = [$template_id];
+if (!$show_all_users) $params[] = $user_id;
+$stmt->execute($params);
+$importedData = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+$insertedIds = array_column($importedData, 'iddatadb');
+
+// Fetch custom field details
+$manualDetails = [];
+if (!empty($insertedIds)) {
+ $placeholders = implode(',', array_fill(0, count($insertedIds), '?'));
+ $stmt = $pdo->prepare("
+ SELECT d.id AS detail_id, d.id AS datadb_id, d.mapping_id, d.field_value,
+ m.field_id, m.field_label, m.data_type, m.is_required, m.manual_default
+ FROM import_data_details d
+ JOIN template_mapping m ON d.mapping_id = m.id
+ WHERE d.id IN ({$placeholders})
+ ");
+ $stmt->execute($insertedIds);
+ $manualDetails = $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+// Recupera il mapping globale per mostrare gli slug leggibili
+$stmt = $pdo->query("SELECT mysql_column_name, user_friendly_slug FROM column_mapping");
+$slugMapping = [];
+foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
+ $slugMapping[$row['mysql_column_name']] = $row['user_friendly_slug'];
+}
+// --- FIX LABELS ONLY (do not change keys) ---
+$slugMapping['AnagraficaCertestObject'] = 'Object';
+$slugMapping['AnagraficaCertestService'] = 'Service';
+
+// ---------------- FIXED FIELDS (from template_fixed_mapping) ----------------
+$fixedStmt = $pdo->prepare("
+ SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
+ FROM template_fixed_mapping
+ WHERE template_id = ? AND is_visible_import = 1
+ ORDER BY id
+");
+$fixedStmt->execute([$template_id]);
+$fixedFieldsRaw = $fixedStmt->fetchAll(PDO::FETCH_ASSOC);
+
+// Ordine desiderato: Cliente Responsabile prima, poi gli altri, ConsegnaRichiesta prima delle 3 speciali
+$desiredOrder = [
+ 'ClienteResponsabile',
+ 'ClienteFornitore',
+ 'ClienteAnalisi',
+ 'AnagraficaCertestObject',
+ 'AnagraficaCertestService',
+ 'MoltiplicatorePrezzo',
+ 'ConsegnaRichiesta',
+ // se ci sono altri campi fixed li mettiamo dopo
+];
+
+// No exclusions: fixed fields will be rendered together at the end
+$excludeFromFixed = [];
+
+$fixedFields = [];
+$tempMap = [];
+foreach ($fixedFieldsRaw as $f) {
+ if (in_array($f['fixed_field_key'], $excludeFromFixed, true)) continue;
+ $tempMap[$f['fixed_field_key']] = $f;
+}
+
+foreach ($desiredOrder as $key) {
+ if (isset($tempMap[$key])) {
+ $fixedFields[] = $tempMap[$key];
+ unset($tempMap[$key]);
+ }
+}
+
+// Aggiunge eventuali campi fixed non elencati sopra (alla fine)
+foreach ($tempMap as $f) {
+ $fixedFields[] = $f;
+}
+
+// Maps logical fixed_field_key → real datadb column name (mirrors JS fixedAliasMap)
+$fixedAliasMap = [
+ 'ClienteResponsabile' => 'cliente_responsabile_id',
+ 'ClienteFornitore' => 'cliente_fornitore_id',
+ 'ClienteAnalisi' => 'clienteAnalisi',
+ 'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
+ 'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
+ 'AnagraficaCertestService' => 'anagrafica_certest_service_id',
+ 'ConsegnaRichiesta' => 'consegna_richiesta',
+];
+
+// helper default (DATE can use 'today' if you already use it elsewhere)
+function fixedDefaultValue(array $f): string
+{
+ $v = $f['default_value'] ?? '';
+ if ($f['data_type'] === 'DATE' && $v === 'today') return date('Y-m-d');
+ return (string)$v;
+}
+
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Edit Imported Data - = htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?>
+
+
+
+
+
+
+
+
+
+
Imported (i)
+
To LIMS (l)
+
+
+
+
(= count($importedData) ?> records= !$show_all_users ? ' — my records' : '' ?>)
+
+
+
+
+
+ $row): ?>
+
+
+
+
+
+ $d['mapping_id'] == $mainFieldMapping['id'] && $d['datadb_id'] == $row['iddatadb']);
+ $detail = reset($detail) ?: ['field_value' => $mainFieldMapping['manual_default']];
+ $fieldValue = $detail['field_value'] ?? $mainFieldMapping['manual_default'] ?? '';
+ ?>
+
+ = htmlspecialchars($fieldValue) ?>
+
+
+
+ To LIMS
+
+ = htmlspecialchars($row['commessaweb']) ?>
+
+
+
+ = htmlspecialchars($row['idclient'] ?? '') ?>
+
+
+ $d['datadb_id'] == $row['iddatadb']);
+ $autoIndex = 0;
+ foreach ($allMappings as $mapping) {
+ if (!$mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) {
+ $detail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mapping['id']);
+ $detail = reset($detail) ?: ['field_value' => $mapping['manual_default']];
+ $fieldValue = $detail['field_value'] ?? $mapping['manual_default'] ?? '';
+ echo "
";
+ echo "" . htmlspecialchars($fieldValue) . "";
+ echo "
";
+ $cellIndex++;
+ $autoIndex++;
+ }
+ }
+ foreach ($allMappings as $mapping) {
+ if ($mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) {
+ $detail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mapping['id']);
+ $detail = reset($detail) ?: ['field_value' => $mapping['manual_default']];
+ $fieldValue = $detail['field_value'] ?? $mapping['manual_default'] ?? '';
+ echo "
";
+ echo "" . htmlspecialchars($fieldValue) . "";
+ echo "
";
+ $cellIndex++;
+ }
+ }
+ // Tested Component (empty for view)
+ echo "
";
+ $cellIndex++;
+ ?>
+
+
+
+
+
+
+
+
+
+ ";
+ echo "
" . htmlspecialchars((string)$val) . "";
+ echo "
";
+ $cellIndex++;
+
+ if ($key === 'ConsegnaRichiesta') {
+ echo "
" . htmlspecialchars($row['importreferencecode'] ?? '') . "
";
+ $cellIndex++;
+ echo "
";
+ $cellIndex++;
+ echo "
" . htmlspecialchars($row['importdate'] ?? '') . "
";
+ $cellIndex++;
+ }
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+