document.addEventListener("DOMContentLoaded", () => { console.log("export_to_lims.js loaded"); const exportButtons = document.querySelectorAll(".export-lims-btn"); console.log(`Found ${exportButtons.length} export-lims-btn buttons`); // Tracks the active confirm handler so it can be replaced on re-open let pendingConfirmHandler = null; let batchRunning = false; // Expose for Save All to check Object.defineProperty(window, "batchRunning", { get: () => batchRunning, }); // ── 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"; } // ── Validation ─────────────────────────────────────────────────────────── /** * Call the validation endpoint for an array of { iddatadb, index } objects. * Returns the parsed JSON response. */ 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(); } /** * Clear all validation-error highlights from the grid. */ function clearValidationErrors() { 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"); }); // Also clear batch-row-error that came from validation clearAllRowErrors(); } /** * Highlight specific cells on a row based on validation errors. * Each error has { field, message }. * field can be: a data-col value, "parts", "field_label:SomeLabel", or null (row-level). */ function showValidationErrors(row, iddatadb, errors) { row.classList.add("validation-row-error"); const messages = []; errors.forEach((err) => { messages.push(err.message); if (!err.field) return; // row-level error, no specific cell let cell = null; if (err.field === "parts") { // No specific cell to highlight, but we highlight the parts button area // just add to messages return; } else if (err.field.startsWith("field_label:")) { // Match by field_label text — find the header with this label, get its index 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) { const rowIndex = row.querySelector(".grid-cell[data-row]")?.getAttribute("data-row"); if (rowIndex !== null) { cell = row.querySelector(`.grid-cell[data-row="${rowIndex}"][data-index="${targetIndex}"]`); } } } else { // Direct data-col match const rowIndex = row.querySelector(".grid-cell[data-row]")?.getAttribute("data-row"); if (rowIndex !== null) { cell = row.querySelector(`.grid-cell[data-col="${err.field}"][data-row="${rowIndex}"]`); } } if (cell) { cell.classList.add("validation-error"); // Mark the input/select inside the cell cell.querySelectorAll("input, select").forEach((el) => { el.classList.add("input-validation-error"); }); // Add tooltip with error message let tooltip = cell.querySelector(".validation-tooltip"); if (!tooltip) { tooltip = document.createElement("div"); tooltip.className = "validation-tooltip"; cell.appendChild(tooltip); } tooltip.textContent = err.message; } }); // Show aggregated error on the row using existing mechanism showRowError(row, iddatadb, messages.join("\n")); } /** * Send a single export request and update the row UI on success. * Returns the parsed JSON response. */ async function sendExport(iddatadb, gridRow, batchUuid = null) { 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 && gridRow) { // Update status badge 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"; } // Insert/update CommessaWeb code span 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;"; cwSpan.title = "CommessaWeb"; const hiddenInput = statusCell.querySelector( 'input[type="hidden"]', ); hiddenInput ? statusCell.insertBefore(cwSpan, hiddenInput) : statusCell.appendChild(cwSpan); } cwSpan.textContent = data.commessaweb; } // Disable export button for this row 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"; exportBtn.title = "Già esportato"; } } return data; } /** * Show the result of a single export in the response modal. */ function showExportResult(data) { const responseModalElement = document.getElementById("exportResponseModal"); if (!responseModalElement) { alert("Errore: Modale di risposta non trovato"); return; } const responseModal = new bootstrap.Modal(responseModalElement, { keyboard: false, }); const responseMessage = document.getElementById( "exportResponseMessage", ); if (data.success) { responseMessage.innerHTML = `${data.message.replace(/\n/g, "
")}` + `
ID CommessaWeb: ${data.idcommessaweb}` + `
Codice CommessaWeb: ${data.commessaweb}` + (data.totalPhotos > 0 ? `
Foto trovate: ${data.totalPhotos}` : ""); document.getElementById("exportResponseModalLabel").textContent = "Esportazione Completata"; } else { responseMessage.textContent = `Errore durante la generazione dei payload: ${data.message}`; document.getElementById("exportResponseModalLabel").textContent = "Errore Esportazione"; } responseModal.show(); responseModalElement.addEventListener( "hidden.bs.modal", cleanupBackdrop, { once: true }, ); } // ── Row button helpers (disable/enable during batch) ──────────────────── function setRowExporting(row, active) { 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"); row.classList.add("batch-disabled"); 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) { row.classList.add("batch-row-error"); const btnCell = row.querySelector(".button-cell"); if (btnCell) { // Remove existing error msg const old = btnCell.querySelector(".batch-error-msg"); if (old) old.remove(); const errorEl = document.createElement("div"); errorEl.className = "batch-error-msg"; errorEl.textContent = "⚠ Errore — clicca per dettagli"; errorEl.addEventListener("click", () => { document.getElementById("exportResponseMessage").innerHTML = message.replace(/\n/g, "
"); document.getElementById("exportResponseModalLabel").textContent = "Errore Validazione (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"); }); // Disable Actions dropdown 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 modal, then send ─────────────── function startExportConfirmFlow(iddatadb, btn) { const gridRow = btn.closest(".grid-row"); const rowIndex = btn.dataset.row; // Validate first clearValidationErrors(); // Show validating state on the row setRowExporting(gridRow, true); const spinner = gridRow.querySelector(".batch-row-spinner"); if (spinner) spinner.innerHTML = ' Validating...'; validateRows([{ iddatadb: parseInt(iddatadb), index: parseInt(rowIndex) }]) .then((validationData) => { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); if (!validationData.success) { showExportResult({ success: false, message: validationData.message || "Errore di validazione" }); return; } const result = validationData.results[rowIndex]; if (result && !result.valid) { // Show validation errors on the row showValidationErrors(gridRow, iddatadb, result.errors); return; } // Validation passed — show confirm modal showConfirmAndExport(iddatadb, btn); }) .catch((error) => { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); console.error("Validation error:", error); showExportResult({ success: false, message: "Errore di validazione: " + error.message }); }); } function showConfirmAndExport(iddatadb, btn) { const confirmModalElement = document.getElementById("exportConfirmModal"); if (!confirmModalElement) { alert("Errore: Modale di conferma non trovato"); 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(); alert("Errore: Pulsante di conferma non trovato"); return; } const confirmHandler = async () => { pendingConfirmHandler = null; console.log(`Confirmed export for iddatadb: ${iddatadb}`); confirmModal.hide(); const gridRow = btn.closest(".grid-row"); setRowExporting(gridRow, true); try { const data = await sendExport(iddatadb, gridRow); console.log("Export response:", data); // Stop spinner, fully restore row setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); if (!data.success) { showRowError(gridRow, iddatadb, data.message || "Errore sconosciuto"); } showExportResult(data); } catch (error) { console.error("Export error:", error); // Stop spinner, fully restore row so user can retry 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 handler ──────────────────────────────────────────── exportButtons.forEach((btn) => { btn.addEventListener("click", (e) => { e.preventDefault(); if (batchRunning) return; const rowIndex = btn.dataset.row; const iddatadb = btn.dataset.iddatadb; console.log( `Export to LIMS clicked for row ${rowIndex}, iddatadb: ${iddatadb}`, ); const gridRow = btn.closest(".grid-row"); if (gridRow && gridRow.querySelector(".cell-changed")) { const unsavedModal = new bootstrap.Modal( document.getElementById("exportUnsavedModal"), { keyboard: false }, ); unsavedModal.show(); document.getElementById("saveAndExportBtn").addEventListener( "click", () => { unsavedModal.hide(); const saveBtn = gridRow.querySelector(".save-btn"); if (!saveBtn) return; const observer = new MutationObserver(() => { if (!gridRow.querySelector(".cell-changed")) { observer.disconnect(); startExportConfirmFlow(iddatadb, btn); } }); observer.observe(gridRow, { subtree: true, attributes: true, attributeFilter: ["class"], }); saveBtn.click(); }, { once: true }, ); return; } // No unsaved changes — go straight to validate + export confirm startExportConfirmFlow(iddatadb, btn); }); }); // ── Batch export (Export All) ─────────────────────────────────────────── const exportAllBtn = document.querySelector(".export-all-lims-btn"); if (!exportAllBtn) return; let batchCancelled = false; let pendingBatchConfirmHandler = null; function collectEligibleRows() { const allRows = document.querySelectorAll(".grid-row[data-id]"); const eligible = []; allRows.forEach((row) => { const statusBadge = row.querySelector( '.grid-cell[data-col="status"] .status-badge', ); if (statusBadge && !statusBadge.classList.contains("status-l")) { const iddatadb = row.dataset.id; if (iddatadb) { eligible.push({ iddatadb, row }); } } }); return eligible; } /** * Get the data-row index for a grid row element. */ function getRowIndex(row) { const cell = row.querySelector(".grid-cell[data-row]"); return cell ? parseInt(cell.getAttribute("data-row")) : null; } function hasUnsavedChanges() { return !!document.querySelector(".grid-row[data-id] .cell-changed"); } /** * Validate all eligible rows, show errors, and return only the valid ones. */ async function validateAndFilter(eligibleRows) { const rowsToValidate = eligibleRows.map(({ iddatadb, row }) => ({ iddatadb: parseInt(iddatadb), index: getRowIndex(row), })); const validationData = await validateRows(rowsToValidate); if (!validationData.success) { throw new Error(validationData.message || "Errore di validazione"); } const validRows = []; let invalidCount = 0; for (const { iddatadb, row } of eligibleRows) { const rowIdx = getRowIndex(row); const result = validationData.results[rowIdx]; if (result && !result.valid) { showValidationErrors(row, iddatadb, result.errors); invalidCount++; } else { validRows.push({ iddatadb, row }); } } return { validRows, invalidCount }; } function showValidationSpinner(show) { const bar = document.getElementById("batchExportBar"); const statusEl = document.getElementById("batchExportStatus"); const cancelBtn = document.getElementById("exportBatchCancelBtn"); if (show) { bar.style.display = ""; statusEl.textContent = "Validazione in corso..."; cancelBtn.style.display = "none"; } else { bar.style.display = "none"; cancelBtn.style.display = ""; } } function showBatchConfirm(eligibleRows) { clearValidationErrors(); showValidationSpinner(true); // Validate before showing confirm validateAndFilter(eligibleRows) .then(({ validRows, invalidCount }) => { showValidationSpinner(false); if (validRows.length === 0) { document.getElementById("exportResponseMessage").innerHTML = `Nessuna riga valida per l'esportazione.
` + `${invalidCount} righe con errori di validazione.`; document.getElementById("exportResponseModalLabel").textContent = "Validazione Fallita"; 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} escluse per errori di validazione)`; } 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); console.error("Batch validation error:", error); document.getElementById("exportResponseMessage").textContent = "Errore di validazione: " + error.message; document.getElementById("exportResponseModalLabel").textContent = "Errore Validazione"; new bootstrap.Modal( document.getElementById("exportResponseModal"), { keyboard: false }, ).show(); }); } exportAllBtn.addEventListener("click", (e) => { e.preventDefault(); if (batchRunning) return; // Check unsaved changes first if (hasUnsavedChanges()) { const unsavedModal = new bootstrap.Modal( document.getElementById("exportBatchUnsavedModal"), { keyboard: false }, ); unsavedModal.show(); document .getElementById("batchSaveAndExportBtn") .addEventListener( "click", () => { unsavedModal.hide(); // Trigger Save All, then proceed const saveAllEl = document.querySelector(".save-all-btn"); if (!saveAllEl) return; // Watch for all .cell-changed to disappear const observer = new MutationObserver(() => { if (!hasUnsavedChanges()) { observer.disconnect(); const eligibleRows = collectEligibleRows(); if (eligibleRows.length === 0) { document.getElementById( "exportResponseMessage", ).textContent = "Tutte le righe sono già state esportate al LIMS."; document.getElementById( "exportResponseModalLabel", ).textContent = "Export All"; new bootstrap.Modal( document.getElementById( "exportResponseModal", ), { keyboard: false }, ).show(); return; } showBatchConfirm(eligibleRows); } }); observer.observe( document.querySelector(".grid-container"), { subtree: true, attributes: true, attributeFilter: ["class"], }, ); saveAllEl.click(); }, { once: true }, ); return; } const eligibleRows = collectEligibleRows(); if (eligibleRows.length === 0) { document.getElementById("exportResponseMessage").textContent = "Tutte le righe sono già state esportate al LIMS."; document.getElementById("exportResponseModalLabel").textContent = "Export All"; const modal = new bootstrap.Modal( document.getElementById("exportResponseModal"), { keyboard: false }, ); modal.show(); return; } showBatchConfirm(eligibleRows); }); function startBatchExport(eligibleRows) { batchCancelled = false; batchRunning = true; // Don't clear validation errors — they should stay visible for invalid rows const batchUuid = crypto.randomUUID(); const total = eligibleRows.length; let processed = 0; let succeeded = 0; let failed = 0; // Disable all row buttons disableAllRowButtons(); // Show inline status bar const bar = document.getElementById("batchExportBar"); const statusEl = document.getElementById("batchExportStatus"); const cancelBtn = document.getElementById("exportBatchCancelBtn"); bar.style.display = ""; cancelBtn.disabled = false; statusEl.textContent = `Esportazione 0 / ${total}...`; // Cancel handler cancelBtn.addEventListener( "click", () => { batchCancelled = true; statusEl.textContent = "Annullamento... (attendi riga corrente)"; cancelBtn.disabled = true; }, { once: true }, ); (async () => { for (let i = 0; i < eligibleRows.length; i++) { if (batchCancelled) break; const { iddatadb, row } = eligibleRows[i]; statusEl.textContent = `Esportazione ${processed + 1} / ${total} (id: ${iddatadb})...`; // Highlight current row setRowExporting(row, true); try { const data = await sendExport(iddatadb, row, batchUuid); processed++; if (data.success) { succeeded++; } else { failed++; showRowError(row, iddatadb, data.message || "Errore sconosciuto"); } } catch (error) { processed++; failed++; showRowError(row, iddatadb, error.message); } // Remove highlight from current row setRowExporting(row, false); } // Finished batchRunning = false; enableAllRowButtons(); bar.style.display = "none"; // Show result modal const msgEl = document.getElementById("exportResponseMessage"); const labelEl = document.getElementById("exportResponseModalLabel"); if (batchCancelled) { labelEl.textContent = "Export All — Annullato"; msgEl.innerHTML = `Esportate: ${succeeded}
` + `Errori: ${failed}
` + `Non processate: ${total - processed}`; } else if (failed === 0) { labelEl.textContent = "Export All — Completato"; msgEl.innerHTML = `Tutte le ${succeeded} righe esportate con successo.`; } else { labelEl.textContent = "Export All — Completato con errori"; msgEl.innerHTML = `Esportate: ${succeeded}
` + `Errori: ${failed}`; } const modalEl = document.getElementById("exportResponseModal"); const modal = new bootstrap.Modal(modalEl, { keyboard: false }); modal.show(); modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true }); })(); } });