Pagina statistiche export LIMS
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddExportDateToDatadb extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Aggiunge export_date su datadb: data/ora di esportazione verso il LIMS,
|
||||||
|
* valorizzata insieme al numero commessa quando status passa a 'l'.
|
||||||
|
*/
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$this->table('datadb')
|
||||||
|
->addColumn('export_date', 'datetime', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'commessaweb',
|
||||||
|
'comment' => 'Data/ora esportazione verso LIMS (valorizzata quando status = l)',
|
||||||
|
])
|
||||||
|
->addIndex(['export_date'], ['name' => 'idx_datadb_export_date'])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateLimsDailySamplesTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Tabella con un dato al giorno: numero totale di campioni inseriti
|
||||||
|
* nel LIMS (loro software) in quella giornata. Serve per calcolare
|
||||||
|
* la % di campioni esportati sul totale inserito nel LIMS.
|
||||||
|
*/
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('lims_daily_samples', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'comment' => 'Totale campioni inseriti nel LIMS per giornata',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('sample_date', 'date', [
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'Giornata di riferimento',
|
||||||
|
])
|
||||||
|
->addColumn('total_samples', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 0,
|
||||||
|
'comment' => 'Totale campioni inseriti nel LIMS in quella giornata',
|
||||||
|
])
|
||||||
|
->addColumn('note', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['sample_date'], [
|
||||||
|
'unique' => true,
|
||||||
|
'name' => 'uq_lims_daily_samples_date',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE datadb
|
UPDATE datadb
|
||||||
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l'
|
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l', export_date = NOW()
|
||||||
WHERE iddatadb = :iddatadb
|
WHERE iddatadb = :iddatadb
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
|
|||||||
+409
-129
@@ -1,136 +1,416 @@
|
|||||||
<?php
|
<?php include('include/headscript.php'); ?>
|
||||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
<!doctype html>
|
||||||
require_once __DIR__ . '/class/db-functions.php';
|
<html lang="en">
|
||||||
include dirname(__DIR__) . '/../extra/auth.php';
|
|
||||||
|
|
||||||
if (!Auth::check()) {
|
<head>
|
||||||
http_response_code(401);
|
<meta charset="utf-8">
|
||||||
echo json_encode(['error' => 'Unauthorized']);
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
exit;
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
}
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<title>TRF-Project - Statistiche Export LIMS</title>
|
||||||
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||||
|
<style>
|
||||||
header('Content-Type: application/json');
|
.stat-card .card-body {
|
||||||
ini_set('display_errors', '0');
|
padding: 1rem 1.25rem;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
file_put_contents($cacheFile, json_encode($data));
|
.stat-value {
|
||||||
}
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
$clients = $data['value'] ?? [];
|
line-height: 1.1;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['results' => []]);
|
.stat-label {
|
||||||
exit;
|
font-size: 0.8rem;
|
||||||
}
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .03em;
|
||||||
// Search by query
|
color: #6c757d;
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode(['results' => $results]);
|
.chart-wrap {
|
||||||
} catch (Exception $e) {
|
position: relative;
|
||||||
http_response_code(500);
|
height: 420px;
|
||||||
echo json_encode(['error' => $e->getMessage()]);
|
}
|
||||||
}
|
|
||||||
|
#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