diff --git a/public/userarea/class/VisualLimsApiClient.class.php b/public/userarea/class/VisualLimsApiClient.class.php
index ca7d5ec..f2d992d 100644
--- a/public/userarea/class/VisualLimsApiClient.class.php
+++ b/public/userarea/class/VisualLimsApiClient.class.php
@@ -24,7 +24,17 @@ class VisualLimsApiClient
public static function getInstance()
{
if (self::$instance === null) {
- self::$instance = new VisualLimsApiClient();
+ $dotenv = Dotenv::createImmutable(dirname(__DIR__, 3));
+ $dotenv->load();
+
+ $simulate = ($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true';
+
+ if ($simulate) {
+ require_once __DIR__ . '/VisualLimsApiClientMock.class.php';
+ self::$instance = new VisualLimsApiClientMock();
+ } else {
+ self::$instance = new VisualLimsApiClient();
+ }
}
return self::$instance;
}
@@ -193,6 +203,56 @@ class VisualLimsApiClient
return json_decode($response, true);
}
+ /**
+ * POST a file as multipart/form-data (used for photo/attachment uploads).
+ *
+ * @param string $endpoint OData endpoint, e.g. "AllegatoCommessaWeb"
+ * @param string $filePath Absolute path to the file on disk
+ * @param string $fileName Original file name to send
+ * @param int $commessaId CommessaWeb ID to link the attachment to
+ * @return array|null Decoded JSON response
+ */
+ public function postMultipart($endpoint, $filePath, $fileName, $commessaId)
+ {
+ $token = $this->getToken();
+ $url = "{$this->baseUrl}/api/odata/{$endpoint}";
+
+ $cfile = new CURLFile($filePath, mime_content_type($filePath) ?: 'application/octet-stream', $fileName);
+
+ $payload = [
+ 'IdCommessa' => $commessaId,
+ 'NomeFile' => $fileName,
+ 'file' => $cfile,
+ ];
+
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ "Authorization: Bearer {$token}",
+ "Accept: application/json",
+ // Content-Type is set automatically to multipart/form-data by cURL
+ ]);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
+
+ $response = curl_exec($ch);
+ $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $curl_error = curl_error($ch);
+ curl_close($ch);
+
+ if ($response === false) {
+ throw new Exception("Errore nella richiesta POST multipart: {$curl_error}");
+ }
+
+ if ($http_code < 200 || $http_code >= 300) {
+ throw new Exception("POST multipart fallito: HTTP {$http_code}, Risposta: " . substr($response, 0, 1000));
+ }
+
+ return json_decode($response, true);
+ }
+
public function getBaseUrl()
{
return $this->baseUrl;
diff --git a/public/userarea/class/VisualLimsApiClientMock.class.php b/public/userarea/class/VisualLimsApiClientMock.class.php
new file mode 100644
index 0000000..2f5be33
--- /dev/null
+++ b/public/userarea/class/VisualLimsApiClientMock.class.php
@@ -0,0 +1,135 @@
+fakeCommessaId = mt_rand(90001, 99999);
+ error_log("[SIMULATE] VisualLimsApiClientMock initialised (fakeCommessaId={$this->fakeCommessaId})");
+ }
+
+ public function get(string $endpoint): array
+ {
+ error_log("[SIMULATE] GET {$endpoint}");
+
+ // --- Fixed-field dropdown lists ---
+
+ if (str_starts_with($endpoint, 'MoltiplicatorePrezzi')) {
+ return ['value' => [
+ ['IdMoltiplicatorePrezzo' => 1, 'Codice' => 'MP-01', 'Descrizione' => 'Standard (1x)'],
+ ['IdMoltiplicatorePrezzo' => 2, 'Codice' => 'MP-02', 'Descrizione' => 'Urgente (1.5x)'],
+ ['IdMoltiplicatorePrezzo' => 3, 'Codice' => 'MP-03', 'Descrizione' => 'Extra Urgente (2x)'],
+ ]];
+ }
+
+ if (str_starts_with($endpoint, 'AnagraficaCertestObject')) {
+ return ['value' => [
+ ['IdAnagrafica' => 1, 'Codice' => 'OBJ-01', 'NomeAnagrafica' => 'Articolo Tessile'],
+ ['IdAnagrafica' => 2, 'Codice' => 'OBJ-02', 'NomeAnagrafica' => 'Componente Meccanico'],
+ ['IdAnagrafica' => 3, 'Codice' => 'OBJ-03', 'NomeAnagrafica' => 'Materiale Plastico'],
+ ]];
+ }
+
+ if (str_starts_with($endpoint, 'AnagraficaCertestService')) {
+ return ['value' => [
+ ['IdAnagrafica' => 1, 'Codice' => 'SRV-01', 'NomeAnagrafica' => 'Analisi Chimica'],
+ ['IdAnagrafica' => 2, 'Codice' => 'SRV-02', 'NomeAnagrafica' => 'Test Meccanico'],
+ ['IdAnagrafica' => 3, 'Codice' => 'SRV-03', 'NomeAnagrafica' => 'Prova Ambientale'],
+ ]];
+ }
+
+ // Cliente? list — get_clienti.php exits early in simulate mode, but guard here too
+ if (str_starts_with($endpoint, 'Cliente?')) {
+ return ['value' => []];
+ }
+
+ // Cliente(N)?$expand=Responsabili
+ if (str_starts_with($endpoint, 'Cliente(')) {
+ preg_match('/Cliente\((\d+)\)/', $endpoint, $m);
+ $clienteId = isset($m[1]) ? (int) $m[1] : 0;
+ return [
+ 'IdCliente' => $clienteId,
+ 'Responsabili' => [
+ ['IdClienteResponsabile' => 1, 'Nominativo' => 'Marco Bianchi'],
+ ['IdClienteResponsabile' => 2, 'Nominativo' => 'Giulia Ferrari'],
+ ['IdClienteResponsabile' => 3, 'Nominativo' => 'Andrea Russo'],
+ ],
+ ];
+ }
+
+ // --- CustomField dropdown values (get_customfield_values.php) ---
+
+ if (str_starts_with($endpoint, 'CustomField(')) {
+ preg_match('/CustomField\((\d+)\)/', $endpoint, $m);
+ $fieldId = isset($m[1]) ? (int) $m[1] : 0;
+ return [
+ 'CustomFieldsValues' => [
+ ['IdCustomFieldsValue' => $fieldId * 10 + 1, 'Valore' => 'Opzione A'],
+ ['IdCustomFieldsValue' => $fieldId * 10 + 2, 'Valore' => 'Opzione B'],
+ ['IdCustomFieldsValue' => $fieldId * 10 + 3, 'Valore' => 'Opzione C'],
+ ],
+ ];
+ }
+
+ // --- CommessaWeb OData calls (STEP 7 GET + STEP 10 verification) ---
+
+ preg_match('/\((\d+)\)/', $endpoint, $m);
+ $id = isset($m[1]) ? (int) $m[1] : $this->fakeCommessaId;
+
+ return [
+ 'IdCommessa' => $id,
+ 'CodiceCommessa' => "SIM-{$id}",
+ 'CommesseCustomFields' => [], // Empty → PATCH step is skipped correctly
+ ];
+ }
+
+ public function post(string $endpoint, array $payload): array
+ {
+ error_log("[SIMULATE] POST {$endpoint} payload=" . json_encode($payload));
+
+ // CommessaWeb creation
+ if ($endpoint === 'CommessaWeb') {
+ return [
+ 'IdCommessa' => $this->fakeCommessaId,
+ 'CodiceCommessa' => "SIM-{$this->fakeCommessaId}",
+ 'Richiedente' => $payload['Richiedente'] ?? '',
+ 'Descrizione' => $payload['Descrizione'] ?? '',
+ ];
+ }
+
+ // Campione creation
+ if ($endpoint === 'Campione') {
+ return [
+ 'IdCampione' => mt_rand(10001, 19999),
+ 'Commessa' => $payload['Commessa'] ?? null,
+ 'Matrice' => $payload['Matrice'] ?? null,
+ ];
+ }
+
+ // InviaCommessa / ImportaCommessa (currently commented out upstream)
+ return ['simulated' => true, 'endpoint' => $endpoint];
+ }
+
+ public function patch(string $endpoint, array $payload): array
+ {
+ error_log("[SIMULATE] PATCH {$endpoint} payload=" . json_encode($payload));
+
+ return [];
+ }
+
+ public function postMultipart(string $endpoint, string $filePath, string $fileName, int $commessaId): array
+ {
+ error_log("[SIMULATE] POST multipart {$endpoint} file={$fileName} commessaId={$commessaId}");
+
+ return ['simulated' => true, 'file' => $fileName];
+ }
+}
diff --git a/public/userarea/export_to_lims.js b/public/userarea/export_to_lims.js
index 74e2be2..c8970da 100644
--- a/public/userarea/export_to_lims.js
+++ b/public/userarea/export_to_lims.js
@@ -1,7 +1,6 @@
document.addEventListener("DOMContentLoaded", () => {
console.log("export_to_lims.js loaded");
- // Debug: verifica che i pulsanti siano trovati
const exportButtons = document.querySelectorAll(".export-lims-btn");
console.log(`Found ${exportButtons.length} export-lims-btn buttons`);
@@ -10,6 +9,170 @@ document.addEventListener("DOMContentLoaded", () => {
return;
}
+ // Tracks the active confirm handler so it can be replaced on re-open
+ let pendingConfirmHandler = null;
+
+ // ── Helpers ──────────────────────────────────────────────────────────────
+
+ function cleanupBackdrop() {
+ document.querySelectorAll(".modal-backdrop").forEach((b) => b.remove());
+ document.body.classList.remove("modal-open");
+ document.body.style.paddingRight = "";
+ const overlay = document.querySelector(".overlay.toggle-icon");
+ if (overlay) overlay.style.display = "none";
+ }
+
+ // ── Step 2: show export-confirm modal, send on "Conferma" ────────────────
+
+ function startExportConfirmFlow(iddatadb, btn) {
+ const confirmModalElement = document.getElementById("exportConfirmModal");
+ if (!confirmModalElement) {
+ alert("Errore: Modale di conferma non trovato");
+ return;
+ }
+
+ const confirmModal = new bootstrap.Modal(confirmModalElement, {
+ keyboard: false,
+ });
+ document.getElementById("exportIddatadb").textContent = iddatadb;
+ confirmModal.show();
+
+ const confirmBtn = document.getElementById("exportConfirmBtn");
+ if (!confirmBtn) {
+ confirmModal.hide();
+ alert("Errore: Pulsante di conferma non trovato");
+ return;
+ }
+
+ const confirmHandler = async () => {
+ pendingConfirmHandler = null;
+ console.log(`Confirmed export for iddatadb: ${iddatadb}`);
+ confirmModal.hide();
+
+ const formData = new FormData();
+ formData.append("iddatadb", iddatadb);
+
+ try {
+ const response = await fetch("export_to_lims.php", {
+ method: "POST",
+ body: formData,
+ });
+ if (!response.ok)
+ throw new Error(`HTTP error! status: ${response.status}`);
+ const data = await response.json();
+
+ console.log("Export response:", data);
+
+ const responseModalElement =
+ document.getElementById("exportResponseModal");
+ if (!responseModalElement) {
+ alert("Errore: Modale di risposta non trovato");
+ return;
+ }
+
+ const responseModal = new bootstrap.Modal(
+ responseModalElement,
+ { keyboard: false },
+ );
+ const responseMessage = document.getElementById(
+ "exportResponseMessage",
+ );
+
+ if (data.success) {
+ responseMessage.innerHTML =
+ `${data.message.replace(/\n/g, "
")}` +
+ `
ID CommessaWeb: ${data.idcommessaweb}` +
+ `
Codice CommessaWeb: ${data.commessaweb}` +
+ (data.totalPhotos > 0
+ ? `
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);
+ 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 },
+ );
+ }
+ };
+
+ if (pendingConfirmHandler) {
+ confirmBtn.removeEventListener("click", pendingConfirmHandler);
+ }
+ pendingConfirmHandler = confirmHandler;
+ confirmBtn.addEventListener("click", confirmHandler, { once: true });
+ }
+
+ // ── Step 1: check unsaved changes, save if needed, then export ───────────
+
exportButtons.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
@@ -19,221 +182,40 @@ document.addEventListener("DOMContentLoaded", () => {
`Export to LIMS clicked for row ${rowIndex}, iddatadb: ${iddatadb}`,
);
- // Mostra il modale di conferma
- const confirmModalElement =
- document.getElementById("exportConfirmModal");
- if (!confirmModalElement) {
- console.error("exportConfirmModal not found in the DOM");
- alert("Errore: Modale di conferma non trovato");
- return;
- }
+ const gridRow = btn.closest(".grid-row");
- const confirmModal = new bootstrap.Modal(confirmModalElement, {
- keyboard: false,
- });
- document.getElementById("exportIddatadb").textContent = iddatadb;
- confirmModal.show();
+ if (gridRow && gridRow.querySelector(".cell-changed")) {
+ const unsavedModal = new bootstrap.Modal(
+ document.getElementById("exportUnsavedModal"),
+ { keyboard: false },
+ );
+ unsavedModal.show();
- // Gestisci il click su "Conferma"
- const confirmBtn = document.getElementById("exportConfirmBtn");
- if (!confirmBtn) {
- console.error("exportConfirmBtn not found in the DOM");
- confirmModal.hide();
- alert("Errore: Pulsante di conferma non trovato");
- return;
- }
+ document.getElementById("saveAndExportBtn").addEventListener("click", () => {
+ unsavedModal.hide();
+ const saveBtn = gridRow.querySelector(".save-btn");
+ if (!saveBtn) return;
- const confirmHandler = async () => {
- console.log(`Confirmed export for iddatadb: ${iddatadb}`);
- confirmModal.hide();
-
- const formData = new FormData();
- formData.append("iddatadb", iddatadb);
-
- try {
- const response = await fetch("export_to_lims.php", {
- method: "POST",
- body: formData,
- });
- if (!response.ok)
- throw new Error(
- `HTTP error! status: ${response.status}`,
- );
- const data = await response.json();
-
- console.log("Export response:", data);
-
- // Mostra il modale di risposta
- const responseModalElement = document.getElementById(
- "exportResponseModal",
- );
- if (!responseModalElement) {
- console.error(
- "exportResponseModal not found in the DOM",
- );
- 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, "
")}
ID CommessaWeb: ${data.idcommessaweb}`;
- document.getElementById(
- "exportResponseModalLabel",
- ).textContent = "Esportazione Completata";
- responseModal.show();
-
- // Aggiorna la UI per riflettere lo stato 'To LIMS'
- const statusCell = btn
- .closest(".grid-row")
- .querySelector(
- '.grid-cell[data-col="status"] .status-badge',
- );
- if (statusCell) {
- statusCell.classList.remove("status-i", "status-P");
- statusCell.classList.add("status-l");
- statusCell.textContent = "To LIMS";
+ const observer = new MutationObserver(() => {
+ if (!gridRow.querySelector(".cell-changed")) {
+ observer.disconnect();
+ startExportConfirmFlow(iddatadb, btn);
}
+ });
+ observer.observe(gridRow, {
+ subtree: true,
+ attributes: true,
+ attributeFilter: ["class"],
+ });
- // Gestisci la chiusura del modale di risposta
- responseModalElement.addEventListener(
- "hidden.bs.modal",
- () => {
- console.log(
- "exportResponseModal closed, cleaning up",
- );
- // Rimuovi tutti i backdrop residui
- document
- .querySelectorAll(".modal-backdrop")
- .forEach((backdrop) => {
- console.log(
- "Removing backdrop:",
- backdrop,
- );
- backdrop.remove();
- });
- // Ripristina il body
- document.body.classList.remove("modal-open");
- document.body.style.paddingRight = "";
- // Nascondi l'overlay
- const overlay = document.querySelector(
- ".overlay.toggle-icon",
- );
- if (overlay) {
- overlay.style.display = "none";
- }
- },
- { once: true },
- );
- } else {
- responseMessage.textContent = `Errore durante la generazione dei payload: ${data.message}`;
- document.getElementById(
- "exportResponseModalLabel",
- ).textContent = "Errore Esportazione";
- responseModal.show();
+ saveBtn.click();
+ }, { once: true });
- // Gestisci la chiusura del modale di risposta anche in caso di errore
- responseModalElement.addEventListener(
- "hidden.bs.modal",
- () => {
- console.log(
- "exportResponseModal closed, cleaning up",
- );
- // Rimuovi tutti i backdrop residui
- document
- .querySelectorAll(".modal-backdrop")
- .forEach((backdrop) => {
- console.log(
- "Removing backdrop:",
- backdrop,
- );
- backdrop.remove();
- });
- // Ripristina il body
- document.body.classList.remove("modal-open");
- document.body.style.paddingRight = "";
- // Nascondi l'overlay
- const overlay = document.querySelector(
- ".overlay.toggle-icon",
- );
- if (overlay) {
- overlay.style.display = "none";
- }
- },
- { once: true },
- );
- }
- } catch (error) {
- console.error("Export error:", error);
- const responseModalElement = document.getElementById(
- "exportResponseModal",
- );
- if (!responseModalElement) {
- console.error(
- "exportResponseModal not found in the DOM",
- );
- alert("Errore: Modale di risposta non trovato");
- return;
- }
- const responseModal = new bootstrap.Modal(
- responseModalElement,
- {
- keyboard: false,
- },
- );
- document.getElementById(
- "exportResponseMessage",
- ).textContent =
- `Errore durante la generazione dei payload: ${error.message}`;
- document.getElementById(
- "exportResponseModalLabel",
- ).textContent = "Errore Esportazione";
- responseModal.show();
+ return;
+ }
- // Gestisci la chiusura del modale di risposta in caso di errore
- responseModalElement.addEventListener(
- "hidden.bs.modal",
- () => {
- console.log(
- "exportResponseModal closed, cleaning up",
- );
- // Rimuovi tutti i backdrop residui
- document
- .querySelectorAll(".modal-backdrop")
- .forEach((backdrop) => {
- console.log("Removing backdrop:", backdrop);
- backdrop.remove();
- });
- // Ripristina il body
- document.body.classList.remove("modal-open");
- document.body.style.paddingRight = "";
- // Nascondi l'overlay
- const overlay = document.querySelector(
- ".overlay.toggle-icon",
- );
- if (overlay) {
- overlay.style.display = "none";
- }
- },
- { once: true },
- );
- }
-
- // Rimuovi il listener dopo l'esecuzione
- confirmBtn.removeEventListener("click", confirmHandler);
- };
-
- // Rimuovi eventuali listener precedenti
- confirmBtn.removeEventListener("click", confirmHandler);
- confirmBtn.addEventListener("click", confirmHandler);
+ // No unsaved changes — go straight to export confirm
+ startExportConfirmFlow(iddatadb, btn);
});
});
});
diff --git a/public/userarea/export_to_lims.php b/public/userarea/export_to_lims.php
index e7f8dfa..2461a1b 100644
--- a/public/userarea/export_to_lims.php
+++ b/public/userarea/export_to_lims.php
@@ -13,6 +13,8 @@ if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
+$uploadDir = realpath(__DIR__ . '/../photostrf') . '/';
+
// 🔹 Base URL API
$apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
@@ -34,8 +36,14 @@ try {
}
// 🔹 STEP 1+2: Fetch Cliente ID from datadb and Schema ID from excel_templates
+ // Also fetch fixed fields stored in datadb
$stmt = $pdo->prepare("
- SELECT d.idclient AS clienteId, et.idschema AS schemaId
+ SELECT d.idclient AS clienteId, et.idschema AS schemaId,
+ d.cliente_responsabile_id,
+ d.moltiplicatore_prezzo_id,
+ d.anagrafica_certest_object_id,
+ d.anagrafica_certest_service_id,
+ d.cliente_fornitore_id
FROM datadb as d
INNER JOIN excel_templates as et ON d.templateid = et.id
WHERE d.iddatadb = :iddatadb
@@ -51,6 +59,13 @@ try {
$clienteId = (int) $result['clienteId'];
$schemaId = (int) $result['schemaId'];
+ // Extract fixed fields (nullable INTs)
+ $clienteResponsabile = !empty($result['cliente_responsabile_id']) ? (int) $result['cliente_responsabile_id'] : null;
+ $moltiplicatorePrezzo = !empty($result['moltiplicatore_prezzo_id']) ? (int) $result['moltiplicatore_prezzo_id'] : null;
+ $anagraficaObject = !empty($result['anagrafica_certest_object_id']) ? (int) $result['anagrafica_certest_object_id'] : null;
+ $anagraficaService = !empty($result['anagrafica_certest_service_id'])? (int) $result['anagrafica_certest_service_id']: null;
+ $clienteFornitore = !empty($result['cliente_fornitore_id'])? (int) $result['cliente_fornitore_id']: null;
+
// 🔹 STEP 3: Fetch Parts (including idmatrice)
$stmt = $pdo->prepare("
SELECT part_number, part_description, material, color, mix, idmatrice
@@ -94,11 +109,18 @@ try {
$api = VisualLimsApiClient::getInstance();
// 🔹 STEP 5: Create CommessaWeb (NOT WebOrder)
+ // Includes fixed fields fetched from datadb in STEP 1+2
$commessaWebPayload = [
- "Cliente" => $clienteId,
- "SchemaCustomField" => $schemaId,
- "Richiedente" => "Test Web Import",
- "Descrizione" => "TEST CommessaWeb",
+ "Cliente" => $clienteId,
+ "SchemaCustomField" => $schemaId,
+ "Richiedente" => "Test Web Import",
+ "Descrizione" => "TEST CommessaWeb",
+ // Fixed fields from datadb
+ "ClienteResponsabile" => $clienteResponsabile,
+ "MoltiplicatorePrezzo" => $moltiplicatorePrezzo,
+ "AnagraficaCertestObject" => $anagraficaObject,
+ "AnagraficaCertestService" => $anagraficaService,
+ "ClienteFornitore" => $clienteFornitore, // PLACEHOLDER — to be implemented
];
// Costruisci log curl-like per STEP 5
@@ -119,6 +141,49 @@ try {
$commessaId = $commessaWeb["IdCommessa"];
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
+ // 🔹 STEP 5.1: Fetch photos linked to this iddatadb
+ $stmtPhotos = $pdo->prepare("
+ SELECT id, file_path, file_name
+ FROM datadb_photos
+ WHERE iddatadb = :iddatadb
+ ORDER BY id ASC
+ ");
+ $stmtPhotos->execute(['iddatadb' => $iddatadb]);
+ $photos = $stmtPhotos->fetchAll(PDO::FETCH_ASSOC);
+
+ // 🔹 STEP 5.2: Upload photos to CommessaWeb
+ // NOTE: The sample number corresponds to the CommessaWeb ID ($commessaId).
+ // Photos may be multiple or may not exist at all.
+
+ $photosUploaded = 0;
+ $logContentPhotos = "Photos for CommessaWeb {$commessaId} (iddatadb={$iddatadb}):\n";
+ $logContentPhotos .= "Total photos found: " . count($photos) . "\n\n";
+
+ foreach ($photos as $photo) {
+ // Build absolute path from the relative path stored in DB
+ $photoPath = $uploadDir . '/' . ltrim($photo['file_path'], './');
+ $fullPath = realpath($photoPath);
+
+ if (!$fullPath || !file_exists($fullPath)) {
+ $logContentPhotos .= "SKIP (file not found): full - $photoPath " . $photo['file_path'] . "\n";
+ continue;
+ }
+
+ // Construct curl-like log entry
+ $logContentPhotos .= "curl --location --request POST '{$apiBaseUrl}AllegatoCommessaWeb' \\\n" .
+ "--header 'Authorization: Bearer ••••••' \\\n" .
+ "--form 'IdCommessa={$commessaId}' \\\n" .
+ "--form 'file=@{$fullPath}'\n\n";
+
+ // ENDPOINT NAME TO BE CONFIRMED
+ $photoResult = $api->postMultipart("AllegatoCommessaWeb", $fullPath, $photo['file_name'], $commessaId);
+ $logContentPhotos .= "RESPONSE:\n" . json_encode($photoResult, JSON_PRETTY_PRINT) . "\n\n---\n";
+ $photosUploaded++;
+ }
+
+ $logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt";
+ file_put_contents($logFilePhotos, $logContentPhotos);
+
// 🔹 STEP 6: Create Campioni (Samples) for each part
$campioni = [];
$logContentStep6 = "";
@@ -239,6 +304,20 @@ try {
file_put_contents($logFileStep9, $logContentStep9);
*/
+ // 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto)
+ // Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa
+
+ $importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", []);
+
+ // Logga il POST
+ $logContentStep91 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/ImportaCommessa' \\\n" .
+ "--header 'Content-Type: application/json' \\\n" .
+ "--header 'Authorization: Bearer ••••••' \\\n" .
+ "--data '{}'\n\n" .
+ "RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
+ $logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
+ file_put_contents($logFileStep91, $logContentStep91);
+
// 🔹 STEP 10: GET di controllo post-PATCH
$expand = "CommesseCustomFields(\$expand=CustomField)";
$commessaAfterPatch = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
@@ -262,17 +341,22 @@ try {
];
echo json_encode([
- "success" => true,
- "commessaWeb" => $finalCommessa,
+ "success" => true,
+ "idcommessaweb" => $commessaId,
+ "commessaweb" => $commessaWebCode,
+ "commessaWeb" => $finalCommessa,
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
- "totalCampioni" => count($campioni),
- "totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
- "message" => "Export successful",
- "logFiles" => [
- "step5_create" => $logFileStep5,
- "step6_campioni" => $logFileStep6,
- "step7_patch" => $logFileStep7,
- "step10_get" => $logFileStep10
+ "totalCampioni" => count($campioni),
+ "totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
+ "totalPhotos" => count($photos),
+ "message" => "Export successful",
+ "logFiles" => [
+ "step5_create" => $logFileStep5,
+ "step5_2_photos" => $logFilePhotos,
+ "step6_campioni" => $logFileStep6,
+ "step7_patch" => $logFileStep7 ?? null,
+ "step9_1_importa" => $logFileStep91,
+ "step10_get" => $logFileStep10
]
]);
} catch (Exception $e) {
@@ -282,10 +366,12 @@ try {
"success" => false,
"message" => "Export failed: " . $e->getMessage(),
"logFiles" => [
- "step5_create" => $logFileStep5 ?? null,
- "step6_campioni" => $logFileStep6 ?? null,
- "step7_patch" => $logFileStep7 ?? null,
- "step10_get" => $logFileStep10 ?? null
+ "step5_create" => $logFileStep5 ?? null,
+ "step5_2_photos" => $logFilePhotos ?? null,
+ "step6_campioni" => $logFileStep6 ?? null,
+ "step7_patch" => $logFileStep7 ?? null,
+ "step9_1_importa" => $logFileStep91 ?? null,
+ "step10_get" => $logFileStep10 ?? null
]
]);
}
diff --git a/public/userarea/get_clienti.php b/public/userarea/get_clienti.php
index 086c5cd..0e6dd83 100644
--- a/public/userarea/get_clienti.php
+++ b/public/userarea/get_clienti.php
@@ -9,7 +9,24 @@ ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
- $api = VisualLimsApiClient::getInstance();
+ $api = VisualLimsApiClient::getInstance(); // also loads dotenv
+
+ // In simulate mode: return fake clients built from idclient values already in datadb.
+ // This ensures client dropdowns auto-select the correct row idclient, which in turn
+ // triggers the ClienteResponsabile select to populate via the mock.
+ if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
+ require_once __DIR__ . '/class/db-functions.php';
+ $pdo = DBHandlerSelect::getInstance()->getConnection();
+ $stmt = $pdo->query("SELECT DISTINCT idclient FROM datadb WHERE idclient IS NOT NULL AND idclient > 0 ORDER BY idclient ASC");
+ $ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
+ $fakeClients = array_map(fn($id) => [
+ 'IdCliente' => (int) $id,
+ 'Nominativo' => "Cliente Simulato {$id}",
+ 'CodiceCliente' => "SIM_{$id}",
+ ], $ids);
+ echo json_encode(['value' => $fakeClients]);
+ exit;
+ }
// Parametri OData
$params = [
diff --git a/public/userarea/get_fixed_field_data.php b/public/userarea/get_fixed_field_data.php
index a9fbd28..e5035b5 100644
--- a/public/userarea/get_fixed_field_data.php
+++ b/public/userarea/get_fixed_field_data.php
@@ -16,7 +16,8 @@ if (!$field) {
exit;
}
-$api = VisualLimsApiClient::getInstance();
+$api = VisualLimsApiClient::getInstance(); // also loads dotenv as a side-effect
+$simulate = ($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true';
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$data = null;
@@ -44,9 +45,12 @@ try {
case 'ClienteResponsabile':
$id_cliente = (int)($_GET['id_cliente'] ?? 0);
if ($id_cliente <= 0) {
- http_response_code(400);
- echo json_encode(['error' => 'Manca o invalido id_cliente']);
- exit;
+ if (!$simulate) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Manca o invalido id_cliente']);
+ exit;
+ }
+ $id_cliente = 1; // dummy — mock ignores the actual ID
}
$endpoint = "Cliente($id_cliente)?\$expand=Responsabili";
$cache_file = __DIR__ . '/cache/cliente_responsabili_' . $id_cliente . '.json';
@@ -62,13 +66,13 @@ try {
exit;
}
- // Opzionale: caching semplice (molto utile se i dati cambiano poco)
- if ($cache_file && file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) { // 1 ora
+ // Caching skipped in simulate mode to avoid polluting real-data cache files
+ if (!$simulate && $cache_file && file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) { // 1 ora
$data = json_decode(file_get_contents($cache_file), true);
} else {
$data = $api->get($endpoint, $options);
- if ($cache_file) {
+ if (!$simulate && $cache_file) {
file_put_contents($cache_file, json_encode($data, JSON_PRETTY_PRINT));
}
}
diff --git a/public/userarea/import_edit2.php b/public/userarea/import_edit2.php
index 220077b..65585b5 100644
--- a/public/userarea/import_edit2.php
+++ b/public/userarea/import_edit2.php
@@ -192,6 +192,16 @@ foreach ($tempMap as $f) {
$fixedFields[] = $f;
}
+// Maps logical fixed_field_key → real datadb column name (mirrors JS fixedAliasMap)
+$fixedAliasMap = [
+ 'ClienteResponsabile' => 'cliente_responsabile_id',
+ 'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
+ 'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
+ 'AnagraficaCertestService' => 'anagrafica_certest_service_id',
+ 'ClienteFornitore' => 'cliente_fornitore_id',
+ 'ConsegnaRichiesta' => 'consegna_richiesta',
+];
+
// helper default (DATE can use 'today' if you already use it elsewhere)
function fixedDefaultValue(array $f): string
{
@@ -638,12 +648,14 @@ function fixedDefaultValue(array $f): string
}
#exportConfirmModal,
- #exportResponseModal {
+ #exportResponseModal,
+ #exportUnsavedModal {
z-index: 1300 !important;
}
#exportConfirmModal .modal-backdrop,
- #exportResponseModal .modal-backdrop {
+ #exportResponseModal .modal-backdrop,
+ #exportUnsavedModal .modal-backdrop {
z-index: 1299 !important;
}
@@ -1055,6 +1067,9 @@ function fixedDefaultValue(array $f): string
= htmlspecialchars($row['status'] === 'i' ? 'Imported' : ($row['status'] === 'P' ? 'In Progress' : 'To LIMS')) ?>
+
+ = htmlspecialchars($row['commessaweb']) ?>
+
{
el.addEventListener("change", () => {
+ if (el.hasAttribute('data-restoring')) return;
hasChanges = true;
const gridCell = el.closest(".grid-cell");
@@ -1443,7 +1434,8 @@ function fixedDefaultValue(array $f): string
hasChanges = changedRows.size > 0;
- alert('Salvataggio riga avvenuto con successo!');
+ const toastEl = document.getElementById('saveSuccessToast');
+ if (toastEl) bootstrap.Toast.getOrCreateInstance(toastEl).show();
} else {
alert('Errore durante il salvataggio: ' + data.message);
}
@@ -1561,7 +1553,7 @@ function fixedDefaultValue(array $f): string
});
// Gestisci la chiusura dei modali per rimuovere i backdrop
- document.querySelectorAll('#exportConfirmModal, #exportResponseModal').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 => {
@@ -1657,10 +1649,12 @@ function fixedDefaultValue(array $f): string
// Ripristina il valore corrente
if (currentValue) {
dropdown.value = currentValue;
+ dropdown.setAttribute('data-restoring', '');
const event = new Event('change', {
bubbles: true
});
dropdown.dispatchEvent(event);
+ dropdown.removeAttribute('data-restoring');
}
});
}
@@ -1969,7 +1963,9 @@ function fixedDefaultValue(array $f): string
s2container.after(dropdown);
}
if (valToSelect) {
+ dropdown.setAttribute('data-restoring', '');
$(dropdown).val(valToSelect).trigger('change');
+ dropdown.removeAttribute('data-restoring');
}
} else {
// Select nativa
@@ -1988,9 +1984,11 @@ function fixedDefaultValue(array $f): string
});
if (valToSelect) {
dropdown.value = valToSelect;
+ dropdown.setAttribute('data-restoring', '');
dropdown.dispatchEvent(new Event('change', {
bubbles: true
}));
+ dropdown.removeAttribute('data-restoring');
}
}
});
@@ -2083,10 +2081,12 @@ function fixedDefaultValue(array $f): string
// Ripristina il valore corrente
if (currentValue) {
dropdown.value = currentValue;
+ dropdown.setAttribute('data-restoring', '');
const event = new Event('change', {
bubbles: true
});
dropdown.dispatchEvent(event);
+ dropdown.removeAttribute('data-restoring');
}
});
}
@@ -2528,7 +2528,9 @@ function fixedDefaultValue(array $f): string
if (currentVal) {
const found = results.find(r => String(r.id) === String(currentVal));
if (found) {
+ $select[0].setAttribute('data-restoring', '');
$select.val(currentVal).trigger('change');
+ $select[0].removeAttribute('data-restoring');
}
}
}
@@ -2715,6 +2717,35 @@ function fixedDefaultValue(array $f): string
+
+