From 233a1b3678afbb730ce606fc39eda7a5d3c5de15 Mon Sep 17 00:00:00 2001 From: "r.mubarakzyanov" Date: Fri, 2 Oct 2026 14:06:44 +0300 Subject: [PATCH] duplicate guard --- public/userarea/errors.php | 5 +- public/userarea/exportLims_gridData.js | 60 +++++++++---- public/userarea/export_to_lims.php | 108 +++++++++++++++++++++++- public/userarea/gridRenderer.js | 3 +- public/userarea/revert_status.php | 7 +- public/userarea/send_export_summary.php | 43 +++++++--- 6 files changed, 194 insertions(+), 32 deletions(-) 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 = "
@@ -250,9 +269,9 @@ $html = "

Riepilogo esportazione verso LIMS" . ($templateName !== '' ? " · Template: " . htmlspecialchars($templateName) . "" : "") . "

- Committente: " . $clientLabel($rows[0]['idclient'] ?? 0) . " + Committente: " . $headerClient('idclient') . "  ·  - FatturazioneAvanzata: " . $clientLabel($rows[0]['ClienteFatturazione'] ?? 0) . " + FatturazioneAvanzata: " . $headerClient('ClienteFatturazione') . "

@@ -260,6 +279,7 @@ $html = " Totale commesse: {$total}  •  Inviate: {$okCount}  •  Errori: {$errCount}" . + ($pendingCount > 0 ? "  •  Non confermate: {$pendingCount}" : "") . ($noPriceTotal > 0 ? "  •  Analisi No price: {$noPriceTotal}" : "") . "

@@ -274,15 +294,16 @@ $html = " {$rowsHtml}
- " . ($hasError ? "

⚠️ Alcune commesse non sono state inviate al LIMS. Controlla la sezione Errors nell'applicazione per i dettagli e per rilanciarle.

" : "") . " + " . ($hasError ? "

⚠️ Alcune commesse non sono state inviate al LIMS. Controlla la sezione Errors nell'applicazione per i dettagli e per rilanciarle.

" : "") . + ($hasPending ? "

⚠️ Per alcune commesse l'esito non è ancora confermato (timeout o errore di rete durante l'invio). Non rilanciarle: ricarica la pagina tra qualche minuto e verifica lo stato.

" : "") . "
Messaggio automatico da SmartTRF — non rispondere a questa email.
"; -$subjectPrefix = $hasError ? '⚠️ Errori Export LIMS' : '✅ Export LIMS OK'; -$subject = "{$subjectPrefix} — {$total} commesse ({$okCount} ok, {$errCount} err)"; +$subjectPrefix = $hasError ? '⚠️ Errori Export LIMS' : ($hasPending ? '⏳ Export LIMS da verificare' : '✅ Export LIMS OK'); +$subject = "{$subjectPrefix} — {$total} commesse ({$okCount} ok, {$errCount} err" . ($pendingCount > 0 ? ", {$pendingCount} non confermate" : "") . ")"; $res = Mailer::send($recipients, $subject, $html); @@ -290,5 +311,5 @@ echo json_encode([ 'success' => $res['ok'], 'message' => $res['ok'] ? 'Mail inviata' : ('Mail non inviata: ' . $res['error']), 'recipients' => $recipients, - 'counts' => ['ok' => $okCount, 'err' => $errCount, 'total' => $total], + 'counts' => ['ok' => $okCount, 'err' => $errCount, 'pending' => $pendingCount, 'total' => $total], ]);