From 2a7b1fae17760d81f0d6d3026a6ddab467f0866c Mon Sep 17 00:00:00 2001 From: "r.mubarakzyanov" Date: Thu, 26 Mar 2026 16:05:17 +0300 Subject: [PATCH] imported && tolims --- public/userarea/add_record.php | 61 ++ public/userarea/exportLims_gridData.js | 555 ++++++++++ public/userarea/gridRenderer.js | 894 ++++++++++++++++ public/userarea/import_edit2.php | 15 +- public/userarea/imported.php | 1238 +++++++++++++++++++++ public/userarea/modal_partsTable.php | 20 +- public/userarea/modals_gridData.js | 208 ++++ public/userarea/partsTable.js | 16 +- public/userarea/photos.js | 5 +- public/userarea/saveAll.js | 128 +++ public/userarea/tolims.php | 1363 ++++++++++++++++++++++++ 11 files changed, 4480 insertions(+), 23 deletions(-) create mode 100644 public/userarea/add_record.php create mode 100644 public/userarea/exportLims_gridData.js create mode 100644 public/userarea/gridRenderer.js create mode 100644 public/userarea/imported.php create mode 100644 public/userarea/modals_gridData.js create mode 100644 public/userarea/saveAll.js create mode 100644 public/userarea/tolims.php 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
@@ -2312,7 +2312,7 @@ function fixedDefaultValue(array $f): string } else { // Select nativa try { - $(dropdown).select2('destroy'); + if ($(dropdown).hasClass('select2-hidden-accessible')) $(dropdown).select2('destroy'); } catch (e) {} dropdown.innerHTML = ''; items.forEach(value => { @@ -2755,7 +2755,8 @@ function fixedDefaultValue(array $f): string if (results.length > 12) { // Select2 con ricerca - $select.select2('destroy').empty().select2({ + if ($select.hasClass('select2-hidden-accessible')) $select.select2('destroy'); + $select.empty().select2({ data: [{ id: '', text: 'Seleziona...' @@ -2766,9 +2767,7 @@ function fixedDefaultValue(array $f): string }); } else { // Select nativa senza Select2 - try { - $select.select2('destroy'); - } catch (e) {} + if ($select.hasClass('select2-hidden-accessible')) $select.select2('destroy'); $select.empty(); $select.append(new Option('Seleziona...', '', true, false)); results.forEach(r => { diff --git a/public/userarea/imported.php b/public/userarea/imported.php new file mode 100644 index 0000000..8afa340 --- /dev/null +++ b/public/userarea/imported.php @@ -0,0 +1,1238 @@ +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='i' 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 = 'i' {$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 +]; + +// ClienteFornitore rendered as standalone column, exclude from fixed fields +$excludeFromFixed = ['ClienteFornitore']; + +$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; +} + +// ──────────────────────────────────────────────────────────── +// Build gridData (JSON) — all row data for client-side rendering +// ──────────────────────────────────────────────────────────── + +$gridDataArray = []; +foreach ($importedData as $index => $row) { + $rowObj = [ + 'iddatadb' => (int)$row['iddatadb'], + 'status' => $row['status'] ?? 'i', + 'idclient' => $row['idclient'] ?? $default_idclient, + 'cliente_fornitore_id' => $row['cliente_fornitore_id'] ?? null, + 'commessaweb' => $row['commessaweb'] ?? null, + 'user_name' => $row['user_name'] ?? '', + 'importreferencecode' => $row['importreferencecode'] ?? '', + 'filename_import' => $row['filename_import'] ?? '', + 'importdate' => $row['importdate'] ?? '', + ]; + + // Fixed fields + $rowObj['fixedFields'] = []; + foreach ($fixedFields as $f) { + $key = $f['fixed_field_key']; + $dbCol = $fixedAliasMap[$key] ?? $key; + $val = $row[$dbCol] ?? ''; + if ($val === '' || $val === null) { + $val = fixedDefaultValue($f); + } + $rowObj['fixedFields'][$key] = (string)$val; + } + + // Detail fields (from import_data_details) + $rowObj['details'] = []; + $rowDetails = array_filter($manualDetails, fn($d) => $d['datadb_id'] == $row['iddatadb']); + foreach ($rowDetails as $d) { + $rowObj['details'][(string)$d['mapping_id']] = $d['field_value'] ?? ''; + } + + // Main field value + if ($mainFieldMapping) { + $mainDetail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mainFieldMapping['id']); + $mainDetail = reset($mainDetail) ?: ['field_value' => $mainFieldMapping['manual_default'] ?? '']; + $rowObj['mainFieldValue'] = $mainDetail['field_value'] ?? $mainFieldMapping['manual_default'] ?? ''; + } + + $rowObj['_dirty'] = false; + $gridDataArray[] = $rowObj; +} + +// Build columns in display order +$gridColumns = []; + +// 1. Main field +if ($mainFieldMapping) { + $gridColumns[] = [ + 'type' => 'main_field', + 'key' => (string)$mainFieldMapping['id'], + 'label' => $mainFieldMapping['field_label'], + 'dataType' => $mainFieldMapping['data_type'], + 'isManual' => (bool)$mainFieldMapping['is_manual'], + 'isRequired' => (bool)$mainFieldMapping['is_required'], + 'fieldId' => $mainFieldMapping['field_id'] ?? null, + 'width' => 150, + ]; +} + +// 2. Status +$gridColumns[] = ['type' => 'status', 'key' => 'status', 'label' => 'Status', 'width' => 150, 'editable' => false]; + +// 3. Client +$gridColumns[] = ['type' => 'idclient', 'key' => 'idclient', 'label' => 'Client', 'width' => 300]; + +// 4. Cliente Fornitore +$gridColumns[] = ['type' => 'cliente_fornitore_id', 'key' => 'cliente_fornitore_id', 'label' => $slugMapping['ClienteFornitore'] ?? 'ClienteFornitore', 'width' => 300]; + +// 5. Auto fields +foreach ($allMappings as $mapping) { + if (!$mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) { + $gridColumns[] = [ + 'type' => 'detail', + 'key' => (string)$mapping['id'], + 'label' => $mapping['field_label'], + 'dataType' => $mapping['data_type'], + 'isManual' => false, + 'isRequired' => (bool)$mapping['is_required'], + 'fieldId' => $mapping['field_id'] ?? null, + 'autoValue' => $mapping['auto_value'] ?? 'none', + 'width' => 150, + ]; + } +} + +// 6. Manual fields +foreach ($allMappings as $mapping) { + if ($mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) { + $gridColumns[] = [ + 'type' => 'detail', + 'key' => (string)$mapping['id'], + 'label' => $mapping['field_label'], + 'dataType' => $mapping['data_type'], + 'isManual' => true, + 'isRequired' => (bool)$mapping['is_required'], + 'fieldId' => $mapping['field_id'] ?? null, + 'manualDefault' => $mapping['manual_default'] ?? '', + 'width' => 150, + ]; + } +} + +// 7. Tested Component +$gridColumns[] = ['type' => 'tested_component', 'key' => 'tested_component', 'label' => 'Tested Component', 'width' => 150]; + +// 8. AWB +$gridColumns[] = ['type' => 'awb', 'key' => 'awb', 'label' => 'AWB Number', 'width' => 200]; + +// 9. Tracking +$gridColumns[] = ['type' => 'tracking', 'key' => 'tracking', 'label' => 'Tracking Info', 'width' => 250]; + +// 10. Fixed fields (with importreferencecode/filename/importdate after ConsegnaRichiesta) +foreach ($fixedFields as $f) { + $key = $f['fixed_field_key']; + $label = $slugMapping[$key] ?? $key; + $gridColumns[] = [ + 'type' => 'fixed', + 'key' => $key, + 'label' => $label, + 'dataType' => $f['data_type'], + 'isRequired' => (bool)$f['is_required'], + 'width' => 180, + ]; + if ($key === 'ConsegnaRichiesta') { + $gridColumns[] = ['type' => 'static', 'key' => 'importreferencecode', 'label' => $slugMapping['importreferencecode'] ?? 'Import Reference Code', 'width' => 150, 'editable' => false]; + $gridColumns[] = ['type' => 'static', 'key' => 'filename_import', 'label' => $slugMapping['filename_import'] ?? 'File', 'width' => 150, 'editable' => false]; + $gridColumns[] = ['type' => 'static', 'key' => 'importdate', 'label' => $slugMapping['importdate'] ?? 'Import Date', 'width' => 150, 'editable' => false]; + } +} + +$gridMeta = [ + 'templateId' => $template_id, + 'defaultIdclient' => $default_idclient, + 'isAdmin' => Auth::user()->hasRole('Admin'), + 'userId' => $user_id, + 'fixedAliasMap' => $fixedAliasMap, + 'slugMapping' => $slugMapping, + 'timeLabels' => $timeLabels, + 'columns' => $gridColumns, + 'mainFieldMapping' => $mainFieldMapping, + 'totalRows' => count($gridDataArray), +]; + +?> + + + + + + + + + + + + + + + + Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + + +
+ + +
+
+
+ Imported (i) + To LIMS (l) + + + + ( records) +
+
+
+
+ + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + diff --git a/public/userarea/modal_partsTable.php b/public/userarea/modal_partsTable.php index 1883c35..4a0af8e 100644 --- a/public/userarea/modal_partsTable.php +++ b/public/userarea/modal_partsTable.php @@ -23,13 +23,13 @@
- + - +
@@ -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) + + + + ( records) +
+
+
+
+ Exported to LIMS +
+
+
+
+
+
+
+ +
+ "; + echo ""; + echo ""; + echo ""; + } elseif ($mainFieldMapping['data_type'] === 'Data') { + echo ""; + echo ""; + } elseif ($mainFieldMapping['data_type'] === 'INT') { + echo ""; + echo ""; + } else { + echo ""; + echo ""; + } + ?> +
+ + +
+ +
+ + + +
+ "; + echo ""; + echo ""; + echo "
"; + } else { + // Show auto import date/time in header too (read-only) + $autoVal = getImportAutoValue($mapping); + + echo "
"; + + if (($mapping['auto_value'] ?? 'none') === 'import_date') { + echo ""; + echo ""; + } elseif (($mapping['auto_value'] ?? 'none') === 'import_time') { + echo ""; + echo ""; + } else { + // keep empty cell for other auto fields + echo ""; + } + + echo "
"; + } + + $autoIndex++; + $topColIndex++; + } + } + + $manualIndex = 0; + + foreach ($allMappings as $mapping) { + if ($mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) { + $fieldValue = $mapping['manual_default'] ?? ''; + if ($mapping['data_type'] === 'Data' && $mapping['manual_default'] === 'today') { + $fieldValue = date('Y-m-d'); + } + $inputClass = 'manual-input'; + if ($mapping['is_required']) $inputClass .= ' required-input'; + echo "
"; + if ($mapping['data_type'] === 'SceltaMultipla') { + echo ""; + } elseif ($mapping['data_type'] === 'Data') { + echo ""; + } elseif ($mapping['data_type'] === 'INT') { + echo ""; + } elseif (in_array($mapping['field_label'], $timeLabels)) { + echo ""; + } else { + echo ""; + } + echo ""; + echo "
"; + + $manualIndex++; + $topColIndex++; + } + } + + echo "
"; + $topColIndex++; + echo "
"; + $topColIndex++; + echo "
"; + $topColIndex++; + // ---------------- FIXED FIELDS TOP (propagate) - same order as header: fixed cols, 3 empties after ConsegnaRichiesta ---------------- + if (!empty($fixedFields)) { + $insertedAfterConsegnaTop = false; + foreach ($fixedFields as $fx => $f) { + + $key = $f['fixed_field_key']; // datadb column + + $val = fixedDefaultValue($f); + + $isRequired = ((int)$f['is_required'] === 1); + $inputClass = 'manual-input' . ($isRequired ? ' required-input' : ''); + $topRequiredClass = ($isRequired && ($val === '' || $val === null)) ? 'missing-required' : ''; + + echo "
"; + $topColIndex++; + + // Forza DATE anche se per errore nel DB è diversa + $isDate = ($f['data_type'] === 'DATE' || $key === 'ConsegnaRichiesta'); + + if ($isDate) { + + echo ""; + } else { + + $isApiField = in_array($key, [ + 'MoltiplicatorePrezzo', + 'ClienteResponsabile', + 'ClienteFornitore', + 'ClienteAnalisi', + 'AnagraficaCertestObject', + 'AnagraficaCertestService' + ], true); + + if ($isApiField) { + $placeholder = ($key === 'ClienteResponsabile') ? 'Seleziona cliente prima...' : 'Seleziona...'; + + echo ""; + } else { + + echo ""; + } + } + + // UNA SOLA freccia + echo ""; + echo "
"; + + if ($key === 'ConsegnaRichiesta' && !$insertedAfterConsegnaTop) { + echo "
"; + $topColIndex++; + echo "
"; + $topColIndex++; + echo "
"; + $topColIndex++; + $insertedAfterConsegnaTop = true; + } + } + } + + + ?> +
+ +
+
View
+ +
+ +
+
+ +
Status
+
+
Client
+
+ " . htmlspecialchars($mapping['field_label']) . "
"; + $headerIndex++; + } + } + foreach ($allMappings as $mapping) { + if ($mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) { + echo "
" . htmlspecialchars($mapping['field_label']) . "
"; + $headerIndex++; + } + } + // Aggiunta header per Tested Component + echo "
Tested Component
"; + $headerIndex++; + echo "
AWB Number
"; + $headerIndex++; + echo "
Tracking Info
"; + $headerIndex++; + + // ---------------- FIXED FIELDS HEADERS ---------------- + if (!empty($fixedFields)) { + $insertedAfterConsegna = false; + foreach ($fixedFields as $f) { + $key = $f['fixed_field_key']; + $label = $slugMapping[$key] ?? $key; + + echo "
" + . htmlspecialchars($label) . + "
"; + $headerIndex++; + + // Inseriamo le 3 colonne SOLO dopo ConsegnaRichiesta + if ($key === 'ConsegnaRichiesta' && !$insertedAfterConsegna) { + echo "
Import Reference Code
"; + $headerIndex++; + echo "
" . ($slugMapping['filename_import'] ?? 'filename_import') . "
"; + $headerIndex++; + echo "
" . ($slugMapping['importdate'] ?? 'importdate') . "
"; + $headerIndex++; + $insertedAfterConsegna = true; + } + } + } + ?> + +
+ + $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'] ?? ''; + ?> +
+ +
+ +
+ To LIMS + + + +
+
+ +
+ + $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++; + } + } + } + ?> + +
+ +
+ + + + + +
+
+ + +
+ + + + + + + +
- +
- - - + + +
- +
- - - + + +
- +
- - - + + +