Compare commits

..

2 Commits

Author SHA1 Message Date
RMubarakzyanov a626c8283e upload photos to main campione only 2026-02-28 15:48:19 +03:00
RMubarakzyanov c7416ba4a9 fix photo upload endpoint and align CommessaWeb field names with API docs
- CommessaWeb POST: rename fields per documentation — CustomerManager, PriceMultiplier, CertestObjectMasterData, CertestServiceMasterData, CustomerSupplier
- Move DeliveryRequest (ConsegnaRichiesta) from CommessaWeb to Campione
- Photo upload: change endpoint from AllegatoCommessaWeb to Campione({id})/UploadCampioneFile, upload to all campioni
- postMultipart(): remove commessaId param, send only file field
2026-02-28 02:07:40 +03:00
35 changed files with 469 additions and 5546 deletions
@@ -209,19 +209,18 @@ class VisualLimsApiClient
* @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, array $extraFields = [])
public function postMultipart($endpoint, $filePath, $fileName)
{
$token = $this->getToken();
$url = "{$this->baseUrl}/api/odata/{$endpoint}";
$cfile = new CURLFile($filePath, mime_content_type($filePath) ?: 'application/octet-stream', $fileName);
$payload = array_merge($extraFields, [
$payload = [
'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 $extraFields = []): array
public function postMultipart(string $endpoint, string $filePath, string $fileName): array
{
error_log("[SIMULATE] POST multipart {$endpoint} file={$fileName}");
File diff suppressed because one or more lines are too long
-2
View File
@@ -312,5 +312,3 @@
2026-02-25 15:23:12 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-26 15:01:32 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-28 21:01:27 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-01 19:11:58 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
+119 -671
View File
@@ -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,340 +22,10 @@ document.addEventListener("DOMContentLoaded", () => {
if (overlay) overlay.style.display = "none";
}
// ── Validation ───────────────────────────────────────────────────────────
/**
* Call the validation endpoint for an array of { iddatadb, index } objects.
* Returns the parsed JSON response.
*/
async function validateRows(rowsToValidate) {
const response = await fetch("validate_export.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows: rowsToValidate }),
});
if (!response.ok)
throw new Error(`Validation HTTP error: ${response.status}`);
return response.json();
}
/**
* Clear all validation-error highlights from the grid.
*/
function clearValidationErrors() {
document.querySelectorAll(".grid-cell.validation-error").forEach((cell) => {
cell.classList.remove("validation-error");
cell.querySelectorAll(".input-validation-error").forEach((el) => {
el.classList.remove("input-validation-error");
});
const tooltip = cell.querySelector(".validation-tooltip");
if (tooltip) tooltip.remove();
});
document.querySelectorAll(".grid-row.validation-row-error").forEach((row) => {
row.classList.remove("validation-row-error");
});
// Also clear batch-row-error that came from validation
clearAllRowErrors();
}
/**
* Highlight specific cells on a row based on validation errors.
* Each error has { field, message }.
* field can be: a data-col value, "parts", "field_label:SomeLabel", or null (row-level).
*/
function showValidationErrors(row, iddatadb, errors) {
row.classList.add("validation-row-error");
const messages = [];
errors.forEach((err) => {
messages.push(err.message);
if (!err.field) return; // row-level error, no specific cell
let cell = null;
if (err.field === "parts") {
// No specific cell to highlight, but we highlight the parts button area
// just add to messages
return;
} else if (err.field.startsWith("field_label:")) {
// Match by field_label text — find the header with this label, get its index
const label = err.field.substring("field_label:".length);
const headers = document.querySelectorAll(".grid-header");
let targetIndex = null;
headers.forEach((h) => {
if (h.textContent.trim() === label) {
targetIndex = h.getAttribute("data-index");
}
});
if (targetIndex) {
const rowIndex = row.querySelector(".grid-cell[data-row]")?.getAttribute("data-row");
if (rowIndex !== null) {
cell = row.querySelector(`.grid-cell[data-row="${rowIndex}"][data-index="${targetIndex}"]`);
}
}
} else {
// Direct data-col match
const rowIndex = row.querySelector(".grid-cell[data-row]")?.getAttribute("data-row");
if (rowIndex !== null) {
cell = row.querySelector(`.grid-cell[data-col="${err.field}"][data-row="${rowIndex}"]`);
}
}
if (cell) {
cell.classList.add("validation-error");
// Mark the input/select inside the cell
cell.querySelectorAll("input, select").forEach((el) => {
el.classList.add("input-validation-error");
});
// Add tooltip with error message
let tooltip = cell.querySelector(".validation-tooltip");
if (!tooltip) {
tooltip = document.createElement("div");
tooltip.className = "validation-tooltip";
cell.appendChild(tooltip);
}
tooltip.textContent = err.message;
}
});
// Show aggregated error on the row using existing mechanism
showRowError(row, iddatadb, messages.join("\n"));
}
/**
* Send a single export request and update the row UI on success.
* Returns the parsed JSON response.
*/
async function sendExport(iddatadb, gridRow, batchUuid = null) {
const formData = new FormData();
formData.append("iddatadb", iddatadb);
if (batchUuid) {
formData.append("batch_uuid", batchUuid);
}
const response = await fetch("export_to_lims.php", {
method: "POST",
body: formData,
});
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
if (data.success && gridRow) {
// Update status badge
const statusBadge = gridRow.querySelector(
'.grid-cell[data-col="status"] .status-badge',
);
if (statusBadge) {
statusBadge.classList.remove("status-i", "status-P");
statusBadge.classList.add("status-l");
statusBadge.textContent = "To LIMS";
}
// Insert/update CommessaWeb code span
const statusCell = gridRow.querySelector(
'.grid-cell[data-col="status"]',
);
if (statusCell && data.commessaweb) {
let cwSpan = statusCell.querySelector(".commessaweb-code");
if (!cwSpan) {
cwSpan = document.createElement("span");
cwSpan.className = "commessaweb-code";
cwSpan.style.cssText =
"display:block; font-size:0.75em; color:#555; margin-top:2px;";
cwSpan.title = "CommessaWeb";
const hiddenInput = statusCell.querySelector(
'input[type="hidden"]',
);
hiddenInput
? statusCell.insertBefore(cwSpan, hiddenInput)
: statusCell.appendChild(cwSpan);
}
cwSpan.textContent = data.commessaweb;
}
// Disable export button for this row
const exportBtn = gridRow.querySelector(".export-lims-btn");
if (exportBtn) {
exportBtn.disabled = true;
exportBtn.style.background = "#ccc";
exportBtn.style.cursor = "not-allowed";
exportBtn.style.opacity = "0.5";
exportBtn.title = "Già esportato";
}
}
return data;
}
/**
* Show the result of a single export in the response modal.
*/
function showExportResult(data) {
const responseModalElement =
document.getElementById("exportResponseModal");
if (!responseModalElement) {
alert("Errore: Modale di risposta non trovato");
return;
}
const responseModal = new bootstrap.Modal(responseModalElement, {
keyboard: false,
});
const responseMessage = document.getElementById(
"exportResponseMessage",
);
if (data.success) {
responseMessage.innerHTML =
`${data.message.replace(/\n/g, "<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 ───────────────
// ── Step 2: show export-confirm modal, send on "Conferma" ────────────────
function startExportConfirmFlow(iddatadb, btn) {
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");
const confirmModalElement = document.getElementById("exportConfirmModal");
if (!confirmModalElement) {
alert("Errore: Modale di conferma non trovato");
return;
@@ -379,17 +49,118 @@ document.addEventListener("DOMContentLoaded", () => {
console.log(`Confirmed export for iddatadb: ${iddatadb}`);
confirmModal.hide();
const formData = new FormData();
formData.append("iddatadb", iddatadb);
try {
const gridRow = btn.closest(".grid-row");
const data = await sendExport(iddatadb, gridRow);
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);
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";
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 },
);
} catch (error) {
console.error("Export error:", error);
showExportResult({
success: false,
message: error.message,
});
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 },
);
}
};
@@ -400,13 +171,11 @@ document.addEventListener("DOMContentLoaded", () => {
confirmBtn.addEventListener("click", confirmHandler, { once: true });
}
// ── Single row click handler ────────────────────────────────────────────
// ── Step 1: check unsaved changes, save if needed, then export ───────────
exportButtons.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
if (batchRunning) return;
const rowIndex = btn.dataset.row;
const iddatadb = btn.dataset.iddatadb;
console.log(
@@ -422,9 +191,7 @@ document.addEventListener("DOMContentLoaded", () => {
);
unsavedModal.show();
document.getElementById("saveAndExportBtn").addEventListener(
"click",
() => {
document.getElementById("saveAndExportBtn").addEventListener("click", () => {
unsavedModal.hide();
const saveBtn = gridRow.querySelector(".save-btn");
if (!saveBtn) return;
@@ -442,332 +209,13 @@ document.addEventListener("DOMContentLoaded", () => {
});
saveBtn.click();
},
{ once: true },
);
}, { once: true });
return;
}
// No unsaved changes — go straight to validate + export confirm
// No unsaved changes — go straight to 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 });
})();
}
});
+23 -77
View File
@@ -18,26 +18,6 @@ $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)
{
@@ -55,11 +35,6 @@ 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("
@@ -102,23 +77,6 @@ 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
@@ -158,13 +116,13 @@ try {
$commessaWebPayload = [
"Cliente" => $clienteId,
"SchemaCustomField" => $schemaId,
"Richiedente" => "From TRFSmart Application", // TODO: replace with real value
"Descrizione" => "From TRFSmart Application", // TODO: replace with real value
"ClienteResponsabile" => $clienteResponsabile,
"MoltiplicatorePrezzo" => $moltiplicatorePrezzo,
"AnagraficaCertestObject" => $anagraficaObject,
"AnagraficaCertestService" => $anagraficaService,
"ClienteFornitore" => $clienteFornitore, // PLACEHOLDER — to be implemented
"Richiedente" => "Test Web Import", // TODO: replace with real value
"Descrizione" => "TEST CommessaWeb", // TODO: replace with real value
"CustomerManager" => $clienteResponsabile,
"PriceMultiplier" => $moltiplicatorePrezzo,
"CertestObjectMasterData" => $anagraficaObject,
"CertestServiceMasterData" => $anagraficaService,
"CustomerSupplier" => $clienteFornitore, // PLACEHOLDER — to be implemented
// DeliveryRequest goes to Campione, not CommessaWeb
];
@@ -181,7 +139,7 @@ try {
// Salva log
$logFileStep5 = $logDir . "commessa_create_step5_" . $iddatadb . "_" . time() . ".txt";
$writeLog($logFileStep5, $logContentStep5, "STEP 5 - Create CommessaWeb (iddatadb={$iddatadb})");
file_put_contents($logFileStep5, $logContentStep5);
$commessaId = $commessaWeb["IdCommessa"];
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
@@ -202,9 +160,8 @@ try {
"Matrice" => $matriceId,
"SottoMatrice" => null,
"SchemaCustomField" => $schemaId,
"Riferimento" => $part["part_description"] ?? "",
"NoteWeb" => $part["part_description"] ?? "",
"ConsegnaRichiesta" => $consegnaRichiesta,
"DeliveryRequest" => $consegnaRichiesta,
];
// Costruisci curl-like per questo campione
@@ -229,11 +186,11 @@ try {
// Salva log per STEP 6
$logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt";
$writeLog($logFileStep6, $logContentStep6, "STEP 6 - Campioni (commessa={$commessaId})");
file_put_contents($logFileStep6, $logContentStep6);
// 🔹 STEP 6.1: Fetch photos linked to this iddatadb
$stmtPhotos = $pdo->prepare("
SELECT id, file_path, file_name, StampaNelRapporto, PrimaPagina
SELECT id, file_path, file_name
FROM datadb_photos
WHERE iddatadb = :iddatadb
ORDER BY id ASC
@@ -264,20 +221,11 @@ 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" .
"--form 'StampaNelRapporto={$stampaNelRapporto}' \\\n" .
"--form 'PrimaPagina={$primaPagina}'\n\n";
"--form 'file=@{$fullPath}'\n\n";
$extraFields = [
'StampaNelRapporto' => $stampaNelRapporto,
'PrimaPagina' => $primaPagina,
];
$photoResult = $api->postMultipart($photoEndpoint, $fullPath, $photo['file_name'], $extraFields);
$photoResult = $api->postMultipart($photoEndpoint, $fullPath, $photo['file_name']);
$logContentPhotos .= "RESPONSE:\n" . json_encode($photoResult, JSON_PRETTY_PRINT) . "\n\n---\n";
$photosUploaded++;
}
@@ -289,7 +237,7 @@ try {
}
$logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt";
$writeLog($logFilePhotos, $logContentPhotos, "STEP 6.2 - Photos (commessa={$commessaId})");
file_put_contents($logFilePhotos, $logContentPhotos);
// 🔹 STEP 7: Update Custom Fields for CommessaWeb
if (!empty($fieldValues)) {
@@ -302,7 +250,7 @@ try {
"--header 'Authorization: Bearer ••••••'\n\n" .
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT);
$logFileGet = $logDir . "commessa_{$commessaId}_get_step7_" . time() . ".txt";
$writeLog($logFileGet, $logContentGet, "STEP 7 - GET CustomFields (commessa={$commessaId})");
file_put_contents($logFileGet, $logContentGet);
// Prepara payload PATCH
$commessaCustomFields = [];
@@ -339,7 +287,7 @@ try {
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
$logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt";
$writeLog($logFileStep7, $logContentStep7, "STEP 7 - PATCH CustomFields (commessa={$commessaId})");
file_put_contents($logFileStep7, $logContentStep7);
}
}
@@ -356,7 +304,7 @@ try {
]);
// 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto)
/*
$sendResult = $api->post("CommessaWeb({$commessaId})/InviaCommessa", []);
// Logga il POST
@@ -366,24 +314,22 @@ try {
"--data '{}'\n\n" .
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
$logFileStep9 = $logDir . "commessa_{$commessaId}_send_step9_" . time() . ".txt";
$writeLog($logFileStep9, $logContentStep9, "STEP 9 - InviaCommessa (commessa={$commessaId})");
file_put_contents($logFileStep9, $logContentStep9);
*/
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto)
// Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa
$importPayload = ["IdUtente" => 285]; // user-id
$importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $importPayload);
$importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", []);
$importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
// Logga il POST
$logContentStep91 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/ImportaCommessa' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$importPayloadLog}'\n\n" .
"--data '{}'\n\n" .
"RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
$logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId})");
file_put_contents($logFileStep91, $logContentStep91);
// 🔹 STEP 10: GET di controllo post-PATCH
$expand = "CommesseCustomFields(\$expand=CustomField)";
@@ -394,7 +340,7 @@ try {
"--header 'Authorization: Bearer ••••••'\n\n" .
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
$writeLog($logFileStep10, $logContentStep10, "STEP 10 - GET verify (commessa={$commessaId})");
file_put_contents($logFileStep10, $logContentStep10);
// 🔹 STEP 11: Prepare final response
$finalCommessa = [
-38
View File
@@ -1,38 +0,0 @@
<?php
require_once __DIR__ . '/include/headscript.php'; // o il tuo bootstrap standard
header('Content-Type: application/json');
try {
if (!isset($_GET['iddatadb']) || !is_numeric($_GET['iddatadb'])) {
echo json_encode(['success' => true, 'field' => null]);
exit;
}
$iddatadb = (int)$_GET['iddatadb'];
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// 1) prendo templateid da datadb
// 2) cerco max 1 record in template_mapping con is_visible_parts=1
$sql = "
SELECT tm.field_id, tm.field_label, tm.data_type, tm.has_list
FROM datadb d
JOIN template_mapping tm ON tm.template_id = d.templateid
WHERE d.iddatadb = ?
AND tm.is_visible_parts = 1
ORDER BY tm.id ASC
LIMIT 1
";
$stmt = $pdo->prepare($sql);
$stmt->execute([$iddatadb]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'field' => $row ? $row : null
]);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
-94
View File
@@ -1,94 +0,0 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
// Disable PHP error display
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// In simulate mode: return fake users
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
$fakeUsers = [
[
'IdUtente' => 1001,
'Nominativo' => 'Utente Simulato 1001'
],
[
'IdUtente' => 1002,
'Nominativo' => 'Utente Simulato 1002'
]
];
echo json_encode(['value' => $fakeUsers]);
exit;
}
// OData parameters
$params = [];
// Build query string
$queryString = http_build_query($params);
// Final endpoint
$endpoint = "Utente?$queryString";
// Function to execute request with retry
function makeApiRequest($api, $endpoint, $maxRetries = 3)
{
for ($retry = 0; $retry < $maxRetries; $retry++) {
try {
$data = $api->get($endpoint);
// Save response for debug
file_put_contents(__DIR__ . '/utenti_response.json', json_encode($data, JSON_PRETTY_PRINT));
return $data;
} catch (Exception $e) {
$errorMessage = $e->getMessage();
// Retry only on token/auth-related issue
if (
strpos($errorMessage, 'HTTP 400') !== false &&
strpos($errorMessage, 'Cannot persist the object') !== false
) {
try {
$api->refreshToken(); // must exist in VisualLimsApiClient
error_log("Tentativo $retry: Refresh token eseguito per endpoint $endpoint");
} catch (Exception $refreshEx) {
error_log("Errore durante il refresh del token: " . $refreshEx->getMessage());
throw new Exception("Impossibile eseguire il refresh del token: " . $refreshEx->getMessage());
}
usleep(500000); // 500 ms
continue;
}
throw $e;
}
}
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
}
// Execute request
$data = makeApiRequest($api, $endpoint);
echo json_encode($data);
} catch (Exception $e) {
http_response_code(500);
$errorResponse = [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
];
error_log("Errore in get_utenti.php: " . json_encode($errorResponse));
echo json_encode($errorResponse);
}
+37 -362
View File
@@ -347,118 +347,6 @@ 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;
@@ -584,10 +472,7 @@ function fixedDefaultValue(array $f): string
.grid-top .save-all-cell {
flex: 0 0 210px;
display: flex;
align-items: center;
justify-content: center;
overflow: visible;
align-items: flex-start;
}
.grid-top .grid-cell input,
@@ -748,51 +633,29 @@ function fixedDefaultValue(array $f): string
transition: background-color 0.3s ease;
}
.actions-dropdown .dropdown-toggle {
background-color: #6c757d;
.save-all-btn {
background-color: #28a745;
color: white;
border: none;
border-radius: 5px;
padding: 6px 14px;
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
font-size: 14px;
}
.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;
.save-all-btn:hover {
background-color: #218838;
}
#exportConfirmModal,
#exportResponseModal,
#exportUnsavedModal,
#exportBatchConfirmModal,
#exportBatchUnsavedModal,
#saveAllConfirmModal,
#saveAllResultModal {
#exportUnsavedModal {
z-index: 1300 !important;
}
#exportConfirmModal .modal-backdrop,
#exportResponseModal .modal-backdrop,
#exportUnsavedModal .modal-backdrop,
#exportBatchConfirmModal .modal-backdrop,
#exportBatchUnsavedModal .modal-backdrop,
#saveAllConfirmModal .modal-backdrop,
#saveAllResultModal .modal-backdrop {
#exportUnsavedModal .modal-backdrop {
z-index: 1299 !important;
}
@@ -882,36 +745,21 @@ function fixedDefaultValue(array $f): string
</div>
<div class="card radius-10">
<div class="card-header">
<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!
<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>
<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"></div>
<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>
<?php
$topColIndex = 1;
@@ -1177,8 +1025,7 @@ 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'))) : ?>
<?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>
<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 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>
@@ -1502,19 +1349,6 @@ 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();
});
});
@@ -1612,65 +1446,21 @@ function fixedDefaultValue(array $f): string
});
});
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;
document.querySelector('.save-all-btn').addEventListener('click', async () => {
const rows = document.querySelectorAll('.grid-row');
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; }
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);
if (!rowIndex || !iddatadb) {
continue;
}
const formData = new FormData();
const inputs = row.querySelectorAll(`input[name^="rows[${rowIndex}][details]"], select[name^="rows[${rowIndex}][details]"]`);
@@ -1689,11 +1479,12 @@ function fixedDefaultValue(array $f): string
if (idclientSelect) {
formData.append('idclient', idclientSelect.value);
}
// ---- FIXED FIELDS ----
// ---- FIXED FIELDS (NEW) ----
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',
@@ -1701,10 +1492,13 @@ function fixedDefaultValue(array $f): string
AnagraficaCertestService: 'anagrafica_certest_service_id',
ConsegnaRichiesta: 'consegna_richiesta'
};
if (m && m[1]) {
const realKey = fixedAliasMap[m[1]] || m[1];
const logicalKey = m[1];
const realKey = fixedAliasMap[logicalKey] || logicalKey;
formData.append(realKey, inp.value);
}
});
formData.append('iddatadb', iddatadb);
@@ -1730,59 +1524,24 @@ 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) {
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.`;
alert(`Tutte le ${successCount} righe salvate con successo!`);
} else {
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>');
alert(`Salvate ${successCount} righe con successo.\nErrori:\n${errorMessages.join('\n')}`);
}
new bootstrap.Modal(document.getElementById('saveAllResultModal'), { keyboard: false }).show();
});
window.addEventListener("beforeunload", function(e) {
@@ -1794,7 +1553,7 @@ function fixedDefaultValue(array $f): string
});
// Gestisci la chiusura dei modali per rimuovere i backdrop
document.querySelectorAll('#exportConfirmModal, #exportResponseModal, #exportUnsavedModal, #exportBatchConfirmModal, #exportBatchUnsavedModal, #saveAllConfirmModal, #saveAllResultModal').forEach(modal => {
document.querySelectorAll('#exportConfirmModal, #exportResponseModal, #exportUnsavedModal').forEach(modal => {
modal.addEventListener('hidden.bs.modal', () => {
// Rimuovi tutti i backdrop residui
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
@@ -1844,11 +1603,7 @@ function fixedDefaultValue(array $f): string
clientData.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile";
const codiceCliente = (client.CodiceCliente || '').toString().trim();
const suffix = (codiceCliente.split('_')[1] || '').trim();
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : '--');
const option = new Option(`${nome.trim()} - ${shortCode} (ID: ${id})`, id);
const option = new Option(`${nome.trim()} - ${client.CodiceNazioneFatturazione} (ID: ${id})`, id);
if (parseInt(id) === parseInt(<?php echo json_encode($default_idclient ?? 0); ?>)) {
option.selected = true;
}
@@ -1884,11 +1639,7 @@ function fixedDefaultValue(array $f): string
clientData.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile";
const codiceCliente = (client.CodiceCliente || '').toString().trim();
const suffix = (codiceCliente.split('_')[1] || '').trim();
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : '--');
const option = new Option(`${nome.trim()} - ${shortCode} (ID: ${id})`, id);
const option = new Option(`${nome.trim()} - ${client.CodiceNazioneFatturazione} (ID: ${id})`, id);
if (String(id) === String(currentValue)) {
option.selected = true;
}
@@ -2278,7 +2029,7 @@ function fixedDefaultValue(array $f): string
const id = client.IdCliente || "ID non disponibile";
// CodiceCliente es: "Bl01858_E" -> vogliamo "E"
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? '').toString().trim();
const codiceCliente = (client.CodiceCliente || '').toString().trim();
const suffix = (codiceCliente.split('_')[1] || '').trim(); // parte dopo "_"
const shortCode = suffix || '--';
@@ -2985,82 +2736,6 @@ 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">
+4 -14
View File
@@ -318,12 +318,8 @@ error_log("Loaded template: " . print_r($template, true));
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
<input type="hidden" name="rows" value="${encodeURIComponent(JSON.stringify(data.rows))}">
<input type="hidden" name="excelrows" value="${encodeURIComponent(JSON.stringify(data.excel_data.map(r => r.excelrow)))}">
<input type="hidden" name="filename" value="${data.filename}">
<!-- TOP BUTTON -->
<div class="d-flex justify-content-end mb-3">
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
</div>
<input type="hidden" name="filename" value="${data.filename}">
<div class="table-container">
<table class="table table-striped table-bordered">
@@ -354,23 +350,17 @@ error_log("Loaded template: " . print_r($template, true));
</tbody>
</table>
</div>
<!-- BOTTOM BUTTON -->
<button type="submit" class="btn btn-primary mt-3" id="proceedButtonBottom" disabled>Prosegui</button>
<button type="submit" class="btn btn-primary mt-3" id="proceedButton" disabled>Prosegui</button>
</form>
`;
tableContainer.innerHTML = html;
const proceedButtonTop = document.getElementById('proceedButtonTop');
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
const proceedButton = document.getElementById('proceedButton');
const selectAllCheckbox = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.row-checkbox');
function updateProceedButton() {
const enabled = Array.from(checkboxes).some(cb => cb.checked);
if (proceedButtonTop) proceedButtonTop.disabled = !enabled;
if (proceedButtonBottom) proceedButtonBottom.disabled = !enabled;
proceedButton.disabled = !Array.from(checkboxes).some(cb => cb.checked);
}
selectAllCheckbox.addEventListener('change', function() {
+2 -48
View File
@@ -14,57 +14,11 @@ if (!$iddatadb) {
}
try {
// 1) prendo templateid da datadb
$stmtTpl = $pdo->prepare("SELECT templateid FROM datadb WHERE iddatadb = :iddatadb LIMIT 1");
$stmtTpl->execute([':iddatadb' => $iddatadb]);
$templateid = $stmtTpl->fetchColumn();
// 2) prendo (max 1) field_id visibile in parts
$extraFieldId = null;
if ($templateid) {
$stmtEF = $pdo->prepare("SELECT field_id FROM template_mapping WHERE template_id = :templateid AND is_visible_parts = 1 ORDER BY id ASC LIMIT 1");
$stmtEF->execute([':templateid' => $templateid]);
$extraFieldId = $stmtEF->fetchColumn();
if ($extraFieldId !== false) $extraFieldId = (int)$extraFieldId;
else $extraFieldId = null;
}
// 3) carico parts + join su tabella figlia
if ($extraFieldId) {
$stmt = $pdo->prepare("
SELECT
p.id, p.iddatadb, p.part_number, p.part_description, p.idmatrice, p.note, p.dateexpiry,
cf.value_id AS extra_value_id,
cf.value_text AS extra_value_text
FROM identification_parts p
LEFT JOIN identification_parts_customfields cf
ON cf.part_id = p.id AND cf.field_id = :extraFieldId
WHERE p.iddatadb = :iddatadb
ORDER BY p.part_number ASC
");
$stmt->execute([
':iddatadb' => $iddatadb,
':extraFieldId' => $extraFieldId
]);
} else {
$stmt = $pdo->prepare("
SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry,
NULL AS extra_value_id, NULL AS extra_value_text
FROM identification_parts
WHERE iddatadb = :iddatadb
ORDER BY part_number ASC
");
$stmt = $pdo->prepare("SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry FROM identification_parts WHERE iddatadb = :iddatadb ORDER BY part_number ASC");
$stmt->execute([':iddatadb' => $iddatadb]);
}
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'parts' => $parts,
'extra_field_id' => $extraFieldId,
'debug_sql' => ($extraFieldId ? 'WITH_CF_JOIN' : 'NO_CF')
]);
echo json_encode(['success' => true, 'parts' => $parts]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Errore nel caricamento: ' . $e->getMessage()]);
}
@@ -1,60 +0,0 @@
CAMPIONE #0
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 563528,
"Matrice": 3879,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "Parte AAA",
"ConsegnaRichiesta": "2026-01-30"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 734582,
"CodiceCampione": "10978",
"CodiceCampioneWeb": "10978",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-11T14:10:02.1595894+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-30T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "Parte AAA",
"GruppiRicercati": null
}
---
CAMPIONE #1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 563528,
"Matrice": 3879,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "PARE BBB",
"ConsegnaRichiesta": "2026-01-30"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 734583,
"CodiceCampione": "10979",
"CodiceCampioneWeb": "10979",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-11T14:10:03.9972635+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-30T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "PARE BBB",
"GruppiRicercati": null
}
---
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,20 +0,0 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(563528)/ImportaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"IdUtente": 285
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 563528,
"CodiceCommessa": "26C0029",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Elaborata",
"DataCreazioneWeb": "2026-03-11T14:10:00.897+01:00",
"CodiceCommessaWeb": "26C0029",
"DataInviatoWeb": "2026-03-11T14:10:11.76+01:00",
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
@@ -1,44 +0,0 @@
Photos for CommessaWeb 563528 (iddatadb=1259):
Total photos found: 2, campioni: 2
=== Campione 734582 (main) ===
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(734582)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1259-20260311130930-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1779561,
"FileName": "1259-20260311130930-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-11T14:10:06.7051549+01:00",
"Titolo": null
}
---
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(734582)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1259-20260311130930-1e672dd9-5420-4432-b422-02d8d271c178.jpg'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1779562,
"FileName": "1259-20260311130930-1e672dd9-5420-4432-b422-02d8d271c178.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-11T14:10:08.5442313+01:00",
"Titolo": null
}
---
@@ -1,18 +0,0 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(563528)/InviaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 563528,
"CodiceCommessa": "26C0029",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-11T14:10:00.897+01:00",
"CodiceCommessaWeb": "26C0029",
"DataInviatoWeb": "2026-03-11T14:10:11.7602299+01:00",
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
@@ -1,226 +0,0 @@
curl --location --request PATCH 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(563528)' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"CommesseCustomFields": [
{
"IdCommesseCustomFields": 23254249,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254250,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254251,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254252,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254253,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254254,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254260,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254261,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254262,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254264,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254265,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254266,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254267,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254268,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254228,
"Valore": "13526"
},
{
"IdCommesseCustomFields": 23254229,
"Valore": "20262"
},
{
"IdCommesseCustomFields": 23254230,
"Valore": "ART. PEACH"
},
{
"IdCommesseCustomFields": 23254269,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254270,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254271,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254272,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254273,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254274,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254275,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254276,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254277,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254278,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254279,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254280,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254281,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254231,
"Valore": "L209A4M00130M7280"
},
{
"IdCommesseCustomFields": 23254232,
"Valore": "BLACK"
},
{
"IdCommesseCustomFields": 23254233,
"Valore": "PE007J"
},
{
"IdCommesseCustomFields": 23254234,
"Valore": "262"
},
{
"IdCommesseCustomFields": 23254235,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254236,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254237,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254238,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254239,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254240,
"Valore": "Oggi"
},
{
"IdCommesseCustomFields": 23254241,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254242,
"Valore": "MONCLER"
},
{
"IdCommesseCustomFields": 23254243,
"Valore": "236"
},
{
"IdCommesseCustomFields": 23254244,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254245,
"Valore": "BBB"
},
{
"IdCommesseCustomFields": 23254246,
"Valore": "solocla"
},
{
"IdCommesseCustomFields": 23254247,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254248,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254255,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254256,
"Valore": "MONCLER"
},
{
"IdCommesseCustomFields": 23254257,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254258,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254259,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254263,
"Valore": ""
}
]
}'
RESPONSE:
null
@@ -1,40 +0,0 @@
CAMPIONE #0
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 95833,
"Matrice": 8413,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "aaa"
}'
RESPONSE:
{
"IdCampione": 14445,
"Commessa": 95833,
"Matrice": 8413
}
---
CAMPIONE #1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 95833,
"Matrice": 3879,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "bbb"
}'
RESPONSE:
{
"IdCampione": 15750,
"Commessa": 95833,
"Matrice": 3879
}
---
@@ -1,9 +0,0 @@
curl --location --request GET 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(95833)?$expand=CommesseCustomFields($expand=CustomField)' \
--header 'Authorization: Bearer ••••••'
RESPONSE:
{
"IdCommessa": 95833,
"CodiceCommessa": "SIM-95833",
"CommesseCustomFields": []
}
@@ -1,9 +0,0 @@
curl --location --request GET 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(95833)?$expand=CommesseCustomFields($expand=CustomField)' \
--header 'Authorization: Bearer ••••••'
RESPONSE:
{
"IdCommessa": 95833,
"CodiceCommessa": "SIM-95833",
"CommesseCustomFields": []
}
@@ -1,10 +0,0 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(95833)/ImportaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{}'
RESPONSE:
{
"simulated": true,
"endpoint": "CommessaWeb(95833)\/ImportaCommessa"
}
@@ -1,15 +0,0 @@
Photos for CommessaWeb 95833 (iddatadb=1237):
Total photos found: 1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/AllegatoCommessaWeb' \
--header 'Authorization: Bearer ••••••' \
--form 'IdCommessa=95833' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1237-20260228192737-Blue and White Simple Daily Vlogger YouTube Banner (15) (1) (1).png'
RESPONSE:
{
"simulated": true,
"file": "1237-20260228192737-Blue and White Simple Daily Vlogger YouTube Banner (15) (1) (1).png"
}
---
@@ -1,22 +0,0 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Cliente": 3378,
"SchemaCustomField": 82,
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb",
"ClienteResponsabile": 3,
"MoltiplicatorePrezzo": 1,
"AnagraficaCertestObject": 2,
"AnagraficaCertestService": 1,
"ClienteFornitore": null
}'
RESPONSE:
{
"IdCommessa": 95833,
"CodiceCommessa": "SIM-95833",
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
@@ -1,28 +0,0 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Cliente": 3378,
"SchemaCustomField": 82,
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb",
"ClienteResponsabile": 7586,
"MoltiplicatorePrezzo": 3,
"AnagraficaCertestObject": 8963,
"AnagraficaCertestService": 9007,
"ClienteFornitore": null
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 562479,
"CodiceCommessa": "26C0013",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-06T09:09:16.9338074+01:00",
"CodiceCommessaWeb": "26C0013",
"DataInviatoWeb": null,
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
@@ -1,28 +0,0 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Cliente": 3378,
"SchemaCustomField": 82,
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb",
"ClienteResponsabile": 10653,
"MoltiplicatorePrezzo": 1,
"AnagraficaCertestObject": 8960,
"AnagraficaCertestService": 9010,
"ClienteFornitore": null
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 563528,
"CodiceCommessa": "26C0029",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-11T14:10:00.8960358+01:00",
"CodiceCommessaWeb": "26C0029",
"DataInviatoWeb": null,
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
@@ -96,66 +96,12 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
/* Force predictable column sizing */
/* Stable sizing */
#schemaFieldsTable {
width: 100%;
table-layout: fixed;
}
/* Checkbox columns = minimal */
#schemaFieldsTable th:nth-child(1),
#schemaFieldsTable td:nth-child(1),
#schemaFieldsTable th:first-child,
#schemaFieldsTable td:first-child,
#schemaFieldsTable th:nth-child(2),
#schemaFieldsTable td:nth-child(2),
#schemaFieldsTable th:nth-child(3),
#schemaFieldsTable td:nth-child(3) {
width: 48px;
/* 🔥 più stretto */
#schemaFieldsTable td:nth-child(2) {
width: 100px;
text-align: center;
padding: 2px 4px;
/* 🔥 meno padding */
white-space: nowrap;
}
/* Make checkbox itself not oversized */
#schemaFieldsTable td:nth-child(1) input[type="checkbox"],
#schemaFieldsTable td:nth-child(2) input[type="checkbox"],
#schemaFieldsTable td:nth-child(3) input[type="checkbox"] {
transform: scale(0.95);
margin: 0;
}
/* Title */
#schemaFieldsTable th:nth-child(4),
#schemaFieldsTable td:nth-child(4) {
width: 300px;
}
/* Type */
#schemaFieldsTable th:nth-child(5),
#schemaFieldsTable td:nth-child(5) {
width: 120px;
white-space: nowrap;
}
/* Mapping = wide but NOT insane */
#schemaFieldsTable th:nth-child(6),
#schemaFieldsTable td:nth-child(6) {
width: 380px;
}
/* Default Value = wider */
#schemaFieldsTable th:nth-child(7),
#schemaFieldsTable td:nth-child(7) {
width: 320px;
}
/* selects fill the cell */
#schemaFieldsTable td:nth-child(6) .form-select,
#schemaFieldsTable td:nth-child(7) .form-control,
#schemaFieldsTable td:nth-child(7) .form-select {
width: 100% !important;
}
/* fix bootstrap/select2 width */
@@ -219,11 +165,11 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
<table id="schemaFieldsTable" class="table table-striped">
<thead>
<tr>
<th style="width:45px; text-align:center;">Main</th>
<th style="width:45px; text-align:center;">Import</th>
<th style="width:45px; text-align:center;">Parts</th>
<th style="width:90px; text-align:center;">Main</th>
<th style="width:110px; text-align:center;">Import</th>
<th style="width:110px; text-align:center;">Parts</th>
<th style="width:320px;">Title</th>
<th style="width:120px;">Type</th>
<th style="width:140px;">Type</th>
<th>Mapping</th>
<th>Default Value</th>
</tr>
@@ -967,22 +913,12 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
cb.checked = cb.dataset.originalChecked === 'true';
});
});
} else if (event.target.classList.contains('visible-parts-checkbox')) {
} else if (event.target.classList.contains('visible-import-checkbox')) {
const checkbox = event.target;
const mappingId = checkbox.dataset.mappingId;
const value = checkbox.checked ? 1 : 0;
// salva stato per rollback
const prevChecked = checkbox.checked;
// ✅ UI: se sto mettendo a 1, tolgo la spunta a tutti gli altri SUBITO
if (value === 1) {
document.querySelectorAll('.visible-parts-checkbox').forEach(cb => {
if (cb !== checkbox) cb.checked = false;
});
}
fetch('update_visible_parts.php', {
fetch('update_visible_import.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -993,25 +929,16 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
value: value
})
})
.then(r => r.json())
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error("❌ Error updating is_visible_parts:", data.message);
// rollback UI
checkbox.checked = !prevChecked;
// se avevo tolto le spunte agli altri, ricarico per riallineare la UI al DB
// (semplice e safe)
location.reload();
console.error("❌ Error updating is_visible_import:", data.message);
checkbox.checked = !checkbox.checked;
}
})
.catch(error => {
console.error("❌ Fetch error:", error);
// rollback UI
checkbox.checked = !prevChecked;
location.reload();
checkbox.checked = !checkbox.checked;
});
} else if (event.target.classList.contains('visible-parts-checkbox')) {
const checkbox = event.target;
+6 -10
View File
@@ -1,5 +1,5 @@
<div class="modal fade" id="partsModal" tabindex="-1" aria-labelledby="partsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" style="max-width: 95vw !important;">
<div class="modal-dialog modal-xl" style="max-width: 80% !important;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="partsModalLabel">Parti per TRF: <span id="trfHeader"></span></h5>
@@ -34,7 +34,7 @@
<table class="table table-striped table-sm" id="partsTable">
<thead>
<tr>
<th style="width: 55px;">Num</th>
<th style="width: 80px;">Numero</th>
<th>Descrizione</th>
<th style="width: 200px;">Matrice</th>
<th style="width: 150px;">
@@ -51,7 +51,7 @@
</thead>
<tbody id="partsTableBody">
<tr data-part-id="new">
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 55px;"></td>
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 80px;"></td>
<td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
<td>
<div style="display: flex; align-items: center;">
@@ -287,13 +287,9 @@
/* Colonna Descrizione (2ª colonna) = 420px */
#partsTable th:nth-child(2),
#partsTable td:nth-child(2) {
width: 300px !important;
min-width: 300px !important;
max-width: 300px !important;
}
#partsTable .part-number {
text-align: center;
width: 350 !important;
min-width: 350px !important;
max-width: 350px !important;
}
#partsTable td:nth-child(2) .part-description {
+22 -238
View File
@@ -7,8 +7,6 @@ $(document).ready(function () {
let matrici = [];
let macroMatrici = [];
let quotations = [];
let partsExtraField = null; // {field_id, field_label} oppure null
let extraFieldOptions = []; // [{id,label}]
// --- ROW ID helpers: niente più cache impazzita di jQuery .data() ---
function getPartId($row) {
@@ -31,148 +29,6 @@ $(document).ready(function () {
return id === "new" || id === "" || id === null ? null : id;
}
function loadPartsExtraField(iddatadb, done) {
partsExtraField = null;
if (!iddatadb) return done();
$.ajax({
url: "get_parts_extra_field.php",
method: "GET",
dataType: "json",
data: { iddatadb },
success: function (res) {
partsExtraField =
res && res.success && res.field ? res.field : null;
if (
partsExtraField &&
(partsExtraField.data_type || "").toLowerCase() ===
"sceltamultipla"
) {
loadExtraFieldOptions(
String(partsExtraField.field_id),
function () {
applyExtraFieldColumn();
done();
},
);
} else {
applyExtraFieldColumn();
done();
}
},
error: function () {
partsExtraField = null;
applyExtraFieldColumn();
done();
},
});
}
function loadExtraFieldOptions(fieldId, done) {
extraFieldOptions = [];
if (!fieldId) return done();
$.ajax({
url: "get_customfield_values.php",
method: "GET",
dataType: "json",
data: { field_ids: fieldId },
success: function (res) {
const arr = res && res[fieldId] ? res[fieldId] : [];
extraFieldOptions = (arr || []).map((x) => ({
id: x.IdCustomFieldsValue,
label: x.Valore,
}));
done();
},
error: function () {
extraFieldOptions = [];
done();
},
});
}
function buildExtraFieldCellHtml($row = null) {
if (!partsExtraField) return "";
const selectedValueId = $row ? $row.data("extra-value-id") || "" : "";
// SceltaMultipla -> select + hidden value id
if (
(partsExtraField.data_type || "").toLowerCase() === "sceltamultipla"
) {
const opts = [`<option value="">Select…</option>`]
.concat(
extraFieldOptions.map(
(o) => `<option value="${o.id}">${o.label}</option>`,
),
)
.join("");
return `
<td class="extra-field-td" style="width:200px;">
<input type="hidden" class="part-extra-field-id" value="${partsExtraField.field_id}">
<input type="hidden" class="part-extra-value-id" value="">
<select class="form-control form-control-sm part-extra-select" data-selected="${selectedValueId || ""}">${opts}</select>
</td>`;
}
// Testo -> input + hidden field_id
return `
<td class="extra-field-td" style="width:200px;">
<input type="hidden" class="part-extra-field-id" value="${partsExtraField.field_id}">
<input type="text" class="form-control form-control-sm part-extra-field" data-type="${partsExtraField.data_type || ""}">
</td>`;
}
$(document).on("change", ".part-extra-select", function () {
const valId = $(this).val() || "";
const $row = $(this).closest("tr");
// salva nello stato della riga (così sopravvive ai re-render)
$row.data("extra-value-id", valId);
$(this).closest("td").find(".part-extra-value-id").val(valId);
saveRow($row);
});
function applyExtraFieldColumn() {
const $theadRow = $("#partsTable thead tr");
// 1) rimuovi header extra
$theadRow.find("th.extra-field-th").remove();
// 2) rimuovi celle extra esistenti
$("#partsTableBody tr").each(function () {
$(this).find("td.extra-field-td").remove();
});
// 3) se non c'è campo extra -> fine
if (!partsExtraField) return;
// 4) inserisci header prima di "Azioni" (ultima colonna)
$theadRow
.find("th:last")
.before(
`<th class="extra-field-th" style="width:200px;">${partsExtraField.field_label}</th>`,
);
// 5) aggiungi cella a ogni riga già presente (una sola volta)
$("#partsTableBody tr").each(function () {
const $row = $(this);
$row.find("td:last").before(buildExtraFieldCellHtml($row));
// ripristina selezione se già presente nello stato riga
const selected = ($row.data("extra-value-id") || "").toString();
if (selected) {
$row.find(".part-extra-value-id").val(selected);
$row.find(".part-extra-select").val(selected);
}
});
}
function setPartId($row, id) {
if (!id) return;
// Sincronizza TUTTO: attributo + cache jQuery + nostra cache
@@ -297,19 +153,15 @@ $(document).ready(function () {
matrici = data.value || [];
loadMacroMatrici();
initializeGlobalSelect2();
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
},
error: function (xhr, status, error) {
matrici = [];
loadMacroMatrici();
initializeGlobalSelect2();
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento delle matrici: ' +
error +
@@ -328,16 +180,12 @@ $(document).ready(function () {
} else {
loadMacroMatrici();
initializeGlobalSelect2();
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
}
} else {
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
}
}
@@ -666,11 +514,6 @@ $(document).ready(function () {
if (response.new_id) return response.new_id;
if (response.partId) return response.partId;
// ✅ RISPOSTA DI save_parts.php: { success:true, results:[{part_id:...}] }
if (Array.isArray(response.results) && response.results[0]) {
if (response.results[0].part_id) return response.results[0].part_id;
if (response.results[0].id) return response.results[0].id;
}
return null;
}
@@ -684,13 +527,6 @@ $(document).ready(function () {
const iddatadb = $("#partsModal").data("iddatadb");
const idquotations = $("#partsModal").data("idquotations");
const isMix = partDescription.startsWith("Mix") ? "Y" : "N";
// EXTRA FIELD (0/1)
const extra_field_id = $row.find(".part-extra-field-id").val() || null;
const extra_value_id = $row.find(".part-extra-value-id").val() || null;
const extra_value_text = $row.find(".part-extra-field").length
? ($row.find(".part-extra-field").val() || "").trim()
: null;
let partId = getPartId($row);
if (partId === "new") partId = null; // difesa extra (non dovrebbe più servire, ma sicura)
const endpoint = idquotations
@@ -723,14 +559,8 @@ $(document).ready(function () {
part_number: partNumber,
part_description: partDescription,
mix: isMix,
idmatrice: $row.find(".part-matrice").val() || null,
dateexpiry: dateexpiry || null,
note: note,
// extra custom field
extra_field_id: extra_field_id,
extra_value_id: extra_value_id,
extra_value_text: extra_value_text,
},
],
}),
@@ -761,8 +591,7 @@ $(document).ready(function () {
$saveStatus.show();
setTimeout(() => $saveStatus.hide(), 2000);
if (!$("#quotationeBtn").hasClass("d-none"))
$("#quotationeBtn").addClass("d-none");
if (!$("#quotationeBtn").hasClass('d-none')) $("#quotationeBtn").addClass("d-none");
} else {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio: ' +
@@ -892,7 +721,6 @@ $(document).ready(function () {
</div>
</td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></td>
${partsExtraField ? buildExtraFieldCellHtml(null) : ``}
<td>
<button type="button" class="btn btn-light btn-sm note-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;" title="Aggiungi/Modifica nota"><i class="fas fa-file-alt"></i></button>
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
@@ -1326,13 +1154,9 @@ $(document).ready(function () {
}
});
$(document).on(
"blur",
".part-description, .part-number, .part-extra-field",
function () {
$(document).on("blur", ".part-description, .part-number", function () {
saveRow($(this).closest("tr"));
},
);
});
function loadExistingParts(iddatadb, idquotations, callback = null) {
const endpoint = idquotations
@@ -1385,7 +1209,6 @@ $(document).ready(function () {
</div>
</td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" value="${part.dateexpiry || ""}" style="width: 130px;"></td>
${partsExtraField ? buildExtraFieldCellHtml() : ``}
<td>
<button type="button" class="btn btn-light btn-sm note-btn ${part.note ? "has-note" : ""}" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;" title="Aggiungi/Modifica nota"><i class="fas fa-file-alt"></i></button>
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
@@ -1395,20 +1218,6 @@ $(document).ready(function () {
</td>
</tr>`;
$("#partsTableBody").append(newRow);
const $row = $(
`#partsTableBody tr[data-part-id="${part.id}"]`,
);
if (
part.extra_value_id !== undefined &&
part.extra_value_id !== null
) {
const vid = String(part.extra_value_id);
$row.data("extra-value-id", vid);
$row.find(".part-extra-value-id").val(vid);
$row.find(".part-extra-select").val(vid);
}
const $select = $("#partsTableBody").find(
`tr[data-part-id="${part.id}"] .part-matrice`,
);
@@ -1536,7 +1345,7 @@ $(document).ready(function () {
partId,
currentValue,
selectedMacro,
true,
true
);
});
});
@@ -1613,7 +1422,7 @@ $(document).ready(function () {
partId,
idmatrice,
selectedMacro = null,
fromFilter = false,
fromFilter = false
) {
if (typeof $.fn.select2 === "undefined") {
$select.replaceWith(
@@ -2000,11 +1809,7 @@ $(document).ready(function () {
reloadQuotations();
},
error: function (xhr, status, error) {
console.error(
"Errore AJAX caricamento quotations:",
error,
xhr.responseText,
);
console.error("Errore AJAX caricamento quotations:", error, xhr.responseText);
let message = `
<div class="alert alert-danger temp-alert" role="alert">
Errore nel caricamento delle quotations: ${error} (${xhr.status})
@@ -2018,7 +1823,7 @@ $(document).ready(function () {
$(this).remove();
});
}, 5000);
},
}
});
}
});
@@ -2026,10 +1831,7 @@ $(document).ready(function () {
$(document).on("click", "#addQuotationBtn", function () {
const quotationId = $("#addQuotationSelect").val();
if (
quotationId &&
confirm("Confermo di collegare la quotazione al campione?")
) {
if (quotationId && confirm("Confermo di collegare la quotazione al campione?")) {
$("#addQuotationModal").modal("hide");
loadParts(null, quotationId, () => {
@@ -2040,17 +1842,12 @@ $(document).ready(function () {
$(this).find(".save-loading").show();
});
let photoList = $("#photoSelector option")
.map(function () {
return this.value?.split("/")?.pop();
})
.get();
let photoList = $('#photoSelector option').map(function () {
return this.value?.split('/')?.pop();
}).get();
if (!photoList.length) {
let path = $("#samplePhoto")
.attr("src")
?.split("/")
?.pop();
let path = $('#samplePhoto').attr("src")?.split('/')?.pop();
if (path) photoList = [path];
}
@@ -2061,11 +1858,9 @@ $(document).ready(function () {
data: JSON.stringify({
iddatadb: $("#partsModal").data("iddatadb"),
photoList,
partIds: $("#partsTableBody tr")
.map(function () {
partIds: $('#partsTableBody tr').map(function () {
return $(this).data("part-id");
})
.get(),
}).get(),
}),
success: function () {
$("#partsTableBody tr").each(function () {
@@ -2078,8 +1873,7 @@ $(document).ready(function () {
setTimeout(() => $saveStatus.hide(), 2000);
});
},
error: function (xhr, status, error) {
}, error: function (xhr, status, error) {
let message = `
<div class="alert alert-danger temp-alert" role="alert">
Errore di salvataggio: ${error} (${xhr.status})
@@ -2093,7 +1887,7 @@ $(document).ready(function () {
$(this).remove();
});
}, 5000);
},
}
});
}, 100);
});
@@ -2104,15 +1898,11 @@ $(document).ready(function () {
if (quotations.length > 0) {
$("#addQuotationSelect").empty();
$("#addQuotationSelect").append(
"<option value=''>Seleziona Quotation</option>",
);
$("#addQuotationSelect").append("<option value=''>Seleziona Quotation</option>");
quotations.forEach((quotation) => {
quotations.forEach(quotation => {
if (quotation?.description) {
$("#addQuotationSelect").append(
`<option value='${quotation?.id}'>${quotation?.description}</option>`,
);
$("#addQuotationSelect").append(`<option value='${quotation?.id}'>${quotation?.description}</option>`);
}
});
@@ -2346,21 +2136,15 @@ $(document).on("click", ".save-common-note-btn", function () {
$(document).on("click", "#showHideImageBtn", function () {
let mainRow = $(this).closest(".parts-row");
let photoContainer = mainRow.find(".col-md-3");
let tableContainer = mainRow
.find("#partsTable")
.closest("div[class*='col-md']");
let tableContainer = mainRow.find("#partsTable").closest("div[class*='col-md']");
if (photoContainer.hasClass("d-none")) {
photoContainer.removeClass("d-none");
tableContainer.removeClass("col-md-12").addClass("col-md-9");
$(this).html(
"<i class='fas fa-eye-slash' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>",
);
$(this).html("<i class='fas fa-eye-slash' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>");
} else {
photoContainer.addClass("d-none");
tableContainer.removeClass("col-md-9").addClass("col-md-12");
$(this).html(
"<i class='fas fa-eye' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>",
);
$(this).html("<i class='fas fa-eye' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>");
}
});
-63
View File
@@ -349,69 +349,6 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
document
.querySelectorAll(".photo-flag-checkbox")
.forEach((checkbox) => {
checkbox.addEventListener("change", async function () {
const photoId = this.getAttribute("data-photo-id");
const field = this.getAttribute("data-field");
const value = this.checked ? 1 : 0;
const currentCheckbox = this;
// Only one PrimaPagina visually in the current popup
if (field === "PrimaPagina" && value === 1) {
document
.querySelectorAll(
".photo-flag-checkbox[data-field='PrimaPagina']",
)
.forEach((cb) => {
if (cb !== currentCheckbox) {
cb.checked = false;
}
});
}
try {
const formData = new URLSearchParams();
formData.append("photo_id", photoId);
formData.append("field", field);
formData.append("value", value);
const response = await fetch("update_photo_flags.php", {
method: "POST",
headers: {
"Content-Type":
"application/x-www-form-urlencoded",
},
body: formData.toString(),
});
const result = await response.json();
if (!result.success) {
throw new Error(
result.message || "Errore salvataggio flag",
);
}
// Safety refresh for PrimaPagina to ensure UI matches DB
if (field === "PrimaPagina") {
loadPopupContent(iddatadb, idquotations);
}
} catch (error) {
alert(
"Errore durante il salvataggio del flag: " +
error.message,
);
// rollback current checkbox
currentCheckbox.checked = !currentCheckbox.checked;
// reload popup to restore coherent state
loadPopupContent(iddatadb, idquotations);
}
});
});
document.querySelectorAll(".thumbnail").forEach((img) => {
img.addEventListener("click", function () {
const enlargedImage = document.getElementById("enlargedImage");
+12 -128
View File
@@ -117,24 +117,12 @@ $code = $row[$field] ?? 'Non disponibile';
// Recupera le foto associate alla riga
try {
$stmt = $pdo->prepare("
SELECT
id,
file_path,
file_name,
uploaded_at,
description,
StampaNelRapporto,
PrimaPagina
FROM {$photoTable}
WHERE {$photoParamName} = ?
ORDER BY uploaded_at DESC
");
$stmt = $pdo->prepare("SELECT id, file_path, file_name, description, uploaded_at FROM {$photoTable} WHERE {$photoParamName} = ? ORDER BY uploaded_at DESC");
$stmt->execute([$paramValue]);
$photos = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log("Errore query foto: " . $e->getMessage());
$photos = [];
$photos = []; // Imposta array vuoto in caso di errore
}
// Definisci il percorso base per le foto
@@ -146,7 +134,7 @@ $uploadUrl = $iddatadb
? $baseUrl . "/upload_photos_mobile.php?iddatadb=" . $iddatadb
: $baseUrl . "/upload_photos_mobile.php?idquotations=" . $idquotations;
// Genera il QR code con endroid/qr-code
// Genera il QR code con endroid/qr-code 6.0.6
$qrCodeDir = '../photostrf/qrcodes/';
if (!is_dir($qrCodeDir)) {
mkdir($qrCodeDir, 0755, true);
@@ -176,7 +164,6 @@ $result->saveToFile($qrCodeFile);
<p>Caricamento in corso...</p>
</div>
</div>
<h3>Manage Photos</h3>
<p><strong>ID:</strong> <?= htmlspecialchars($id) ?></p>
<p><strong>Code:</strong> <?= htmlspecialchars($code) ?></p>
@@ -195,7 +182,6 @@ $result->saveToFile($qrCodeFile);
<p>Drag the photo here or click to select</p>
<input type="file" id="photoInput" multiple accept="image/*" style="display: none;">
</div>
<!-- Area per la webcam -->
<div id="webcamArea" style="display: none; text-align: center; margin-bottom: 20px;">
<p>Webcam Preview</p>
@@ -208,9 +194,7 @@ $result->saveToFile($qrCodeFile);
<button id="closeWebcamBtn" style="padding: 10px 20px; background: #dc3545; color: white; border: none; cursor: pointer;">Close Webcam</button>
</div>
</div>
<button id="openWebcamBtn" style="padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; margin-bottom: 20px;">Take Photo with Webcam</button>
<!-- Elenco delle foto -->
<div id="photosList">
<?php if (empty($photos)): ?>
@@ -220,57 +204,25 @@ $result->saveToFile($qrCodeFile);
<?php
$filePath = $photoBasePath . $photo['file_path'];
$fileExists = file_exists($filePath);
$stampaNelRapporto = !empty($photo['StampaNelRapporto']) ? 1 : 0;
$primaPagina = !empty($photo['PrimaPagina']) ? 1 : 0;
?>
<div class="photo-item" data-photo-id="<?= (int)$photo['id'] ?>">
<div class="photo-thumb-area">
<div class="photo-item" data-photo-id="<?= $photo['id'] ?>" style="display: flex; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 10px;">
<div style="flex: 1;">
<?php if ($fileExists): ?>
<img src="../photostrf/<?= htmlspecialchars($photo['file_path']) ?>"
alt="<?= htmlspecialchars($photo['file_name']) ?>"
class="thumbnail"
style="max-width: 100px; max-height: 100px; cursor: pointer;">
<img src="../photostrf/<?= htmlspecialchars($photo['file_path']) ?>" alt="<?= htmlspecialchars($photo['file_name']) ?>" class="thumbnail" style="max-width: 100px; max-height: 100px; margin-right: 10px; cursor: pointer;">
<?php else: ?>
<div class="missing-file-box">[File non trovato]</div>
<p style="color: red;">[File non trovato]</p>
<?php endif; ?>
</div>
<div class="photo-info-area">
<p class="photo-name">
<strong>Name:</strong> <?= htmlspecialchars($photo['file_name']) ?>
<p style="margin: 0;">
<strong>Nome:</strong> <?= htmlspecialchars($photo['file_name']) ?><br>
<strong>Caricata il:</strong> <?= htmlspecialchars($photo['uploaded_at']) ?><br>
<strong>Descrizione:</strong> <?= htmlspecialchars($photo['description'] ?? 'Nessuna descrizione') ?>
</p>
<div class="photo-flags">
<label class="flag-label">
<input
type="checkbox"
class="photo-flag-checkbox"
data-photo-id="<?= (int)$photo['id'] ?>"
data-field="StampaNelRapporto"
<?= $stampaNelRapporto ? 'checked' : '' ?>>
Print in Report
</label>
<label class="flag-label">
<input
type="checkbox"
class="photo-flag-checkbox prima-pagina-checkbox"
data-photo-id="<?= (int)$photo['id'] ?>"
data-field="PrimaPagina"
<?= $primaPagina ? 'checked' : '' ?>>
First Page
</label>
</div>
</div>
<div class="photo-actions-area">
<button class="delete-photo-btn" data-photo-id="<?= (int)$photo['id'] ?>" type="button">
<button class="delete-photo-btn" data-photo-id="<?= $photo['id'] ?>" style="background: none; border: none; color: #dc3545; cursor: pointer;">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<?php endforeach; ?>
<!-- Bottone Crea Collage -->
<button id="createCollageBtn" style="padding: 10px 20px; background: #ffc107; color: white; border: none; cursor: pointer; margin-top: 20px;">Crea Collage</button>
<?php endif; ?>
@@ -331,12 +283,6 @@ $result->saveToFile($qrCodeFile);
<style>
.photo-item {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 12px;
border-bottom: 1px solid #eee;
padding: 12px 0;
transition: background-color 0.3s;
}
@@ -344,68 +290,6 @@ $result->saveToFile($qrCodeFile);
background-color: #f8f9fa;
}
.photo-thumb-area {
flex: 0 0 110px;
text-align: center;
}
.photo-info-area {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.photo-actions-area {
flex: 0 0 40px;
display: flex;
justify-content: center;
align-items: center;
}
.photo-name {
margin: 0;
word-break: break-word;
}
.photo-flags {
display: flex;
flex-wrap: wrap;
gap: 18px;
margin-top: 4px;
}
.flag-label {
display: flex;
align-items: center;
gap: 6px;
margin: 0;
font-size: 14px;
cursor: pointer;
}
.flag-label input[type="checkbox"] {
transform: scale(1.1);
cursor: pointer;
}
.delete-photo-btn {
background: none;
border: none;
color: #dc3545;
cursor: pointer;
font-size: 18px;
}
.delete-photo-btn:hover {
color: #b02a37;
}
.missing-file-box {
color: red;
font-size: 13px;
}
#dropArea.highlight {
border-color: #28a745;
background-color: #e9ecef;
+7 -96
View File
@@ -4,7 +4,6 @@ include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$data = json_decode(file_get_contents('php://input'), true);
@@ -20,21 +19,6 @@ try {
$pdo->beginTransaction();
$results = [];
// Custom fields statements (child table)
$stmtUpsertCF = $pdo->prepare("
INSERT INTO identification_parts_customfields (part_id, field_id, value_id, value_text)
VALUES (:part_id, :field_id, :value_id, :value_text)
ON DUPLICATE KEY UPDATE
value_id = VALUES(value_id),
value_text = VALUES(value_text),
updated_at = NOW()
");
$stmtDeleteCF = $pdo->prepare("
DELETE FROM identification_parts_customfields
WHERE part_id = :part_id AND field_id = :field_id
");
foreach ($parts as $part) {
$partId = $part['id'] ?? null;
$partNumber = $part['part_number'] ?? null;
@@ -44,23 +28,9 @@ try {
$note = $part['note'] ?? null;
$dateexpiry = $part['dateexpiry'] ?? null;
// Extra field (0/1)
$extraFieldId = $part['extra_field_id'] ?? null;
$extraValueId = $part['extra_value_id'] ?? null;
$extraValueText = $part['extra_value_text'] ?? null;
// Normalizza vuoti
if ($extraFieldId !== null && $extraFieldId !== '') $extraFieldId = (int)$extraFieldId;
else $extraFieldId = null;
if ($extraValueId !== null && $extraValueId !== '') $extraValueId = (int)$extraValueId;
else $extraValueId = null;
if ($extraValueText !== null) {
$extraValueText = trim((string)$extraValueText);
if ($extraValueText === '') $extraValueText = null;
}
if ($partDescription || $note || $dateexpiry) {
if ($partId) {
// UPDATE se la parte esiste (sempre)
// UPDATE se la parte esiste
$stmt = $pdo->prepare("UPDATE identification_parts
SET part_number = :part_number,
part_description = :part_description,
@@ -79,39 +49,9 @@ try {
':note' => $note,
':dateexpiry' => $dateexpiry,
]);
// Save extra custom field (if provided)
if ($extraFieldId !== null) {
if ($extraValueId === null && $extraValueText === null) {
$stmtDeleteCF->execute([
':part_id' => $partId,
':field_id' => $extraFieldId,
]);
$results[] = ['part_id' => $partId, 'part_number' => $partNumber, 'message' => 'Parte aggiornata con successo'];
} else {
$stmtUpsertCF->execute([
':part_id' => $partId,
':field_id' => $extraFieldId,
':value_id' => $extraValueId,
':value_text' => $extraValueText,
]);
}
}
$cf_row = null;
if ($extraFieldId !== null) {
$chk = $pdo->prepare("SELECT id, value_id, value_text FROM identification_parts_customfields WHERE part_id = ? AND field_id = ?");
$chk->execute([$partId, $extraFieldId]);
$cf_row = $chk->fetch(PDO::FETCH_ASSOC);
}
$results[] = [
'part_id' => $partId,
'part_number' => $partNumber,
'message' => 'Parte aggiornata con successo',
'cf_row' => $cf_row
];
} else if ($partDescription || $note || $dateexpiry) {
// INSERT per nuova parte (solo se ha contenuto)
// INSERT per nuova parte
$stmt = $pdo->prepare("INSERT INTO identification_parts
(iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
VALUES (:iddatadb, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())");
@@ -124,43 +64,14 @@ try {
':note' => $note,
':dateexpiry' => $dateexpiry,
]);
$newId = (int)$pdo->lastInsertId();
if ($extraFieldId !== null) {
if ($extraValueId === null && $extraValueText === null) {
$stmtDeleteCF->execute([
':part_id' => $newId,
':field_id' => $extraFieldId,
]);
} else {
$stmtUpsertCF->execute([
':part_id' => $newId,
':field_id' => $extraFieldId,
':value_id' => $extraValueId,
':value_text' => $extraValueText,
]);
$newId = $pdo->lastInsertId();
$results[] = ['part_id' => $newId, 'part_number' => $partNumber, 'message' => 'Parte salvata con successo'];
}
}
$results[] = [
'part_id' => $newId,
'part_number' => $partNumber,
'message' => 'Parte salvata con successo'
];
}
}
$pdo->commit();
echo json_encode([
'success' => true,
'results' => $results,
'debug_last' => [
'extra_field_id' => $extraFieldId ?? null,
'extra_value_id' => $extraValueId ?? null,
'extra_value_text' => $extraValueText ?? null,
'part_id' => $partId ?? ($newId ?? null),
]
]);
echo json_encode(['success' => true, 'results' => $results]);
} catch (PDOException $e) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Errore nel salvataggio: ' . $e->getMessage()]);
-91
View File
@@ -1,91 +0,0 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('Metodo non valido');
}
$photoId = isset($_POST['photo_id']) ? (int)$_POST['photo_id'] : 0;
$field = isset($_POST['field']) ? trim($_POST['field']) : '';
$value = isset($_POST['value']) ? (int)$_POST['value'] : 0;
if ($photoId <= 0) {
throw new Exception('photo_id mancante o non valido');
}
$allowedFields = ['StampaNelRapporto', 'PrimaPagina'];
if (!in_array($field, $allowedFields, true)) {
throw new Exception('Campo non consentito');
}
$value = $value === 1 ? 1 : 0;
// Recupera la foto per sapere a quale record padre appartiene
$stmt = $pdo->prepare("
SELECT id, iddatadb, idquotations
FROM datadb_photos
WHERE id = ?
LIMIT 1
");
$stmt->execute([$photoId]);
$photo = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$photo) {
throw new Exception('Foto non trovata');
}
$iddatadb = !empty($photo['iddatadb']) ? (int)$photo['iddatadb'] : null;
$idquotations = !empty($photo['idquotations']) ? (int)$photo['idquotations'] : null;
$pdo->beginTransaction();
if ($field === 'PrimaPagina' && $value === 1) {
// Solo una foto può essere PrimaPagina per lo stesso record padre
if ($iddatadb) {
$stmtReset = $pdo->prepare("
UPDATE datadb_photos
SET PrimaPagina = 0
WHERE iddatadb = ?
");
$stmtReset->execute([$iddatadb]);
} elseif ($idquotations) {
$stmtReset = $pdo->prepare("
UPDATE datadb_photos
SET PrimaPagina = 0
WHERE idquotations = ?
");
$stmtReset->execute([$idquotations]);
}
}
$stmtUpdate = $pdo->prepare("
UPDATE datadb_photos
SET {$field} = ?
WHERE id = ?
");
$stmtUpdate->execute([$value, $photoId]);
$pdo->commit();
echo json_encode([
'success' => true,
'photo_id' => $photoId,
'field' => $field,
'value' => $value
]);
} catch (Exception $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+6 -32
View File
@@ -21,37 +21,11 @@ $value = ($value === 1) ? 1 : 0;
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare("UPDATE template_mapping SET is_visible_parts = ? WHERE id = ? AND template_id = ?");
$result = $stmt->execute([$value, $mapping_id, $template_id]);
if ($value === 1) {
// 1) Force ONLY ONE row visible_parts per template:
// set all to 0 for this template
$stmtReset = $pdo->prepare("
UPDATE template_mapping
SET is_visible_parts = 0
WHERE template_id = ?
");
$stmtReset->execute([$template_id]);
}
// 2) Set requested mapping to 1 or 0
$stmtSet = $pdo->prepare("
UPDATE template_mapping
SET is_visible_parts = ?
WHERE id = ? AND template_id = ?
");
$stmtSet->execute([$value, $mapping_id, $template_id]);
$pdo->commit();
echo json_encode([
'success' => true,
'template_id' => $template_id,
'mapping_id' => $mapping_id,
'value' => $value
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
if ($result) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to update is_visible_parts']);
}
-120
View File
@@ -1,120 +0,0 @@
<?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;
}
// ── 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'];
// 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()]);
}