Pagina statistiche export LIMS
This commit is contained in:
@@ -675,10 +675,10 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, and status
|
||||
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, status and export date
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE datadb
|
||||
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l'
|
||||
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l', export_date = NOW()
|
||||
WHERE iddatadb = :iddatadb
|
||||
");
|
||||
$stmt->execute([
|
||||
|
||||
+409
-129
@@ -1,136 +1,416 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/class/db-functions.php';
|
||||
include dirname(__DIR__) . '/../extra/auth.php';
|
||||
<?php include('include/headscript.php'); ?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$q = mb_strtolower(trim($_GET['q'] ?? ''));
|
||||
$limit = max(1, min(50, intval($_GET['limit'] ?? 20)));
|
||||
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
|
||||
|
||||
$tipo = $_GET['tipo'] ?? ''; // 'attivo' | 'inattivo' | '' (nessun filtro)
|
||||
|
||||
function isInattivo(array $c): bool
|
||||
{
|
||||
$v = $c['Inattivo'] ?? false;
|
||||
return $v === true || $v === 1 || $v === '1' || $v === 'true';
|
||||
}
|
||||
|
||||
function formatClientLabel(array $client): string
|
||||
{
|
||||
$name = trim($client['Nominativo'] ?? '');
|
||||
$id = trim((string)($client['IdCliente'] ?? ''));
|
||||
$code = trim((string)($client['CodiceCliente'] ?? ''));
|
||||
|
||||
$parts = explode('_', $code);
|
||||
$suffix = trim($parts[1] ?? '');
|
||||
|
||||
if ($suffix === '' && $code !== '') {
|
||||
$suffix = substr($code, 0, 1);
|
||||
}
|
||||
|
||||
if ($suffix === '') {
|
||||
$suffix = '--';
|
||||
}
|
||||
|
||||
return $name . ' - ' . $suffix . ' - ' . $id;
|
||||
}
|
||||
|
||||
try {
|
||||
// Load from cache or API
|
||||
$cacheFile = __DIR__ . '/cache/clienti.json';
|
||||
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
} else {
|
||||
$api = VisualLimsApiClient::getInstance();
|
||||
|
||||
$params = [
|
||||
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
|
||||
'$orderby' => 'Nominativo asc'
|
||||
];
|
||||
|
||||
$data = $api->get("Cliente?" . http_build_query($params));
|
||||
|
||||
if (!is_dir(__DIR__ . '/cache')) {
|
||||
mkdir(__DIR__ . '/cache', 0777, true);
|
||||
<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'); ?>
|
||||
<title>TRF-Project - Statistiche Export LIMS</title>
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
<style>
|
||||
.stat-card .card-body {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
file_put_contents($cacheFile, json_encode($data));
|
||||
}
|
||||
|
||||
$clients = $data['value'] ?? [];
|
||||
|
||||
// Filtro tipo, applicato SOLO alla ricerca, non al lookup per id
|
||||
$searchClients = $clients;
|
||||
if ($tipo === 'attivo') {
|
||||
$searchClients = array_values(array_filter($clients, fn($c) => !isInattivo($c)));
|
||||
} elseif ($tipo === 'inattivo') {
|
||||
$searchClients = array_values(array_filter($clients, fn($c) => isInattivo($c)));
|
||||
}
|
||||
|
||||
// If requesting by specific ID, used for loading selected value
|
||||
if ($id !== null) {
|
||||
foreach ($clients as $c) {
|
||||
if ((int)$c['IdCliente'] === $id) {
|
||||
echo json_encode([
|
||||
'results' => [[
|
||||
'id' => $c['IdCliente'],
|
||||
'text' => formatClientLabel($c),
|
||||
'IdCliente' => $c['IdCliente'],
|
||||
'Nominativo' => trim($c['Nominativo'] ?? ''),
|
||||
'CodiceCliente' => trim($c['CodiceCliente'] ?? '')
|
||||
]]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
echo json_encode(['results' => []]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Search by query
|
||||
$results = [];
|
||||
|
||||
foreach ($searchClients as $c) {
|
||||
$name = trim($c['Nominativo'] ?? '');
|
||||
$code = trim($c['CodiceCliente'] ?? '');
|
||||
|
||||
// Cerca solo su ciò che viene visualizzato: Nome - Lettera - ID
|
||||
$label = formatClientLabel($c);
|
||||
|
||||
if (
|
||||
$q === '' ||
|
||||
mb_strpos(mb_strtolower($label), $q) !== false
|
||||
) {
|
||||
$results[] = [
|
||||
'id' => $c['IdCliente'],
|
||||
'text' => $label,
|
||||
'IdCliente' => $c['IdCliente'],
|
||||
'Nominativo' => $name,
|
||||
'CodiceCliente' => $code
|
||||
];
|
||||
|
||||
if (count($results) >= $limit) {
|
||||
break;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .03em;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['results' => $results]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
.chart-wrap {
|
||||
position: relative;
|
||||
height: 420px;
|
||||
}
|
||||
|
||||
#tblClienti,
|
||||
#tblUtenti {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#tblClienti th,
|
||||
#tblClienti td,
|
||||
#tblUtenti th,
|
||||
#tblUtenti td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
gap: .75rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<!-- ===== FILTRO GIORNATA ===== -->
|
||||
<div class="card radius-10">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center flex-wrap filter-bar">
|
||||
<h6 class="mb-0 me-3"><i class="bx bx-bar-chart-alt-2"></i> Statistiche Export LIMS</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
<label for="filterDate" class="me-2 mb-0 fw-semibold">Giornata:</label>
|
||||
<input type="date" id="filterDate" class="form-control form-control-sm" style="width:auto;">
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm ms-2" role="group">
|
||||
<button class="btn btn-outline-secondary" id="btnToday">Oggi</button>
|
||||
<button class="btn btn-outline-secondary" id="btnYesterday">Ieri</button>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm ms-auto" id="btnRefresh">
|
||||
<i class="bx bx-refresh"></i> Aggiorna
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== KPI ===== -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-3 col-md-6">
|
||||
<div class="card radius-10 stat-card">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Export (status L)</div>
|
||||
<div class="stat-value text-primary" id="kpiExport">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 col-md-6">
|
||||
<div class="card radius-10 stat-card">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Campioni inseriti LIMS</div>
|
||||
<div class="stat-value text-info" id="kpiLimsSamples">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 col-md-6">
|
||||
<div class="card radius-10 stat-card">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">% Esportati / inseriti</div>
|
||||
<div class="stat-value text-success" id="kpiPerc">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 col-md-6">
|
||||
<div class="card radius-10 stat-card">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Clienti distinti</div>
|
||||
<div class="stat-value text-warning" id="kpiClienti">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== DATO GIORNALIERO CAMPIONI LIMS ===== -->
|
||||
<div class="card radius-10">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center flex-wrap" style="gap:.75rem;">
|
||||
<span class="fw-semibold">Campioni totali inseriti nel LIMS per questa giornata:</span>
|
||||
<input type="number" min="0" id="limsSamplesInput" class="form-control form-control-sm" style="width:140px;" placeholder="0">
|
||||
<button class="btn btn-success btn-sm" id="btnSaveLimsSamples">
|
||||
<i class="bx bx-save"></i> Salva dato
|
||||
</button>
|
||||
<small class="text-muted">Un valore al giorno. Usato per calcolare la % di export.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== GRAFICI ===== -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Export per cliente (idclient) <small class="text-muted">— top 15</small></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-wrap"><canvas id="chartClienti"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Export per utente (user_id) <small class="text-muted">— top 15</small></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-wrap"><canvas id="chartUtenti"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== LISTE ===== -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Dettaglio per cliente</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="tblClienti" class="table table-striped table-bordered table-sm w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Cliente</th>
|
||||
<th>Cliente</th>
|
||||
<th>Export</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Dettaglio per utente</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="tblUtenti" class="table table-striped table-bordered table-sm w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Utente</th>
|
||||
<th>Utente</th>
|
||||
<th>Export</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
|
||||
<script>
|
||||
let chartClienti = null;
|
||||
let chartUtenti = null;
|
||||
let dtClienti = null;
|
||||
let dtUtenti = null;
|
||||
|
||||
function todayStr() {
|
||||
const d = new Date();
|
||||
const off = d.getTimezoneOffset();
|
||||
return new Date(d.getTime() - off * 60000).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function initTables() {
|
||||
dtClienti = $('#tblClienti').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
order: [
|
||||
[2, 'desc']
|
||||
],
|
||||
columns: [{
|
||||
data: 'idclient'
|
||||
},
|
||||
{
|
||||
data: 'clientname',
|
||||
defaultContent: '—'
|
||||
},
|
||||
{
|
||||
data: 'tot',
|
||||
className: 'text-end'
|
||||
}
|
||||
],
|
||||
language: {
|
||||
emptyTable: 'Nessun export in questa giornata'
|
||||
}
|
||||
});
|
||||
dtUtenti = $('#tblUtenti').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
order: [
|
||||
[2, 'desc']
|
||||
],
|
||||
columns: [{
|
||||
data: 'user_id'
|
||||
},
|
||||
{
|
||||
data: 'username',
|
||||
defaultContent: '—'
|
||||
},
|
||||
{
|
||||
data: 'tot',
|
||||
className: 'text-end'
|
||||
}
|
||||
],
|
||||
language: {
|
||||
emptyTable: 'Nessun export in questa giornata'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderBarChart(existing, canvasId, labels, values, label) {
|
||||
if (existing) existing.destroy();
|
||||
const ctx = document.getElementById(canvasId).getContext('2d');
|
||||
return new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: label,
|
||||
data: values,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
precision: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Prende i primi N record (già ordinati desc dal backend) per il grafico
|
||||
const CHART_TOP_N = 15;
|
||||
|
||||
function topN(arr) {
|
||||
return arr.slice(0, CHART_TOP_N);
|
||||
}
|
||||
|
||||
function loadStats() {
|
||||
const date = $('#filterDate').val() || todayStr();
|
||||
|
||||
$.getJSON('stats_export_lims_data.php', {
|
||||
date: date
|
||||
}, function(res) {
|
||||
if (!res || !res.success) {
|
||||
Swal.fire('Errore', (res && res.message) || 'Errore nel caricamento', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// KPI
|
||||
$('#kpiExport').text(res.totals.export);
|
||||
$('#kpiClienti').text(res.byClient.length);
|
||||
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
|
||||
$('#limsSamplesInput').val(res.limsSamples ?? '');
|
||||
|
||||
const limsTot = parseInt(res.limsSamples ?? 0, 10);
|
||||
if (limsTot > 0) {
|
||||
const perc = (res.totals.export / limsTot) * 100;
|
||||
$('#kpiPerc').text(perc.toFixed(1) + '%');
|
||||
} else {
|
||||
$('#kpiPerc').text('—');
|
||||
}
|
||||
|
||||
// Grafici
|
||||
const clientTop = topN(res.byClient);
|
||||
const userTop = topN(res.byUser);
|
||||
chartClienti = renderBarChart(chartClienti, 'chartClienti',
|
||||
clientTop.map(r => r.clientname ? (r.clientname + ' (' + r.idclient + ')') : ('ID ' + r.idclient)),
|
||||
clientTop.map(r => r.tot), 'Export');
|
||||
chartUtenti = renderBarChart(chartUtenti, 'chartUtenti',
|
||||
userTop.map(r => r.username ? (r.username + ' (' + r.user_id + ')') : ('ID ' + r.user_id)),
|
||||
userTop.map(r => r.tot), 'Export');
|
||||
|
||||
// Tabelle
|
||||
dtClienti.clear().rows.add(res.byClient).draw();
|
||||
dtUtenti.clear().rows.add(res.byUser).draw();
|
||||
}).fail(function(xhr) {
|
||||
Swal.fire('Errore', 'Richiesta fallita: ' + xhr.status, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#filterDate').val(todayStr());
|
||||
initTables();
|
||||
loadStats();
|
||||
|
||||
$('#btnRefresh').on('click', loadStats);
|
||||
$('#filterDate').on('change', loadStats);
|
||||
|
||||
$('#btnToday').on('click', function() {
|
||||
$('#filterDate').val(todayStr());
|
||||
loadStats();
|
||||
});
|
||||
$('#btnYesterday').on('click', function() {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - 1);
|
||||
const off = d.getTimezoneOffset();
|
||||
$('#filterDate').val(new Date(d.getTime() - off * 60000).toISOString().slice(0, 10));
|
||||
loadStats();
|
||||
});
|
||||
|
||||
$('#btnSaveLimsSamples').on('click', function() {
|
||||
const date = $('#filterDate').val() || todayStr();
|
||||
const total = parseInt($('#limsSamplesInput').val(), 10);
|
||||
if (isNaN(total) || total < 0) {
|
||||
Swal.fire('Attenzione', 'Inserisci un numero valido', 'warning');
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: 'stats_save_lims_samples.php',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
date: date,
|
||||
total_samples: total
|
||||
},
|
||||
success: function(res) {
|
||||
if (res && res.success) {
|
||||
Swal.fire({
|
||||
title: 'Salvato',
|
||||
icon: 'success',
|
||||
timer: 1200,
|
||||
showConfirmButton: false
|
||||
});
|
||||
loadStats();
|
||||
} else {
|
||||
Swal.fire('Errore', (res && res.message) || 'Salvataggio fallito', 'error');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
Swal.fire('Errore', 'Richiesta fallita: ' + xhr.status, 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,481 @@
|
||||
<?php include('include/headscript.php'); ?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<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'); ?>
|
||||
<title>TRF-Project - Statistiche Export LIMS</title>
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
<style>
|
||||
.stat-card .card-body {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .03em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.chart-wrap {
|
||||
position: relative;
|
||||
height: 420px;
|
||||
}
|
||||
|
||||
#tblClienti,
|
||||
#tblUtenti {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#tblClienti th,
|
||||
#tblClienti td,
|
||||
#tblUtenti th,
|
||||
#tblUtenti td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
gap: .75rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<!-- ===== FILTRO GIORNATA ===== -->
|
||||
<div class="card radius-10">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center flex-wrap filter-bar">
|
||||
<h6 class="mb-0 me-3"><i class="bx bx-bar-chart-alt-2"></i> Statistiche Export LIMS</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
<label for="filterDate" class="me-2 mb-0 fw-semibold">Giornata:</label>
|
||||
<input type="date" id="filterDate" class="form-control form-control-sm" style="width:auto;">
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm ms-2" role="group">
|
||||
<button class="btn btn-outline-secondary" id="btnToday">Oggi</button>
|
||||
<button class="btn btn-outline-secondary" id="btnYesterday">Ieri</button>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm ms-auto" id="btnRefresh">
|
||||
<i class="bx bx-refresh"></i> Aggiorna
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== KPI ===== -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-3 col-md-6">
|
||||
<div class="card radius-10 stat-card">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Export (status L)</div>
|
||||
<div class="stat-value text-primary" id="kpiExport">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 col-md-6">
|
||||
<div class="card radius-10 stat-card">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Campioni inseriti LIMS</div>
|
||||
<div class="stat-value text-info" id="kpiLimsSamples">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 col-md-6">
|
||||
<div class="card radius-10 stat-card">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">% Esportati / inseriti</div>
|
||||
<div class="stat-value text-success" id="kpiPerc">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 col-md-6">
|
||||
<div class="card radius-10 stat-card">
|
||||
<div class="card-body">
|
||||
<div class="stat-label">Clienti distinti</div>
|
||||
<div class="stat-value text-warning" id="kpiClienti">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== DATO GIORNALIERO CAMPIONI LIMS ===== -->
|
||||
<div class="card radius-10">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center flex-wrap" style="gap:.75rem;">
|
||||
<span class="fw-semibold">Campioni totali inseriti nel LIMS per questa giornata:</span>
|
||||
<input type="number" min="0" id="limsSamplesInput" class="form-control form-control-sm" style="width:140px;" placeholder="0">
|
||||
<button class="btn btn-success btn-sm" id="btnSaveLimsSamples">
|
||||
<i class="bx bx-save"></i> Salva dato
|
||||
</button>
|
||||
<small class="text-muted">Un valore al giorno. Usato per calcolare la % di export.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== GRAFICO TORTA ESPORTATI vs TOTALE LIMS ===== -->
|
||||
<div class="row" id="pieRow" style="display:none;">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Esportati vs totale inserito nel LIMS</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-wrap" style="height:340px;"><canvas id="chartPie"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== GRAFICI ===== -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Export per cliente (idclient) <small class="text-muted">— top 15</small></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-wrap"><canvas id="chartClienti"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Export per utente (user_id) <small class="text-muted">— top 15</small></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-wrap"><canvas id="chartUtenti"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== LISTE ===== -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Dettaglio per cliente</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="tblClienti" class="table table-striped table-bordered table-sm w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Cliente</th>
|
||||
<th>Cliente</th>
|
||||
<th>Export</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card radius-10">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Dettaglio per utente</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="tblUtenti" class="table table-striped table-bordered table-sm w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Utente</th>
|
||||
<th>Utente</th>
|
||||
<th>Export</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay toggle-icon"></div>
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
|
||||
<script>
|
||||
let chartClienti = null;
|
||||
let chartUtenti = null;
|
||||
let chartPie = null;
|
||||
let dtClienti = null;
|
||||
let dtUtenti = null;
|
||||
|
||||
function todayStr() {
|
||||
const d = new Date();
|
||||
const off = d.getTimezoneOffset();
|
||||
return new Date(d.getTime() - off * 60000).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function initTables() {
|
||||
dtClienti = $('#tblClienti').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
order: [
|
||||
[2, 'desc']
|
||||
],
|
||||
columns: [{
|
||||
data: 'idclient'
|
||||
},
|
||||
{
|
||||
data: 'clientname',
|
||||
defaultContent: '—'
|
||||
},
|
||||
{
|
||||
data: 'tot',
|
||||
className: 'text-end'
|
||||
}
|
||||
],
|
||||
language: {
|
||||
emptyTable: 'Nessun export in questa giornata'
|
||||
}
|
||||
});
|
||||
dtUtenti = $('#tblUtenti').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
info: false,
|
||||
order: [
|
||||
[2, 'desc']
|
||||
],
|
||||
columns: [{
|
||||
data: 'user_id'
|
||||
},
|
||||
{
|
||||
data: 'username',
|
||||
defaultContent: '—'
|
||||
},
|
||||
{
|
||||
data: 'tot',
|
||||
className: 'text-end'
|
||||
}
|
||||
],
|
||||
language: {
|
||||
emptyTable: 'Nessun export in questa giornata'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderBarChart(existing, canvasId, labels, values, label) {
|
||||
if (existing) existing.destroy();
|
||||
const ctx = document.getElementById(canvasId).getContext('2d');
|
||||
return new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: label,
|
||||
data: values,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
precision: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Torta: esportati vs non esportati (totale LIMS - esportati)
|
||||
function renderPie(exported, limsTotal) {
|
||||
const pieRow = document.getElementById('pieRow');
|
||||
// Mostra solo se il totale è inserito e valido
|
||||
if (!limsTotal || limsTotal <= 0) {
|
||||
pieRow.style.display = 'none';
|
||||
if (chartPie) {
|
||||
chartPie.destroy();
|
||||
chartPie = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
pieRow.style.display = '';
|
||||
|
||||
const notExported = Math.max(0, limsTotal - exported);
|
||||
if (chartPie) chartPie.destroy();
|
||||
const ctx = document.getElementById('chartPie').getContext('2d');
|
||||
chartPie = new Chart(ctx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Esportati', 'Totali LIMS'],
|
||||
datasets: [{
|
||||
data: [exported, notExported],
|
||||
backgroundColor: ['#198754', '#dee2e6'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(c) {
|
||||
const perc = limsTotal > 0 ? ((c.parsed / limsTotal) * 100).toFixed(1) : 0;
|
||||
return c.label + ': ' + c.parsed + ' (' + perc + '%)';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Prende i primi N record (già ordinati desc dal backend) per il grafico
|
||||
const CHART_TOP_N = 15;
|
||||
|
||||
function topN(arr) {
|
||||
return arr.slice(0, CHART_TOP_N);
|
||||
}
|
||||
|
||||
function loadStats() {
|
||||
const date = $('#filterDate').val() || todayStr();
|
||||
|
||||
$.getJSON('stats_export_lims_data.php', {
|
||||
date: date
|
||||
}, function(res) {
|
||||
if (!res || !res.success) {
|
||||
Swal.fire('Errore', (res && res.message) || 'Errore nel caricamento', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// KPI
|
||||
$('#kpiExport').text(res.totals.export);
|
||||
$('#kpiClienti').text(res.byClient.length);
|
||||
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
|
||||
$('#limsSamplesInput').val(res.limsSamples ?? '');
|
||||
|
||||
const limsTot = parseInt(res.limsSamples ?? 0, 10);
|
||||
if (limsTot > 0) {
|
||||
const perc = (res.totals.export / limsTot) * 100;
|
||||
$('#kpiPerc').text(perc.toFixed(1) + '%');
|
||||
} else {
|
||||
$('#kpiPerc').text('—');
|
||||
}
|
||||
|
||||
// Torta esportati vs totale
|
||||
renderPie(res.totals.export, limsTot);
|
||||
|
||||
// Grafici
|
||||
const clientTop = topN(res.byClient);
|
||||
const userTop = topN(res.byUser);
|
||||
chartClienti = renderBarChart(chartClienti, 'chartClienti',
|
||||
clientTop.map(r => r.clientname ? (r.clientname + ' (' + r.idclient + ')') : ('ID ' + r.idclient)),
|
||||
clientTop.map(r => r.tot), 'Export');
|
||||
chartUtenti = renderBarChart(chartUtenti, 'chartUtenti',
|
||||
userTop.map(r => r.username ? (r.username + ' (' + r.user_id + ')') : ('ID ' + r.user_id)),
|
||||
userTop.map(r => r.tot), 'Export');
|
||||
|
||||
// Tabelle
|
||||
dtClienti.clear().rows.add(res.byClient).draw();
|
||||
dtUtenti.clear().rows.add(res.byUser).draw();
|
||||
}).fail(function(xhr) {
|
||||
Swal.fire('Errore', 'Richiesta fallita: ' + xhr.status, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#filterDate').val(todayStr());
|
||||
initTables();
|
||||
loadStats();
|
||||
|
||||
$('#btnRefresh').on('click', loadStats);
|
||||
$('#filterDate').on('change', loadStats);
|
||||
|
||||
$('#btnToday').on('click', function() {
|
||||
$('#filterDate').val(todayStr());
|
||||
loadStats();
|
||||
});
|
||||
$('#btnYesterday').on('click', function() {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - 1);
|
||||
const off = d.getTimezoneOffset();
|
||||
$('#filterDate').val(new Date(d.getTime() - off * 60000).toISOString().slice(0, 10));
|
||||
loadStats();
|
||||
});
|
||||
|
||||
$('#btnSaveLimsSamples').on('click', function() {
|
||||
const date = $('#filterDate').val() || todayStr();
|
||||
const total = parseInt($('#limsSamplesInput').val(), 10);
|
||||
if (isNaN(total) || total < 0) {
|
||||
Swal.fire('Attenzione', 'Inserisci un numero valido', 'warning');
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: 'stats_save_lims_samples.php',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
date: date,
|
||||
total_samples: total
|
||||
},
|
||||
success: function(res) {
|
||||
if (res && res.success) {
|
||||
Swal.fire({
|
||||
title: 'Salvato',
|
||||
icon: 'success',
|
||||
timer: 1200,
|
||||
showConfirmButton: false
|
||||
});
|
||||
loadStats();
|
||||
} else {
|
||||
Swal.fire('Errore', (res && res.message) || 'Salvataggio fallito', 'error');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
Swal.fire('Errore', 'Richiesta fallita: ' + xhr.status, 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$dbHandler = DBHandlerSelect::getInstance();
|
||||
$pdo = $dbHandler->getConnection();
|
||||
|
||||
try {
|
||||
// Data richiesta (default oggi). Formato atteso: Y-m-d
|
||||
$date = $_GET['date'] ?? date('Y-m-d');
|
||||
$d = DateTime::createFromFormat('Y-m-d', $date);
|
||||
if (!$d || $d->format('Y-m-d') !== $date) {
|
||||
throw new Exception('Data non valida');
|
||||
}
|
||||
|
||||
// --- Export per cliente (status = 'l', giornata su export_date) ---
|
||||
$stmtC = $pdo->prepare("
|
||||
SELECT d.idclient AS idclient,
|
||||
COUNT(*) AS tot
|
||||
FROM datadb d
|
||||
WHERE d.status = 'l'
|
||||
AND DATE(d.export_date) = :d
|
||||
GROUP BY d.idclient
|
||||
ORDER BY tot DESC
|
||||
");
|
||||
$stmtC->execute(['d' => $date]);
|
||||
$byClient = $stmtC->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// --- Risolvi Nominativo cliente dalla cache locale (search_clienti.php) ---
|
||||
$clientNames = [];
|
||||
$cacheFile = __DIR__ . '/cache/clienti.json';
|
||||
if (is_file($cacheFile)) {
|
||||
$cache = json_decode(file_get_contents($cacheFile), true);
|
||||
foreach (($cache['value'] ?? []) as $c) {
|
||||
$clientNames[(int)($c['IdCliente'] ?? 0)] = trim($c['Nominativo'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Export per utente (status = 'l', giornata su export_date) ---
|
||||
$stmtU = $pdo->prepare("
|
||||
SELECT d.user_id AS user_id,
|
||||
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS username,
|
||||
COUNT(*) AS tot
|
||||
FROM datadb d
|
||||
LEFT JOIN auth_users u ON u.id = d.user_id
|
||||
WHERE d.status = 'l'
|
||||
AND DATE(d.export_date) = :d
|
||||
GROUP BY d.user_id, username
|
||||
ORDER BY tot DESC
|
||||
");
|
||||
$stmtU->execute(['d' => $date]);
|
||||
$byUser = $stmtU->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Normalizza tipi (COUNT torna stringa in PDO)
|
||||
foreach ($byClient as &$r) {
|
||||
$r['idclient'] = $r['idclient'] !== null ? (int)$r['idclient'] : null;
|
||||
$r['tot'] = (int)$r['tot'];
|
||||
$nm = $clientNames[$r['idclient']] ?? '';
|
||||
$r['clientname'] = $nm !== '' ? $nm : null;
|
||||
}
|
||||
unset($r);
|
||||
foreach ($byUser as &$r) {
|
||||
$r['user_id'] = $r['user_id'] !== null ? (int)$r['user_id'] : null;
|
||||
$r['tot'] = (int)$r['tot'];
|
||||
if (isset($r['username'])) $r['username'] = $r['username'] !== null && $r['username'] !== '' ? $r['username'] : null;
|
||||
}
|
||||
unset($r);
|
||||
|
||||
// --- Totale export della giornata ---
|
||||
$totExport = array_sum(array_column($byClient, 'tot'));
|
||||
|
||||
// --- Campioni totali inseriti nel LIMS per la giornata (tabella lims_daily_samples) ---
|
||||
$stmtL = $pdo->prepare("SELECT total_samples FROM lims_daily_samples WHERE sample_date = :d LIMIT 1");
|
||||
$stmtL->execute(['d' => $date]);
|
||||
$limsSamples = $stmtL->fetchColumn();
|
||||
$limsSamples = ($limsSamples !== false) ? (int)$limsSamples : null;
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'date' => $date,
|
||||
'totals' => ['export' => (int)$totExport],
|
||||
'byClient' => $byClient,
|
||||
'byUser' => $byUser,
|
||||
'limsSamples' => $limsSamples,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
error_log('Stats export LIMS error: ' . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Usa la connessione in scrittura se disponibile nel tuo progetto;
|
||||
// qui riuso il pattern del resto dell'app.
|
||||
$dbHandler = DBHandlerSelect::getInstance();
|
||||
$pdo = $dbHandler->getConnection();
|
||||
|
||||
try {
|
||||
$date = $_POST['date'] ?? null;
|
||||
$total = $_POST['total_samples'] ?? null;
|
||||
|
||||
$d = $date ? DateTime::createFromFormat('Y-m-d', $date) : null;
|
||||
if (!$d || $d->format('Y-m-d') !== $date) {
|
||||
throw new Exception('Data non valida');
|
||||
}
|
||||
if (!is_numeric($total) || (int)$total < 0) {
|
||||
throw new Exception('Valore campioni non valido');
|
||||
}
|
||||
$total = (int)$total;
|
||||
|
||||
// Upsert: un solo record per giornata (unique su sample_date)
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO lims_daily_samples (sample_date, total_samples)
|
||||
VALUES (:d, :t)
|
||||
ON DUPLICATE KEY UPDATE total_samples = VALUES(total_samples)
|
||||
");
|
||||
$stmt->execute(['d' => $date, 't' => $total]);
|
||||
|
||||
echo json_encode(['success' => true, 'date' => $date, 'total_samples' => $total]);
|
||||
} catch (Exception $e) {
|
||||
error_log('Save LIMS samples error: ' . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
Reference in New Issue
Block a user