1,2,3,4,5,6 points of milestone
This commit is contained in:
parent
c516589483
commit
1b97bf4362
@ -206,21 +206,22 @@ class VisualLimsApiClient
|
||||
/**
|
||||
* POST a file as multipart/form-data (used for photo/attachment uploads).
|
||||
*
|
||||
* @param string $endpoint OData endpoint, e.g. "Campione(613388)/UploadCampioneFile"
|
||||
* @param string $filePath Absolute path to the file on disk
|
||||
* @param string $fileName Original file name to send
|
||||
* @return array|null Decoded JSON response
|
||||
* @param string $endpoint OData endpoint, e.g. "Campione(613388)/UploadCampioneFile"
|
||||
* @param string $filePath Absolute path to the file on disk
|
||||
* @param string $fileName Original file name to send
|
||||
* @param array $extraFields Additional form fields to include
|
||||
* @return array|null Decoded JSON response
|
||||
*/
|
||||
public function postMultipart($endpoint, $filePath, $fileName)
|
||||
public function postMultipart($endpoint, $filePath, $fileName, array $extraFields = [])
|
||||
{
|
||||
$token = $this->getToken();
|
||||
$url = "{$this->baseUrl}/api/odata/{$endpoint}";
|
||||
|
||||
$cfile = new CURLFile($filePath, mime_content_type($filePath) ?: 'application/octet-stream', $fileName);
|
||||
|
||||
$payload = [
|
||||
$payload = array_merge($extraFields, [
|
||||
'file' => $cfile,
|
||||
];
|
||||
]);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
@ -126,7 +126,7 @@ class VisualLimsApiClientMock
|
||||
return [];
|
||||
}
|
||||
|
||||
public function postMultipart(string $endpoint, string $filePath, string $fileName): array
|
||||
public function postMultipart(string $endpoint, string $filePath, string $fileName, array $extraFields = []): array
|
||||
{
|
||||
error_log("[SIMULATE] POST multipart {$endpoint} file={$fileName}");
|
||||
|
||||
|
||||
@ -4,13 +4,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const exportButtons = document.querySelectorAll(".export-lims-btn");
|
||||
console.log(`Found ${exportButtons.length} export-lims-btn buttons`);
|
||||
|
||||
if (exportButtons.length === 0) {
|
||||
console.warn("No .export-lims-btn buttons found in the DOM");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
@ -22,10 +22,340 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (overlay) overlay.style.display = "none";
|
||||
}
|
||||
|
||||
// ── Step 2: show export-confirm modal, send on "Conferma" ────────────────
|
||||
// ── 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, "<br>")}` +
|
||||
`<br>ID CommessaWeb: ${data.idcommessaweb}` +
|
||||
`<br>Codice CommessaWeb: ${data.commessaweb}` +
|
||||
(data.totalPhotos > 0
|
||||
? `<br>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 =
|
||||
'<i class="fas fa-spinner fa-spin"></i> 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, "<br>");
|
||||
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 confirmModalElement = document.getElementById("exportConfirmModal");
|
||||
const gridRow = btn.closest(".grid-row");
|
||||
const rowIndex = btn.dataset.row;
|
||||
|
||||
// Validate first
|
||||
clearValidationErrors();
|
||||
|
||||
validateRows([{ iddatadb: parseInt(iddatadb), index: parseInt(rowIndex) }])
|
||||
.then((validationData) => {
|
||||
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) => {
|
||||
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;
|
||||
@ -49,118 +379,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log(`Confirmed export for iddatadb: ${iddatadb}`);
|
||||
confirmModal.hide();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("iddatadb", iddatadb);
|
||||
|
||||
try {
|
||||
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();
|
||||
|
||||
const gridRow = btn.closest(".grid-row");
|
||||
const data = await sendExport(iddatadb, gridRow);
|
||||
console.log("Export response:", 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, "<br>")}` +
|
||||
`<br>ID CommessaWeb: ${data.idcommessaweb}` +
|
||||
`<br>Codice CommessaWeb: ${data.commessaweb}` +
|
||||
(data.totalPhotos > 0
|
||||
? `<br>Foto trovate: ${data.totalPhotos}`
|
||||
: "");
|
||||
document.getElementById(
|
||||
"exportResponseModalLabel",
|
||||
).textContent = "Esportazione Completata";
|
||||
responseModal.show();
|
||||
|
||||
// Update status badge
|
||||
const gridRow = btn.closest(".grid-row");
|
||||
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;
|
||||
}
|
||||
} 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 },
|
||||
);
|
||||
showExportResult(data);
|
||||
} catch (error) {
|
||||
console.error("Export error:", error);
|
||||
const responseModalElement =
|
||||
document.getElementById("exportResponseModal");
|
||||
if (!responseModalElement) {
|
||||
alert("Errore: Modale di risposta non trovato");
|
||||
return;
|
||||
}
|
||||
const responseModal = new bootstrap.Modal(
|
||||
responseModalElement,
|
||||
{ keyboard: false },
|
||||
);
|
||||
document.getElementById(
|
||||
"exportResponseMessage",
|
||||
).textContent = `Errore: ${error.message}`;
|
||||
document.getElementById(
|
||||
"exportResponseModalLabel",
|
||||
).textContent = "Errore Esportazione";
|
||||
responseModal.show();
|
||||
responseModalElement.addEventListener(
|
||||
"hidden.bs.modal",
|
||||
cleanupBackdrop,
|
||||
{ once: true },
|
||||
);
|
||||
showExportResult({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -171,11 +400,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
confirmBtn.addEventListener("click", confirmHandler, { once: true });
|
||||
}
|
||||
|
||||
// ── Step 1: check unsaved changes, save if needed, then export ───────────
|
||||
// ── 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(
|
||||
@ -191,31 +422,352 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
);
|
||||
unsavedModal.show();
|
||||
|
||||
document.getElementById("saveAndExportBtn").addEventListener("click", () => {
|
||||
unsavedModal.hide();
|
||||
const saveBtn = gridRow.querySelector(".save-btn");
|
||||
if (!saveBtn) return;
|
||||
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"],
|
||||
});
|
||||
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 });
|
||||
saveBtn.click();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// No unsaved changes — go straight to export confirm
|
||||
// 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.<br>` +
|
||||
`<strong>${invalidCount}</strong> 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: <strong>${succeeded}</strong><br>` +
|
||||
`Errori: <strong>${failed}</strong><br>` +
|
||||
`Non processate: <strong>${total - processed}</strong>`;
|
||||
} else if (failed === 0) {
|
||||
labelEl.textContent = "Export All — Completato";
|
||||
msgEl.innerHTML = `Tutte le <strong>${succeeded}</strong> righe esportate con successo.`;
|
||||
} else {
|
||||
labelEl.textContent = "Export All — Completato con errori";
|
||||
msgEl.innerHTML =
|
||||
`Esportate: <strong>${succeeded}</strong><br>` +
|
||||
`Errori: <strong>${failed}</strong>`;
|
||||
}
|
||||
|
||||
const modalEl = document.getElementById("exportResponseModal");
|
||||
const modal = new bootstrap.Modal(modalEl, { keyboard: false });
|
||||
modal.show();
|
||||
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
|
||||
})();
|
||||
}
|
||||
});
|
||||
|
||||
@ -18,6 +18,26 @@ $uploadDir = realpath(__DIR__ . '/../photostrf') . '/';
|
||||
// 🔹 Base URL API
|
||||
$apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
|
||||
|
||||
// 🔹 Batch UUID — if present, all logs go to a single file
|
||||
$batchUuid = $_POST['batch_uuid'] ?? null;
|
||||
|
||||
$writeLog = (function () use ($batchUuid, $logDir) {
|
||||
$batchLogFile = $batchUuid ? $logDir . "batch_export_{$batchUuid}.log" : null;
|
||||
|
||||
return function ($individualPath, $content, $stepLabel = null) use ($batchLogFile) {
|
||||
if ($batchLogFile) {
|
||||
$header = "\n" . str_repeat("=", 60) . "\n";
|
||||
if ($stepLabel) {
|
||||
$header .= "[{$stepLabel}] " . date('Y-m-d H:i:s') . "\n";
|
||||
}
|
||||
$header .= str_repeat("=", 60) . "\n";
|
||||
file_put_contents($batchLogFile, $header . $content . "\n", FILE_APPEND);
|
||||
} else {
|
||||
file_put_contents($individualPath, $content);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
// 🔹 Funzione per validare e convertire date
|
||||
function validateDate($value)
|
||||
{
|
||||
@ -35,6 +55,11 @@ try {
|
||||
throw new Exception("Missing iddatadb");
|
||||
}
|
||||
|
||||
// TEMP: simulate error on every other row for testing
|
||||
if (env('SIMULATE_EXPORT_LIMS') && $iddatadb % 2 === 0) {
|
||||
throw new Exception("Simulated error for iddatadb $iddatadb");
|
||||
}
|
||||
|
||||
// 🔹 STEP 1+2: Fetch Cliente ID from datadb and Schema ID from excel_templates
|
||||
// Also fetch fixed fields stored in datadb
|
||||
$stmt = $pdo->prepare("
|
||||
@ -77,6 +102,23 @@ try {
|
||||
$stmt->execute(['iddatadb' => $iddatadb]);
|
||||
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 🔹 STEP 4a: Auto-populate export_date / export_time fields
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE import_data_details idd
|
||||
JOIN template_mapping m ON idd.mapping_id = m.id
|
||||
SET idd.field_value = CASE m.auto_value
|
||||
WHEN 'export_date' THEN :export_date
|
||||
WHEN 'export_time' THEN :export_time
|
||||
END
|
||||
WHERE idd.id = :iddatadb
|
||||
AND m.auto_value IN ('export_date', 'export_time')
|
||||
");
|
||||
$stmt->execute([
|
||||
'iddatadb' => $iddatadb,
|
||||
'export_date' => date('Y-m-d'),
|
||||
'export_time' => date('H:i'),
|
||||
]);
|
||||
|
||||
// 🔹 STEP 4: Fetch Field Values with Labels
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
@ -139,7 +181,7 @@ try {
|
||||
|
||||
// Salva log
|
||||
$logFileStep5 = $logDir . "commessa_create_step5_" . $iddatadb . "_" . time() . ".txt";
|
||||
file_put_contents($logFileStep5, $logContentStep5);
|
||||
$writeLog($logFileStep5, $logContentStep5, "STEP 5 - Create CommessaWeb (iddatadb={$iddatadb})");
|
||||
|
||||
$commessaId = $commessaWeb["IdCommessa"];
|
||||
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
|
||||
@ -187,11 +229,11 @@ try {
|
||||
|
||||
// Salva log per STEP 6
|
||||
$logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt";
|
||||
file_put_contents($logFileStep6, $logContentStep6);
|
||||
$writeLog($logFileStep6, $logContentStep6, "STEP 6 - Campioni (commessa={$commessaId})");
|
||||
|
||||
// 🔹 STEP 6.1: Fetch photos linked to this iddatadb
|
||||
$stmtPhotos = $pdo->prepare("
|
||||
SELECT id, file_path, file_name
|
||||
SELECT id, file_path, file_name, StampaNelRapporto, PrimaPagina
|
||||
FROM datadb_photos
|
||||
WHERE iddatadb = :iddatadb
|
||||
ORDER BY id ASC
|
||||
@ -222,11 +264,20 @@ try {
|
||||
}
|
||||
|
||||
$photoEndpoint = "Campione({$campioneId})/UploadCampioneFile";
|
||||
$stampaNelRapporto = !empty($photo['StampaNelRapporto']) ? 'true' : 'false';
|
||||
$primaPagina = !empty($photo['PrimaPagina']) ? 'true' : 'false';
|
||||
|
||||
$logContentPhotos .= "curl --location --request POST '{$apiBaseUrl}{$photoEndpoint}' \\\n" .
|
||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||
"--form 'file=@{$fullPath}'\n\n";
|
||||
"--form 'file=@{$fullPath}' \\\n" .
|
||||
"--form 'StampaNelRapporto={$stampaNelRapporto}' \\\n" .
|
||||
"--form 'PrimaPagina={$primaPagina}'\n\n";
|
||||
|
||||
$photoResult = $api->postMultipart($photoEndpoint, $fullPath, $photo['file_name']);
|
||||
$extraFields = [
|
||||
'StampaNelRapporto' => $stampaNelRapporto,
|
||||
'PrimaPagina' => $primaPagina,
|
||||
];
|
||||
$photoResult = $api->postMultipart($photoEndpoint, $fullPath, $photo['file_name'], $extraFields);
|
||||
$logContentPhotos .= "RESPONSE:\n" . json_encode($photoResult, JSON_PRETTY_PRINT) . "\n\n---\n";
|
||||
$photosUploaded++;
|
||||
}
|
||||
@ -238,7 +289,7 @@ try {
|
||||
}
|
||||
|
||||
$logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt";
|
||||
file_put_contents($logFilePhotos, $logContentPhotos);
|
||||
$writeLog($logFilePhotos, $logContentPhotos, "STEP 6.2 - Photos (commessa={$commessaId})");
|
||||
|
||||
// 🔹 STEP 7: Update Custom Fields for CommessaWeb
|
||||
if (!empty($fieldValues)) {
|
||||
@ -251,7 +302,7 @@ try {
|
||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT);
|
||||
$logFileGet = $logDir . "commessa_{$commessaId}_get_step7_" . time() . ".txt";
|
||||
file_put_contents($logFileGet, $logContentGet);
|
||||
$writeLog($logFileGet, $logContentGet, "STEP 7 - GET CustomFields (commessa={$commessaId})");
|
||||
|
||||
// Prepara payload PATCH
|
||||
$commessaCustomFields = [];
|
||||
@ -288,7 +339,7 @@ try {
|
||||
|
||||
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
|
||||
$logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt";
|
||||
file_put_contents($logFileStep7, $logContentStep7);
|
||||
$writeLog($logFileStep7, $logContentStep7, "STEP 7 - PATCH CustomFields (commessa={$commessaId})");
|
||||
}
|
||||
}
|
||||
|
||||
@ -315,7 +366,7 @@ try {
|
||||
"--data '{}'\n\n" .
|
||||
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
|
||||
$logFileStep9 = $logDir . "commessa_{$commessaId}_send_step9_" . time() . ".txt";
|
||||
file_put_contents($logFileStep9, $logContentStep9);
|
||||
$writeLog($logFileStep9, $logContentStep9, "STEP 9 - InviaCommessa (commessa={$commessaId})");
|
||||
|
||||
|
||||
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto)
|
||||
@ -332,7 +383,7 @@ try {
|
||||
"--data '{$importPayloadLog}'\n\n" .
|
||||
"RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
|
||||
$logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
|
||||
file_put_contents($logFileStep91, $logContentStep91);
|
||||
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId})");
|
||||
|
||||
// 🔹 STEP 10: GET di controllo post-PATCH
|
||||
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
||||
@ -343,7 +394,7 @@ try {
|
||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
|
||||
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
|
||||
file_put_contents($logFileStep10, $logContentStep10);
|
||||
$writeLog($logFileStep10, $logContentStep10, "STEP 10 - GET verify (commessa={$commessaId})");
|
||||
|
||||
// 🔹 STEP 11: Prepare final response
|
||||
$finalCommessa = [
|
||||
|
||||
@ -347,6 +347,118 @@ function fixedDefaultValue(array $f): string
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.grid-row.batch-exporting {
|
||||
background: linear-gradient(90deg, #fff0f0 0%, #ffe0e0 50%, #fff0f0 100%) !important;
|
||||
background-size: 200% 100% !important;
|
||||
animation: batch-pulse 1.5s ease-in-out infinite;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.grid-row.batch-exporting .button-cell {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
@keyframes batch-pulse {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
.batch-row-spinner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #eb0b0b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.batch-row-spinner i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.grid-row.batch-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.grid-row.batch-disabled .action-btn {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.grid-row.batch-row-error {
|
||||
background: #fff3f3 !important;
|
||||
border-left: 3px solid #dc3545;
|
||||
}
|
||||
|
||||
.grid-row.batch-row-error .button-cell {
|
||||
background: #fff3f3 !important;
|
||||
}
|
||||
|
||||
.batch-error-msg {
|
||||
color: #dc3545;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
display: block;
|
||||
padding: 2px 0;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.batch-error-msg:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.grid-row.validation-row-error {
|
||||
background: #fff3f3 !important;
|
||||
border-left: 3px solid #dc3545;
|
||||
}
|
||||
|
||||
.grid-cell.validation-error {
|
||||
background-color: #f8d7da !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-validation-error,
|
||||
.input-validation-error:focus {
|
||||
border: 2px solid #dc3545 !important;
|
||||
background-color: #fff5f5 !important;
|
||||
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.validation-tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
z-index: 1050;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.validation-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: #dc3545;
|
||||
}
|
||||
|
||||
.grid-cell.validation-error:hover .validation-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.grid-header,
|
||||
.grid-cell {
|
||||
flex: 1;
|
||||
@ -472,7 +584,10 @@ function fixedDefaultValue(array $f): string
|
||||
|
||||
.grid-top .save-all-cell {
|
||||
flex: 0 0 210px;
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.grid-top .grid-cell input,
|
||||
@ -633,29 +748,51 @@ function fixedDefaultValue(array $f): string
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.save-all-btn {
|
||||
background-color: #28a745;
|
||||
.actions-dropdown .dropdown-toggle {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 8px 16px;
|
||||
padding: 6px 14px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.save-all-btn:hover {
|
||||
background-color: #218838;
|
||||
.actions-dropdown .dropdown-toggle:hover,
|
||||
.actions-dropdown .dropdown-toggle:focus {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
.actions-dropdown .dropdown-menu {
|
||||
min-width: 160px;
|
||||
font-size: 13px;
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
|
||||
.actions-dropdown .dropdown-item i {
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
#exportConfirmModal,
|
||||
#exportResponseModal,
|
||||
#exportUnsavedModal {
|
||||
#exportUnsavedModal,
|
||||
#exportBatchConfirmModal,
|
||||
#exportBatchUnsavedModal,
|
||||
#saveAllConfirmModal,
|
||||
#saveAllResultModal {
|
||||
z-index: 1300 !important;
|
||||
}
|
||||
|
||||
#exportConfirmModal .modal-backdrop,
|
||||
#exportResponseModal .modal-backdrop,
|
||||
#exportUnsavedModal .modal-backdrop {
|
||||
#exportUnsavedModal .modal-backdrop,
|
||||
#exportBatchConfirmModal .modal-backdrop,
|
||||
#exportBatchUnsavedModal .modal-backdrop,
|
||||
#saveAllConfirmModal .modal-backdrop,
|
||||
#saveAllResultModal .modal-backdrop {
|
||||
z-index: 1299 !important;
|
||||
}
|
||||
|
||||
@ -745,21 +882,36 @@ function fixedDefaultValue(array $f): string
|
||||
</div>
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<div id="unsavedChanges" style="display:none; color: red; font-weight: bold; margin:10px 0;">
|
||||
⚠️ Unsaved changes detected! Please save before leaving this page.<br>
|
||||
<div class="d-flex align-items-center" style="min-height: 42px; gap: 12px;">
|
||||
<div class="dropdown actions-dropdown" style="flex-shrink: 0;">
|
||||
<button class="dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fas fa-cogs"></i> Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<?php if ((Auth::user()->hasRole('Admin'))) : ?>
|
||||
<li><a class="dropdown-item export-all-lims-btn" href="#"><i class="fas fa-upload" style="color: #eb0b0b;"></i>Export All</a></li>
|
||||
<?php endif; ?>
|
||||
<li><a class="dropdown-item save-all-btn" href="#"><i class="fas fa-save" style="color: #28a745;"></i>Save All</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="unsavedChanges" style="display:none; color: red; font-weight: bold; font-size: 13px;">
|
||||
⚠️ Unsaved changes detected!
|
||||
<span id="changedRows" style="font-weight:normal; color:darkred;"></span>
|
||||
</div>
|
||||
|
||||
<div id="batchExportBar" style="display:none; flex: 1; min-width: 0;">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="fas fa-spinner fa-spin" style="color: #eb0b0b;"></i>
|
||||
<span id="batchExportStatus" style="font-size: 13px; font-weight: 500;">Esportazione...</span>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" id="exportBatchCancelBtn" style="padding: 2px 10px; font-size: 12px;">Annulla</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="editForm">
|
||||
<div class="grid-container">
|
||||
<div class="grid-top">
|
||||
<div class="grid-cell save-all-cell">
|
||||
<button type="button" class="save-all-btn" title="Save All Rows"><i class="fas fa-save"></i> Save All</button>
|
||||
</div>
|
||||
<div class="grid-cell save-all-cell"></div>
|
||||
<?php
|
||||
|
||||
$topColIndex = 1;
|
||||
@ -1025,7 +1177,8 @@ function fixedDefaultValue(array $f): string
|
||||
<div class="grid-cell button-cell" style="flex: 0 0 210px; position: relative;">
|
||||
<!-- commented only for admin roles -->
|
||||
<?php if ((Auth::user()->hasRole('Admin'))) : ?>
|
||||
<button type="button" class="export-lims-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" title="Export to LIMS" style="background: #eb0b0bff; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-upload"></i></button>
|
||||
<?php $isExported = (($row['status'] ?? '') === 'l'); ?>
|
||||
<button type="button" class="export-lims-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" title="<?= $isExported ? 'Già esportato' : 'Export to LIMS' ?>" style="background: <?= $isExported ? '#ccc' : '#eb0b0b' ?>; color: white; border: none; border-radius: 5px; cursor: <?= $isExported ? 'not-allowed' : 'pointer' ?>; <?= $isExported ? 'opacity: 0.5;' : '' ?>"<?= $isExported ? ' disabled' : '' ?>><i class="fas fa-upload"></i></button>
|
||||
<?php endif; ?>
|
||||
|
||||
<button type="button" class="save-btn action-btn" data-row="<?= $index ?>" title="Save" style="background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-save"></i></button>
|
||||
@ -1349,6 +1502,19 @@ function fixedDefaultValue(array $f): string
|
||||
// (se vuoi mantenere highlight cella gialla, lascia questa riga)
|
||||
if (gridCell) gridCell.classList.add("cell-changed");
|
||||
|
||||
// Clear validation error on this field when user edits it
|
||||
el.classList.remove("input-validation-error");
|
||||
if (gridCell) {
|
||||
gridCell.classList.remove("validation-error");
|
||||
const tooltip = gridCell.querySelector(".validation-tooltip");
|
||||
if (tooltip) tooltip.remove();
|
||||
// If no more validation errors on the row, remove row highlight
|
||||
const gridRow = gridCell.closest(".grid-row");
|
||||
if (gridRow && !gridRow.querySelector(".validation-error")) {
|
||||
gridRow.classList.remove("validation-row-error");
|
||||
}
|
||||
}
|
||||
|
||||
renderChangedRows();
|
||||
});
|
||||
});
|
||||
@ -1446,21 +1612,65 @@ function fixedDefaultValue(array $f): string
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('.save-all-btn').addEventListener('click', async () => {
|
||||
const rows = document.querySelectorAll('.grid-row');
|
||||
let saveAllRunning = false;
|
||||
document.querySelector('.save-all-btn').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (saveAllRunning || window.batchRunning) return;
|
||||
const confirmModal = new bootstrap.Modal(document.getElementById('saveAllConfirmModal'), { keyboard: false });
|
||||
confirmModal.show();
|
||||
});
|
||||
|
||||
document.getElementById('saveAllConfirmBtn').addEventListener('click', async () => {
|
||||
bootstrap.Modal.getInstance(document.getElementById('saveAllConfirmModal')).hide();
|
||||
saveAllRunning = true;
|
||||
|
||||
// Clear previous row errors
|
||||
document.querySelectorAll('.grid-row.batch-row-error').forEach(r => {
|
||||
r.classList.remove('batch-row-error');
|
||||
const msg = r.querySelector('.batch-error-msg');
|
||||
if (msg) msg.remove();
|
||||
});
|
||||
|
||||
// Disable all row buttons
|
||||
document.querySelectorAll('.grid-row[data-id]').forEach(r => r.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'; }
|
||||
|
||||
// Show status bar
|
||||
const bar = document.getElementById('batchExportBar');
|
||||
const statusEl = document.getElementById('batchExportStatus');
|
||||
const cancelBtn = document.getElementById('exportBatchCancelBtn');
|
||||
bar.style.display = '';
|
||||
cancelBtn.style.display = 'none';
|
||||
|
||||
const rows = document.querySelectorAll('.grid-row[data-id]');
|
||||
const total = rows.length;
|
||||
let processed = 0;
|
||||
let successCount = 0;
|
||||
let errorMessages = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const saveBtn = row.querySelector('.save-btn');
|
||||
if (!saveBtn) {
|
||||
continue;
|
||||
}
|
||||
if (!saveBtn) { continue; }
|
||||
const rowIndex = saveBtn.dataset.row;
|
||||
const iddatadb = row.getAttribute('data-id');
|
||||
if (!rowIndex || !iddatadb) {
|
||||
continue;
|
||||
if (!rowIndex || !iddatadb) { continue; }
|
||||
|
||||
processed++;
|
||||
statusEl.textContent = `Salvataggio ${processed} / ${total} (id: ${iddatadb})...`;
|
||||
|
||||
// Highlight current row
|
||||
row.classList.remove('batch-disabled');
|
||||
row.classList.add('batch-exporting');
|
||||
const btnCell = row.querySelector('.button-cell');
|
||||
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 = '<i class="fas fa-spinner fa-spin"></i> Saving...';
|
||||
btnCell.appendChild(spinner);
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
const inputs = row.querySelectorAll(`input[name^="rows[${rowIndex}][details]"], select[name^="rows[${rowIndex}][details]"]`);
|
||||
@ -1479,12 +1689,11 @@ function fixedDefaultValue(array $f): string
|
||||
if (idclientSelect) {
|
||||
formData.append('idclient', idclientSelect.value);
|
||||
}
|
||||
// ---- FIXED FIELDS (NEW) ----
|
||||
// ---- FIXED FIELDS ----
|
||||
const fixedInputs = row.querySelectorAll(`input[name^="rows[${rowIndex}]["], select[name^="rows[${rowIndex}]["]`);
|
||||
fixedInputs.forEach(inp => {
|
||||
if (!inp.classList.contains('fixed-input')) return;
|
||||
const m = inp.name.match(/rows\[\d+\]\[([^\]]+)\]/);
|
||||
// Map: fixed key (logical) -> datadb real column
|
||||
const fixedAliasMap = {
|
||||
ClienteResponsabile: 'cliente_responsabile_id',
|
||||
MoltiplicatorePrezzo: 'moltiplicatore_prezzo_id',
|
||||
@ -1492,13 +1701,10 @@ function fixedDefaultValue(array $f): string
|
||||
AnagraficaCertestService: 'anagrafica_certest_service_id',
|
||||
ConsegnaRichiesta: 'consegna_richiesta'
|
||||
};
|
||||
|
||||
if (m && m[1]) {
|
||||
const logicalKey = m[1];
|
||||
const realKey = fixedAliasMap[logicalKey] || logicalKey;
|
||||
const realKey = fixedAliasMap[m[1]] || m[1];
|
||||
formData.append(realKey, inp.value);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
formData.append('iddatadb', iddatadb);
|
||||
@ -1524,24 +1730,59 @@ function fixedDefaultValue(array $f): string
|
||||
changedRows.delete(rowIndex);
|
||||
document.querySelectorAll(`.grid-cell[data-row="${rowIndex}"]`)
|
||||
.forEach(cell => cell.classList.remove("cell-changed"));
|
||||
|
||||
} else {
|
||||
errorMessages.push(`Riga ${parseInt(rowIndex) + 1}: ${data.message}`);
|
||||
// Show error on the row
|
||||
row.classList.add('batch-row-error');
|
||||
if (btnCell) {
|
||||
const errEl = document.createElement('div');
|
||||
errEl.className = 'batch-error-msg';
|
||||
errEl.textContent = '⚠ ' + (data.message || 'Errore');
|
||||
btnCell.appendChild(errEl);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessages.push(`Riga ${parseInt(rowIndex) + 1}: ${error.message}`);
|
||||
row.classList.add('batch-row-error');
|
||||
if (btnCell) {
|
||||
const errEl = document.createElement('div');
|
||||
errEl.className = 'batch-error-msg';
|
||||
errEl.textContent = '⚠ ' + error.message;
|
||||
btnCell.appendChild(errEl);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove highlight from current row
|
||||
row.classList.remove('batch-exporting');
|
||||
row.classList.add('batch-disabled');
|
||||
if (btnCell) {
|
||||
const sp = btnCell.querySelector('.batch-row-spinner');
|
||||
if (sp) sp.remove();
|
||||
btnCell.querySelectorAll('.action-btn').forEach(b => { b.style.display = b.dataset.prevDisplay || ''; delete b.dataset.prevDisplay; });
|
||||
}
|
||||
}
|
||||
|
||||
// Finished
|
||||
renderChangedRows();
|
||||
hasChanges = changedRows.size > 0;
|
||||
saveAllRunning = false;
|
||||
bar.style.display = 'none';
|
||||
cancelBtn.style.display = '';
|
||||
|
||||
// Re-enable all row buttons
|
||||
document.querySelectorAll('.grid-row[data-id]').forEach(r => r.classList.remove('batch-disabled'));
|
||||
if (toggle) { toggle.disabled = false; toggle.style.opacity = ''; toggle.style.pointerEvents = ''; }
|
||||
|
||||
const resultEl = document.getElementById('saveAllResultMessage');
|
||||
const resultLabel = document.getElementById('saveAllResultModalLabel');
|
||||
if (errorMessages.length === 0) {
|
||||
alert(`Tutte le ${successCount} righe salvate con successo!`);
|
||||
resultLabel.textContent = 'Salvataggio Completato';
|
||||
resultEl.innerHTML = `<i class="fas fa-check-circle" style="color:#28a745;"></i> Tutte le <strong>${successCount}</strong> righe salvate con successo.`;
|
||||
} else {
|
||||
alert(`Salvate ${successCount} righe con successo.\nErrori:\n${errorMessages.join('\n')}`);
|
||||
resultLabel.textContent = 'Salvataggio con Errori';
|
||||
resultEl.innerHTML = `<i class="fas fa-exclamation-triangle" style="color:#ffc107;"></i> Salvate <strong>${successCount}</strong> righe con successo.<br><br><strong>Errori:</strong><br>` + errorMessages.join('<br>');
|
||||
}
|
||||
new bootstrap.Modal(document.getElementById('saveAllResultModal'), { keyboard: false }).show();
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", function(e) {
|
||||
@ -1553,7 +1794,7 @@ function fixedDefaultValue(array $f): string
|
||||
});
|
||||
|
||||
// Gestisci la chiusura dei modali per rimuovere i backdrop
|
||||
document.querySelectorAll('#exportConfirmModal, #exportResponseModal, #exportUnsavedModal').forEach(modal => {
|
||||
document.querySelectorAll('#exportConfirmModal, #exportResponseModal, #exportUnsavedModal, #exportBatchConfirmModal, #exportBatchUnsavedModal, #saveAllConfirmModal, #saveAllResultModal').forEach(modal => {
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
// Rimuovi tutti i backdrop residui
|
||||
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
|
||||
@ -2744,6 +2985,82 @@ function fixedDefaultValue(array $f): string
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: batch export confirm -->
|
||||
<div class="modal fade" id="exportBatchConfirmModal" tabindex="-1" aria-labelledby="exportBatchConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exportBatchConfirmModalLabel">Conferma Export All</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Verranno esportate <strong><span id="exportBatchCount"></span></strong> righe al LIMS, una alla volta dall'alto verso il basso.<br>
|
||||
Potrai annullare il batch in qualsiasi momento.
|
||||
</div>
|
||||
<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-danger btn-sm" id="exportBatchConfirmBtn"><i class="fas fa-upload"></i> Avvia Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: batch export unsaved changes -->
|
||||
<div class="modal fade" id="exportBatchUnsavedModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Modifiche non salvate</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Ci sono modifiche non salvate. Salvare tutte le righe prima di procedere con l'esportazione?
|
||||
</div>
|
||||
<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-primary btn-sm" id="batchSaveAndExportBtn"><i class="fas fa-save"></i> Salva ed Esporta</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Save All confirm -->
|
||||
<div class="modal fade" id="saveAllConfirmModal" tabindex="-1" aria-labelledby="saveAllConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="saveAllConfirmModalLabel">Conferma Salvataggio</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Confermi il salvataggio di tutte le righe?
|
||||
</div>
|
||||
<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-success btn-sm" id="saveAllConfirmBtn"><i class="fas fa-save"></i> Conferma</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Save All result -->
|
||||
<div class="modal fade" id="saveAllResultModal" tabindex="-1" aria-labelledby="saveAllResultModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="saveAllResultModalLabel">Risultato Salvataggio</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="saveAllResultMessage"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-dismiss="modal">Chiudi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast: row saved successfully -->
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:9999">
|
||||
<div id="saveSuccessToast" class="toast align-items-center text-bg-success border-0" role="alert" data-bs-delay="2500">
|
||||
|
||||
137
public/userarea/validate_export.php
Normal file
137
public/userarea/validate_export.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* Validate rows before export to LIMS.
|
||||
*
|
||||
* Expects POST JSON: { rows: [ { iddatadb: int, index: int }, ... ] }
|
||||
* Returns JSON:
|
||||
* {
|
||||
* success: true,
|
||||
* results: {
|
||||
* "<index>": {
|
||||
* valid: bool,
|
||||
* iddatadb: int,
|
||||
* errors: [ { field: "<data-col>", message: "..." }, ... ]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Validators are closures registered in $validators[].
|
||||
* Each receives ($iddatadb, $ctx) and returns an array of errors (or empty []).
|
||||
* $ctx holds all prefetched data (record, parts, field values, mappings, etc.).
|
||||
* To add a new rule — just append another closure to $validators.
|
||||
*/
|
||||
include('include/headscript.php');
|
||||
|
||||
$dbHandler = DBHandlerSelect::getInstance();
|
||||
$pdo = $dbHandler->getConnection();
|
||||
|
||||
header("Content-Type: application/json");
|
||||
|
||||
// ── Validators ──────────────────────────────────────────────────────────────
|
||||
// Each validator is a closure: fn(int $iddatadb, array $ctx): array<{field,message}>
|
||||
// $ctx keys: record, parts, fieldValues, requiredFixed, requiredMappings, fixedAliasMap
|
||||
|
||||
$validators = [];
|
||||
|
||||
// 1. Every part must have a valid Matrice
|
||||
// Without Matrice the LIMS API rejects the Campione creation.
|
||||
$validators[] = function (int $iddatadb, array $ctx): array {
|
||||
$parts = $ctx['parts'];
|
||||
if (empty($parts)) {
|
||||
return [];
|
||||
}
|
||||
$missing = [];
|
||||
foreach ($parts as $p) {
|
||||
$matrice = (int)($p['idmatrice'] ?? 0);
|
||||
if ($matrice <= 0) {
|
||||
$label = $p['part_number'] ?: ($p['part_description'] ?: '(senza nome)');
|
||||
$missing[] = $label;
|
||||
}
|
||||
}
|
||||
if (!empty($missing)) {
|
||||
return [[
|
||||
'field' => 'parts',
|
||||
'message' => 'Matrice mancante — il LIMS rifiuterà l\'esportazione. '
|
||||
. 'Part senza matrice: ' . implode(', ', $missing)
|
||||
. '. Aprire la sezione Parts e assegnare la matrice.',
|
||||
]];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// ── Main ────────────────────────────────────────────────────────────────────
|
||||
|
||||
try {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$rows = $input['rows'] ?? [];
|
||||
|
||||
if (empty($rows)) {
|
||||
echo json_encode(['success' => false, 'message' => 'No rows provided']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$simulate = ($_ENV['SIMULATE_EXPORT_LIMS'] ?? env('SIMULATE_EXPORT_LIMS') ?? '') === 'true';
|
||||
|
||||
// ── Prefetch all data in bulk ───────────────────────────────────────────
|
||||
|
||||
$iddatadbList = array_column($rows, 'iddatadb');
|
||||
$placeholders = implode(',', array_fill(0, count($iddatadbList), '?'));
|
||||
|
||||
// Parts (full rows, so validators can report which parts have issues)
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT iddatadb, part_number, part_description, idmatrice
|
||||
FROM identification_parts
|
||||
WHERE iddatadb IN ($placeholders)
|
||||
");
|
||||
$stmt->execute($iddatadbList);
|
||||
$partsInfo = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$partsInfo[(int)$r['iddatadb']][] = $r;
|
||||
}
|
||||
|
||||
// ── Run validators per row ──────────────────────────────────────────────
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($rows as $rowInfo) {
|
||||
$iddatadb = (int)$rowInfo['iddatadb'];
|
||||
$index = $rowInfo['index'];
|
||||
|
||||
// Simulation: every even iddatadb gets a fake matrice error
|
||||
if ($simulate && $iddatadb % 2 === 0) {
|
||||
$results[$index] = [
|
||||
'valid' => false,
|
||||
'iddatadb' => $iddatadb,
|
||||
'errors' => [[
|
||||
'field' => 'parts',
|
||||
'message' => 'Matrice mancante — il LIMS rifiuterà l\'esportazione. '
|
||||
. 'Part senza matrice: PART-001, PART-003. '
|
||||
. 'Aprire la sezione Parts e assegnare la matrice.',
|
||||
]],
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build context for validators
|
||||
$ctx = [
|
||||
'parts' => $partsInfo[$iddatadb] ?? [],
|
||||
];
|
||||
|
||||
$errors = [];
|
||||
foreach ($validators as $validator) {
|
||||
$errors = array_merge($errors, $validator($iddatadb, $ctx));
|
||||
}
|
||||
|
||||
$results[$index] = [
|
||||
'valid' => empty($errors),
|
||||
'iddatadb' => $iddatadb,
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'results' => $results]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Validation error: " . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user