/** * 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 }); })(); } })();