fixed export and save to selection

This commit is contained in:
2026-07-17 10:46:33 +02:00
parent 86b1c8420d
commit c2fd2dc470
5 changed files with 423 additions and 159 deletions
@@ -14,6 +14,7 @@ $targetList = $data['target_iddatadb_list'] ?? [];
// (comportamento di default richiesto, coerente con la checkbox pre-spuntata) // (comportamento di default richiesto, coerente con la checkbox pre-spuntata)
$cloneNotes = array_key_exists('clone_notes', $data) ? !empty($data['clone_notes']) : true; $cloneNotes = array_key_exists('clone_notes', $data) ? !empty($data['clone_notes']) : true;
$cloneAnalyses = !empty($data['clone_analyses']); $cloneAnalyses = !empty($data['clone_analyses']);
$overwrite = !empty($data['overwrite']);
$targetIds = array_values(array_unique(array_filter(array_map('intval', (array)$targetList), function ($v) use ($sourceIddatadb) { $targetIds = array_values(array_unique(array_filter(array_map('intval', (array)$targetList), function ($v) use ($sourceIddatadb) {
return $v > 0 && $v !== $sourceIddatadb; return $v > 0 && $v !== $sourceIddatadb;
@@ -93,6 +94,28 @@ try {
$details = []; $details = [];
$totalClonedParts = 0; $totalClonedParts = 0;
$totalClonedAnalyses = 0; $totalClonedAnalyses = 0;
$totalDeletedParts = 0;
// 2b. Se overwrite: elimina le parti esistenti dei target (e i loro figli)
if ($overwrite) {
$stmtSelectExisting = $pdo->prepare("SELECT id FROM identification_parts WHERE iddatadb = ?");
$stmtDeleteCFByPart = $pdo->prepare("DELETE FROM identification_parts_customfields WHERE part_id = ?");
$stmtDeleteAnByPart = $pdo->prepare("DELETE FROM identification_parts_analyses WHERE part_id = ?");
$stmtDeletePart = $pdo->prepare("DELETE FROM identification_parts WHERE id = ?");
foreach ($targetIds as $targetIddatadb) {
$stmtSelectExisting->execute([$targetIddatadb]);
$existingIds = $stmtSelectExisting->fetchAll(PDO::FETCH_COLUMN);
foreach ($existingIds as $existingPartId) {
$existingPartId = (int)$existingPartId;
$stmtDeleteCFByPart->execute([$existingPartId]);
$stmtDeleteAnByPart->execute([$existingPartId]);
$stmtDeletePart->execute([$existingPartId]);
$totalDeletedParts++;
}
}
}
// 3. Clone source parts to each target record // 3. Clone source parts to each target record
foreach ($targetIds as $targetIddatadb) { foreach ($targetIds as $targetIddatadb) {
+301 -116
View File
@@ -5,7 +5,7 @@
* Single export + batch export (Export All) with validation. * Single export + batch export (Export All) with validation.
*/ */
(function () { (function () {
'use strict'; "use strict";
let pendingConfirmHandler = null; let pendingConfirmHandler = null;
let batchRunning = false; let batchRunning = false;
@@ -17,7 +17,7 @@
// ── Helpers ────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────
function cleanupBackdrop() { function cleanupBackdrop() {
document.querySelectorAll(".modal-backdrop").forEach(b => b.remove()); document.querySelectorAll(".modal-backdrop").forEach((b) => b.remove());
document.body.classList.remove("modal-open"); document.body.classList.remove("modal-open");
document.body.style.paddingRight = ""; document.body.style.paddingRight = "";
const overlay = document.querySelector(".overlay.toggle-icon"); const overlay = document.querySelector(".overlay.toggle-icon");
@@ -29,7 +29,9 @@
} }
function getRowIndexByIddatadb(iddatadb) { function getRowIndexByIddatadb(iddatadb) {
return (window.gridData || []).findIndex(r => String(r.iddatadb) === String(iddatadb)); return (window.gridData || []).findIndex(
(r) => String(r.iddatadb) === String(iddatadb),
);
} }
// ── Validation ────────────────────────────────────────────────────── // ── Validation ──────────────────────────────────────────────────────
@@ -40,21 +42,31 @@
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows: rowsToValidate }), body: JSON.stringify({ rows: rowsToValidate }),
}); });
if (!response.ok) throw new Error(`Validation HTTP error: ${response.status}`); if (!response.ok)
throw new Error(`Validation HTTP error: ${response.status}`);
return response.json(); return response.json();
} }
function clearValidationErrors() { function clearValidationErrors() {
// Clear from gridData // Clear from gridData
(window.gridData || []).forEach(row => { delete row._validationErrors; delete row._exportError; }); (window.gridData || []).forEach((row) => {
delete row._validationErrors;
document.querySelectorAll(".grid-cell.validation-error").forEach(cell => { delete row._exportError;
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"));
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(); clearAllRowErrors();
} }
@@ -67,7 +79,7 @@
gridRow.classList.add("validation-row-error"); gridRow.classList.add("validation-row-error");
const messages = []; const messages = [];
errors.forEach(err => { errors.forEach((err) => {
messages.push(err.message); messages.push(err.message);
if (!err.field) return; if (!err.field) return;
@@ -76,19 +88,26 @@
const label = err.field.substring("field_label:".length); const label = err.field.substring("field_label:".length);
const headers = document.querySelectorAll(".grid-header"); const headers = document.querySelectorAll(".grid-header");
let targetIndex = null; let targetIndex = null;
headers.forEach(h => { headers.forEach((h) => {
if (h.textContent.trim() === label) targetIndex = h.getAttribute("data-index"); if (h.textContent.trim() === label)
targetIndex = h.getAttribute("data-index");
}); });
if (targetIndex) { if (targetIndex) {
cell = gridRow.querySelector(`.grid-cell[data-index="${targetIndex}"]`); cell = gridRow.querySelector(
`.grid-cell[data-index="${targetIndex}"]`,
);
} }
} else { } else {
cell = gridRow.querySelector(`.grid-cell[data-col="${err.field}"]`); cell = gridRow.querySelector(
`.grid-cell[data-col="${err.field}"]`,
);
} }
if (cell) { if (cell) {
cell.classList.add("validation-error"); cell.classList.add("validation-error");
cell.querySelectorAll("input, select").forEach(el => el.classList.add("input-validation-error")); cell.querySelectorAll("input, select").forEach((el) =>
el.classList.add("input-validation-error"),
);
let tooltip = cell.querySelector(".validation-tooltip"); let tooltip = cell.querySelector(".validation-tooltip");
if (!tooltip) { if (!tooltip) {
tooltip = document.createElement("div"); tooltip = document.createElement("div");
@@ -109,34 +128,43 @@
formData.append("iddatadb", iddatadb); formData.append("iddatadb", iddatadb);
if (batchUuid) formData.append("batch_uuid", batchUuid); if (batchUuid) formData.append("batch_uuid", batchUuid);
const response = await fetch("export_to_lims.php", { method: "POST", body: formData }); const response = await fetch("export_to_lims.php", {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); method: "POST",
body: formData,
});
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
// Update gridData // Update gridData
const idx = getRowIndexByIddatadb(iddatadb); const idx = getRowIndexByIddatadb(iddatadb);
if (idx >= 0) { if (idx >= 0) {
window.gridData[idx].status = 'l'; window.gridData[idx].status = "l";
window.gridData[idx].commessaweb = data.commessaweb; window.gridData[idx].commessaweb = data.commessaweb;
} }
// Update visible DOM row // Update visible DOM row
const gridRow = getGridRow(iddatadb); const gridRow = getGridRow(iddatadb);
if (gridRow) { if (gridRow) {
const statusBadge = gridRow.querySelector('.grid-cell[data-col="status"] .status-badge'); const statusBadge = gridRow.querySelector(
'.grid-cell[data-col="status"] .status-badge',
);
if (statusBadge) { if (statusBadge) {
statusBadge.classList.remove("status-i", "status-P"); statusBadge.classList.remove("status-i", "status-P");
statusBadge.classList.add("status-l"); statusBadge.classList.add("status-l");
statusBadge.textContent = "To LIMS"; statusBadge.textContent = "To LIMS";
} }
const statusCell = gridRow.querySelector('.grid-cell[data-col="status"]'); const statusCell = gridRow.querySelector(
'.grid-cell[data-col="status"]',
);
if (statusCell && data.commessaweb) { if (statusCell && data.commessaweb) {
let cwSpan = statusCell.querySelector(".commessaweb-code"); let cwSpan = statusCell.querySelector(".commessaweb-code");
if (!cwSpan) { if (!cwSpan) {
cwSpan = document.createElement("span"); cwSpan = document.createElement("span");
cwSpan.className = "commessaweb-code"; cwSpan.className = "commessaweb-code";
cwSpan.style.cssText = "display:block; font-size:0.75em; color:#555; margin-top:2px;"; cwSpan.style.cssText =
"display:block; font-size:0.75em; color:#555; margin-top:2px;";
statusCell.appendChild(cwSpan); statusCell.appendChild(cwSpan);
} }
cwSpan.textContent = data.commessaweb; cwSpan.textContent = data.commessaweb;
@@ -164,7 +192,8 @@
const label = document.getElementById("exportResponseModalLabel"); const label = document.getElementById("exportResponseModalLabel");
if (data.success) { if (data.success) {
msg.innerHTML = `${data.message.replace(/\n/g, "<br>")}` + msg.innerHTML =
`${data.message.replace(/\n/g, "<br>")}` +
`<br>ID CommessaWeb: ${data.idcommessaweb}` + `<br>ID CommessaWeb: ${data.idcommessaweb}` +
`<br>Codice CommessaWeb: ${data.commessaweb}` + `<br>Codice CommessaWeb: ${data.commessaweb}` +
(data.totalPhotos > 0 ? `<br>Foto: ${data.totalPhotos}` : ""); (data.totalPhotos > 0 ? `<br>Foto: ${data.totalPhotos}` : "");
@@ -186,10 +215,14 @@
row.classList.remove("batch-disabled"); row.classList.remove("batch-disabled");
row.classList.add("batch-exporting"); row.classList.add("batch-exporting");
if (btnCell) { if (btnCell) {
btnCell.querySelectorAll(".action-btn").forEach(b => { b.dataset.prevDisplay = b.style.display; b.style.display = "none"; }); btnCell.querySelectorAll(".action-btn").forEach((b) => {
b.dataset.prevDisplay = b.style.display;
b.style.display = "none";
});
const spinner = document.createElement("span"); const spinner = document.createElement("span");
spinner.className = "batch-row-spinner"; spinner.className = "batch-row-spinner";
spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Exporting...'; spinner.innerHTML =
'<i class="fas fa-spinner fa-spin"></i> Exporting...';
btnCell.appendChild(spinner); btnCell.appendChild(spinner);
} }
} else { } else {
@@ -197,7 +230,10 @@
if (btnCell) { if (btnCell) {
const spinner = btnCell.querySelector(".batch-row-spinner"); const spinner = btnCell.querySelector(".batch-row-spinner");
if (spinner) spinner.remove(); if (spinner) spinner.remove();
btnCell.querySelectorAll(".action-btn").forEach(b => { b.style.display = b.dataset.prevDisplay || ""; delete b.dataset.prevDisplay; }); btnCell.querySelectorAll(".action-btn").forEach((b) => {
b.style.display = b.dataset.prevDisplay || "";
delete b.dataset.prevDisplay;
});
} }
} }
} }
@@ -213,32 +249,56 @@
errorEl.className = "batch-error-msg"; errorEl.className = "batch-error-msg";
errorEl.textContent = "Warning — click for details"; errorEl.textContent = "Warning — click for details";
errorEl.addEventListener("click", () => { errorEl.addEventListener("click", () => {
document.getElementById("exportResponseMessage").innerHTML = message.replace(/\n/g, "<br>"); document.getElementById("exportResponseMessage").innerHTML =
document.getElementById("exportResponseModalLabel").textContent = "Error (id: " + iddatadb + ")"; message.replace(/\n/g, "<br>");
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show(); document.getElementById(
"exportResponseModalLabel",
).textContent = "Error (id: " + iddatadb + ")";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
}); });
btnCell.appendChild(errorEl); btnCell.appendChild(errorEl);
} }
} }
function clearAllRowErrors() { function clearAllRowErrors() {
document.querySelectorAll(".grid-row.batch-row-error").forEach(row => { document
row.classList.remove("batch-row-error"); .querySelectorAll(".grid-row.batch-row-error")
const msg = row.querySelector(".batch-error-msg"); .forEach((row) => {
if (msg) msg.remove(); row.classList.remove("batch-row-error");
}); const msg = row.querySelector(".batch-error-msg");
if (msg) msg.remove();
});
} }
function disableAllRowButtons() { function disableAllRowButtons() {
document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.add("batch-disabled")); document
const toggle = document.querySelector(".actions-dropdown .dropdown-toggle"); .querySelectorAll(".grid-row[data-id]")
if (toggle) { toggle.disabled = true; toggle.style.opacity = "0.5"; toggle.style.pointerEvents = "none"; } .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() { function enableAllRowButtons() {
document.querySelectorAll(".grid-row[data-id]").forEach(row => row.classList.remove("batch-disabled")); document
const toggle = document.querySelector(".actions-dropdown .dropdown-toggle"); .querySelectorAll(".grid-row[data-id]")
if (toggle) { toggle.disabled = false; toggle.style.opacity = ""; toggle.style.pointerEvents = ""; } .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 ──────────────────── // ── Single row export: validate → confirm → send ────────────────────
@@ -250,40 +310,61 @@
if (gridRow) { if (gridRow) {
setRowExporting(gridRow, true); setRowExporting(gridRow, true);
const spinner = gridRow.querySelector(".batch-row-spinner"); const spinner = gridRow.querySelector(".batch-row-spinner");
if (spinner) spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Validating...'; if (spinner)
spinner.innerHTML =
'<i class="fas fa-spinner fa-spin"></i> Validating...';
} }
validateRows([{ iddatadb: parseInt(iddatadb), index: rowIndex }]) validateRows([{ iddatadb: parseInt(iddatadb), index: rowIndex }])
.then(validationData => { .then((validationData) => {
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); } if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
if (!validationData.success) { if (!validationData.success) {
showExportResult({ success: false, message: validationData.message || "Validation error" }); showExportResult({
success: false,
message: validationData.message || "Validation error",
});
return; return;
} }
const result = validationData.results[rowIndex]; const result = validationData.results[rowIndex];
if (result && !result.valid) { if (result && !result.valid) {
if (gridRow) showValidationErrors(gridRow, iddatadb, result.errors); if (gridRow)
showValidationErrors(gridRow, iddatadb, result.errors);
return; return;
} }
showConfirmAndExport(iddatadb, rowIndex); showConfirmAndExport(iddatadb, rowIndex);
}) })
.catch(error => { .catch((error) => {
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); } if (gridRow) {
showExportResult({ success: false, message: "Validation error: " + error.message }); setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
showExportResult({
success: false,
message: "Validation error: " + error.message,
});
}); });
} }
function showConfirmAndExport(iddatadb, rowIndex) { function showConfirmAndExport(iddatadb, rowIndex) {
const confirmModalElement = document.getElementById("exportConfirmModal"); const confirmModalElement =
document.getElementById("exportConfirmModal");
if (!confirmModalElement) return; if (!confirmModalElement) return;
const confirmModal = new bootstrap.Modal(confirmModalElement, { keyboard: false }); const confirmModal = new bootstrap.Modal(confirmModalElement, {
keyboard: false,
});
document.getElementById("exportIddatadb").textContent = iddatadb; document.getElementById("exportIddatadb").textContent = iddatadb;
confirmModal.show(); confirmModal.show();
const confirmBtn = document.getElementById("exportConfirmBtn"); const confirmBtn = document.getElementById("exportConfirmBtn");
if (!confirmBtn) { confirmModal.hide(); return; } if (!confirmBtn) {
confirmModal.hide();
return;
}
const confirmHandler = async () => { const confirmHandler = async () => {
pendingConfirmHandler = null; pendingConfirmHandler = null;
@@ -294,24 +375,36 @@
try { try {
const data = await sendExport(iddatadb); const data = await sendExport(iddatadb);
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); } if (gridRow) {
if (!data.success) showRowError(gridRow, iddatadb, data.message || "Unknown error"); setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
if (!data.success)
showRowError(
gridRow,
iddatadb,
data.message || "Unknown error",
);
showExportResult(data); showExportResult(data);
} catch (error) { } catch (error) {
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); } if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
showRowError(gridRow, iddatadb, error.message); showRowError(gridRow, iddatadb, error.message);
showExportResult({ success: false, message: error.message }); showExportResult({ success: false, message: error.message });
} }
}; };
if (pendingConfirmHandler) confirmBtn.removeEventListener("click", pendingConfirmHandler); if (pendingConfirmHandler)
confirmBtn.removeEventListener("click", pendingConfirmHandler);
pendingConfirmHandler = confirmHandler; pendingConfirmHandler = confirmHandler;
confirmBtn.addEventListener("click", confirmHandler, { once: true }); confirmBtn.addEventListener("click", confirmHandler, { once: true });
} }
// ── Single row click (event delegation) ───────────────────────────── // ── Single row click (event delegation) ─────────────────────────────
$(document).on('click', '.export-lims-btn', function (e) { $(document).on("click", ".export-lims-btn", function (e) {
e.preventDefault(); e.preventDefault();
if (batchRunning) return; if (batchRunning) return;
@@ -322,24 +415,34 @@
// Check unsaved changes for this row // Check unsaved changes for this row
const dataRow = window.gridData?.[rowIndex]; const dataRow = window.gridData?.[rowIndex];
if (dataRow && dataRow._dirty) { if (dataRow && dataRow._dirty) {
const unsavedModal = new bootstrap.Modal(document.getElementById("exportUnsavedModal"), { keyboard: false }); const unsavedModal = new bootstrap.Modal(
document.getElementById("exportUnsavedModal"),
{ keyboard: false },
);
unsavedModal.show(); unsavedModal.show();
document.getElementById("saveAndExportBtn")?.addEventListener("click", () => { document.getElementById("saveAndExportBtn")?.addEventListener(
unsavedModal.hide(); "click",
// Save first, then export () => {
const formData = window.buildSavePayload(rowIndex); unsavedModal.hide();
fetch('save_edited_row.php', { method: 'POST', body: formData }) // Save first, then export
.then(r => r.json()) const formData = window.buildSavePayload(rowIndex);
.then(result => { fetch("save_edited_row.php", {
if (result.success) { method: "POST",
dataRow._dirty = false; body: formData,
startExportConfirmFlow(iddatadb, rowIndex); })
} else { .then((r) => r.json())
alert('Save failed: ' + result.message); .then((result) => {
} if (result.success) {
}); dataRow._dirty = false;
}, { once: true }); startExportConfirmFlow(iddatadb, rowIndex);
} else {
alert("Save failed: " + result.message);
}
});
},
{ once: true },
);
return; return;
} }
@@ -349,18 +452,33 @@
// ── Batch export (Export All) ─────────────────────────────────────── // ── Batch export (Export All) ───────────────────────────────────────
function collectEligibleRows() { function collectEligibleRows() {
// Read from gridData, not DOM // Read from gridData, not DOM. Se i filtri sono attivi, limita al target set.
const target =
typeof window.getTargetGridIds === "function"
? window.getTargetGridIds()
: null;
const eligible = []; const eligible = [];
(window.gridData || []).forEach((row, index) => { (window.gridData || []).forEach((row, index) => {
if (row.status !== 'l') { if (row.status === "l") return;
eligible.push({ iddatadb: row.iddatadb, index, row: getGridRow(row.iddatadb) }); if (target && !target.has(parseInt(row.iddatadb, 10))) return;
} eligible.push({
iddatadb: row.iddatadb,
index,
row: getGridRow(row.iddatadb),
});
}); });
return eligible; return eligible;
} }
function hasUnsavedChanges() { function hasUnsavedChanges() {
return (window.gridData || []).some(r => r._dirty); const target =
typeof window.getTargetGridIds === "function"
? window.getTargetGridIds()
: null;
return (window.gridData || []).some(
(r) =>
r._dirty && (!target || target.has(parseInt(r.iddatadb, 10))),
);
} }
async function validateAndFilter(eligibleRows) { async function validateAndFilter(eligibleRows) {
@@ -369,7 +487,8 @@
index, index,
})); }));
const validationData = await validateRows(rowsToValidate); const validationData = await validateRows(rowsToValidate);
if (!validationData.success) throw new Error(validationData.message || "Validation error"); if (!validationData.success)
throw new Error(validationData.message || "Validation error");
const validRows = []; const validRows = [];
let invalidCount = 0; let invalidCount = 0;
@@ -377,7 +496,12 @@
for (const item of eligibleRows) { for (const item of eligibleRows) {
const result = validationData.results[item.index]; const result = validationData.results[item.index];
if (result && !result.valid) { if (result && !result.valid) {
if (item.row) showValidationErrors(item.row, item.iddatadb, result.errors); if (item.row)
showValidationErrors(
item.row,
item.iddatadb,
result.errors,
);
invalidCount++; invalidCount++;
} else { } else {
validRows.push(item); validRows.push(item);
@@ -410,54 +534,92 @@
if (validRows.length === 0) { if (validRows.length === 0) {
document.getElementById("exportResponseMessage").innerHTML = document.getElementById("exportResponseMessage").innerHTML =
`No valid rows for export.<br><strong>${invalidCount}</strong> rows with validation errors.`; `No valid rows for export.<br><strong>${invalidCount}</strong> rows with validation errors.`;
document.getElementById("exportResponseModalLabel").textContent = "Validation Failed"; document.getElementById(
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show(); "exportResponseModalLabel",
).textContent = "Validation Failed";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
return; return;
} }
const confirmModal = new bootstrap.Modal(document.getElementById("exportBatchConfirmModal"), { keyboard: false }); const confirmModal = new bootstrap.Modal(
document.getElementById("exportBatchConfirmModal"),
{ keyboard: false },
);
let countText = String(validRows.length); let countText = String(validRows.length);
if (invalidCount > 0) countText += ` (${invalidCount} excluded due to errors)`; if (invalidCount > 0)
document.getElementById("exportBatchCount").textContent = countText; countText += ` (${invalidCount} excluded due to errors)`;
document.getElementById("exportBatchCount").textContent =
countText;
confirmModal.show(); confirmModal.show();
const confirmBtn = document.getElementById("exportBatchConfirmBtn"); const confirmBtn = document.getElementById(
if (pendingBatchConfirmHandler) confirmBtn.removeEventListener("click", pendingBatchConfirmHandler); "exportBatchConfirmBtn",
);
if (pendingBatchConfirmHandler)
confirmBtn.removeEventListener(
"click",
pendingBatchConfirmHandler,
);
pendingBatchConfirmHandler = () => { pendingBatchConfirmHandler = () => {
pendingBatchConfirmHandler = null; pendingBatchConfirmHandler = null;
confirmModal.hide(); confirmModal.hide();
startBatchExport(validRows); startBatchExport(validRows);
}; };
confirmBtn.addEventListener("click", pendingBatchConfirmHandler, { once: true }); confirmBtn.addEventListener(
"click",
pendingBatchConfirmHandler,
{ once: true },
);
}) })
.catch(error => { .catch((error) => {
showValidationSpinner(false); showValidationSpinner(false);
document.getElementById("exportResponseMessage").textContent = "Validation error: " + error.message; document.getElementById("exportResponseMessage").textContent =
document.getElementById("exportResponseModalLabel").textContent = "Validation Error"; "Validation error: " + error.message;
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show(); document.getElementById(
"exportResponseModalLabel",
).textContent = "Validation Error";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
}); });
} }
$(document).on('click', '.export-all-lims-btn', function (e) { $(document).on("click", ".export-all-lims-btn", function (e) {
e.preventDefault(); e.preventDefault();
if (batchRunning) return; if (batchRunning) return;
if (hasUnsavedChanges()) { if (hasUnsavedChanges()) {
const unsavedModal = new bootstrap.Modal(document.getElementById("exportBatchUnsavedModal"), { keyboard: false }); const unsavedModal = new bootstrap.Modal(
document.getElementById("exportBatchUnsavedModal"),
{ keyboard: false },
);
unsavedModal.show(); unsavedModal.show();
document.getElementById("batchSaveAndExportBtn")?.addEventListener("click", () => { document.getElementById("batchSaveAndExportBtn")?.addEventListener(
unsavedModal.hide(); "click",
// Trigger save all first — listen for completion () => {
alert("Please Save All first, then Export All."); unsavedModal.hide();
}, { once: true }); // Trigger save all first — listen for completion
alert("Please Save All first, then Export All.");
},
{ once: true },
);
return; return;
} }
const eligibleRows = collectEligibleRows(); const eligibleRows = collectEligibleRows();
if (eligibleRows.length === 0) { if (eligibleRows.length === 0) {
document.getElementById("exportResponseMessage").textContent = "All rows already exported to LIMS."; document.getElementById("exportResponseMessage").textContent =
document.getElementById("exportResponseModalLabel").textContent = "Export All"; "All rows already exported to LIMS.";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show(); document.getElementById("exportResponseModalLabel").textContent =
"Export All";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
return; return;
} }
showBatchConfirm(eligibleRows); showBatchConfirm(eligibleRows);
@@ -468,7 +630,9 @@
batchRunning = true; batchRunning = true;
const batchUuid = crypto.randomUUID(); const batchUuid = crypto.randomUUID();
const total = eligibleRows.length; const total = eligibleRows.length;
let processed = 0, succeeded = 0, failed = 0; let processed = 0,
succeeded = 0,
failed = 0;
disableAllRowButtons(); disableAllRowButtons();
@@ -479,18 +643,25 @@
if (cancelBtn) cancelBtn.disabled = false; if (cancelBtn) cancelBtn.disabled = false;
if (statusEl) statusEl.textContent = `Exporting 0 / ${total}...`; if (statusEl) statusEl.textContent = `Exporting 0 / ${total}...`;
cancelBtn?.addEventListener("click", () => { cancelBtn?.addEventListener(
batchCancelled = true; "click",
if (statusEl) statusEl.textContent = "Cancelling... (waiting for current row)"; () => {
if (cancelBtn) cancelBtn.disabled = true; batchCancelled = true;
}, { once: true }); if (statusEl)
statusEl.textContent =
"Cancelling... (waiting for current row)";
if (cancelBtn) cancelBtn.disabled = true;
},
{ once: true },
);
(async () => { (async () => {
for (let i = 0; i < eligibleRows.length; i++) { for (let i = 0; i < eligibleRows.length; i++) {
if (batchCancelled) break; if (batchCancelled) break;
const { iddatadb, row } = eligibleRows[i]; const { iddatadb, row } = eligibleRows[i];
if (statusEl) statusEl.textContent = `Exporting ${processed + 1} / ${total} (id: ${iddatadb})...`; if (statusEl)
statusEl.textContent = `Exporting ${processed + 1} / ${total} (id: ${iddatadb})...`;
const gridRow = row || getGridRow(iddatadb); const gridRow = row || getGridRow(iddatadb);
if (gridRow) setRowExporting(gridRow, true); if (gridRow) setRowExporting(gridRow, true);
@@ -503,18 +674,29 @@
} else { } else {
failed++; failed++;
const errIdx = getRowIndexByIddatadb(iddatadb); const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0) window.gridData[errIdx]._exportError = data.message || "Unknown error"; if (errIdx >= 0)
if (gridRow) showRowError(gridRow, iddatadb, data.message || "Unknown error"); window.gridData[errIdx]._exportError =
data.message || "Unknown error";
if (gridRow)
showRowError(
gridRow,
iddatadb,
data.message || "Unknown error",
);
} }
} catch (error) { } catch (error) {
processed++; processed++;
failed++; failed++;
const errIdx = getRowIndexByIddatadb(iddatadb); const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0) window.gridData[errIdx]._exportError = error.message; if (errIdx >= 0)
window.gridData[errIdx]._exportError = error.message;
if (gridRow) showRowError(gridRow, iddatadb, error.message); if (gridRow) showRowError(gridRow, iddatadb, error.message);
} }
if (gridRow) { setRowExporting(gridRow, false); gridRow.classList.remove("batch-disabled"); } if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
} }
batchRunning = false; batchRunning = false;
@@ -529,7 +711,8 @@
(window.gridData || []).forEach((row, idx) => { (window.gridData || []).forEach((row, idx) => {
if (row._exportError) { if (row._exportError) {
const gridRow = getGridRow(row.iddatadb); const gridRow = getGridRow(row.iddatadb);
if (gridRow) showRowError(gridRow, row.iddatadb, row._exportError); if (gridRow)
showRowError(gridRow, row.iddatadb, row._exportError);
} }
}); });
@@ -549,7 +732,9 @@
const modalEl = document.getElementById("exportResponseModal"); const modalEl = document.getElementById("exportResponseModal");
new bootstrap.Modal(modalEl, { keyboard: false }).show(); new bootstrap.Modal(modalEl, { keyboard: false }).show();
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true }); modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, {
once: true,
});
})(); })();
} }
})(); })();
+6
View File
@@ -214,6 +214,12 @@
Duplica anche le analisi collegate Duplica anche le analisi collegate
</label> </label>
</div> </div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="cloneOverwriteCheckbox" checked>
<label class="form-check-label" for="cloneOverwriteCheckbox">
<strong>Sovrascrivi parti esistenti</strong> (elimina le parti già presenti nei record di destinazione prima di inserire)
</label>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Annulla</button> <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Annulla</button>
+16 -2
View File
@@ -555,6 +555,14 @@ $(document).ready(function () {
// =================== // ===================
function getVisiblePartsRecordList() { function getVisiblePartsRecordList() {
// Se i filtri sono attivi, usa la regola selezione/visibili di gridFilter.
if (typeof window.getTargetGridIds === "function") {
const target = window.getTargetGridIds();
if (target && target.size > 0) {
return [...target].map((v) => parseInt(v, 10)).filter(Boolean);
}
}
const listFromModal = $("#partsModal").data("visible-iddatadb-list"); const listFromModal = $("#partsModal").data("visible-iddatadb-list");
if (Array.isArray(listFromModal) && listFromModal.length > 0) { if (Array.isArray(listFromModal) && listFromModal.length > 0) {
@@ -2265,8 +2273,11 @@ $(document).ready(function () {
const sourceIddatadb = const sourceIddatadb =
parseInt($("#partsModal").data("iddatadb"), 10) || null; parseInt($("#partsModal").data("iddatadb"), 10) || null;
const visibleListRaw = // Target = selezione/visibili secondo gridFilter, con fallback alla lista del modal
$("#partsModal").data("visible-iddatadb-list") || []; let visibleListRaw =
typeof getVisiblePartsRecordList === "function"
? getVisiblePartsRecordList()
: $("#partsModal").data("visible-iddatadb-list") || [];
if (!sourceIddatadb) { if (!sourceIddatadb) {
const errorMsg = $( const errorMsg = $(
@@ -2308,6 +2319,7 @@ $(document).ready(function () {
); );
$("#cloneNotesCheckbox").prop("checked", true); $("#cloneNotesCheckbox").prop("checked", true);
$("#cloneAnalysesCheckbox").prop("checked", false); $("#cloneAnalysesCheckbox").prop("checked", false);
$("#cloneOverwriteCheckbox").prop("checked", true);
const modalInstance = new bootstrap.Modal( const modalInstance = new bootstrap.Modal(
document.getElementById("cloneConfirmModal"), document.getElementById("cloneConfirmModal"),
@@ -2323,6 +2335,7 @@ $(document).ready(function () {
const targetIds = $("#cloneConfirmModal").data("target-ids") || []; const targetIds = $("#cloneConfirmModal").data("target-ids") || [];
const cloneNotes = $("#cloneNotesCheckbox").is(":checked"); const cloneNotes = $("#cloneNotesCheckbox").is(":checked");
const cloneAnalyses = $("#cloneAnalysesCheckbox").is(":checked"); const cloneAnalyses = $("#cloneAnalysesCheckbox").is(":checked");
const cloneOverwrite = $("#cloneOverwriteCheckbox").is(":checked");
if (!sourceIddatadb || !targetIds.length) { if (!sourceIddatadb || !targetIds.length) {
return; return;
@@ -2351,6 +2364,7 @@ $(document).ready(function () {
target_iddatadb_list: targetIds, target_iddatadb_list: targetIds,
clone_notes: cloneNotes, clone_notes: cloneNotes,
clone_analyses: cloneAnalyses, clone_analyses: cloneAnalyses,
overwrite: cloneOverwrite,
}), }),
success: function (response) { success: function (response) {
$btn.prop("disabled", false).html(originalHtml); $btn.prop("disabled", false).html(originalHtml);
+77 -41
View File
@@ -1,8 +1,8 @@
/** /**
* saveAll.js — Save All functionality using gridData * saveAll.js — Save All functionality using gridData
*/ */
(function() { (function () {
'use strict'; "use strict";
let saveAllRunning = false; let saveAllRunning = false;
@@ -11,7 +11,7 @@
} }
// ── Save single row ────────────────────────────────────────────────── // ── Save single row ──────────────────────────────────────────────────
$(document).on('click', '.save-btn', async function() { $(document).on("click", ".save-btn", async function () {
const btn = this; const btn = this;
const rowIndex = parseInt(btn.dataset.row); const rowIndex = parseInt(btn.dataset.row);
const row = window.gridData?.[rowIndex]; const row = window.gridData?.[rowIndex];
@@ -23,29 +23,44 @@
try { try {
const formData = window.buildSavePayload(rowIndex); const formData = window.buildSavePayload(rowIndex);
const resp = await fetch('save_edited_row.php', { method: 'POST', body: formData }); const resp = await fetch("save_edited_row.php", {
method: "POST",
body: formData,
});
const result = await resp.json(); const result = await resp.json();
if (result.success) { if (result.success) {
row._dirty = false; row._dirty = false;
if (window.gridRenderer?.clearDirty) window.gridRenderer.clearDirty(rowIndex); if (window.gridRenderer?.clearDirty)
window.gridRenderer.clearDirty(rowIndex);
// Flash success on row without re-rendering (preserves Select2 state) // Flash success on row without re-rendering (preserves Select2 state)
const gridRow = document.querySelector(`.grid-row[data-id="${row.iddatadb}"]`); const gridRow = document.querySelector(
`.grid-row[data-id="${row.iddatadb}"]`,
);
if (gridRow) { if (gridRow) {
gridRow.classList.remove('row-dirty'); gridRow.classList.remove("row-dirty");
gridRow.querySelectorAll('.grid-cell').forEach(cell => { gridRow.querySelectorAll(".grid-cell").forEach((cell) => {
cell.classList.remove('cell-changed'); cell.classList.remove("cell-changed");
cell.classList.add('flash-success'); cell.classList.add("flash-success");
}); });
setTimeout(() => gridRow.querySelectorAll('.flash-success').forEach(c => c.classList.remove('flash-success')), 500); setTimeout(
() =>
gridRow
.querySelectorAll(".flash-success")
.forEach((c) =>
c.classList.remove("flash-success"),
),
500,
);
} }
const toastEl = document.getElementById('saveSuccessToast'); const toastEl = document.getElementById("saveSuccessToast");
if (toastEl) bootstrap.Toast.getOrCreateInstance(toastEl).show(); if (toastEl)
bootstrap.Toast.getOrCreateInstance(toastEl).show();
} else { } else {
alert('Errore: ' + result.message); alert("Errore: " + result.message);
} }
} catch (e) { } catch (e) {
alert('Errore: ' + e.message); alert("Errore: " + e.message);
} finally { } finally {
btn.innerHTML = origHtml; btn.innerHTML = origHtml;
btn.disabled = false; btn.disabled = false;
@@ -53,49 +68,68 @@
}); });
// ── Save All ───────────────────────────────────────────────────────── // ── Save All ─────────────────────────────────────────────────────────
$(document).on('click', '.save-all-btn', function(e) { $(document).on("click", ".save-all-btn", function (e) {
e.preventDefault(); e.preventDefault();
if (isBusy()) return; if (isBusy()) return;
const modalEl = document.getElementById('saveAllConfirmModal'); const modalEl = document.getElementById("saveAllConfirmModal");
if (!modalEl) return; if (!modalEl) return;
new bootstrap.Modal(modalEl, { keyboard: false }).show(); new bootstrap.Modal(modalEl, { keyboard: false }).show();
}); });
$(document).on('click', '#saveAllConfirmBtn', async function() { $(document).on("click", "#saveAllConfirmBtn", async function () {
const confirmModal = bootstrap.Modal.getInstance(document.getElementById('saveAllConfirmModal')); const confirmModal = bootstrap.Modal.getInstance(
document.getElementById("saveAllConfirmModal"),
);
if (confirmModal) confirmModal.hide(); if (confirmModal) confirmModal.hide();
saveAllRunning = true; saveAllRunning = true;
const bar = document.getElementById('batchExportBar'); const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById('batchExportStatus'); const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById('exportBatchCancelBtn'); const cancelBtn = document.getElementById("exportBatchCancelBtn");
if (bar) bar.style.display = ''; if (bar) bar.style.display = "";
if (cancelBtn) cancelBtn.style.display = 'none'; if (cancelBtn) cancelBtn.style.display = "none";
if (statusEl) statusEl.textContent = 'Saving...'; if (statusEl) statusEl.textContent = "Saving...";
const data = window.gridData || []; const data = window.gridData || [];
const dirtyRows = data.map((r, i) => r._dirty ? i : -1).filter(i => i >= 0); const target =
typeof window.getTargetGridIds === "function"
? window.getTargetGridIds()
: null;
const dirtyRows = data
.map((r, i) => (r._dirty ? i : -1))
.filter((i) => i >= 0)
.filter(
(i) => !target || target.has(parseInt(data[i].iddatadb, 10)),
);
if (dirtyRows.length === 0) { if (dirtyRows.length === 0) {
saveAllRunning = false; saveAllRunning = false;
if (bar) bar.style.display = 'none'; if (bar) bar.style.display = "none";
const msgEl = document.getElementById('saveAllResultMessage'); const msgEl = document.getElementById("saveAllResultMessage");
if (msgEl) msgEl.textContent = 'No changes to save.'; if (msgEl) msgEl.textContent = "No changes to save.";
new bootstrap.Modal(document.getElementById('saveAllResultModal')).show(); new bootstrap.Modal(
document.getElementById("saveAllResultModal"),
).show();
return; return;
} }
let success = 0, fail = 0; let success = 0,
fail = 0;
for (const idx of dirtyRows) { for (const idx of dirtyRows) {
if (statusEl) statusEl.textContent = `Saving ${success + fail + 1} / ${dirtyRows.length}...`; if (statusEl)
statusEl.textContent = `Saving ${success + fail + 1} / ${dirtyRows.length}...`;
try { try {
const formData = window.buildSavePayload(idx); const formData = window.buildSavePayload(idx);
const resp = await fetch('save_edited_row.php', { method: 'POST', body: formData }); const resp = await fetch("save_edited_row.php", {
method: "POST",
body: formData,
});
const result = await resp.json(); const result = await resp.json();
if (result.success) { if (result.success) {
data[idx]._dirty = false; data[idx]._dirty = false;
if (window.gridRenderer?.clearDirty) window.gridRenderer.clearDirty(idx); if (window.gridRenderer?.clearDirty)
window.gridRenderer.clearDirty(idx);
success++; success++;
} else { } else {
fail++; fail++;
@@ -106,22 +140,24 @@
} }
saveAllRunning = false; saveAllRunning = false;
if (bar) bar.style.display = 'none'; if (bar) bar.style.display = "none";
const gr = window.gridRenderer; const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows(); if (gr) gr.renderVisibleRows();
const msg = `Saved: ${success}` + (fail > 0 ? `, Errors: ${fail}` : ''); const msg = `Saved: ${success}` + (fail > 0 ? `, Errors: ${fail}` : "");
const msgEl = document.getElementById('saveAllResultMessage'); const msgEl = document.getElementById("saveAllResultMessage");
if (msgEl) msgEl.textContent = msg; if (msgEl) msgEl.textContent = msg;
new bootstrap.Modal(document.getElementById('saveAllResultModal')).show(); new bootstrap.Modal(
document.getElementById("saveAllResultModal"),
).show();
}); });
// ── beforeunload ───────────────────────────────────────────────────── // ── beforeunload ─────────────────────────────────────────────────────
window.addEventListener('beforeunload', function(e) { window.addEventListener("beforeunload", function (e) {
if (window.gridData && window.gridData.some(r => r._dirty)) { if (window.gridData && window.gridData.some((r) => r._dirty)) {
e.preventDefault(); e.preventDefault();
e.returnValue = ''; e.returnValue = "";
} }
}); });