duplicate guard

This commit is contained in:
2026-10-02 14:06:44 +03:00
parent 65608712ad
commit 233a1b3678
6 changed files with 194 additions and 32 deletions
+3 -2
View File
@@ -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() { $(document).on('click', '.resend-btn', function() {
const $btn = $(this); const $btn = $(this);
const iddatadb = $btn.data('iddatadb'); 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(); const original = $btn.html();
$btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>'); $btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
+45 -15
View File
@@ -169,15 +169,28 @@
formData.append("iddatadb", iddatadb); formData.append("iddatadb", iddatadb);
if (batchUuid) formData.append("batch_uuid", batchUuid); if (batchUuid) formData.append("batch_uuid", batchUuid);
const response = await fetch("export_to_lims.php", { // Marca la riga come "in export" nei dati: se il grid virtuale la ri-renderizza
method: "POST", // durante l'attesa, il pulsante Export resta disabilitato.
body: formData, const startIdx = getRowIndexByIddatadb(iddatadb);
}); if (startIdx >= 0) window.gridData[startIdx]._exporting = true;
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
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 // Update gridData
const idx = getRowIndexByIddatadb(iddatadb); const idx = getRowIndexByIddatadb(iddatadb);
if (idx >= 0) { if (idx >= 0) {
@@ -444,8 +457,9 @@
data.message || "Unknown error", data.message || "Unknown error",
); );
showExportResult(data); showExportResult(data);
// Mail di riepilogo per la singola commessa // Mail di riepilogo per la singola commessa (non per un export rifiutato:
sendExportSummaryMail([iddatadb]); // non è stato inviato nulla e la riga può risultare già 'l')
if (!data.rejected) sendExportSummaryMail([iddatadb]);
} catch (error) { } catch (error) {
if (gridRow) { if (gridRow) {
setRowExporting(gridRow, false); setRowExporting(gridRow, false);
@@ -453,6 +467,9 @@
} }
showRowError(gridRow, iddatadb, error.message); showRowError(gridRow, iddatadb, error.message);
showExportResult({ success: false, message: 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; const total = eligibleRows.length;
let processed = 0, let processed = 0,
succeeded = 0, succeeded = 0,
failed = 0; failed = 0,
skipped = 0;
const exportedList = []; // { commessaweb, noPriceCount, noPriceAnalyses } const exportedList = []; // { commessaweb, noPriceCount, noPriceAnalyses }
const processedIds = []; // iddatadb processati (ok o errore) per la mail const processedIds = []; // iddatadb processati (ok o errore) per la mail
@@ -728,11 +746,16 @@
const gridRow = row || getGridRow(iddatadb); const gridRow = row || getGridRow(iddatadb);
if (gridRow) setRowExporting(gridRow, true); if (gridRow) setRowExporting(gridRow, true);
processedIds.push(iddatadb);
try { try {
const data = await sendExport(iddatadb, batchUuid); const data = await sendExport(iddatadb, batchUuid);
processed++; 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++; succeeded++;
exportedList.push({ exportedList.push({
commessaweb: data.commessaweb, commessaweb: data.commessaweb,
@@ -741,6 +764,7 @@
}); });
} else { } else {
failed++; failed++;
processedIds.push(iddatadb);
const errIdx = getRowIndexByIddatadb(iddatadb); const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0) if (errIdx >= 0)
window.gridData[errIdx]._exportError = window.gridData[errIdx]._exportError =
@@ -755,6 +779,7 @@
} catch (error) { } catch (error) {
processed++; processed++;
failed++; failed++;
processedIds.push(iddatadb);
const errIdx = getRowIndexByIddatadb(iddatadb); const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0) if (errIdx >= 0)
window.gridData[errIdx]._exportError = error.message; window.gridData[errIdx]._exportError = error.message;
@@ -793,13 +818,18 @@
if (batchCancelled) { if (batchCancelled) {
labelEl.textContent = "Export All — Cancelled"; labelEl.textContent = "Export All — Cancelled";
msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong><br>Not processed: <strong>${total - processed}</strong>`; msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong><br>Not processed: <strong>${total - processed}</strong>`;
} else if (failed === 0) { } else if (failed === 0 && skipped === 0) {
labelEl.textContent = "Export All — Complete"; labelEl.textContent = "Export All — Complete";
msgEl.innerHTML = `All <strong>${succeeded}</strong> rows exported successfully.`; msgEl.innerHTML = `All <strong>${succeeded}</strong> rows exported successfully.`;
} else { } else {
labelEl.textContent = "Export All — Completed with errors"; labelEl.textContent =
failed > 0
? "Export All — Completed with errors"
: "Export All — Complete";
msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong>`; msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong>`;
} }
if (skipped > 0)
msgEl.innerHTML += `<br>Skipped (already exported / in progress — reload the page): <strong>${skipped}</strong>`;
if (exportedList.length) { if (exportedList.length) {
const npCommesse = exportedList.filter( const npCommesse = exportedList.filter(
+107 -1
View File
@@ -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 { try {
$iddatadb = $_POST['iddatadb'] ?? null; $iddatadb = $_POST['iddatadb'] ?? null;
if (!$iddatadb) { if (!$iddatadb) {
@@ -348,6 +442,11 @@ try {
$commessaId = $commessaWeb["IdCommessa"]; $commessaId = $commessaWeb["IdCommessa"];
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri $commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
// 🔹 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 // 🔹 STEP 6: Create Campioni (Samples) for each part
$campioni = []; $campioni = [];
$logContentStep6 = ""; $logContentStep6 = "";
@@ -768,6 +867,13 @@ try {
$logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt"; $logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId}, succeeded=" . ($importSucceeded ? '1' : '0') . ")"); $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 // 🔹 STEP 10: GET di controllo post-PATCH
$expand = "CommesseCustomFields(\$expand=CustomField)"; $expand = "CommesseCustomFields(\$expand=CustomField)";
$commessaAfterPatch = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand); $commessaAfterPatch = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
@@ -834,7 +940,7 @@ try {
"step10_get" => $logFileStep10 "step10_get" => $logFileStep10
] ]
]); ]);
} catch (Exception $e) { } catch (Throwable $e) {
// --- LOG ERRORE DEDICATO (accanto agli altri log in logs/api/) --- // --- LOG ERRORE DEDICATO (accanto agli altri log in logs/api/) ---
$errIddatadb = $iddatadb ?? 'unknown'; $errIddatadb = $iddatadb ?? 'unknown';
$errCommessaId = $commessaId ?? null; $errCommessaId = $commessaId ?? null;
+2 -1
View File
@@ -755,7 +755,8 @@
function renderActionButtons(rowIndex) { function renderActionButtons(rowIndex) {
const row = data[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"); const div = document.createElement("div");
div.className = "grid-cell button-cell"; div.className = "grid-cell button-cell";
div.style.flex = "0 0 auto"; div.style.flex = "0 0 auto";
+5 -2
View File
@@ -24,8 +24,11 @@ try {
$db = DBHandlerSelect::getInstance(); $db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection(); $pdo = $db->getConnection();
// Aggiorna solo se attualmente è in stato 'l' (To LIMS) // Aggiorna solo se attualmente è in stato 'l' (To LIMS).
$stmt = $pdo->prepare("UPDATE datadb SET status = ? WHERE iddatadb = ? AND status = 'l'"); // 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]); $stmt->execute([$newStatus, $iddatadb]);
if ($stmt->rowCount() > 0) { if ($stmt->rowCount() > 0) {
+32 -11
View File
@@ -91,9 +91,10 @@ $sql = "
FROM datadb d FROM datadb d
LEFT JOIN auth_users u ON d.user_id = u.id LEFT JOIN auth_users u ON d.user_id = u.id
WHERE d.iddatadb IN ($ph) WHERE d.iddatadb IN ($ph)
ORDER BY FIELD(d.iddatadb, $ph)
"; ";
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
$stmt->execute($ids); $stmt->execute(array_merge($ids, $ids));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) { if (empty($rows)) {
echo json_encode(['success' => false, 'message' => 'Righe non trovate']); echo json_encode(['success' => false, 'message' => 'Righe non trovate']);
@@ -174,14 +175,18 @@ if (empty($recipients)) {
exit; 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; $okCount = 0;
$errCount = 0; $errCount = 0;
$pendingCount = 0;
foreach ($rows as $r) { foreach ($rows as $r) {
if ($r['status'] === 'l') $okCount++; if ($r['status'] === 'l') $okCount++;
elseif ($r['status'] === 'x') $errCount++; elseif ($r['status'] === 'x') $errCount++;
else $pendingCount++;
} }
$hasError = $errCount > 0; $hasError = $errCount > 0;
$hasPending = $pendingCount > 0;
// --- tabella righe --- // --- tabella righe ---
$rowsHtml = ''; $rowsHtml = '';
@@ -196,7 +201,7 @@ foreach ($rows as $r) {
$bg = '#fdecea'; $bg = '#fdecea';
$col = '#c0392b'; $col = '#c0392b';
} else { } else {
$label = htmlspecialchars((string)$st); $label = 'NON CONFERMATA';
$bg = '#fff8e1'; $bg = '#fff8e1';
$col = '#8a6d3b'; $col = '#8a6d3b';
} }
@@ -239,10 +244,24 @@ foreach ($rows as $r) {
} }
} }
$headerColor = $hasError ? '#c0392b' : '#1e7e34'; if ($hasError) {
$headerText = $hasError ? 'Export completato con errori' : 'Export completato con successo'; $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); $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 = " $html = "
<div style='font-family:Arial,Helvetica,sans-serif;max-width:820px;margin:0 auto;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;'> <div style='font-family:Arial,Helvetica,sans-serif;max-width:820px;margin:0 auto;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;'>
<div style='background:{$headerColor};color:#fff;padding:18px 24px;'> <div style='background:{$headerColor};color:#fff;padding:18px 24px;'>
@@ -250,9 +269,9 @@ $html = "
<p style='margin:6px 0 0;font-size:13px;opacity:.9;'>Riepilogo esportazione verso LIMS" . <p style='margin:6px 0 0;font-size:13px;opacity:.9;'>Riepilogo esportazione verso LIMS" .
($templateName !== '' ? " &middot; Template: <strong>" . htmlspecialchars($templateName) . "</strong>" : "") . "</p> ($templateName !== '' ? " &middot; Template: <strong>" . htmlspecialchars($templateName) . "</strong>" : "") . "</p>
<p style='margin:6px 0 0;font-size:13px;opacity:.95;'> <p style='margin:6px 0 0;font-size:13px;opacity:.95;'>
Committente: <strong>" . $clientLabel($rows[0]['idclient'] ?? 0) . "</strong> Committente: <strong>" . $headerClient('idclient') . "</strong>
&nbsp;&middot;&nbsp; &nbsp;&middot;&nbsp;
FatturazioneAvanzata: <strong>" . $clientLabel($rows[0]['ClienteFatturazione'] ?? 0) . "</strong> FatturazioneAvanzata: <strong>" . $headerClient('ClienteFatturazione') . "</strong>
</p> </p>
</div> </div>
<div style='padding:20px 24px;'> <div style='padding:20px 24px;'>
@@ -260,6 +279,7 @@ $html = "
Totale commesse: <strong>{$total}</strong> &nbsp;•&nbsp; Totale commesse: <strong>{$total}</strong> &nbsp;•&nbsp;
<span style='color:#1e7e34;'>Inviate: {$okCount}</span> &nbsp;•&nbsp; <span style='color:#1e7e34;'>Inviate: {$okCount}</span> &nbsp;•&nbsp;
<span style='color:#c0392b;'>Errori: {$errCount}</span>" . <span style='color:#c0392b;'>Errori: {$errCount}</span>" .
($pendingCount > 0 ? " &nbsp;•&nbsp; <span style='color:#8a6d3b;'>Non confermate: {$pendingCount}</span>" : "") .
($noPriceTotal > 0 ? " &nbsp;•&nbsp; <span style='color:#c0392b;'>Analisi No price: {$noPriceTotal}</span>" : "") . " ($noPriceTotal > 0 ? " &nbsp;•&nbsp; <span style='color:#c0392b;'>Analisi No price: {$noPriceTotal}</span>" : "") . "
</p> </p>
<table style='width:100%;border-collapse:collapse;font-size:14px;table-layout:fixed;'> <table style='width:100%;border-collapse:collapse;font-size:14px;table-layout:fixed;'>
@@ -274,15 +294,16 @@ $html = "
</thead> </thead>
<tbody>{$rowsHtml}</tbody> <tbody>{$rowsHtml}</tbody>
</table> </table>
" . ($hasError ? "<p style='margin:18px 0 0;padding:12px;background:#fdecea;border-left:4px solid #c0392b;color:#c0392b;font-size:13px;border-radius:4px;'>⚠️ Alcune commesse non sono state inviate al LIMS. Controlla la sezione <strong>Errors</strong> nell'applicazione per i dettagli e per rilanciarle.</p>" : "") . " " . ($hasError ? "<p style='margin:18px 0 0;padding:12px;background:#fdecea;border-left:4px solid #c0392b;color:#c0392b;font-size:13px;border-radius:4px;'>⚠️ Alcune commesse non sono state inviate al LIMS. Controlla la sezione <strong>Errors</strong> nell'applicazione per i dettagli e per rilanciarle.</p>" : "") .
($hasPending ? "<p style='margin:18px 0 0;padding:12px;background:#fff8e1;border-left:4px solid #b7791f;color:#8a6d3b;font-size:13px;border-radius:4px;'>⚠️ Per alcune commesse l'esito non è ancora confermato (timeout o errore di rete durante l'invio). <strong>Non rilanciarle</strong>: ricarica la pagina tra qualche minuto e verifica lo stato.</p>" : "") . "
</div> </div>
<div style='background:#fafafa;padding:14px 24px;font-size:12px;color:#999;border-top:1px solid #eee;'> <div style='background:#fafafa;padding:14px 24px;font-size:12px;color:#999;border-top:1px solid #eee;'>
Messaggio automatico da SmartTRF — non rispondere a questa email. Messaggio automatico da SmartTRF — non rispondere a questa email.
</div> </div>
</div>"; </div>";
$subjectPrefix = $hasError ? '⚠️ Errori Export LIMS' : '✅ Export LIMS OK'; $subjectPrefix = $hasError ? '⚠️ Errori Export LIMS' : ($hasPending ? '⏳ Export LIMS da verificare' : '✅ Export LIMS OK');
$subject = "{$subjectPrefix} — {$total} commesse ({$okCount} ok, {$errCount} err)"; $subject = "{$subjectPrefix} — {$total} commesse ({$okCount} ok, {$errCount} err" . ($pendingCount > 0 ? ", {$pendingCount} non confermate" : "") . ")";
$res = Mailer::send($recipients, $subject, $html); $res = Mailer::send($recipients, $subject, $html);
@@ -290,5 +311,5 @@ echo json_encode([
'success' => $res['ok'], 'success' => $res['ok'],
'message' => $res['ok'] ? 'Mail inviata' : ('Mail non inviata: ' . $res['error']), 'message' => $res['ok'] ? 'Mail inviata' : ('Mail non inviata: ' . $res['error']),
'recipients' => $recipients, 'recipients' => $recipients,
'counts' => ['ok' => $okCount, 'err' => $errCount, 'total' => $total], 'counts' => ['ok' => $okCount, 'err' => $errCount, 'pending' => $pendingCount, 'total' => $total],
]); ]);