update stats e nota template
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddTemplateNoteToExcelTemplates extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$this->table('excel_templates')
|
||||||
|
->addColumn('template_note', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'idroutine',
|
||||||
|
'comment' => 'Nota opzionale del template, aggiunta in fondo alle note in esportazione',
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -265,7 +265,11 @@ if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonText
|
|||||||
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
|
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
|
||||||
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description'] ?? ''); ?></textarea>
|
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description'] ?? ''); ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Template Note</label>
|
||||||
|
<textarea name="template_note" class="form-control" rows="3"><?php echo htmlspecialchars($template['template_note'] ?? ''); ?></textarea>
|
||||||
|
<small class="text-muted">If set, this note will be appended at the end of the notes during export.</small>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label"><?= htmlspecialchars($desttable, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
<label class="form-label"><?= htmlspecialchars($desttable, ENT_QUOTES, 'UTF-8'); ?> *</label>
|
||||||
<input type="text" name="target_table" class="form-control" value="<?php echo htmlspecialchars($template['target_table'] ?? 'datadb'); ?>" readonly required>
|
<input type="text" name="target_table" class="form-control" value="<?php echo htmlspecialchars($template['target_table'] ?? 'datadb'); ?>" readonly required>
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ try {
|
|||||||
// Also fetch fixed fields stored in datadb
|
// Also fetch fixed fields stored in datadb
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT d.idclient AS clienteId, et.idschema AS schemaId, et.id AS templateId,
|
SELECT d.idclient AS clienteId, et.idschema AS schemaId, et.id AS templateId,
|
||||||
|
et.template_note,
|
||||||
d.cliente_responsabile_id,
|
d.cliente_responsabile_id,
|
||||||
d.moltiplicatore_prezzo_id,
|
d.moltiplicatore_prezzo_id,
|
||||||
d.anagrafica_certest_object_id,
|
d.anagrafica_certest_object_id,
|
||||||
@@ -280,6 +281,14 @@ try {
|
|||||||
|
|
||||||
$addnoteText = implode(' - ', $addnoteChunks);
|
$addnoteText = implode(' - ', $addnoteChunks);
|
||||||
|
|
||||||
|
// Nota del template: appesa in fondo alle note in esportazione
|
||||||
|
$templateNote = trim((string)($result['template_note'] ?? ''));
|
||||||
|
if ($templateNote !== '') {
|
||||||
|
$addnoteText = $addnoteText !== ''
|
||||||
|
? $addnoteText . ' - ' . $templateNote
|
||||||
|
: $templateNote;
|
||||||
|
}
|
||||||
|
|
||||||
// Logga i fieldValues in error_log
|
// Logga i fieldValues in error_log
|
||||||
$logFieldValues = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT);
|
$logFieldValues = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT);
|
||||||
error_log($logFieldValues);
|
error_log($logFieldValues);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ try {
|
|||||||
$button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff');
|
$button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff');
|
||||||
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
|
$button_text_color = trim($_POST['button_text_color'] ?? '#ffffff');
|
||||||
$button_label = trim($_POST['button_label'] ?? 'Click Me');
|
$button_label = trim($_POST['button_label'] ?? 'Click Me');
|
||||||
|
$template_note = trim($_POST['template_note'] ?? '');
|
||||||
|
|
||||||
// Allowed source types
|
// Allowed source types
|
||||||
if (!in_array($source_type, ['XLS', 'API', 'JSON', 'PDF'], true)) {
|
if (!in_array($source_type, ['XLS', 'API', 'JSON', 'PDF'], true)) {
|
||||||
@@ -122,6 +123,7 @@ try {
|
|||||||
button_bg_color = ?,
|
button_bg_color = ?,
|
||||||
button_text_color = ?,
|
button_text_color = ?,
|
||||||
button_label = ?,
|
button_label = ?,
|
||||||
|
template_note = ?,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
");
|
");
|
||||||
@@ -144,6 +146,7 @@ try {
|
|||||||
$button_bg_color,
|
$button_bg_color,
|
||||||
$button_text_color,
|
$button_text_color,
|
||||||
$button_label,
|
$button_label,
|
||||||
|
$template_note,
|
||||||
$id
|
$id
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,27 @@
|
|||||||
.filter-bar {
|
.filter-bar {
|
||||||
gap: .75rem;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -71,7 +92,10 @@
|
|||||||
<button class="btn btn-outline-secondary" id="btnToday">Oggi</button>
|
<button class="btn btn-outline-secondary" id="btnToday">Oggi</button>
|
||||||
<button class="btn btn-outline-secondary" id="btnYesterday">Ieri</button>
|
<button class="btn btn-outline-secondary" id="btnYesterday">Ieri</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary btn-sm ms-auto" id="btnRefresh">
|
<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
|
<i class="bx bx-refresh"></i> Aggiorna
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,33 +103,51 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== KPI ===== -->
|
<!-- ===== KPI ===== -->
|
||||||
<div class="row">
|
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3 kpi-row">
|
||||||
<div class="col-12 col-lg-3 col-md-6">
|
<div class="col">
|
||||||
<div class="card radius-10 stat-card">
|
<div class="card radius-10 stat-card stat-card-extra h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="stat-label">Export (status L)</div>
|
<div class="stat-label">Export mese <span id="kpiMonthLabel"></span></div>
|
||||||
|
<div class="stat-value text-primary" id="kpiExportMonth">0</div>
|
||||||
|
<small class="text-muted">Dati 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">Export anno <span id="kpiYearLabel"></span></div>
|
||||||
|
<div class="stat-value text-dark" id="kpiExportYear">0</div>
|
||||||
|
<small class="text-muted">Dati 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">Export giorno (status L)</div>
|
||||||
<div class="stat-value text-primary" id="kpiExport">0</div>
|
<div class="stat-value text-primary" id="kpiExport">0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-3 col-md-6">
|
<div class="col">
|
||||||
<div class="card radius-10 stat-card">
|
<div class="card radius-10 stat-card h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="stat-label">Campioni inseriti LIMS</div>
|
<div class="stat-label">Campioni inseriti LIMS</div>
|
||||||
<div class="stat-value text-info" id="kpiLimsSamples">0</div>
|
<div class="stat-value text-info" id="kpiLimsSamples">0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-3 col-md-6">
|
<div class="col">
|
||||||
<div class="card radius-10 stat-card">
|
<div class="card radius-10 stat-card h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="stat-label">% Esportati / inseriti</div>
|
<div class="stat-label">% Esportati / inseriti</div>
|
||||||
<div class="stat-value text-success" id="kpiPerc">—</div>
|
<div class="stat-value text-success" id="kpiPerc">—</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-3 col-md-6">
|
<div class="col">
|
||||||
<div class="card radius-10 stat-card">
|
<div class="card radius-10 stat-card h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="stat-label">Clienti distinti</div>
|
<div class="stat-label">Clienti distinti</div>
|
||||||
<div class="stat-value text-warning" id="kpiClienti">0</div>
|
<div class="stat-value text-warning" id="kpiClienti">0</div>
|
||||||
@@ -388,10 +430,27 @@
|
|||||||
|
|
||||||
// KPI
|
// KPI
|
||||||
$('#kpiExport').text(res.totals.export);
|
$('#kpiExport').text(res.totals.export);
|
||||||
|
$('#kpiExportMonth').text(res.totalsMonth ? res.totalsMonth.export : 0);
|
||||||
|
$('#kpiExportYear').text(res.totalsYear ? res.totalsYear.export : 0);
|
||||||
$('#kpiClienti').text(res.byClient.length);
|
$('#kpiClienti').text(res.byClient.length);
|
||||||
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
|
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
|
||||||
$('#limsSamplesInput').val(res.limsSamples ?? '');
|
$('#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 limsTot = parseInt(res.limsSamples ?? 0, 10);
|
||||||
if (limsTot > 0) {
|
if (limsTot > 0) {
|
||||||
const perc = (res.totals.export / limsTot) * 100;
|
const perc = (res.totals.export / limsTot) * 100;
|
||||||
|
|||||||
@@ -70,6 +70,26 @@ try {
|
|||||||
// --- Totale export della giornata ---
|
// --- Totale export della giornata ---
|
||||||
$totExport = array_sum(array_column($byClient, 'tot'));
|
$totExport = array_sum(array_column($byClient, 'tot'));
|
||||||
|
|
||||||
|
// --- Totale export del MESE della data selezionata (status = 'l') ---
|
||||||
|
$stmtM = $pdo->prepare("
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM datadb d
|
||||||
|
WHERE d.status = 'l'
|
||||||
|
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
|
||||||
|
");
|
||||||
|
$stmtM->execute(['m' => $d->format('Y-m')]);
|
||||||
|
$totExportMonth = (int)$stmtM->fetchColumn();
|
||||||
|
|
||||||
|
// --- Totale export dell'ANNO della data selezionata (status = 'l') ---
|
||||||
|
$stmtY = $pdo->prepare("
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM datadb d
|
||||||
|
WHERE d.status = 'l'
|
||||||
|
AND YEAR(d.export_date) = :y
|
||||||
|
");
|
||||||
|
$stmtY->execute(['y' => $d->format('Y')]);
|
||||||
|
$totExportYear = (int)$stmtY->fetchColumn();
|
||||||
|
|
||||||
// --- Campioni totali inseriti nel LIMS per la giornata (tabella lims_daily_samples) ---
|
// --- 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 = $pdo->prepare("SELECT total_samples FROM lims_daily_samples WHERE sample_date = :d LIMIT 1");
|
||||||
$stmtL->execute(['d' => $date]);
|
$stmtL->execute(['d' => $date]);
|
||||||
@@ -79,7 +99,11 @@ try {
|
|||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'date' => $date,
|
'date' => $date,
|
||||||
|
'month' => $d->format('Y-m'),
|
||||||
|
'year' => (int)$d->format('Y'),
|
||||||
'totals' => ['export' => (int)$totExport],
|
'totals' => ['export' => (int)$totExport],
|
||||||
|
'totalsMonth' => ['export' => $totExportMonth],
|
||||||
|
'totalsYear' => ['export' => $totExportYear],
|
||||||
'byClient' => $byClient,
|
'byClient' => $byClient,
|
||||||
'byUser' => $byUser,
|
'byUser' => $byUser,
|
||||||
'limsSamples' => $limsSamples,
|
'limsSamples' => $limsSamples,
|
||||||
|
|||||||
@@ -0,0 +1,426 @@
|
|||||||
|
<?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 Mensili</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-label {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-extra {
|
||||||
|
background: linear-gradient(135deg, #021bff 0%, #010983 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-extra .stat-label {
|
||||||
|
color: #c9cfff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-extra .stat-value {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-extra small.text-muted {
|
||||||
|
color: #b3bbff !important;
|
||||||
|
}
|
||||||
|
</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 MESE ===== -->
|
||||||
|
<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 — Mensili</h6>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<label for="filterMonth" class="me-2 mb-0 fw-semibold">Mese:</label>
|
||||||
|
<input type="month" id="filterMonth" 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="btnCurrentMonth">Mese corrente</button>
|
||||||
|
<button class="btn btn-outline-secondary" id="btnPrevMonth">Mese precedente</button>
|
||||||
|
</div>
|
||||||
|
<span class="month-label ms-2 text-primary" id="monthLabel">—</span>
|
||||||
|
<a href="stats_export_lims.php" class="btn btn-outline-primary btn-sm ms-auto">
|
||||||
|
<i class="bx bx-calendar"></i> Vista giornaliera
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-primary btn-sm ms-2" id="btnRefresh">
|
||||||
|
<i class="bx bx-refresh"></i> Aggiorna
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== KPI (una sola riga) ===== -->
|
||||||
|
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-5 g-3">
|
||||||
|
<div class="col">
|
||||||
|
<div class="card radius-10 stat-card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="stat-label">Export mese (status L)</div>
|
||||||
|
<div class="stat-value text-primary" id="kpiExport">0</div>
|
||||||
|
</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">Export anno <span id="yearLabel">—</span> (status L)</div>
|
||||||
|
<div class="stat-value text-dark" id="kpiExportYear">0</div>
|
||||||
|
<small class="text-muted">Dati a partire 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 inseriti LIMS (mese)</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 class="col">
|
||||||
|
<div class="card radius-10 stat-card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="stat-label">Clienti distinti (mese)</div>
|
||||||
|
<div class="stat-value text-warning" id="kpiClienti">0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== GRAFICI ===== -->
|
||||||
|
<div class="row mt-1">
|
||||||
|
<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;
|
||||||
|
|
||||||
|
const MESI_IT = ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno',
|
||||||
|
'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'
|
||||||
|
];
|
||||||
|
|
||||||
|
function currentMonthStr() {
|
||||||
|
const d = new Date();
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
return `${y}-${m}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function monthLabelFromStr(monthStr) {
|
||||||
|
const parts = (monthStr || '').split('-');
|
||||||
|
if (parts.length !== 2) return '—';
|
||||||
|
const y = parseInt(parts[0], 10);
|
||||||
|
const m = parseInt(parts[1], 10);
|
||||||
|
if (isNaN(y) || isNaN(m) || m < 1 || m > 12) return '—';
|
||||||
|
return `${MESI_IT[m - 1]} ${y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 questo mese'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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 questo mese'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHART_TOP_N = 15;
|
||||||
|
|
||||||
|
function topN(arr) {
|
||||||
|
return arr.slice(0, CHART_TOP_N);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStats() {
|
||||||
|
const month = $('#filterMonth').val() || currentMonthStr();
|
||||||
|
$('#monthLabel').text(monthLabelFromStr(month));
|
||||||
|
|
||||||
|
$.getJSON('stats_export_lims_monthly_data.php', {
|
||||||
|
month: month
|
||||||
|
}, function(res) {
|
||||||
|
if (!res || !res.success) {
|
||||||
|
Swal.fire('Errore', (res && res.message) || 'Errore nel caricamento', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// KPI mese
|
||||||
|
$('#kpiExport').text(res.totals.export);
|
||||||
|
$('#kpiClienti').text(res.byClient.length);
|
||||||
|
$('#kpiLimsSamples').text(res.limsSamples ?? 0);
|
||||||
|
|
||||||
|
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('—');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box annuale
|
||||||
|
$('#kpiExportYear').text(res.totalsYear.export);
|
||||||
|
$('#yearLabel').text(res.year);
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
$('#filterMonth').val(currentMonthStr());
|
||||||
|
initTables();
|
||||||
|
loadStats();
|
||||||
|
|
||||||
|
$('#btnRefresh').on('click', loadStats);
|
||||||
|
$('#filterMonth').on('change', loadStats);
|
||||||
|
|
||||||
|
$('#btnCurrentMonth').on('click', function() {
|
||||||
|
$('#filterMonth').val(currentMonthStr());
|
||||||
|
loadStats();
|
||||||
|
});
|
||||||
|
$('#btnPrevMonth').on('click', function() {
|
||||||
|
const cur = $('#filterMonth').val() || currentMonthStr();
|
||||||
|
const parts = cur.split('-');
|
||||||
|
let y = parseInt(parts[0], 10);
|
||||||
|
let m = parseInt(parts[1], 10);
|
||||||
|
m -= 1;
|
||||||
|
if (m < 1) {
|
||||||
|
m = 12;
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
$('#filterMonth').val(`${y}-${String(m).padStart(2, '0')}`);
|
||||||
|
loadStats();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Mese richiesto (default mese corrente). Formato atteso: Y-m
|
||||||
|
$month = $_GET['month'] ?? date('Y-m');
|
||||||
|
$dm = DateTime::createFromFormat('Y-m', $month);
|
||||||
|
if (!$dm || $dm->format('Y-m') !== $month) {
|
||||||
|
throw new Exception('Mese non valido');
|
||||||
|
}
|
||||||
|
|
||||||
|
$year = $dm->format('Y');
|
||||||
|
|
||||||
|
// --- Export per cliente (status = 'l', giornata su export_date, filtro mese) ---
|
||||||
|
$stmtC = $pdo->prepare("
|
||||||
|
SELECT d.idclient AS idclient,
|
||||||
|
COUNT(*) AS tot
|
||||||
|
FROM datadb d
|
||||||
|
WHERE d.status = 'l'
|
||||||
|
AND DATE_FORMAT(d.export_date, '%Y-%m') = :m
|
||||||
|
GROUP BY d.idclient
|
||||||
|
ORDER BY tot DESC
|
||||||
|
");
|
||||||
|
$stmtC->execute(['m' => $month]);
|
||||||
|
$byClient = $stmtC->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// --- Risolvi Nominativo cliente dalla cache locale ---
|
||||||
|
$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', filtro mese) ---
|
||||||
|
$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_FORMAT(d.export_date, '%Y-%m') = :m
|
||||||
|
GROUP BY d.user_id, username
|
||||||
|
ORDER BY tot DESC
|
||||||
|
");
|
||||||
|
$stmtU->execute(['m' => $month]);
|
||||||
|
$byUser = $stmtU->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Normalizza tipi
|
||||||
|
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 del mese ---
|
||||||
|
$totExport = array_sum(array_column($byClient, 'tot'));
|
||||||
|
|
||||||
|
// --- Totale export dell'ANNO del mese selezionato ---
|
||||||
|
$stmtY = $pdo->prepare("
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM datadb d
|
||||||
|
WHERE d.status = 'l'
|
||||||
|
AND YEAR(d.export_date) = :y
|
||||||
|
");
|
||||||
|
$stmtY->execute(['y' => $year]);
|
||||||
|
$totExportYear = (int)$stmtY->fetchColumn();
|
||||||
|
|
||||||
|
// --- Campioni LIMS: somma dei total_samples di tutti i giorni del mese ---
|
||||||
|
$stmtL = $pdo->prepare("
|
||||||
|
SELECT COALESCE(SUM(total_samples), 0)
|
||||||
|
FROM lims_daily_samples
|
||||||
|
WHERE DATE_FORMAT(sample_date, '%Y-%m') = :m
|
||||||
|
");
|
||||||
|
$stmtL->execute(['m' => $month]);
|
||||||
|
$limsSamples = (int)$stmtL->fetchColumn();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'month' => $month,
|
||||||
|
'year' => (int)$year,
|
||||||
|
'totals' => ['export' => (int)$totExport],
|
||||||
|
'totalsYear' => ['export' => $totExportYear],
|
||||||
|
'byClient' => $byClient,
|
||||||
|
'byUser' => $byUser,
|
||||||
|
'limsSamples' => $limsSamples,
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Stats export LIMS monthly error: ' . $e->getMessage());
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user