Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae78bf0393 | ||
|
|
65f02b9212 | ||
|
|
e6c8549cea | ||
|
|
9a0770391e | ||
|
|
ea33db6c48 | ||
|
|
764cdade18 | ||
|
|
1293bcf4fd | ||
|
|
76f4a8e291 | ||
|
|
233a1b3678 | ||
|
|
65608712ad | ||
|
|
6a7a56cd0f |
@@ -43,6 +43,7 @@ class VisualLimsApiClient
|
||||
{
|
||||
$ch = curl_init("{$this->baseUrl}/api/authentication/authenticate");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'VisualLims-Client/1.0');
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||
'Username' => $this->username,
|
||||
@@ -105,6 +106,7 @@ class VisualLimsApiClient
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'VisualLims-Client/1.0');
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer {$token}",
|
||||
"Accept: application/json"
|
||||
@@ -144,6 +146,7 @@ class VisualLimsApiClient
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'VisualLims-Client/1.0');
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
@@ -177,6 +180,7 @@ class VisualLimsApiClient
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'VisualLims-Client/1.0');
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
@@ -225,6 +229,7 @@ class VisualLimsApiClient
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'VisualLims-Client/1.0');
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
@@ -268,6 +273,7 @@ class VisualLimsApiClient
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'VisualLims-Client/1.0');
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer {$token}",
|
||||
"Accept: */*"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Controllo blocco cliente.
|
||||
*
|
||||
* Il cliente usa il campo CodiceASL dell'anagrafica LIMS come nota di blocco
|
||||
* (es. "BLOCCO PER PAGAMENTO"), perché i campi nativi del Blocco Accettazione
|
||||
* non sono esposti dall'API.
|
||||
*
|
||||
* Regola: se è indicato il cliente di Fatturazione avanzata si controlla quello,
|
||||
* altrimenti il Client.
|
||||
*
|
||||
* NESSUNA chiamata API: legge solo il file di cache scritto da get_clienti.php
|
||||
* (cache/clienti.json), con la sua durata già esistente. Se il file non c'è,
|
||||
* il controllo viene saltato.
|
||||
*
|
||||
* Restituisce null se non c'è blocco o se il controllo non è possibile:
|
||||
* è solo un avviso e non deve mai far fallire l'export o la mail.
|
||||
*/
|
||||
if (!function_exists('getClientBlockAlert')) {
|
||||
function getClientBlockAlert($clienteId, $clienteFatturazione = null): ?array
|
||||
{
|
||||
// Mappa IdCliente => [Nominativo, CodiceASL], letta una sola volta per richiesta
|
||||
static $clienti = null;
|
||||
|
||||
$useFatturazione = !empty($clienteFatturazione);
|
||||
$targetId = $useFatturazione ? (int) $clienteFatturazione : (int) $clienteId;
|
||||
|
||||
if ($targetId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($clienti === null) {
|
||||
$clienti = [];
|
||||
$cacheFile = dirname(__DIR__) . '/cache/clienti.json';
|
||||
|
||||
if (!is_file($cacheFile)) {
|
||||
error_log("getClientBlockAlert: cache clienti non trovata ({$cacheFile}), controllo saltato");
|
||||
return null;
|
||||
}
|
||||
|
||||
$json = json_decode((string) file_get_contents($cacheFile), true);
|
||||
foreach (($json['value'] ?? []) as $c) {
|
||||
$clienti[(int) ($c['IdCliente'] ?? 0)] = [
|
||||
'nominativo' => trim((string) ($c['Nominativo'] ?? '')),
|
||||
'codiceASL' => trim((string) ($c['CodiceASL'] ?? '')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$cliente = $clienti[$targetId] ?? null;
|
||||
if (!$cliente) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$motivo = $cliente['codiceASL'];
|
||||
if ($motivo === '' || stripos($motivo, 'blocco') === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'idCliente' => $targetId,
|
||||
'nominativo' => $cliente['nominativo'],
|
||||
'motivo' => $motivo,
|
||||
'campo' => $useFatturazione ? 'Fatturazione avanzata' : 'Client',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ try {
|
||||
|
||||
$stmtLoadAnalyses = $pdo->prepare("
|
||||
SELECT idmatrice, analysis_recordkey, analysis_name, analysis_method,
|
||||
analysis_level, is_web_selectable, is_accredited
|
||||
analysis_level, analysis_price, is_web_selectable, is_accredited
|
||||
FROM identification_parts_analyses
|
||||
WHERE part_id = ?
|
||||
ORDER BY id ASC
|
||||
@@ -78,10 +78,10 @@ try {
|
||||
$stmtInsertAnalysis = $pdo->prepare("
|
||||
INSERT INTO identification_parts_analyses
|
||||
(part_id, iddatadb, idmatrice, analysis_recordkey, analysis_name, analysis_method,
|
||||
analysis_level, is_web_selectable, is_accredited, created_at, updated_at)
|
||||
analysis_level, analysis_price, is_web_selectable, is_accredited, created_at, updated_at)
|
||||
VALUES
|
||||
(:part_id, :iddatadb, :idmatrice, :analysis_recordkey, :analysis_name, :analysis_method,
|
||||
:analysis_level, :is_web_selectable, :is_accredited, NOW(), NOW())
|
||||
:analysis_level, :analysis_price, :is_web_selectable, :is_accredited, NOW(), NOW())
|
||||
");
|
||||
|
||||
$stmtInsertCF = $pdo->prepare("
|
||||
@@ -161,6 +161,7 @@ try {
|
||||
':analysis_name' => $an['analysis_name'] ?? null,
|
||||
':analysis_method' => $an['analysis_method'] ?? null,
|
||||
':analysis_level' => ($an['analysis_level'] !== null && $an['analysis_level'] !== '') ? (int)$an['analysis_level'] : null,
|
||||
':analysis_price' => ($an['analysis_price'] !== null && $an['analysis_price'] !== '') ? $an['analysis_price'] : null,
|
||||
':is_web_selectable' => (int)($an['is_web_selectable'] ?? 0),
|
||||
':is_accredited' => (int)($an['is_accredited'] ?? 0),
|
||||
]);
|
||||
|
||||
@@ -146,6 +146,10 @@ $rowsData = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
background: #fd7e14;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.pager-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -232,7 +236,7 @@ $rowsData = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<table class="err-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:170px;">Azioni</th>
|
||||
<th style="width:210px;">Azioni</th>
|
||||
<th>Status</th>
|
||||
<th>Client</th>
|
||||
<th>Errore</th>
|
||||
@@ -250,6 +254,7 @@ $rowsData = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<a href="download_logs.php?iddatadb=<?= (int)$row['iddatadb'] ?>" class="action-btn btn-log" title="Scarica log"><i class="fas fa-file-download"></i></a>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="action-btn btn-revert revert-btn" data-iddatadb="<?= (int)$row['iddatadb'] ?>" title="Riporta a Imported"><i class="fas fa-rotate-left"></i></button>
|
||||
<button type="button" class="action-btn btn-delete delete-btn" data-iddatadb="<?= (int)$row['iddatadb'] ?>" title="Elimina record"><i class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge status-x">Error</span>
|
||||
@@ -297,11 +302,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('<i class="fas fa-spinner fa-spin"></i>');
|
||||
@@ -330,6 +336,39 @@ $rowsData = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
});
|
||||
});
|
||||
|
||||
// Elimina record
|
||||
$(document).on('click', '.delete-btn', function() {
|
||||
const $btn = $(this);
|
||||
const iddatadb = $btn.data('iddatadb');
|
||||
if (!confirm('Eliminare definitivamente questo record? L\'operazione non è reversibile.')) return;
|
||||
|
||||
const original = $btn.html();
|
||||
$btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
|
||||
$.ajax({
|
||||
url: 'revert_status.php',
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
iddatadb: iddatadb,
|
||||
action: 'delete'
|
||||
},
|
||||
success: function(res) {
|
||||
if (res && res.success) {
|
||||
const $tr = $('tr[data-id="' + iddatadb + '"]');
|
||||
$tr.fadeOut(300, () => $tr.remove());
|
||||
} else {
|
||||
alert('Errore: ' + ((res && res.message) ? res.message : 'cancellazione non riuscita'));
|
||||
$btn.prop('disabled', false).html(original);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Errore di rete.');
|
||||
$btn.prop('disabled', false).html(original);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Revert a Imported (i)
|
||||
$(document).on('click', '.revert-btn', function() {
|
||||
const $btn = $(this);
|
||||
|
||||
@@ -42,6 +42,23 @@
|
||||
}
|
||||
window.noPriceBadgeHTML = noPriceBadgeHTML;
|
||||
|
||||
// Riquadro "cliente bloccato" per la finestra di export singolo
|
||||
function blockAlertHTML(b) {
|
||||
if (!b || !b.motivo) return "";
|
||||
return (
|
||||
`<div class="alert alert-danger py-2 px-3 mt-3 mb-0 small">` +
|
||||
`<i class="fas fa-ban me-1"></i><strong>Cliente bloccato</strong> (${escHtml(b.campo)}): ` +
|
||||
`${escHtml(b.nominativo)} - ${escHtml(b.idCliente)}<br>` +
|
||||
`<strong>${escHtml(b.motivo)}</strong></div>`
|
||||
);
|
||||
}
|
||||
|
||||
// Badge "Bloccato" per l'elenco commesse dell'Export All
|
||||
function blockBadgeHTML(b) {
|
||||
if (!b || !b.motivo) return "";
|
||||
return `<span class="badge" title="${escHtml(b.nominativo + ": " + b.motivo)}" style="margin-left:6px; font-size:0.85em; cursor:help; vertical-align:middle; background:#f8d7da; color:#842029;"><i class="fas fa-ban"></i> Bloccato</span>`;
|
||||
}
|
||||
|
||||
function getGridRow(iddatadb) {
|
||||
return document.querySelector(`.grid-row[data-id="${iddatadb}"]`);
|
||||
}
|
||||
@@ -169,15 +186,28 @@
|
||||
formData.append("iddatadb", iddatadb);
|
||||
if (batchUuid) formData.append("batch_uuid", batchUuid);
|
||||
|
||||
// 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;
|
||||
|
||||
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}`);
|
||||
const data = await response.json();
|
||||
data = await response.json();
|
||||
} finally {
|
||||
const endIdx = getRowIndexByIddatadb(iddatadb);
|
||||
if (endIdx >= 0) delete window.gridData[endIdx]._exporting;
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
// 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) {
|
||||
@@ -254,7 +284,8 @@
|
||||
.map((n) => `<li>${escHtml(n)}</li>`)
|
||||
.join("") +
|
||||
`</ul>`
|
||||
: "");
|
||||
: "") +
|
||||
blockAlertHTML(data.blockAlert);
|
||||
label.textContent = "Esportazione Completata";
|
||||
} else {
|
||||
msg.textContent = `Errore: ${data.message}`;
|
||||
@@ -444,8 +475,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 +485,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 +727,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,19 +764,26 @@
|
||||
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,
|
||||
noPriceCount: data.noPriceCount || 0,
|
||||
noPriceAnalyses: data.noPriceAnalyses || [],
|
||||
blockAlert: data.blockAlert || null,
|
||||
});
|
||||
} else {
|
||||
failed++;
|
||||
processedIds.push(iddatadb);
|
||||
const errIdx = getRowIndexByIddatadb(iddatadb);
|
||||
if (errIdx >= 0)
|
||||
window.gridData[errIdx]._exportError =
|
||||
@@ -755,6 +798,7 @@
|
||||
} catch (error) {
|
||||
processed++;
|
||||
failed++;
|
||||
processedIds.push(iddatadb);
|
||||
const errIdx = getRowIndexByIddatadb(iddatadb);
|
||||
if (errIdx >= 0)
|
||||
window.gridData[errIdx]._exportError = error.message;
|
||||
@@ -793,23 +837,34 @@
|
||||
if (batchCancelled) {
|
||||
labelEl.textContent = "Export All — Cancelled";
|
||||
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";
|
||||
msgEl.innerHTML = `All <strong>${succeeded}</strong> 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: <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) {
|
||||
const npCommesse = exportedList.filter(
|
||||
(x) => x.noPriceCount > 0,
|
||||
).length;
|
||||
const blockedCommesse = exportedList.filter(
|
||||
(x) => x.blockAlert,
|
||||
).length;
|
||||
msgEl.innerHTML +=
|
||||
`<hr class="my-2">` +
|
||||
(npCommesse > 0
|
||||
? `<div class="small text-danger fw-semibold mb-1"><i class="fas fa-exclamation-triangle"></i> ${npCommesse} commesse con analisi senza prezzo</div>`
|
||||
: "") +
|
||||
(blockedCommesse > 0
|
||||
? `<div class="small fw-semibold mb-1" style="color:#842029;"><i class="fas fa-ban"></i> ${blockedCommesse} commesse con cliente bloccato</div>`
|
||||
: "") +
|
||||
`<div class="small fw-semibold mb-1">Commesse esportate:</div>` +
|
||||
`<ul class="mb-0 ps-3">` +
|
||||
exportedList
|
||||
@@ -820,6 +875,7 @@
|
||||
x.noPriceCount,
|
||||
x.noPriceAnalyses,
|
||||
) +
|
||||
blockBadgeHTML(x.blockAlert) +
|
||||
`</li>`,
|
||||
)
|
||||
.join("") +
|
||||
|
||||
@@ -43,15 +43,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
* Clear all validation-error highlights from the grid.
|
||||
*/
|
||||
function clearValidationErrors() {
|
||||
document.querySelectorAll(".grid-cell.validation-error").forEach((cell) => {
|
||||
document
|
||||
.querySelectorAll(".grid-cell.validation-error")
|
||||
.forEach((cell) => {
|
||||
cell.classList.remove("validation-error");
|
||||
cell.querySelectorAll(".input-validation-error").forEach((el) => {
|
||||
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) => {
|
||||
document
|
||||
.querySelectorAll(".grid-row.validation-row-error")
|
||||
.forEach((row) => {
|
||||
row.classList.remove("validation-row-error");
|
||||
});
|
||||
// Also clear batch-row-error that came from validation
|
||||
@@ -90,16 +96,24 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
});
|
||||
if (targetIndex) {
|
||||
const rowIndex = row.querySelector(".grid-cell[data-row]")?.getAttribute("data-row");
|
||||
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}"]`);
|
||||
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");
|
||||
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}"]`);
|
||||
cell = row.querySelector(
|
||||
`.grid-cell[data-col="${err.field}"][data-row="${rowIndex}"]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +208,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
* Show the result of a single export in the response modal.
|
||||
*/
|
||||
function showExportResult(data) {
|
||||
const responseModalElement =
|
||||
document.getElementById("exportResponseModal");
|
||||
const responseModalElement = document.getElementById(
|
||||
"exportResponseModal",
|
||||
);
|
||||
if (!responseModalElement) {
|
||||
alert("Errore: Modale di risposta non trovato");
|
||||
return;
|
||||
@@ -276,16 +291,24 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
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();
|
||||
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) => {
|
||||
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();
|
||||
@@ -333,15 +356,23 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
// Show validating state on the row
|
||||
setRowExporting(gridRow, true);
|
||||
const spinner = gridRow.querySelector(".batch-row-spinner");
|
||||
if (spinner) spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Validating...';
|
||||
if (spinner)
|
||||
spinner.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> Validating...';
|
||||
|
||||
validateRows([{ iddatadb: parseInt(iddatadb), index: parseInt(rowIndex) }])
|
||||
validateRows([
|
||||
{ iddatadb: parseInt(iddatadb), index: parseInt(rowIndex) },
|
||||
])
|
||||
.then((validationData) => {
|
||||
setRowExporting(gridRow, false);
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
|
||||
if (!validationData.success) {
|
||||
showExportResult({ success: false, message: validationData.message || "Errore di validazione" });
|
||||
showExportResult({
|
||||
success: false,
|
||||
message:
|
||||
validationData.message || "Errore di validazione",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -360,7 +391,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
|
||||
console.error("Validation error:", error);
|
||||
showExportResult({ success: false, message: "Errore di validazione: " + error.message });
|
||||
showExportResult({
|
||||
success: false,
|
||||
message: "Errore di validazione: " + error.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -402,7 +436,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
gridRow.classList.remove("batch-disabled");
|
||||
|
||||
if (!data.success) {
|
||||
showRowError(gridRow, iddatadb, data.message || "Errore sconosciuto");
|
||||
showRowError(
|
||||
gridRow,
|
||||
iddatadb,
|
||||
data.message || "Errore sconosciuto",
|
||||
);
|
||||
}
|
||||
showExportResult(data);
|
||||
} catch (error) {
|
||||
@@ -577,8 +615,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
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";
|
||||
document.getElementById(
|
||||
"exportResponseModalLabel",
|
||||
).textContent = "Validazione Fallita";
|
||||
new bootstrap.Modal(
|
||||
document.getElementById("exportResponseModal"),
|
||||
{ keyboard: false },
|
||||
@@ -595,27 +634,38 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (invalidCount > 0) {
|
||||
countText += ` (${invalidCount} escluse per errori di validazione)`;
|
||||
}
|
||||
document.getElementById("exportBatchCount").textContent = countText;
|
||||
document.getElementById("exportBatchCount").textContent =
|
||||
countText;
|
||||
confirmModal.show();
|
||||
|
||||
const confirmBtn = document.getElementById("exportBatchConfirmBtn");
|
||||
const confirmBtn = document.getElementById(
|
||||
"exportBatchConfirmBtn",
|
||||
);
|
||||
if (pendingBatchConfirmHandler) {
|
||||
confirmBtn.removeEventListener("click", pendingBatchConfirmHandler);
|
||||
confirmBtn.removeEventListener(
|
||||
"click",
|
||||
pendingBatchConfirmHandler,
|
||||
);
|
||||
}
|
||||
pendingBatchConfirmHandler = () => {
|
||||
pendingBatchConfirmHandler = null;
|
||||
confirmModal.hide();
|
||||
startBatchExport(validRows);
|
||||
};
|
||||
confirmBtn.addEventListener("click", pendingBatchConfirmHandler, { once: true });
|
||||
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";
|
||||
document.getElementById(
|
||||
"exportResponseModalLabel",
|
||||
).textContent = "Errore Validazione";
|
||||
new bootstrap.Modal(
|
||||
document.getElementById("exportResponseModal"),
|
||||
{ keyboard: false },
|
||||
@@ -635,15 +685,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
);
|
||||
unsavedModal.show();
|
||||
|
||||
document
|
||||
.getElementById("batchSaveAndExportBtn")
|
||||
.addEventListener(
|
||||
document.getElementById("batchSaveAndExportBtn").addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
unsavedModal.hide();
|
||||
// Trigger Save All, then proceed
|
||||
const saveAllEl =
|
||||
document.querySelector(".save-all-btn");
|
||||
const saveAllEl = document.querySelector(".save-all-btn");
|
||||
if (!saveAllEl) return;
|
||||
|
||||
// Watch for all .cell-changed to disappear
|
||||
@@ -754,7 +801,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
succeeded++;
|
||||
} else {
|
||||
failed++;
|
||||
showRowError(row, iddatadb, data.message || "Errore sconosciuto");
|
||||
showRowError(
|
||||
row,
|
||||
iddatadb,
|
||||
data.message || "Errore sconosciuto",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
processed++;
|
||||
@@ -794,7 +845,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const modalEl = document.getElementById("exportResponseModal");
|
||||
const modal = new bootstrap.Modal(modalEl, { keyboard: false });
|
||||
modal.show();
|
||||
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
|
||||
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, {
|
||||
once: true,
|
||||
});
|
||||
})();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
require_once "class/VisualLimsApiClient.class.php";
|
||||
require_once "class/block_alert.php";
|
||||
include('include/headscript.php');
|
||||
|
||||
$dbHandler = DBHandlerSelect::getInstance();
|
||||
@@ -143,6 +144,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 +443,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 +868,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);
|
||||
@@ -798,6 +905,19 @@ try {
|
||||
|
||||
// Indice di lotto: esito OK di questa commessa
|
||||
$writeBatchIndex("OK iddatadb={$iddatadb} commessa={$commessaId} codice={$finalCodiceCommessa} log=" . basename($commessaLogFile));
|
||||
// 🔹 STEP 10.2: Controllo blocco cliente (solo avviso, non blocca mai l'export)
|
||||
$blockAlert = null;
|
||||
try {
|
||||
$blockAlert = getClientBlockAlert($clienteId, $clienteFatturazione);
|
||||
} catch (Throwable $e) {
|
||||
error_log("Controllo blocco cliente fallito (iddatadb={$iddatadb}): " . $e->getMessage());
|
||||
}
|
||||
$writeLog(
|
||||
null,
|
||||
$blockAlert ? json_encode($blockAlert, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : "Nessun blocco",
|
||||
"STEP 10.2 - Controllo blocco cliente (iddatadb={$iddatadb})"
|
||||
);
|
||||
|
||||
// 🔹 STEP 11: Prepare final response
|
||||
$finalCommessa = [
|
||||
"Cliente" => $clienteId,
|
||||
@@ -823,6 +943,7 @@ try {
|
||||
"failedAnalyses" => $failedAnalyses,
|
||||
"noPriceCount" => count($noPriceAnalyses),
|
||||
"noPriceAnalyses" => $noPriceAnalyses,
|
||||
"blockAlert" => $blockAlert,
|
||||
"message" => "Export successful",
|
||||
"logFiles" => [
|
||||
"step5_create" => $logFileStep5,
|
||||
@@ -834,7 +955,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;
|
||||
|
||||
@@ -39,8 +39,14 @@ if (!empty($explicitIds)) {
|
||||
// Scoping di sicurezza: solo record del template/utente/import corretti.
|
||||
$conds = ['d.templateid = ?', 'd.status = ?'];
|
||||
$params = [$templateId, $status];
|
||||
if (!$showAll) { $conds[] = 'd.user_id = ?'; $params[] = $userId; }
|
||||
if ($importref !== '') { $conds[] = 'd.importreferencecode = ?'; $params[] = $importref; }
|
||||
if (!$showAll) {
|
||||
$conds[] = 'd.user_id = ?';
|
||||
$params[] = $userId;
|
||||
}
|
||||
if ($importref !== '') {
|
||||
$conds[] = 'd.importreferencecode = ?';
|
||||
$params[] = $importref;
|
||||
}
|
||||
$ph = implode(',', array_fill(0, count($explicitIds), '?'));
|
||||
$conds[] = "d.iddatadb IN ($ph)";
|
||||
foreach ($explicitIds as $id) $params[] = $id;
|
||||
@@ -57,7 +63,7 @@ if (!empty($explicitIds)) {
|
||||
$idStmt = $pdo->prepare("
|
||||
SELECT d.iddatadb FROM datadb d
|
||||
WHERE {$whereSql}
|
||||
ORDER BY d.excelrow ASC, d.iddatadb ASC
|
||||
ORDER BY CAST(d.excelrow AS UNSIGNED) ASC, d.excelrow ASC, d.iddatadb ASC
|
||||
LIMIT {$perPage} OFFSET {$offset}
|
||||
");
|
||||
$idStmt->execute($params);
|
||||
@@ -356,7 +362,7 @@ try {
|
||||
SELECT d.iddatadb
|
||||
FROM datadb d {$joinSql}
|
||||
WHERE {$whereSql}
|
||||
ORDER BY d.excelrow ASC, d.iddatadb ASC
|
||||
ORDER BY CAST(d.excelrow AS UNSIGNED) ASC, d.excelrow ASC, d.iddatadb ASC
|
||||
LIMIT {$perPage} OFFSET {$offset}
|
||||
");
|
||||
$pageStmt->execute($params);
|
||||
@@ -379,7 +385,7 @@ try {
|
||||
$allStmt = $pdo->prepare("
|
||||
SELECT d.iddatadb FROM datadb d {$joinSql}
|
||||
WHERE {$whereSql}
|
||||
ORDER BY d.excelrow ASC, d.iddatadb ASC
|
||||
ORDER BY CAST(d.excelrow AS UNSIGNED) ASC, d.excelrow ASC, d.iddatadb ASC
|
||||
LIMIT " . FILTER_ALLIDS_CAP . "
|
||||
");
|
||||
$allStmt->execute($params);
|
||||
|
||||
@@ -23,14 +23,17 @@ try {
|
||||
'IdCliente' => (int) $id,
|
||||
'Nominativo' => "Cliente Simulato {$id}",
|
||||
'CodiceCliente' => "SIM_{$id}",
|
||||
'CodiceASL' => null, // campo usato come nota di blocco (es. "BLOCCO PER PAGAMENTO")
|
||||
], $ids);
|
||||
echo json_encode(['value' => $fakeClients]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Parametri OData
|
||||
// CodiceASL viene usato dal cliente come nota di blocco (es. "BLOCCO PER PAGAMENTO"),
|
||||
// perché i campi nativi del Blocco Accettazione non sono esposti dall'API.
|
||||
$params = [
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo,CodiceASL',
|
||||
'$orderby' => 'Nominativo asc'
|
||||
];
|
||||
|
||||
@@ -54,10 +57,12 @@ try {
|
||||
$errorMessage = $e->getMessage();
|
||||
// Controlla se l'errore è legato all'autenticazione (HTTP 400 con messaggio specifico)
|
||||
if (strpos($errorMessage, 'HTTP 400') !== false && strpos($errorMessage, 'Cannot persist the object') !== false) {
|
||||
// Forza il refresh del token
|
||||
// Forza il refresh del token, solo se la classe lo supporta
|
||||
if (!method_exists($api, 'refreshToken')) {
|
||||
throw $e;
|
||||
}
|
||||
try {
|
||||
// Assumi che VisualLimsApiClient abbia un metodo per il refresh del token
|
||||
$api->refreshToken(); // Da implementare in VisualLimsApiClient se non esiste
|
||||
$api->refreshToken();
|
||||
error_log("Tentativo $retry: Refresh token eseguito per endpoint $endpoint");
|
||||
} catch (Exception $refreshEx) {
|
||||
error_log("Errore durante il refresh del token: " . $refreshEx->getMessage());
|
||||
@@ -74,7 +79,7 @@ try {
|
||||
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
|
||||
}
|
||||
|
||||
// Cache file (1 hour TTL)
|
||||
// Cache file (15 minuti TTL: così un blocco impostato nel LIMS compare entro 15 minuti)
|
||||
$cacheFile = __DIR__ . '/cache/clienti.json';
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 7200)) {
|
||||
readfile($cacheFile);
|
||||
@@ -89,7 +94,7 @@ try {
|
||||
file_put_contents($cacheFile, $json);
|
||||
|
||||
echo $json;
|
||||
} catch (Exception $e) {
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
$errorResponse = [
|
||||
'error' => $e->getMessage(),
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
// Script di TEST: recupera TUTTI i campi di pochi clienti (nessun $select, nessuna cache).
|
||||
// Da mettere nella stessa cartella di get_clienti.php.
|
||||
//
|
||||
// Uso:
|
||||
// test_clienti_full.php -> primi 3 clienti
|
||||
// test_clienti_full.php?top=5 -> primi 5 clienti
|
||||
// test_clienti_full.php?id=123 -> solo il cliente con IdCliente = 123
|
||||
// test_clienti_full.php?expand=XYZ -> include anche la relazione XYZ
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$top = isset($_GET['top']) ? max(1, min(20, (int) $_GET['top'])) : 3;
|
||||
|
||||
$params = [
|
||||
'$top' => $top,
|
||||
'$orderby' => 'Nominativo asc',
|
||||
];
|
||||
|
||||
if (isset($_GET['id']) && ctype_digit($_GET['id'])) {
|
||||
$params['$filter'] = 'IdCliente eq ' . (int) $_GET['id'];
|
||||
}
|
||||
|
||||
if (!empty($_GET['expand']) && preg_match('/^[A-Za-z0-9_,\/()$=;]+$/', $_GET['expand'])) {
|
||||
$params['$expand'] = $_GET['expand'];
|
||||
}
|
||||
|
||||
// PHP_QUERY_RFC3986 codifica gli spazi come %20 (più compatibile con OData)
|
||||
$endpoint = 'Cliente?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
|
||||
|
||||
$data = $api->get($endpoint);
|
||||
|
||||
// Copia su file per consultarla con calma
|
||||
file_put_contents(
|
||||
__DIR__ . '/clienti_full_response.json',
|
||||
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
|
||||
// Elenco dei nomi dei campi trovati, utile per capire cosa è disponibile
|
||||
$campi = [];
|
||||
foreach ($data['value'] ?? [] as $row) {
|
||||
$campi = array_unique(array_merge($campi, array_keys($row)));
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'endpoint' => $endpoint,
|
||||
'campi' => array_values($campi),
|
||||
'dati' => $data,
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'error' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -263,6 +263,116 @@
|
||||
await resolveClientNames();
|
||||
}
|
||||
|
||||
// ── Blocco clienti (CodiceASL contenente "blocco") ─────────────────────
|
||||
|
||||
const clientBlockMap = {}; // IdCliente -> testo completo di CodiceASL
|
||||
|
||||
async function loadClientBlocks() {
|
||||
try {
|
||||
const resp = await fetch("get_clienti.php", { cache: "no-store" });
|
||||
const json = await resp.json();
|
||||
(json.value || []).forEach((c) => {
|
||||
const txt = (c.CodiceASL || "").trim();
|
||||
if (txt && /blocco/i.test(txt)) {
|
||||
clientBlockMap[String(c.IdCliente)] = txt;
|
||||
}
|
||||
});
|
||||
console.log(
|
||||
"[gridRenderer] clienti bloccati:",
|
||||
Object.keys(clientBlockMap).length,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"[gridRenderer] errore caricamento blocchi clienti",
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Regola: se "Fatturazione avanzata" è compilata si controlla quella, altrimenti il Client
|
||||
function getRowBlockInfo(rowIndex) {
|
||||
const row = data[rowIndex];
|
||||
if (!row) return null;
|
||||
const fatt =
|
||||
(row.fixedFields && row.fixedFields.ClienteFatturazione) || "";
|
||||
if (fatt) {
|
||||
return {
|
||||
colKey: "ClienteFatturazione",
|
||||
reason: clientBlockMap[String(fatt)] || "",
|
||||
};
|
||||
}
|
||||
return {
|
||||
colKey: "idclient",
|
||||
reason: clientBlockMap[String(row.idclient || "")] || "",
|
||||
};
|
||||
}
|
||||
|
||||
function hideBlockTooltip(el) {
|
||||
if (window.bootstrap && bootstrap.Tooltip) {
|
||||
const t = bootstrap.Tooltip.getInstance(el);
|
||||
if (t) t.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function applyBlockMarker(rowIndex, rowEl) {
|
||||
const row = data[rowIndex];
|
||||
if (!row) return;
|
||||
rowEl =
|
||||
rowEl ||
|
||||
(rowContainer &&
|
||||
rowContainer.querySelector(
|
||||
`.grid-row[data-id="${row.iddatadb}"]`,
|
||||
));
|
||||
if (!rowEl) return;
|
||||
|
||||
// Rimuove marcatori precedenti (sia su Client sia su Fatturazione avanzata)
|
||||
rowEl
|
||||
.querySelectorAll(".grid-cell.client-blocked")
|
||||
.forEach((c) => c.classList.remove("client-blocked"));
|
||||
rowEl.querySelectorAll(".client-block-icon").forEach((i) => {
|
||||
hideBlockTooltip(i);
|
||||
i.remove();
|
||||
});
|
||||
|
||||
const info = getRowBlockInfo(rowIndex);
|
||||
if (!info || !info.reason) return;
|
||||
|
||||
const cell = rowEl.querySelector(
|
||||
`.grid-cell[data-col="${info.colKey}"]`,
|
||||
);
|
||||
if (!cell) return;
|
||||
|
||||
cell.classList.add("client-blocked");
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "client-block-icon";
|
||||
icon.innerHTML = '<i class="fas fa-exclamation-circle"></i>';
|
||||
icon.dataset.reason = info.reason;
|
||||
icon.setAttribute("aria-label", info.reason);
|
||||
cell.appendChild(icon);
|
||||
}
|
||||
|
||||
function refreshAllBlockMarkers() {
|
||||
data.forEach((_, i) => applyBlockMarker(i));
|
||||
}
|
||||
|
||||
function initBlockTooltips() {
|
||||
$(document).on("mouseenter", ".client-block-icon", function () {
|
||||
if (!(window.bootstrap && bootstrap.Tooltip)) {
|
||||
this.title = this.dataset.reason;
|
||||
return;
|
||||
}
|
||||
bootstrap.Tooltip.getOrCreateInstance(this, {
|
||||
title: this.dataset.reason,
|
||||
placement: "top",
|
||||
container: "body",
|
||||
trigger: "manual",
|
||||
}).show();
|
||||
});
|
||||
$(document).on("mouseleave", ".client-block-icon", function () {
|
||||
hideBlockTooltip(this);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Fixed field data loading ───────────────────────────────────────────
|
||||
|
||||
const fixedFieldApiConfig = {
|
||||
@@ -463,6 +573,7 @@
|
||||
async function preloadAllData() {
|
||||
// 1. Clients
|
||||
await loadClientData();
|
||||
await loadClientBlocks();
|
||||
|
||||
// 2. Non-dependent fixed fields
|
||||
const nonDependent = Object.keys(fixedFieldApiConfig).filter(
|
||||
@@ -755,7 +866,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";
|
||||
@@ -874,6 +986,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
applyBlockMarker(rowIndex, rowDiv);
|
||||
|
||||
return rowDiv;
|
||||
}
|
||||
|
||||
@@ -970,6 +1084,8 @@
|
||||
|
||||
cell.classList.add("cell-changed");
|
||||
});
|
||||
|
||||
refreshAllBlockMarkers();
|
||||
}
|
||||
|
||||
function renderSingleRow(rowIndex) {
|
||||
@@ -1078,6 +1194,9 @@
|
||||
if (columns[colPos]) {
|
||||
columns[colPos].width = newWidth;
|
||||
}
|
||||
|
||||
// Riallinea larghezza/posizione delle tendine Select2 aperte
|
||||
$(window).trigger("resize.select2");
|
||||
}
|
||||
|
||||
function syncVisibleRowsToGridData() {
|
||||
@@ -1714,6 +1833,8 @@
|
||||
}
|
||||
cell.classList.add("cell-changed");
|
||||
updateDirtyIndicator();
|
||||
|
||||
if (colType === "idclient") applyBlockMarker(rowIndex);
|
||||
});
|
||||
|
||||
// Handle SceltaMultipla changes and persist them into gridData.
|
||||
@@ -1765,6 +1886,8 @@
|
||||
data[rowIndex]._dirty = true;
|
||||
cell.classList.add("cell-changed");
|
||||
updateDirtyIndicator();
|
||||
|
||||
if (key === "ClienteFatturazione") applyBlockMarker(rowIndex);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1908,6 +2031,9 @@
|
||||
if (columns[colPos]) {
|
||||
columns[colPos].width = newWidth;
|
||||
}
|
||||
|
||||
// Riallinea larghezza/posizione delle tendine Select2 aperte
|
||||
$(window).trigger("resize.select2");
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
@@ -1971,6 +2097,7 @@
|
||||
// Events
|
||||
attachEvents();
|
||||
initLazySelect2();
|
||||
initBlockTooltips();
|
||||
}
|
||||
|
||||
// Start when DOM ready
|
||||
|
||||
@@ -1518,6 +1518,38 @@ $gridMeta = [
|
||||
}
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
/* ── Cliente bloccato (CodiceASL contiene "blocco") ── */
|
||||
.grid-container .grid-cell.client-blocked {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.grid-container .grid-cell.client-blocked>select,
|
||||
.grid-container .grid-cell.client-blocked .select2-container {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.grid-container .grid-cell.client-blocked select,
|
||||
.grid-container .grid-cell.client-blocked .select2-container--default .select2-selection--single {
|
||||
background-color: #f8d7da !important;
|
||||
border-color: #f1aeb5 !important;
|
||||
}
|
||||
|
||||
.grid-container .grid-cell.client-blocked .select2-selection__rendered {
|
||||
color: #842029 !important;
|
||||
}
|
||||
|
||||
.client-block-icon {
|
||||
flex: 0 0 auto;
|
||||
color: #dc3545;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: help;
|
||||
}
|
||||
</style>
|
||||
<title>Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
</head>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row parts-row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-md-12">
|
||||
<!-- Prima riga: Elenco Parti, Rinumera, Voce -->
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; min-width: 0;">
|
||||
<h6 style="margin: 0; white-space: nowrap;">Elenco Parti</h6>
|
||||
@@ -55,7 +55,7 @@
|
||||
<i class="fas fa-microphone"></i> Voce
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm ms-2" id="showHideImageBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
|
||||
<i class="fas fa-eye-slash" style="font-size: 0.8rem;"></i>
|
||||
<i class="fas fa-eye" style="font-size: 0.8rem;"></i>
|
||||
<i class="fas fa-image ms-1" style="font-size: 0.8rem;"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -119,7 +119,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-3 d-none">
|
||||
<h6>Foto del Campione</h6>
|
||||
<div style="display: flex; align-items: center; margin-bottom: 10px;">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="downloadPhotoBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem; margin-right: 10px;"><i class="fas fa-download"></i></button>
|
||||
@@ -432,7 +432,7 @@
|
||||
}
|
||||
|
||||
/* Evita stretching del flex nella riga dei filtri */
|
||||
#partsModal .modal-body>.row .col-md-9>div[style*="display: flex"]>* {
|
||||
#partsModal .parts-row>div:first-child>div[style*="display: flex"]>* {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -611,7 +611,7 @@
|
||||
|
||||
#partsTable {
|
||||
table-layout: fixed !important;
|
||||
width: max-content !important;
|
||||
width: 100% !important;
|
||||
min-width: unset !important;
|
||||
}
|
||||
|
||||
@@ -733,9 +733,6 @@
|
||||
|
||||
/* Extra field propagation header - full column width */
|
||||
#partsTable th.extra-field-th {
|
||||
width: auto !important;
|
||||
min-width: 220px !important;
|
||||
max-width: none !important;
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
|
||||
@@ -757,9 +754,9 @@
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
color: #222;
|
||||
white-space: nowrap !important;
|
||||
overflow: visible !important;
|
||||
text-overflow: unset !important;
|
||||
white-space: normal !important;
|
||||
word-break: break-word;
|
||||
overflow: hidden !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -806,5 +803,69 @@
|
||||
z-index: 1065 !important
|
||||
}
|
||||
|
||||
/* Campi nelle celle: occupano tutta la cella */
|
||||
#partsTable td input.form-control,
|
||||
#partsTable td textarea.form-control {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
#partsTable td>div[style*="display: flex"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Select2 nelle celle (Matrice ed Extra): occupano la cella e vanno a capo */
|
||||
#partsTable td .select2-container {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
#partsTable td .part-matrice+.select2 {
|
||||
flex: 1 1 auto !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
#partsTable td .select2-selection--single {
|
||||
height: auto !important;
|
||||
min-height: 24px !important;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#partsTable td .select2-selection__rendered {
|
||||
flex: 1 1 auto;
|
||||
white-space: normal !important;
|
||||
word-break: break-word;
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
line-height: 1.2 !important;
|
||||
padding: 2px 22px 2px 4px !important;
|
||||
}
|
||||
|
||||
#partsTable td .select2-selection__arrow {
|
||||
height: 100% !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
/* Campo extra di tipo testo: textarea che cresce col contenuto */
|
||||
#partsTable textarea.part-extra-field {
|
||||
field-sizing: content;
|
||||
min-height: 24px;
|
||||
height: auto;
|
||||
padding: 0.1rem 0.3rem;
|
||||
line-height: 1.2;
|
||||
font-size: 0.8rem;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Voci del dropdown che vanno a capo */
|
||||
.select2-results__option {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* rosso */
|
||||
</style>
|
||||
+151
-78
@@ -147,10 +147,9 @@ $(document).ready(function () {
|
||||
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 || ""}" value="${selectedValueText || ""}">
|
||||
<textarea rows="1" class="form-control form-control-sm part-extra-field" data-type="${partsExtraField.data_type || ""}">${selectedValueText || ""}</textarea>
|
||||
</td>`;
|
||||
}
|
||||
|
||||
function buildExtraFieldHeaderHtml() {
|
||||
if (!partsExtraField) return "";
|
||||
|
||||
@@ -888,6 +887,17 @@ $(document).ready(function () {
|
||||
$("#partsTableBody").empty();
|
||||
$("#global-matrice").empty();
|
||||
$("#macro-matrice-filter").empty();
|
||||
|
||||
// Ripristina lo stato di default: foto nascosta
|
||||
const $partsRow = $("#partsModal .parts-row");
|
||||
$partsRow.find("> .col-md-3").addClass("d-none");
|
||||
$partsRow
|
||||
.find("> div:first-child")
|
||||
.removeClass("col-md-9")
|
||||
.addClass("col-md-12");
|
||||
$("#showHideImageBtn").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>",
|
||||
);
|
||||
$(".temp-alert").remove();
|
||||
$(".modal-backdrop").remove();
|
||||
$("body").removeClass("modal-open").css("padding-right", "");
|
||||
@@ -2154,6 +2164,9 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
window.saveAllParts = saveAllParts;
|
||||
window.markUnsaved = function () {
|
||||
markUnsaved();
|
||||
};
|
||||
function renumberParts() {
|
||||
console.log(
|
||||
"Inizio rinumera parts, numero righe:",
|
||||
@@ -2672,135 +2685,195 @@ $(document).on("click", "#showHideImageBtn", function () {
|
||||
// RESIZABLE PARTS TABLE COLUMNS - FIXED COLGROUP VERSION
|
||||
// ===================
|
||||
(function () {
|
||||
// Larghezze default per indice colonna (0-based)
|
||||
const defaultWidths = [55, 320, 360, 150, 220, 230];
|
||||
const savedWidths = [...defaultWidths];
|
||||
const NUM_W = 55; // colonna Num fissa
|
||||
const MIN_W = 70; // larghezza minima di ogni colonna
|
||||
|
||||
// Pesi di default delle colonne dopo Num
|
||||
// senza extra: Descrizione, Matrice, Data, Azioni
|
||||
// con extra: Descrizione, Matrice, Data, Extra, Azioni
|
||||
const DEFAULT_NO_EXTRA = [320, 360, 150, 200];
|
||||
const DEFAULT_EXTRA = [280, 320, 140, 240, 180];
|
||||
|
||||
let weights = null;
|
||||
let weightsCount = 0;
|
||||
|
||||
function getColgroup() {
|
||||
return $("#partsTableColgroup");
|
||||
}
|
||||
|
||||
function syncColgroupToHeaders() {
|
||||
const $table = $("#partsTable");
|
||||
const $ths = $table.find("thead tr:first th");
|
||||
const $colgroup = getColgroup();
|
||||
const thCount = $ths.length;
|
||||
|
||||
// Assicura che ci siano esattamente tante <col> quante <th>
|
||||
while ($colgroup.find("col").length < thCount) {
|
||||
$colgroup.append("<col>");
|
||||
}
|
||||
while ($colgroup.find("col").length > thCount) {
|
||||
$colgroup.find("col:last").remove();
|
||||
function getThs() {
|
||||
return $("#partsTable thead tr:first th");
|
||||
}
|
||||
|
||||
// Applica le larghezze salvate
|
||||
$colgroup.find("col").each(function (i) {
|
||||
const w = savedWidths[i] !== undefined ? savedWidths[i] : 150;
|
||||
$(this).css("width", w + "px");
|
||||
function getAvail() {
|
||||
const w = $("#partsModal .parts-table-scroll").innerWidth();
|
||||
return w > 0 ? Math.floor(w) : 0;
|
||||
}
|
||||
|
||||
function ensureCols(n) {
|
||||
const $cg = getColgroup();
|
||||
while ($cg.find("col").length < n) $cg.append("<col>");
|
||||
while ($cg.find("col").length > n) $cg.find("col:last").remove();
|
||||
}
|
||||
|
||||
function ensureWeights(n) {
|
||||
const k = n - 1;
|
||||
if (weights && weightsCount === k) return;
|
||||
if (k === 4) weights = DEFAULT_NO_EXTRA.slice();
|
||||
else if (k === 5) weights = DEFAULT_EXTRA.slice();
|
||||
else weights = Array(k).fill(150);
|
||||
weightsCount = k;
|
||||
}
|
||||
|
||||
// Converte i pesi in pixel: la somma è sempre = larghezza disponibile
|
||||
function computePx(n) {
|
||||
const avail = getAvail();
|
||||
const rest = Math.max(avail - NUM_W, MIN_W * (n - 1));
|
||||
const sum = weights.reduce((a, b) => a + b, 0) || 1;
|
||||
const px = [NUM_W];
|
||||
let used = 0;
|
||||
for (let i = 0; i < weights.length; i++) {
|
||||
const v =
|
||||
i === weights.length - 1
|
||||
? rest - used
|
||||
: Math.floor((rest * weights[i]) / sum);
|
||||
px.push(v);
|
||||
used += v;
|
||||
}
|
||||
return px;
|
||||
}
|
||||
|
||||
// Riallinea larghezza e posizione delle tendine Select2 aperte nella tabella
|
||||
function syncOpenSelect2() {
|
||||
$("#partsTable select.select2-hidden-accessible").each(function () {
|
||||
const inst = $(this).data("select2");
|
||||
if (!inst || typeof inst.isOpen !== "function" || !inst.isOpen()) {
|
||||
return;
|
||||
}
|
||||
const dd = inst.dropdown;
|
||||
try {
|
||||
if (dd && typeof dd._resizeDropdown === "function") {
|
||||
dd._resizeDropdown();
|
||||
}
|
||||
if (dd && typeof dd._positionDropdown === "function") {
|
||||
dd._positionDropdown();
|
||||
}
|
||||
} catch (e) {
|
||||
// Se la versione di Select2 non lo supporta, chiude la tendina
|
||||
inst.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
const n = getThs().length;
|
||||
if (!n || !getAvail()) return;
|
||||
ensureCols(n);
|
||||
ensureWeights(n);
|
||||
const px = computePx(n);
|
||||
getColgroup()
|
||||
.find("col")
|
||||
.each(function (i) {
|
||||
$(this).css("width", px[i] + "px");
|
||||
});
|
||||
|
||||
// Imposta larghezza totale della tabella = somma colonne (evita reflow)
|
||||
const total = savedWidths.slice(0, thCount).reduce((a, b) => a + b, 0);
|
||||
$table.css("width", total + "px");
|
||||
}
|
||||
|
||||
function applyColumnWidth(colIndex, newWidth) {
|
||||
const w = Math.max(40, Math.round(newWidth));
|
||||
savedWidths[colIndex] = w;
|
||||
|
||||
const $col = getColgroup().find("col").eq(colIndex);
|
||||
if ($col.length) {
|
||||
$col.css("width", w + "px");
|
||||
}
|
||||
|
||||
// Aggiorna larghezza totale tabella senza toccare le altre colonne
|
||||
const thCount = $("#partsTable thead tr:first th").length;
|
||||
const total = savedWidths.slice(0, thCount).reduce((a, b) => a + b, 0);
|
||||
$("#partsTable").css("width", total + "px");
|
||||
syncOpenSelect2();
|
||||
}
|
||||
|
||||
function addResizers() {
|
||||
const $table = $("#partsTable");
|
||||
if (!$table.length) return;
|
||||
const $ths = getThs();
|
||||
const n = $ths.length;
|
||||
if (!n) return;
|
||||
|
||||
$ths.find(".parts-resizer").remove();
|
||||
|
||||
// Resizer sul bordo destro delle colonne da 1 a n-2:
|
||||
// sposta spazio tra la colonna e quella alla sua destra
|
||||
$ths.each(function (i) {
|
||||
if (i === 0 || i >= n - 1) return;
|
||||
|
||||
$table.find("thead tr:first th").each(function (colIndex) {
|
||||
const $th = $(this);
|
||||
|
||||
// Salta colonna Num (indice 0) — non ridimensionabile
|
||||
if (colIndex === 0) return;
|
||||
|
||||
// Non aggiungere due volte
|
||||
if ($th.find(".parts-resizer").length) return;
|
||||
|
||||
$th.css("position", "relative");
|
||||
const $resizer = $("<span class='parts-resizer'></span>");
|
||||
$th.css("position", "relative"); // necessario per il posizionamento assoluto
|
||||
$th.append($resizer);
|
||||
|
||||
$resizer.on("mousedown", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
ensureWeights(n);
|
||||
const startPx = computePx(n);
|
||||
const a = startPx[i];
|
||||
const b = startPx[i + 1];
|
||||
const startX = e.pageX;
|
||||
// Leggi la larghezza ESATTA dalla <col>, non dal <th>
|
||||
const startWidth =
|
||||
savedWidths[colIndex] !== undefined
|
||||
? savedWidths[colIndex]
|
||||
: parseInt(
|
||||
getColgroup()
|
||||
.find("col")
|
||||
.eq(colIndex)
|
||||
.css("width"),
|
||||
10,
|
||||
) || 150;
|
||||
|
||||
$("body")
|
||||
.css("user-select", "none")
|
||||
.css("cursor", "col-resize");
|
||||
$("body").css({ "user-select": "none", cursor: "col-resize" });
|
||||
|
||||
$(document).on("mousemove.partsResize", function (ev) {
|
||||
const delta = ev.pageX - startX;
|
||||
applyColumnWidth(colIndex, startWidth + delta);
|
||||
const newA = Math.min(
|
||||
Math.max(a + delta, MIN_W),
|
||||
a + b - MIN_W,
|
||||
);
|
||||
const newB = a + b - newA;
|
||||
|
||||
const px = startPx.slice();
|
||||
px[i] = newA;
|
||||
px[i + 1] = newB;
|
||||
weights = px.slice(1); // i pixel diventano i nuovi pesi
|
||||
render();
|
||||
});
|
||||
|
||||
$(document).on("mouseup.partsResize", function () {
|
||||
$("body").css("user-select", "").css("cursor", "");
|
||||
$("body").css({ "user-select": "", cursor: "" });
|
||||
$(document).off(".partsResize");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
syncColgroupToHeaders();
|
||||
function refresh() {
|
||||
render();
|
||||
addResizers();
|
||||
}
|
||||
|
||||
// Init al primo show del modale
|
||||
$(document).on("shown.bs.modal", "#partsModal", function () {
|
||||
setTimeout(init, 120);
|
||||
setTimeout(refresh, 120);
|
||||
});
|
||||
|
||||
// Re-init dopo ogni AJAX (nuove righe, caricamenti)
|
||||
$(document).ajaxComplete(function () {
|
||||
if ($("#partsModal").hasClass("show")) {
|
||||
setTimeout(syncColgroupToHeaders, 200);
|
||||
setTimeout(addResizers, 220);
|
||||
setTimeout(refresh, 200);
|
||||
}
|
||||
});
|
||||
|
||||
// Re-init dopo aggiunta/rimozione righe
|
||||
$(document).on(
|
||||
"click",
|
||||
".add-row-global, .add-mix-global, .add-mix-row, .remove-row, #renumberPartsBtn, #clonePartsBtn",
|
||||
function () {
|
||||
setTimeout(syncColgroupToHeaders, 120);
|
||||
setTimeout(addResizers, 140);
|
||||
setTimeout(refresh, 120);
|
||||
},
|
||||
);
|
||||
|
||||
window.initPartsResizableColumns = init;
|
||||
window.applyPartsColumnWidths = syncColgroupToHeaders;
|
||||
let resizeTimer = null;
|
||||
$(window).on("resize.partsCols", function () {
|
||||
if (!$("#partsModal").hasClass("show")) return;
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(render, 150);
|
||||
});
|
||||
|
||||
// Textarea campo extra: auto-altezza (per browser senza field-sizing)
|
||||
$(document).on(
|
||||
"input focus",
|
||||
"#partsTable textarea.part-extra-field",
|
||||
function () {
|
||||
this.style.height = "auto";
|
||||
this.style.height = this.scrollHeight + "px";
|
||||
},
|
||||
);
|
||||
|
||||
window.initPartsResizableColumns = refresh;
|
||||
window.applyPartsColumnWidths = refresh;
|
||||
})();
|
||||
// ===================
|
||||
// NAVIGAZIONE TABELLA PARTI CON FRECCE TASTIERA
|
||||
|
||||
@@ -8,12 +8,41 @@ header('Content-Type: application/json');
|
||||
|
||||
$iddatadb = (int)($_POST['iddatadb'] ?? 0);
|
||||
$newStatus = $_POST['status'] ?? 'i';
|
||||
$action = $_POST['action'] ?? 'revert';
|
||||
|
||||
if ($iddatadb <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID record mancante']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cancellazione record (consentita solo da stato 'i' o 'x')
|
||||
if ($action === 'delete') {
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM datadb WHERE iddatadb = ? AND status IN ('i', 'x')");
|
||||
$stmt->execute([$iddatadb]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
$chk = $pdo->prepare("SELECT status FROM datadb WHERE iddatadb = ?");
|
||||
$chk->execute([$iddatadb]);
|
||||
$current = $chk->fetchColumn();
|
||||
|
||||
if ($current === false) {
|
||||
echo json_encode(['success' => false, 'message' => 'Record non trovato']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => "Cancellazione non consentita: stato attuale '$current'"]);
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// Da questa azione consentiamo solo il ritorno a 'i'
|
||||
if ($newStatus !== 'i') {
|
||||
echo json_encode(['success' => false, 'message' => 'Stato non consentito']);
|
||||
@@ -24,8 +53,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 IN ('l', 'x')");
|
||||
$stmt->execute([$newStatus, $iddatadb]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
|
||||
@@ -57,7 +57,7 @@ try {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$params = [
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo,CodiceASL',
|
||||
'$orderby' => 'Nominativo asc'
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
require_once __DIR__ . '/class/Mailer.class.php';
|
||||
require_once __DIR__ . '/class/block_alert.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
/**
|
||||
@@ -91,9 +92,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']);
|
||||
@@ -157,6 +159,16 @@ foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
}
|
||||
$noPriceTotal = array_sum(array_map('count', $noPriceByRow));
|
||||
|
||||
// cliente bloccato per ogni iddatadb (CodiceASL con "blocco", letto dalla cache clienti, nessuna chiamata API)
|
||||
$blockByRow = []; // iddatadb => ['idCliente','nominativo','motivo','campo']
|
||||
foreach ($rows as $r) {
|
||||
$bl = getClientBlockAlert($r['idclient'] ?? 0, $r['ClienteFatturazione'] ?? null);
|
||||
if ($bl) {
|
||||
$blockByRow[(int)$r['iddatadb']] = $bl;
|
||||
}
|
||||
}
|
||||
$blockTotal = count($blockByRow);
|
||||
|
||||
// destinatari: utente/i dell'export che hanno la notifica attiva + admin fisso
|
||||
$recipients = [];
|
||||
foreach ($rows as $r) {
|
||||
@@ -174,14 +186,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 +212,7 @@ foreach ($rows as $r) {
|
||||
$bg = '#fdecea';
|
||||
$col = '#c0392b';
|
||||
} else {
|
||||
$label = htmlspecialchars((string)$st);
|
||||
$label = 'NON CONFERMATA';
|
||||
$bg = '#fff8e1';
|
||||
$col = '#8a6d3b';
|
||||
}
|
||||
@@ -237,12 +253,44 @@ foreach ($rows as $r) {
|
||||
</td>
|
||||
</tr>";
|
||||
}
|
||||
|
||||
// sotto-riga: cliente bloccato
|
||||
$bl = $blockByRow[(int)$r['iddatadb']] ?? null;
|
||||
if ($bl) {
|
||||
$blCampo = htmlspecialchars($bl['campo']);
|
||||
$blNome = htmlspecialchars($bl['nominativo'] !== '' ? $bl['nominativo'] : (string)$bl['idCliente']);
|
||||
$blMotivo = htmlspecialchars($bl['motivo']);
|
||||
|
||||
$rowsHtml .= "<tr>
|
||||
<td colspan='5' style='padding:8px 12px 12px 24px;border-bottom:1px solid #eee;background:#fdecea;'>
|
||||
<div style='font-size:12px;color:#842029;font-weight:600;'>
|
||||
<span style='display:inline-block;padding:2px 8px;border-radius:10px;background:#842029;color:#fff;font-size:11px;'>Cliente bloccato</span>
|
||||
{$blCampo}: {$blNome}
|
||||
</div>
|
||||
<div style='margin-top:4px;font-size:13px;color:#842029;font-weight:700;'>{$blMotivo}</div>
|
||||
</td>
|
||||
</tr>";
|
||||
}
|
||||
}
|
||||
|
||||
$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 = "
|
||||
<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;'>
|
||||
@@ -250,9 +298,9 @@ $html = "
|
||||
<p style='margin:6px 0 0;font-size:13px;opacity:.9;'>Riepilogo esportazione verso LIMS" .
|
||||
($templateName !== '' ? " · Template: <strong>" . htmlspecialchars($templateName) . "</strong>" : "") . "</p>
|
||||
<p style='margin:6px 0 0;font-size:13px;opacity:.95;'>
|
||||
Committente: <strong>" . $clientLabel($rows[0]['idclient'] ?? 0) . "</strong>
|
||||
Committente: <strong>" . $headerClient('idclient') . "</strong>
|
||||
·
|
||||
FatturazioneAvanzata: <strong>" . $clientLabel($rows[0]['ClienteFatturazione'] ?? 0) . "</strong>
|
||||
FatturazioneAvanzata: <strong>" . $headerClient('ClienteFatturazione') . "</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div style='padding:20px 24px;'>
|
||||
@@ -260,7 +308,9 @@ $html = "
|
||||
Totale commesse: <strong>{$total}</strong> •
|
||||
<span style='color:#1e7e34;'>Inviate: {$okCount}</span> •
|
||||
<span style='color:#c0392b;'>Errori: {$errCount}</span>" .
|
||||
($noPriceTotal > 0 ? " • <span style='color:#c0392b;'>Analisi No price: {$noPriceTotal}</span>" : "") . "
|
||||
($pendingCount > 0 ? " • <span style='color:#8a6d3b;'>Non confermate: {$pendingCount}</span>" : "") .
|
||||
($noPriceTotal > 0 ? " • <span style='color:#c0392b;'>Analisi No price: {$noPriceTotal}</span>" : "") .
|
||||
($blockTotal > 0 ? " • <span style='color:#842029;'>Clienti bloccati: {$blockTotal}</span>" : "") . "
|
||||
</p>
|
||||
<table style='width:100%;border-collapse:collapse;font-size:14px;table-layout:fixed;'>
|
||||
<thead>
|
||||
@@ -274,15 +324,16 @@ $html = "
|
||||
</thead>
|
||||
<tbody>{$rowsHtml}</tbody>
|
||||
</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 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.
|
||||
</div>
|
||||
</div>";
|
||||
|
||||
$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 +341,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],
|
||||
]);
|
||||
|
||||
@@ -78,7 +78,7 @@ try {
|
||||
|
||||
// 1. Clients
|
||||
warmLog('[clients] Fetching...', $isCli);
|
||||
$params = ['$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo', '$orderby' => 'Nominativo asc'];
|
||||
$params = ['$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo,CodiceASL', '$orderby' => 'Nominativo asc'];
|
||||
$clientData = $api->get("Cliente?" . http_build_query($params));
|
||||
file_put_contents($cacheDir . '/clienti.json', json_encode($clientData));
|
||||
$clientCount = count($clientData['value'] ?? []);
|
||||
|
||||
Reference in New Issue
Block a user