added error page and logs linked

This commit is contained in:
2026-09-22 13:58:14 +02:00
parent 9bed740223
commit 5c12d97d0e
8 changed files with 513 additions and 29 deletions
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddLogFileToDatadb extends AbstractMigration
{
public function change(): void
{
$table = $this->table('datadb');
if (!$table->hasColumn('log_file')) {
$table->addColumn('log_file', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'after' => 'commessaweb', // aggiusta se la colonna non esiste
])->update();
}
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddErrorStatusToDatadb extends AbstractMigration
{
public function up(): void
{
// Aggiunge 'x' (errore export) all'enum status, preservando i valori esistenti
$this->execute("ALTER TABLE `datadb`
MODIFY `status` ENUM('i','P','l','x') NULL DEFAULT NULL");
}
public function down(): void
{
// Rollback: riporta le eventuali 'x' a NULL prima di togliere il valore
$this->execute("UPDATE `datadb` SET `status` = NULL WHERE `status` = 'x'");
$this->execute("ALTER TABLE `datadb`
MODIFY `status` ENUM('i','P','l') NULL DEFAULT NULL");
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$logDir = __DIR__ . '/logs/api/';
$iddatadb = (int)($_GET['iddatadb'] ?? 0);
if ($iddatadb <= 0) {
http_response_code(400);
exit('iddatadb mancante');
}
$stmt = $pdo->prepare("SELECT log_file, commessaweb FROM datadb WHERE iddatadb = :id LIMIT 1");
$stmt->execute(['id' => $iddatadb]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row || empty($row['log_file'])) {
http_response_code(404);
exit('Nessun log per questa commessa');
}
// solo basename: niente path traversal
$path = $logDir . basename($row['log_file']);
if (!is_file($path)) {
http_response_code(404);
exit('File di log non trovato: ' . basename($row['log_file']));
}
while (ob_get_level() > 0) {
ob_end_clean();
}
$label = preg_replace('/[^A-Za-z0-9_\-]/', '_', (string)($row['commessaweb'] ?: $iddatadb));
header('Content-Type: text/plain; charset=utf-8');
header('Content-Disposition: attachment; filename="log_' . $label . '_' . $iddatadb . '.log"');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
+363
View File
@@ -0,0 +1,363 @@
<?php
include('include/headscript.php');
$template_id = intval($_GET['id'] ?? 0);
if (!$template_id) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Template ID mancante"));
exit;
}
$user_id = $iduserlogin ?? 1;
$show_all_users = isset($_GET['all_users']) && $_GET['all_users'] == '1';
$allowedLimits = [20, 40, 60, 100];
$rawLimit = (int)($_GET['limit'] ?? 20);
$perPage = in_array($rawLimit, $allowedLimits) ? $rawLimit : 20;
$page = max(1, (int)($_GET['page'] ?? 1));
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$logDir = __DIR__ . '/logs/api/';
// Estrae un riassunto breve dell'errore dal file di log della commessa
function extractErrorSummary(string $logDir, ?string $logFile): string
{
if (empty($logFile)) return '';
$path = $logDir . basename($logFile);
if (!is_file($path)) return '';
$content = @file_get_contents($path);
if ($content === false || $content === '') return '';
// 1) prova col "detail" del JSON del 500
if (preg_match('/"detail"\s*:\s*"([^"]+)"/', $content, $m)) {
return trim(stripcslashes($m[1]));
}
// 2) fallback: ultima riga MESSAGE:
if (preg_match_all('/^MESSAGE:\s*(.+)$/m', $content, $m) && !empty($m[1])) {
return trim(end($m[1]));
}
return '';
}
// Query: solo le righe in errore per questo template
$userFilter = $show_all_users ? '' : 'AND d.user_id = ?';
$baseWhere = "WHERE d.templateid = ? AND d.status = 'x' {$userFilter}";
$baseParams = [$template_id];
if (!$show_all_users) $baseParams[] = $user_id;
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM datadb d {$baseWhere}");
$countStmt->execute($baseParams);
$totalRows = (int)$countStmt->fetchColumn();
$totalPages = max(1, (int)ceil($totalRows / $perPage));
if ($page > $totalPages) $page = $totalPages;
$stmt = $pdo->prepare("
SELECT d.*, CONCAT(u.first_name, ' ', u.last_name) AS user_name
FROM datadb d
LEFT JOIN auth_users u ON d.user_id = u.id
{$baseWhere}
ORDER BY d.export_date DESC, d.iddatadb DESC
LIMIT {$perPage} OFFSET " . (($page - 1) * $perPage) . "
");
$stmt->execute($baseParams);
$rowsData = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
.err-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.err-table th,
.err-table td {
padding: 8px 10px;
border-bottom: 1px solid #dee2e6;
vertical-align: top;
}
.err-table th {
background: #f8f9fa;
text-align: left;
font-weight: 600;
color: #495057;
}
.err-table tr:hover {
background: #fff8f8;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
font-size: 12px;
font-weight: 500;
border-radius: 12px;
}
.status-x {
background: #dc3545;
color: #fff;
}
.err-msg {
color: #b02a37;
max-width: 420px;
white-space: normal;
word-break: break-word;
}
.commessa-code {
font-size: 0.8em;
color: #555;
display: block;
margin-top: 2px;
}
.action-btn {
padding: 5px 8px;
margin-right: 4px;
border: none;
border-radius: 5px;
cursor: pointer;
color: #fff;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.btn-resend {
background: #28a745;
}
.btn-log {
background: #17a2b8;
}
.btn-revert {
background: #fd7e14;
}
.pager-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 6px 14px;
font-size: 13px;
margin-bottom: 10px;
}
.pager-num,
.pager-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
padding: 0 6px;
border: 1px solid #ced4da;
border-radius: 4px;
text-decoration: none;
color: #495057;
margin-left: 4px;
}
.pager-num.active {
background: #0d6efd;
color: #fff;
border-color: #0d6efd;
}
</style>
<title>Export Errors - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="mb-3 d-flex align-items-center gap-2">
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success me-2">To LIMS (l)</a>
<a href="errors.php?id=<?= $template_id ?>" class="btn btn-danger">Errors (x)</a>
<span class="ms-3">
<label class="form-check-label" style="font-size:13px; cursor:pointer;">
<?php $tp = $_GET;
unset($tp['all_users']);
$toggleBase = 'errors.php?' . http_build_query($tp); ?>
<input type="checkbox" class="form-check-input" <?= $show_all_users ? 'checked' : '' ?>
onchange="window.location.href='<?= htmlspecialchars($toggleBase) ?>' + (this.checked ? '&all_users=1' : '')">
Show all users
</label>
</span>
<span class="text-muted" style="font-size:12px;">(<?= count($rowsData) ?> of <?= $totalRows ?> errori<?= !$show_all_users ? ' — my records' : '' ?>)</span>
</div>
<?php if ($totalPages > 1):
$pageQuery = $_GET;
unset($pageQuery['page']);
$pageBase = 'errors.php?' . http_build_query($pageQuery); ?>
<div class="pager-bar">
<span>Pagina <?= $page ?> di <?= $totalPages ?> — <?= $totalRows ?> righe</span>
<span>
<?php for ($p = 1; $p <= $totalPages; $p++): ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $p) ?>" class="pager-num <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
<?php endfor; ?>
</span>
</div>
<?php endif; ?>
<div class="card radius-10">
<div class="card-header">
<span style="font-weight:600;"><i class="fas fa-triangle-exclamation" style="color:#dc3545;"></i> Export falliti verso il LIMS</span>
</div>
<div class="card-body">
<?php if (empty($rowsData)): ?>
<p class="text-muted mb-0">Nessun errore di export. 🎉</p>
<?php else: ?>
<div style="overflow-x:auto;">
<table class="err-table">
<thead>
<tr>
<th style="width:170px;">Azioni</th>
<th>Status</th>
<th>Client</th>
<th>Errore</th>
<th style="width:150px;">Data export</th>
<th>File import</th>
</tr>
</thead>
<tbody>
<?php foreach ($rowsData as $row):
$errMsg = extractErrorSummary($logDir, $row['log_file'] ?? null); ?>
<tr data-id="<?= (int)$row['iddatadb'] ?>">
<td>
<button type="button" class="action-btn btn-resend resend-btn" data-iddatadb="<?= (int)$row['iddatadb'] ?>" title="Reinvia al LIMS"><i class="fas fa-paper-plane"></i></button>
<?php if (!empty($row['log_file'])): ?>
<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>
</td>
<td>
<span class="status-badge status-x">Error</span>
<?php if (!empty($row['commessaweb'])): ?>
<span class="commessa-code"><?= htmlspecialchars($row['commessaweb']) ?></span>
<?php endif; ?>
</td>
<td><span class="client-text" data-client-id="<?= htmlspecialchars((string)($row['idclient'] ?? '')) ?>"><?= htmlspecialchars((string)($row['idclient'] ?? '')) ?></span></td>
<td class="err-msg"><?= htmlspecialchars($errMsg ?: '—') ?></td>
<td><?= htmlspecialchars((string)($row['export_date'] ?? '')) ?></td>
<td><?php if (!empty($row['filename_import'])): ?><a href="imported_trf/<?= htmlspecialchars($row['filename_import']) ?>" target="_blank">File</a><?php endif; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(function() {
// Risoluzione nomi cliente (come in tolims.php)
(function() {
const ids = new Set();
document.querySelectorAll('.client-text').forEach(el => {
const id = el.dataset.clientId;
if (id) ids.add(id);
});
ids.forEach(id => {
fetch('search_clienti.php?id=' + encodeURIComponent(id))
.then(r => r.json())
.then(j => {
const it = (j.results || [])[0];
if (it) document.querySelectorAll('.client-text[data-client-id="' + id + '"]').forEach(el => el.textContent = it.text);
})
.catch(() => {});
});
})();
// Reinvia: richiama export_to_lims.php da capo (create incluso)
$(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;
const original = $btn.html();
$btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
$.ajax({
url: 'export_to_lims.php',
method: 'POST',
dataType: 'json',
data: {
iddatadb: iddatadb
},
success: function(res) {
if (res && res.success) {
const $tr = $('tr[data-id="' + iddatadb + '"]');
$tr.css('background', '#d4edda');
setTimeout(() => $tr.fadeOut(300, () => $tr.remove()), 500);
} else {
alert('Ancora errore: ' + ((res && res.message) ? res.message : 'invio non riuscito'));
$btn.prop('disabled', false).html(original);
}
},
error: function() {
alert('Errore di rete durante il reinvio.');
$btn.prop('disabled', false).html(original);
}
});
});
// Revert a Imported (i)
$(document).on('click', '.revert-btn', function() {
const $btn = $(this);
const iddatadb = $btn.data('iddatadb');
if (!confirm('Riportare questo record a Imported (i)?')) return;
$.ajax({
url: 'revert_status.php',
method: 'POST',
dataType: 'json',
data: {
iddatadb: iddatadb,
status: 'i'
},
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 : 'operazione non riuscita'));
}
},
error: function() {
alert('Errore di rete.');
}
});
});
});
</script>
</body>
</html>
+60 -24
View File
@@ -55,22 +55,29 @@ $apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
// 🔹 Batch UUID — if present, all logs go to a single file
$batchUuid = $_POST['batch_uuid'] ?? null;
$writeLog = (function () use ($batchUuid, $logDir) {
$batchLogFile = $batchUuid ? $logDir . "batch_export_{$batchUuid}.log" : null;
// File unico per questa commessa (nominato per iddatadb, che conosciamo sempre)
$iddatadbForLog = $_POST['iddatadb'] ?? 'unknown';
$commessaLogFile = $logDir . "commessa_{$iddatadbForLog}_" . time() . ".log";
return function ($individualPath, $content, $stepLabel = null) use ($batchLogFile) {
if ($batchLogFile) {
$header = "\n" . str_repeat("=", 60) . "\n";
if ($stepLabel) {
$header .= "[{$stepLabel}] " . date('Y-m-d H:i:s') . "\n";
}
$header .= str_repeat("=", 60) . "\n";
file_put_contents($batchLogFile, $header . $content . "\n", FILE_APPEND);
} else {
file_put_contents($individualPath, $content);
}
};
})();
// File di lotto: resta come INDICE (quali commesse nel lotto), non più tutti gli step
$batchIndexFile = $batchUuid ? $logDir . "batch_export_{$batchUuid}.log" : null;
// $writeLog ora appende sempre sul file unico della commessa, con header di step
$writeLog = function ($individualPath, $content, $stepLabel = null) use ($commessaLogFile) {
$header = "\n" . str_repeat("=", 60) . "\n";
if ($stepLabel) {
$header .= "[{$stepLabel}] " . date('Y-m-d H:i:s') . "\n";
}
$header .= str_repeat("=", 60) . "\n";
file_put_contents($commessaLogFile, $header . $content . "\n", FILE_APPEND);
};
// Helper per registrare una riga nell'indice di lotto
$writeBatchIndex = function ($line) use ($batchIndexFile) {
if ($batchIndexFile) {
file_put_contents($batchIndexFile, "[" . date('Y-m-d H:i:s') . "] " . $line . "\n", FILE_APPEND);
}
};
// 🔹 Funzione per validare e convertire date
function validateDate($value)
@@ -693,16 +700,20 @@ try {
}
}
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, status and export date
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, log_file and export date
// NB: lo status 'l' viene impostato SOLO dopo invio+import ok (vedi più sotto),
// così una riga rimasta indietro non risulta "To LIMS" per errore.
$stmt = $pdo->prepare("
UPDATE datadb
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l', export_date = NOW()
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb,
log_file = :log_file, export_date = NOW()
WHERE iddatadb = :iddatadb
");
$stmt->execute([
'idcommessaweb' => $commessaId,
'commessaweb' => $commessaWebCode,
'iddatadb' => $iddatadb
'commessaweb' => $commessaWebCode,
'log_file' => basename($commessaLogFile),
'iddatadb' => $iddatadb
]);
// 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto)
@@ -770,9 +781,12 @@ try {
");
$stmt->execute([
'commessaweb' => substr($finalCodiceCommessa, 0, 30),
'iddatadb' => $iddatadb
'iddatadb' => $iddatadb
]);
}
// Indice di lotto: esito OK di questa commessa
$writeBatchIndex("OK iddatadb={$iddatadb} commessa={$commessaId} codice={$finalCodiceCommessa} log=" . basename($commessaLogFile));
// 🔹 STEP 11: Prepare final response
$finalCommessa = [
"Cliente" => $clienteId,
@@ -818,10 +832,32 @@ try {
$errContent .= "MESSAGE: " . $e->getMessage() . "\n\n";
$errContent .= "TRACE:\n" . $e->getTraceAsString() . "\n";
$errName = $errCommessaId
? "commessa_{$errCommessaId}_ERROR_{$errIddatadb}_" . time() . ".txt"
: "commessa_ERROR_{$errIddatadb}_" . time() . ".txt";
@file_put_contents($logDir . $errName, $errContent);
// Appendi l'errore in coda al file unico della commessa (se esiste),
// così resta tutto in un posto solo.
if (isset($commessaLogFile)) {
$header = "\n" . str_repeat("=", 60) . "\n[ERROR] " . date('Y-m-d H:i:s') . "\n" . str_repeat("=", 60) . "\n";
@file_put_contents($commessaLogFile, $header . $errContent . "\n", FILE_APPEND);
$errFileToSave = basename($commessaLogFile);
} else {
// fallback: file d'errore dedicato
$errName = "commessa_ERROR_{$errIddatadb}_" . time() . ".txt";
@file_put_contents($logDir . $errName, $errContent);
$errFileToSave = $errName;
}
// Segna la riga come errore ('x') e registra il log_file, senza lasciarla 'l'
if ($errIddatadb !== 'unknown') {
try {
$up = $pdo->prepare("UPDATE datadb SET status = 'x', log_file = :log_file WHERE iddatadb = :iddatadb");
$up->execute(['log_file' => $errFileToSave, 'iddatadb' => $errIddatadb]);
} catch (Exception $ignore) { /* non bloccare la risposta d'errore */ }
}
}
// Indice di lotto: esito KO di questa commessa
if (isset($writeBatchIndex)) {
$writeBatchIndex("FAIL iddatadb={$errIddatadb} commessa=" . ($errCommessaId ?? '-') . " log={$errFileToSave} :: " . $e->getMessage());
}
error_log("LIMS Export Error: " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
+1
View File
@@ -164,6 +164,7 @@ error_log("Loaded template: " . print_r($template, true));
<div class="mb-3 text">
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success">To LIMS (l)</a>
<a href="errors.php?id=<?= $id ?>" class="btn btn-danger">Errors (x)</a>
</div>
<div class="card radius-10">
<div class="card-header">
+2 -1
View File
@@ -1530,7 +1530,8 @@ $gridMeta = [
<div class="page-content">
<div class="mb-3 text d-flex align-items-center gap-2">
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success">To LIMS (l)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success me-2">To LIMS (l)</a>
<a href="errors.php?id=<?= $template_id ?>" class="btn btn-danger">Errors (x)</a>
<?php if ($importref === ''): ?>
<span class="ms-3">
<label class="form-check-label" style="font-size: 13px; cursor: pointer;">
+6 -4
View File
@@ -822,12 +822,12 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string
}
.action-btn {
padding: 6px 8px;
margin-right: 5px;
padding: 6px 4px;
margin-right: 3px;
border: none;
border-radius: 5px;
cursor: pointer;
width: 35px;
width: 26px;
box-sizing: border-box;
}
@@ -1243,7 +1243,8 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string
<div class="page-content">
<div class="mb-3 text d-flex align-items-center gap-2">
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success">To LIMS (l)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success me-2">To LIMS (l)</a>
<a href="errors.php?id=<?= $template_id ?>" class="btn btn-danger">Errors (x)</a>
<span class="ms-3">
<label class="form-check-label" style="font-size: 13px; cursor: pointer;">
<?php
@@ -1717,6 +1718,7 @@ function resolveFixedValue(string $key, $val, array $fixedLookup): string
<button type="button" class="photos-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" title="Photos" style="background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-camera"></i></button>
<button type="button" class="parts-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" title="Parts" style="background: #ffc107; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-puzzle-piece"></i></button>
<button type="button" class="revert-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" data-commessaweb="<?= htmlspecialchars($row['commessaweb'] ?? '', ENT_QUOTES) ?>" title="Riporta a Imported (i)" style="background: #fd7e14; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-rotate-left"></i></button>
<a href="download_logs.php?iddatadb=<?= (int)$row['iddatadb'] ?>" class="logs-btn action-btn" title="Scarica log LIMS" style="background:#17a2b8; color:white; border:none; border-radius:5px; cursor:pointer; display:inline-flex; align-items:center; justify-content:center; text-decoration:none;"><i class="fas fa-file-download"></i></a>
</div>
<?php if ($mainFieldMapping):
$detail = array_filter($manualDetails, fn($d) => $d['mapping_id'] == $mainFieldMapping['id'] && $d['datadb_id'] == $row['iddatadb']);