diff --git a/public/userarea/errors.php b/public/userarea/errors.php
index 585302f4..b7314827 100644
--- a/public/userarea/errors.php
+++ b/public/userarea/errors.php
@@ -297,11 +297,12 @@ $rowsData = $stmt->fetchAll(PDO::FETCH_ASSOC);
});
})();
- // Reinvia: richiama export_to_lims.php da capo (create incluso)
+ // Reinvia: richiama export_to_lims.php. Se la commessa era già stata creata,
+ // il CHECK PRELIMINARE la verifica nel LIMS e non ne crea una seconda.
$(document).on('click', '.resend-btn', function() {
const $btn = $(this);
const iddatadb = $btn.data('iddatadb');
- if (!confirm('Reinviare questa commessa al LIMS? Verrà creata una nuova commessa nel LIMS.')) return;
+ if (!confirm('Reinviare questa commessa al LIMS? Se risulta già nel LIMS verrà solo riallineata, altrimenti ne verrà creata una nuova.')) return;
const original = $btn.html();
$btn.prop('disabled', true).html('');
diff --git a/public/userarea/exportLims_gridData.js b/public/userarea/exportLims_gridData.js
index dbb85d0d..cfd68f48 100644
--- a/public/userarea/exportLims_gridData.js
+++ b/public/userarea/exportLims_gridData.js
@@ -169,15 +169,28 @@
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();
+ // Marca la riga come "in export" nei dati: se il grid virtuale la ri-renderizza
+ // durante l'attesa, il pulsante Export resta disabilitato.
+ const startIdx = getRowIndexByIddatadb(iddatadb);
+ if (startIdx >= 0) window.gridData[startIdx]._exporting = true;
- if (data.success) {
+ let data;
+ try {
+ const response = await fetch("export_to_lims.php", {
+ method: "POST",
+ body: formData,
+ });
+ if (!response.ok)
+ throw new Error(`HTTP error! status: ${response.status}`);
+ data = await response.json();
+ } finally {
+ const endIdx = getRowIndexByIddatadb(iddatadb);
+ if (endIdx >= 0) delete window.gridData[endIdx]._exporting;
+ }
+
+ // Rifiutato perché la riga risulta già esportata (pagina non aggiornata):
+ // allinea la griglia allo stato reale invece di lasciarla esportabile.
+ if (data.success || (data.rejected && data.status === "l")) {
// Update gridData
const idx = getRowIndexByIddatadb(iddatadb);
if (idx >= 0) {
@@ -444,8 +457,9 @@
data.message || "Unknown error",
);
showExportResult(data);
- // Mail di riepilogo per la singola commessa
- sendExportSummaryMail([iddatadb]);
+ // Mail di riepilogo per la singola commessa (non per un export rifiutato:
+ // non è stato inviato nulla e la riga può risultare già 'l')
+ if (!data.rejected) sendExportSummaryMail([iddatadb]);
} catch (error) {
if (gridRow) {
setRowExporting(gridRow, false);
@@ -453,6 +467,9 @@
}
showRowError(gridRow, iddatadb, error.message);
showExportResult({ success: false, message: error.message });
+ // Timeout/errore di rete: il server può aver completato lo stesso.
+ // La mail riporta lo stato reale della riga in DB.
+ sendExportSummaryMail([iddatadb]);
}
};
@@ -692,7 +709,8 @@
const total = eligibleRows.length;
let processed = 0,
succeeded = 0,
- failed = 0;
+ failed = 0,
+ skipped = 0;
const exportedList = []; // { commessaweb, noPriceCount, noPriceAnalyses }
const processedIds = []; // iddatadb processati (ok o errore) per la mail
@@ -728,11 +746,16 @@
const gridRow = row || getGridRow(iddatadb);
if (gridRow) setRowExporting(gridRow, true);
- processedIds.push(iddatadb);
try {
const data = await sendExport(iddatadb, batchUuid);
processed++;
- if (data.success) {
+ if (data.rejected) {
+ // Già esportata / export in corso altrove: niente inviato, fuori dalla mail
+ skipped++;
+ if (data.status !== "l" && gridRow)
+ showRowError(gridRow, iddatadb, data.message);
+ } else if (data.success) {
+ processedIds.push(iddatadb);
succeeded++;
exportedList.push({
commessaweb: data.commessaweb,
@@ -741,6 +764,7 @@
});
} else {
failed++;
+ processedIds.push(iddatadb);
const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0)
window.gridData[errIdx]._exportError =
@@ -755,6 +779,7 @@
} catch (error) {
processed++;
failed++;
+ processedIds.push(iddatadb);
const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0)
window.gridData[errIdx]._exportError = error.message;
@@ -793,13 +818,18 @@
if (batchCancelled) {
labelEl.textContent = "Export All — Cancelled";
msgEl.innerHTML = `Exported: ${succeeded}
Errors: ${failed}
Not processed: ${total - processed}`;
- } else if (failed === 0) {
+ } else if (failed === 0 && skipped === 0) {
labelEl.textContent = "Export All — Complete";
msgEl.innerHTML = `All ${succeeded} rows exported successfully.`;
} else {
- labelEl.textContent = "Export All — Completed with errors";
+ labelEl.textContent =
+ failed > 0
+ ? "Export All — Completed with errors"
+ : "Export All — Complete";
msgEl.innerHTML = `Exported: ${succeeded}
Errors: ${failed}`;
}
+ if (skipped > 0)
+ msgEl.innerHTML += `
Skipped (already exported / in progress — reload the page): ${skipped}`;
if (exportedList.length) {
const npCommesse = exportedList.filter(
diff --git a/public/userarea/export_to_lims.php b/public/userarea/export_to_lims.php
index bbd3b7e0..084cbd67 100644
--- a/public/userarea/export_to_lims.php
+++ b/public/userarea/export_to_lims.php
@@ -143,6 +143,100 @@ function importaCommessaWithRetry($api, $commessaId, array $payload, $maxRetries
];
}
+// 🔹 CHECK PRELIMINARE + LOCK: una sola esportazione per riga, mai una seconda commessa.
+// Protegge da pagine non aggiornate (due schede, pagina Errors vecchia, doppio click,
+// LIMS lento con richiesta ancora in corso).
+$chkId = (int)($_POST['iddatadb'] ?? 0);
+if ($chkId > 0) {
+ $rejectExport = function ($reason, $cur = null) use ($chkId, $commessaLogFile, $writeBatchIndex, $batchUuid) {
+ file_put_contents(
+ $commessaLogFile,
+ "\n[EXPORT RIFIUTATO] " . date('Y-m-d H:i:s') . " | {$reason}" .
+ " | user_id=" . ($GLOBALS['iduserlogin'] ?? 'n/d') .
+ " | IP=" . ($_SERVER['REMOTE_ADDR'] ?? '-') .
+ " | batch=" . ($batchUuid ?? '-') . "\n",
+ FILE_APPEND
+ );
+ $writeBatchIndex("SKIP iddatadb={$chkId} :: {$reason}");
+ echo json_encode([
+ "success" => false,
+ "rejected" => true,
+ "status" => $cur['status'] ?? null,
+ "commessaweb" => $cur['commessaweb'] ?? null,
+ "message" => "Commessa non inviata (riga {$chkId}): {$reason}. Ricarica la pagina.",
+ ]);
+ exit;
+ };
+
+ // Lock per riga: tenuto fino a fine script (rilasciato da MySQL alla chiusura connessione)
+ $gotLock = (int)$pdo->query("SELECT GET_LOCK(" . $pdo->quote("lims_export_{$chkId}") . ", 0)")->fetchColumn();
+ if ($gotLock !== 1) {
+ $rejectExport("esportazione già in corso");
+ }
+
+ $chk = $pdo->prepare("SELECT status, idcommessaweb, commessaweb FROM datadb WHERE iddatadb = ?");
+ $chk->execute([$chkId]);
+ $cur = $chk->fetch(PDO::FETCH_ASSOC);
+
+ if (!$cur) {
+ $rejectExport("riga non trovata");
+ }
+ if (!in_array($cur['status'], ['i', 'x'], true)) {
+ $rejectExport("già esportata (status={$cur['status']}, commessa=" . ($cur['commessaweb'] ?: '-') . ")", $cur);
+ }
+
+ $existingId = (int)($cur['idcommessaweb'] ?? 0);
+ if ($existingId > 0) {
+ // Commessa già creata da un export precedente (fallito, o interrotto da un crash/timeout PHP):
+ // controlla lo stato nel LIMS invece di crearne una nuova (sarebbe un duplicato).
+ // NB: il revert a 'i' azzera idcommessaweb, quindi qui non arrivano le righe riportate a mano.
+ $api = VisualLimsApiClient::getInstance();
+ try {
+ $existing = $api->get("CommessaWeb({$existingId})");
+ } catch (Exception $e) {
+ $existing = null;
+ if (strpos($e->getMessage(), 'HTTP 404') === false) {
+ $rejectExport("impossibile verificare la commessa {$existingId} nel LIMS: " . $e->getMessage(), $cur);
+ }
+ // 404: la commessa non esiste più nel LIMS → si può esportare da capo
+ }
+
+ $stato = $existing['StatoCommessaWeb'] ?? null;
+ $notSentYet = $existing !== null && $stato === 'Nuova' && empty($existing['DataInviatoWeb']);
+
+ $writeLog(null, "IdCommessa esistente: {$existingId}\nStatoCommessaWeb: " . ($stato ?? '404') .
+ "\nDataInviatoWeb: " . ($existing['DataInviatoWeb'] ?? '-'), "PRECHECK - commessa esistente (iddatadb={$chkId})");
+
+ if ($stato === 'Elaborata') {
+ // Già importata nel LIMS: riallinea la riga, nessuna nuova commessa
+ $codice = substr(trim((string)($existing['CodiceCommessa'] ?? '')), 0, 30);
+ $pdo->prepare("UPDATE datadb SET status = 'l', commessaweb = :c, log_file = :lf WHERE iddatadb = :id")
+ ->execute(['c' => $codice, 'lf' => basename($commessaLogFile), 'id' => $chkId]);
+
+ $writeLog(null, "Riga riallineata: status='l', commessaweb={$codice}", "PRECHECK - RECOVERED (iddatadb={$chkId})");
+ $writeBatchIndex("RECOVERED iddatadb={$chkId} commessa={$existingId} codice={$codice}");
+
+ echo json_encode([
+ "success" => true,
+ "recovered" => true,
+ "idcommessaweb" => $existingId,
+ "commessaweb" => $codice,
+ "message" => "Commessa già presente nel LIMS: riga riallineata senza crearne una nuova",
+ ]);
+ exit;
+ }
+
+ if ($existing !== null && !$notSentYet) {
+ // Inviata ma non (ancora) importata: rifare l'export creerebbe un duplicato → verifica manuale
+ $rejectExport("commessa {$existingId} già inviata al LIMS ma non ancora importata (stato: {$stato}). " .
+ "Riprova tra qualche minuto o verifica nel LIMS", $cur);
+ }
+
+ // 404 o mai inviata al laboratorio: si ricrea da capo (la vecchia CommessaWeb resta orfana in "Nuova")
+ $writeBatchIndex("RECREATE iddatadb={$chkId} old_commessa={$existingId} stato=" . ($stato ?? '404'));
+ }
+}
+
try {
$iddatadb = $_POST['iddatadb'] ?? null;
if (!$iddatadb) {
@@ -348,6 +442,11 @@ try {
$commessaId = $commessaWeb["IdCommessa"];
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
+ // 🔹 STEP 5.1: salva SUBITO l'id della commessa creata: se l'export fallisce più avanti,
+ // il CHECK PRELIMINARE sa che nel LIMS esiste già e non ne crea una seconda.
+ $pdo->prepare("UPDATE datadb SET idcommessaweb = :idc, commessaweb = :cw WHERE iddatadb = :id")
+ ->execute(['idc' => $commessaId, 'cw' => $commessaWebCode, 'id' => $iddatadb]);
+
// 🔹 STEP 6: Create Campioni (Samples) for each part
$campioni = [];
$logContentStep6 = "";
@@ -768,6 +867,13 @@ try {
$logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId}, succeeded=" . ($importSucceeded ? '1' : '0') . ")");
+ // Import non confermato → errore ('x'), non 'l'. Prima veniva comunque segnata 'l'
+ // perché CodiceCommessa (codice web) non è mai vuoto. Al reinvio il CHECK PRELIMINARE
+ // ricontrolla lo stato nel LIMS: se nel frattempo è "Elaborata" la riga viene solo riallineata.
+ if (!$importSucceeded) {
+ throw new Exception("ImportaCommessa non confermata dal LIMS (stato: " . ($importStato ?? 'null') . ")");
+ }
+
// 🔹 STEP 10: GET di controllo post-PATCH
$expand = "CommesseCustomFields(\$expand=CustomField)";
$commessaAfterPatch = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
@@ -834,7 +940,7 @@ try {
"step10_get" => $logFileStep10
]
]);
-} catch (Exception $e) {
+} catch (Throwable $e) {
// --- LOG ERRORE DEDICATO (accanto agli altri log in logs/api/) ---
$errIddatadb = $iddatadb ?? 'unknown';
$errCommessaId = $commessaId ?? null;
diff --git a/public/userarea/gridRenderer.js b/public/userarea/gridRenderer.js
index 76a7cc98..b4fc60fd 100644
--- a/public/userarea/gridRenderer.js
+++ b/public/userarea/gridRenderer.js
@@ -755,7 +755,8 @@
function renderActionButtons(rowIndex) {
const row = data[rowIndex];
- const isExported = row.status === "l";
+ // _exporting: export in corso per questa riga (la riga può essere ri-renderizzata durante l'attesa)
+ const isExported = row.status === "l" || row._exporting === true;
const div = document.createElement("div");
div.className = "grid-cell button-cell";
div.style.flex = "0 0 auto";
diff --git a/public/userarea/revert_status.php b/public/userarea/revert_status.php
index 157b5b5c..de613f46 100644
--- a/public/userarea/revert_status.php
+++ b/public/userarea/revert_status.php
@@ -24,8 +24,11 @@ try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
- // Aggiorna solo se attualmente è in stato 'l' (To LIMS)
- $stmt = $pdo->prepare("UPDATE datadb SET status = ? WHERE iddatadb = ? AND status = 'l'");
+ // Aggiorna solo se attualmente è in stato 'l' (To LIMS).
+ // Azzera idcommessaweb: il revert è una scelta esplicita di riesportare, altrimenti il
+ // CHECK PRELIMINARE di export_to_lims.php riallineerebbe la riga alla vecchia commessa.
+ // commessaweb resta come traccia del codice precedente.
+ $stmt = $pdo->prepare("UPDATE datadb SET status = ?, idcommessaweb = NULL WHERE iddatadb = ? AND status = 'l'");
$stmt->execute([$newStatus, $iddatadb]);
if ($stmt->rowCount() > 0) {
diff --git a/public/userarea/send_export_summary.php b/public/userarea/send_export_summary.php
index ac866a0f..bc14d53c 100644
--- a/public/userarea/send_export_summary.php
+++ b/public/userarea/send_export_summary.php
@@ -91,9 +91,10 @@ $sql = "
FROM datadb d
LEFT JOIN auth_users u ON d.user_id = u.id
WHERE d.iddatadb IN ($ph)
+ ORDER BY FIELD(d.iddatadb, $ph)
";
$stmt = $pdo->prepare($sql);
-$stmt->execute($ids);
+$stmt->execute(array_merge($ids, $ids));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) {
echo json_encode(['success' => false, 'message' => 'Righe non trovate']);
@@ -174,14 +175,18 @@ if (empty($recipients)) {
exit;
}
-// conteggi
+// conteggi. La mail riflette lo stato in DB al momento dell'invio: una riga ancora 'i'
+// (export interrotto da timeout/errore di rete, o ancora in corso) NON è un successo.
$okCount = 0;
$errCount = 0;
+$pendingCount = 0;
foreach ($rows as $r) {
if ($r['status'] === 'l') $okCount++;
elseif ($r['status'] === 'x') $errCount++;
+ else $pendingCount++;
}
$hasError = $errCount > 0;
+$hasPending = $pendingCount > 0;
// --- tabella righe ---
$rowsHtml = '';
@@ -196,7 +201,7 @@ foreach ($rows as $r) {
$bg = '#fdecea';
$col = '#c0392b';
} else {
- $label = htmlspecialchars((string)$st);
+ $label = 'NON CONFERMATA';
$bg = '#fff8e1';
$col = '#8a6d3b';
}
@@ -239,10 +244,24 @@ foreach ($rows as $r) {
}
}
-$headerColor = $hasError ? '#c0392b' : '#1e7e34';
-$headerText = $hasError ? 'Export completato con errori' : 'Export completato con successo';
+if ($hasError) {
+ $headerColor = '#c0392b';
+ $headerText = 'Export completato con errori';
+} elseif ($hasPending) {
+ $headerColor = '#b7791f';
+ $headerText = 'Export da verificare';
+} else {
+ $headerColor = '#1e7e34';
+ $headerText = 'Export completato con successo';
+}
$total = count($rows);
+// Committente / Fatturazione in testata solo se uguali per tutte le righe (batch misti → "vari")
+$headerClient = function ($col) use ($rows, $clientLabel) {
+ $vals = array_unique(array_map(fn($r) => (int)($r[$col] ?? 0), $rows));
+ return count($vals) === 1 ? $clientLabel(reset($vals)) : 'vari';
+};
+
$html = "