1,2,3,4,5,6 points of milestone
This commit is contained in:
@@ -209,18 +209,19 @@ class VisualLimsApiClient
|
|||||||
* @param string $endpoint OData endpoint, e.g. "Campione(613388)/UploadCampioneFile"
|
* @param string $endpoint OData endpoint, e.g. "Campione(613388)/UploadCampioneFile"
|
||||||
* @param string $filePath Absolute path to the file on disk
|
* @param string $filePath Absolute path to the file on disk
|
||||||
* @param string $fileName Original file name to send
|
* @param string $fileName Original file name to send
|
||||||
|
* @param array $extraFields Additional form fields to include
|
||||||
* @return array|null Decoded JSON response
|
* @return array|null Decoded JSON response
|
||||||
*/
|
*/
|
||||||
public function postMultipart($endpoint, $filePath, $fileName)
|
public function postMultipart($endpoint, $filePath, $fileName, array $extraFields = [])
|
||||||
{
|
{
|
||||||
$token = $this->getToken();
|
$token = $this->getToken();
|
||||||
$url = "{$this->baseUrl}/api/odata/{$endpoint}";
|
$url = "{$this->baseUrl}/api/odata/{$endpoint}";
|
||||||
|
|
||||||
$cfile = new CURLFile($filePath, mime_content_type($filePath) ?: 'application/octet-stream', $fileName);
|
$cfile = new CURLFile($filePath, mime_content_type($filePath) ?: 'application/octet-stream', $fileName);
|
||||||
|
|
||||||
$payload = [
|
$payload = array_merge($extraFields, [
|
||||||
'file' => $cfile,
|
'file' => $cfile,
|
||||||
];
|
]);
|
||||||
|
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class VisualLimsApiClientMock
|
|||||||
return [];
|
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}");
|
error_log("[SIMULATE] POST multipart {$endpoint} file={$fileName}");
|
||||||
|
|
||||||
|
|||||||
+671
-119
@@ -4,13 +4,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const exportButtons = document.querySelectorAll(".export-lims-btn");
|
const exportButtons = document.querySelectorAll(".export-lims-btn");
|
||||||
console.log(`Found ${exportButtons.length} export-lims-btn buttons`);
|
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
|
// Tracks the active confirm handler so it can be replaced on re-open
|
||||||
let pendingConfirmHandler = null;
|
let pendingConfirmHandler = null;
|
||||||
|
let batchRunning = false;
|
||||||
|
// Expose for Save All to check
|
||||||
|
Object.defineProperty(window, "batchRunning", {
|
||||||
|
get: () => batchRunning,
|
||||||
|
});
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -22,10 +22,340 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (overlay) overlay.style.display = "none";
|
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) {
|
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) {
|
if (!confirmModalElement) {
|
||||||
alert("Errore: Modale di conferma non trovato");
|
alert("Errore: Modale di conferma non trovato");
|
||||||
return;
|
return;
|
||||||
@@ -49,118 +379,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
console.log(`Confirmed export for iddatadb: ${iddatadb}`);
|
console.log(`Confirmed export for iddatadb: ${iddatadb}`);
|
||||||
confirmModal.hide();
|
confirmModal.hide();
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("iddatadb", iddatadb);
|
|
||||||
|
|
||||||
try {
|
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();
|
|
||||||
|
|
||||||
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 gridRow = btn.closest(".grid-row");
|
||||||
const statusBadge = gridRow?.querySelector(
|
const data = await sendExport(iddatadb, gridRow);
|
||||||
'.grid-cell[data-col="status"] .status-badge',
|
console.log("Export response:", data);
|
||||||
);
|
showExportResult(data);
|
||||||
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 },
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Export error:", error);
|
console.error("Export error:", error);
|
||||||
const responseModalElement =
|
showExportResult({
|
||||||
document.getElementById("exportResponseModal");
|
success: false,
|
||||||
if (!responseModalElement) {
|
message: error.message,
|
||||||
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 },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,11 +400,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
confirmBtn.addEventListener("click", confirmHandler, { once: true });
|
confirmBtn.addEventListener("click", confirmHandler, { once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Step 1: check unsaved changes, save if needed, then export ───────────
|
// ── Single row click handler ────────────────────────────────────────────
|
||||||
|
|
||||||
exportButtons.forEach((btn) => {
|
exportButtons.forEach((btn) => {
|
||||||
btn.addEventListener("click", (e) => {
|
btn.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (batchRunning) return;
|
||||||
|
|
||||||
const rowIndex = btn.dataset.row;
|
const rowIndex = btn.dataset.row;
|
||||||
const iddatadb = btn.dataset.iddatadb;
|
const iddatadb = btn.dataset.iddatadb;
|
||||||
console.log(
|
console.log(
|
||||||
@@ -191,7 +422,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
);
|
);
|
||||||
unsavedModal.show();
|
unsavedModal.show();
|
||||||
|
|
||||||
document.getElementById("saveAndExportBtn").addEventListener("click", () => {
|
document.getElementById("saveAndExportBtn").addEventListener(
|
||||||
|
"click",
|
||||||
|
() => {
|
||||||
unsavedModal.hide();
|
unsavedModal.hide();
|
||||||
const saveBtn = gridRow.querySelector(".save-btn");
|
const saveBtn = gridRow.querySelector(".save-btn");
|
||||||
if (!saveBtn) return;
|
if (!saveBtn) return;
|
||||||
@@ -209,13 +442,332 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
saveBtn.click();
|
saveBtn.click();
|
||||||
}, { once: true });
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No unsaved changes — go straight to export confirm
|
// No unsaved changes — go straight to validate + export confirm
|
||||||
startExportConfirmFlow(iddatadb, btn);
|
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
|
// 🔹 Base URL API
|
||||||
$apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
|
$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
|
// 🔹 Funzione per validare e convertire date
|
||||||
function validateDate($value)
|
function validateDate($value)
|
||||||
{
|
{
|
||||||
@@ -35,6 +55,11 @@ try {
|
|||||||
throw new Exception("Missing iddatadb");
|
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
|
// 🔹 STEP 1+2: Fetch Cliente ID from datadb and Schema ID from excel_templates
|
||||||
// Also fetch fixed fields stored in datadb
|
// Also fetch fixed fields stored in datadb
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
@@ -77,6 +102,23 @@ try {
|
|||||||
$stmt->execute(['iddatadb' => $iddatadb]);
|
$stmt->execute(['iddatadb' => $iddatadb]);
|
||||||
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$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
|
// 🔹 STEP 4: Fetch Field Values with Labels
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT
|
SELECT
|
||||||
@@ -139,7 +181,7 @@ try {
|
|||||||
|
|
||||||
// Salva log
|
// Salva log
|
||||||
$logFileStep5 = $logDir . "commessa_create_step5_" . $iddatadb . "_" . time() . ".txt";
|
$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"];
|
$commessaId = $commessaWeb["IdCommessa"];
|
||||||
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
|
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
|
||||||
@@ -187,11 +229,11 @@ try {
|
|||||||
|
|
||||||
// Salva log per STEP 6
|
// Salva log per STEP 6
|
||||||
$logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt";
|
$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
|
// 🔹 STEP 6.1: Fetch photos linked to this iddatadb
|
||||||
$stmtPhotos = $pdo->prepare("
|
$stmtPhotos = $pdo->prepare("
|
||||||
SELECT id, file_path, file_name
|
SELECT id, file_path, file_name, StampaNelRapporto, PrimaPagina
|
||||||
FROM datadb_photos
|
FROM datadb_photos
|
||||||
WHERE iddatadb = :iddatadb
|
WHERE iddatadb = :iddatadb
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
@@ -222,11 +264,20 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$photoEndpoint = "Campione({$campioneId})/UploadCampioneFile";
|
$photoEndpoint = "Campione({$campioneId})/UploadCampioneFile";
|
||||||
|
$stampaNelRapporto = !empty($photo['StampaNelRapporto']) ? 'true' : 'false';
|
||||||
|
$primaPagina = !empty($photo['PrimaPagina']) ? 'true' : 'false';
|
||||||
|
|
||||||
$logContentPhotos .= "curl --location --request POST '{$apiBaseUrl}{$photoEndpoint}' \\\n" .
|
$logContentPhotos .= "curl --location --request POST '{$apiBaseUrl}{$photoEndpoint}' \\\n" .
|
||||||
"--header 'Authorization: Bearer ••••••' \\\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";
|
$logContentPhotos .= "RESPONSE:\n" . json_encode($photoResult, JSON_PRETTY_PRINT) . "\n\n---\n";
|
||||||
$photosUploaded++;
|
$photosUploaded++;
|
||||||
}
|
}
|
||||||
@@ -238,7 +289,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt";
|
$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
|
// 🔹 STEP 7: Update Custom Fields for CommessaWeb
|
||||||
if (!empty($fieldValues)) {
|
if (!empty($fieldValues)) {
|
||||||
@@ -251,7 +302,7 @@ try {
|
|||||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||||
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT);
|
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT);
|
||||||
$logFileGet = $logDir . "commessa_{$commessaId}_get_step7_" . time() . ".txt";
|
$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
|
// Prepara payload PATCH
|
||||||
$commessaCustomFields = [];
|
$commessaCustomFields = [];
|
||||||
@@ -288,7 +339,7 @@ try {
|
|||||||
|
|
||||||
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
|
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
|
||||||
$logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt";
|
$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" .
|
"--data '{}'\n\n" .
|
||||||
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
|
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
|
||||||
$logFileStep9 = $logDir . "commessa_{$commessaId}_send_step9_" . time() . ".txt";
|
$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)
|
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto)
|
||||||
@@ -332,7 +383,7 @@ try {
|
|||||||
"--data '{$importPayloadLog}'\n\n" .
|
"--data '{$importPayloadLog}'\n\n" .
|
||||||
"RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
|
"RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
|
||||||
$logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
|
$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
|
// 🔹 STEP 10: GET di controllo post-PATCH
|
||||||
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
||||||
@@ -343,7 +394,7 @@ try {
|
|||||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||||
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
|
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
|
||||||
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
|
$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
|
// 🔹 STEP 11: Prepare final response
|
||||||
$finalCommessa = [
|
$finalCommessa = [
|
||||||
|
|||||||
@@ -347,6 +347,118 @@ function fixedDefaultValue(array $f): string
|
|||||||
background-color: #e9ecef;
|
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-header,
|
||||||
.grid-cell {
|
.grid-cell {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -472,7 +584,10 @@ function fixedDefaultValue(array $f): string
|
|||||||
|
|
||||||
.grid-top .save-all-cell {
|
.grid-top .save-all-cell {
|
||||||
flex: 0 0 210px;
|
flex: 0 0 210px;
|
||||||
align-items: flex-start;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-top .grid-cell input,
|
.grid-top .grid-cell input,
|
||||||
@@ -633,29 +748,51 @@ function fixedDefaultValue(array $f): string
|
|||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-all-btn {
|
.actions-dropdown .dropdown-toggle {
|
||||||
background-color: #28a745;
|
background-color: #6c757d;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 8px 16px;
|
padding: 6px 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-all-btn:hover {
|
.actions-dropdown .dropdown-toggle:hover,
|
||||||
background-color: #218838;
|
.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,
|
#exportConfirmModal,
|
||||||
#exportResponseModal,
|
#exportResponseModal,
|
||||||
#exportUnsavedModal {
|
#exportUnsavedModal,
|
||||||
|
#exportBatchConfirmModal,
|
||||||
|
#exportBatchUnsavedModal,
|
||||||
|
#saveAllConfirmModal,
|
||||||
|
#saveAllResultModal {
|
||||||
z-index: 1300 !important;
|
z-index: 1300 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#exportConfirmModal .modal-backdrop,
|
#exportConfirmModal .modal-backdrop,
|
||||||
#exportResponseModal .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;
|
z-index: 1299 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,21 +882,36 @@ function fixedDefaultValue(array $f): string
|
|||||||
</div>
|
</div>
|
||||||
<div class="card radius-10">
|
<div class="card radius-10">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center" style="min-height: 42px; gap: 12px;">
|
||||||
<div id="unsavedChanges" style="display:none; color: red; font-weight: bold; margin:10px 0;">
|
<div class="dropdown actions-dropdown" style="flex-shrink: 0;">
|
||||||
⚠️ Unsaved changes detected! Please save before leaving this page.<br>
|
<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>
|
<span id="changedRows" style="font-weight:normal; color:darkred;"></span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="editForm">
|
<form id="editForm">
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
<div class="grid-top">
|
<div class="grid-top">
|
||||||
<div class="grid-cell save-all-cell">
|
<div class="grid-cell save-all-cell"></div>
|
||||||
<button type="button" class="save-all-btn" title="Save All Rows"><i class="fas fa-save"></i> Save All</button>
|
|
||||||
</div>
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
$topColIndex = 1;
|
$topColIndex = 1;
|
||||||
@@ -1025,7 +1177,8 @@ function fixedDefaultValue(array $f): string
|
|||||||
<div class="grid-cell button-cell" style="flex: 0 0 210px; position: relative;">
|
<div class="grid-cell button-cell" style="flex: 0 0 210px; position: relative;">
|
||||||
<!-- commented only for admin roles -->
|
<!-- commented only for admin roles -->
|
||||||
<?php if ((Auth::user()->hasRole('Admin'))) : ?>
|
<?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; ?>
|
<?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>
|
<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)
|
// (se vuoi mantenere highlight cella gialla, lascia questa riga)
|
||||||
if (gridCell) gridCell.classList.add("cell-changed");
|
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();
|
renderChangedRows();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1446,21 +1612,65 @@ function fixedDefaultValue(array $f): string
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector('.save-all-btn').addEventListener('click', async () => {
|
let saveAllRunning = false;
|
||||||
const rows = document.querySelectorAll('.grid-row');
|
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 successCount = 0;
|
||||||
let errorMessages = [];
|
let errorMessages = [];
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const saveBtn = row.querySelector('.save-btn');
|
const saveBtn = row.querySelector('.save-btn');
|
||||||
if (!saveBtn) {
|
if (!saveBtn) { continue; }
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const rowIndex = saveBtn.dataset.row;
|
const rowIndex = saveBtn.dataset.row;
|
||||||
const iddatadb = row.getAttribute('data-id');
|
const iddatadb = row.getAttribute('data-id');
|
||||||
if (!rowIndex || !iddatadb) {
|
if (!rowIndex || !iddatadb) { continue; }
|
||||||
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 formData = new FormData();
|
||||||
|
|
||||||
const inputs = row.querySelectorAll(`input[name^="rows[${rowIndex}][details]"], select[name^="rows[${rowIndex}][details]"]`);
|
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) {
|
if (idclientSelect) {
|
||||||
formData.append('idclient', idclientSelect.value);
|
formData.append('idclient', idclientSelect.value);
|
||||||
}
|
}
|
||||||
// ---- FIXED FIELDS (NEW) ----
|
// ---- FIXED FIELDS ----
|
||||||
const fixedInputs = row.querySelectorAll(`input[name^="rows[${rowIndex}]["], select[name^="rows[${rowIndex}]["]`);
|
const fixedInputs = row.querySelectorAll(`input[name^="rows[${rowIndex}]["], select[name^="rows[${rowIndex}]["]`);
|
||||||
fixedInputs.forEach(inp => {
|
fixedInputs.forEach(inp => {
|
||||||
if (!inp.classList.contains('fixed-input')) return;
|
if (!inp.classList.contains('fixed-input')) return;
|
||||||
const m = inp.name.match(/rows\[\d+\]\[([^\]]+)\]/);
|
const m = inp.name.match(/rows\[\d+\]\[([^\]]+)\]/);
|
||||||
// Map: fixed key (logical) -> datadb real column
|
|
||||||
const fixedAliasMap = {
|
const fixedAliasMap = {
|
||||||
ClienteResponsabile: 'cliente_responsabile_id',
|
ClienteResponsabile: 'cliente_responsabile_id',
|
||||||
MoltiplicatorePrezzo: 'moltiplicatore_prezzo_id',
|
MoltiplicatorePrezzo: 'moltiplicatore_prezzo_id',
|
||||||
@@ -1492,13 +1701,10 @@ function fixedDefaultValue(array $f): string
|
|||||||
AnagraficaCertestService: 'anagrafica_certest_service_id',
|
AnagraficaCertestService: 'anagrafica_certest_service_id',
|
||||||
ConsegnaRichiesta: 'consegna_richiesta'
|
ConsegnaRichiesta: 'consegna_richiesta'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (m && m[1]) {
|
if (m && m[1]) {
|
||||||
const logicalKey = m[1];
|
const realKey = fixedAliasMap[m[1]] || m[1];
|
||||||
const realKey = fixedAliasMap[logicalKey] || logicalKey;
|
|
||||||
formData.append(realKey, inp.value);
|
formData.append(realKey, inp.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
formData.append('iddatadb', iddatadb);
|
formData.append('iddatadb', iddatadb);
|
||||||
@@ -1524,24 +1730,59 @@ function fixedDefaultValue(array $f): string
|
|||||||
changedRows.delete(rowIndex);
|
changedRows.delete(rowIndex);
|
||||||
document.querySelectorAll(`.grid-cell[data-row="${rowIndex}"]`)
|
document.querySelectorAll(`.grid-cell[data-row="${rowIndex}"]`)
|
||||||
.forEach(cell => cell.classList.remove("cell-changed"));
|
.forEach(cell => cell.classList.remove("cell-changed"));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
errorMessages.push(`Riga ${parseInt(rowIndex) + 1}: ${data.message}`);
|
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) {
|
} catch (error) {
|
||||||
errorMessages.push(`Riga ${parseInt(rowIndex) + 1}: ${error.message}`);
|
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();
|
renderChangedRows();
|
||||||
hasChanges = changedRows.size > 0;
|
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) {
|
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 {
|
} 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) {
|
window.addEventListener("beforeunload", function(e) {
|
||||||
@@ -1553,7 +1794,7 @@ function fixedDefaultValue(array $f): string
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Gestisci la chiusura dei modali per rimuovere i backdrop
|
// 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', () => {
|
modal.addEventListener('hidden.bs.modal', () => {
|
||||||
// Rimuovi tutti i backdrop residui
|
// Rimuovi tutti i backdrop residui
|
||||||
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
|
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
|
||||||
@@ -2744,6 +2985,82 @@ function fixedDefaultValue(array $f): string
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Toast: row saved successfully -->
|
||||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:9999">
|
<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">
|
<div id="saveSuccessToast" class="toast align-items-center text-bg-success border-0" role="alert" data-bs-delay="2500">
|
||||||
|
|||||||
@@ -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()]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user