546 lines
22 KiB
PHP
546 lines
22 KiB
PHP
<?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;
|
|
}
|
|
|
|
.stat-card-extra {
|
|
background: linear-gradient(135deg, #021bff 0%, #010983 100%);
|
|
border: none;
|
|
}
|
|
|
|
.stat-card-extra .stat-label {
|
|
color: #d7d2f5;
|
|
}
|
|
|
|
.stat-card-extra .stat-value {
|
|
color: #ffffff !important;
|
|
}
|
|
|
|
.stat-card-extra small.text-muted {
|
|
color: #c9c2ec !important;
|
|
}
|
|
|
|
.kpi-row {
|
|
margin-bottom: 1rem;
|
|
}
|
|
</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>
|
|
<a href="stats_export_lims_monthly.php" class="btn btn-outline-primary btn-sm ms-auto">
|
|
<i class="bx bx-calendar-alt"></i> Vista mensile
|
|
</a>
|
|
<button class="btn btn-primary btn-sm ms-2" id="btnRefresh">
|
|
<i class="bx bx-refresh"></i> Aggiorna
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== KPI ===== -->
|
|
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-5 g-3 kpi-row">
|
|
<div class="col">
|
|
<div class="card radius-10 stat-card stat-card-extra h-100">
|
|
<div class="card-body">
|
|
<div class="stat-label">Campioni mese <span id="kpiMonthLabel"></span></div>
|
|
<div class="stat-value" id="kpiPartsMonth">0</div>
|
|
<small class="text-muted">Commesse: <span id="kpiExportMonth">0</span> · dal 16/07/2026</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card radius-10 stat-card stat-card-extra h-100">
|
|
<div class="card-body">
|
|
<div class="stat-label">Campioni anno <span id="kpiYearLabel"></span></div>
|
|
<div class="stat-value" id="kpiPartsYear">0</div>
|
|
<small class="text-muted">Commesse: <span id="kpiExportYear">0</span> · dal 16/07/2026</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card radius-10 stat-card h-100">
|
|
<div class="card-body">
|
|
<div class="stat-label">Campioni esportati (giorno)</div>
|
|
<div class="stat-value text-primary" id="kpiParts">0</div>
|
|
<small class="text-muted">Commesse: <span id="kpiExport">0</span></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card radius-10 stat-card h-100">
|
|
<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">
|
|
<div class="card radius-10 stat-card h-100">
|
|
<div class="card-body">
|
|
<div class="stat-label">% Esportati / inseriti</div>
|
|
<div class="stat-value text-success" id="kpiPerc">—</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>Campioni</th>
|
|
<th>Commesse</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>Campioni</th>
|
|
<th>Commesse</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: 'parts',
|
|
className: 'text-end'
|
|
},
|
|
{
|
|
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: 'parts',
|
|
className: 'text-end'
|
|
},
|
|
{
|
|
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 — dato principale = parti/campioni, sotto le commesse
|
|
$('#kpiParts').text(res.totals.parts ?? 0);
|
|
$('#kpiExport').text(res.totals.export ?? 0);
|
|
$('#kpiPartsMonth').text(res.totalsMonth ? (res.totalsMonth.parts ?? 0) : 0);
|
|
$('#kpiExportMonth').text(res.totalsMonth ? (res.totalsMonth.export ?? 0) : 0);
|
|
$('#kpiPartsYear').text(res.totalsYear ? (res.totalsYear.parts ?? 0) : 0);
|
|
$('#kpiExportYear').text(res.totalsYear ? (res.totalsYear.export ?? 0) : 0);
|
|
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
|
|
$('#limsSamplesInput').val(res.limsSamples ?? '');
|
|
|
|
// Etichette mese/anno dei box totali
|
|
if (res.month) {
|
|
const MESI_IT = ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno',
|
|
'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'
|
|
];
|
|
const mParts = res.month.split('-');
|
|
const mIdx = parseInt(mParts[1], 10) - 1;
|
|
if (mIdx >= 0 && mIdx < 12) {
|
|
$('#kpiMonthLabel').text('(' + MESI_IT[mIdx] + ' ' + mParts[0] + ')');
|
|
}
|
|
}
|
|
if (res.year) {
|
|
$('#kpiYearLabel').text('(' + res.year + ')');
|
|
}
|
|
|
|
const limsTot = parseInt(res.limsSamples ?? 0, 10);
|
|
const partsTot = parseInt(res.totals.parts ?? 0, 10);
|
|
if (limsTot > 0) {
|
|
const perc = (partsTot / limsTot) * 100;
|
|
$('#kpiPerc').text(perc.toFixed(1) + '%');
|
|
} else {
|
|
$('#kpiPerc').text('—');
|
|
}
|
|
|
|
// Torta parti esportate vs totale inserito nel LIMS
|
|
renderPie(partsTot, limsTot);
|
|
|
|
// Grafici — valori = campioni/parti
|
|
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.parts), 'Campioni');
|
|
chartUtenti = renderBarChart(chartUtenti, 'chartUtenti',
|
|
userTop.map(r => r.username ? (r.username + ' (' + r.user_id + ')') : ('ID ' + r.user_id)),
|
|
userTop.map(r => r.parts), 'Campioni');
|
|
|
|
// 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>
|