Files
yogibook_aury_new/public/statistiche.php
T

1165 lines
49 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php require_once('include/headscript.php'); ?>
<?php
/**
* YogiBook - Statistiche
* Ordini, persone, lezioni, riprogrammazioni, riempimento classi,
* lezioni perse/cancellate e classifica utenti, con filtri per periodo,
* classe, categoria e insegnante.
*
* Convenzioni:
* - gli ORDINI sono filtrati per data ordine (orderbook.order_date_created)
* - le LEZIONI sono filtrate per data della lezione (serviceschedule.dateschedule)
* - il riempimento % è calcolato sulle lezioni già svolte (posti occupati / capienza)
*/
$pdo = DBHandlerSelect::getInstance()->getConnection();
if (!function_exists('e')) {
function e($value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
}
function fetchAllQ(PDO $pdo, string $sql, array $params = []): array
{
$st = $pdo->prepare($sql);
$st->execute($params);
return $st->fetchAll();
}
function pct($num, $den): ?float
{
return $den > 0 ? round($num / $den * 100, 1) : null;
}
function fmtPct(?float $v): string
{
return $v === null ? '–' : number_format($v, 0, ',', '.') . '%';
}
function n($v): string
{
return number_format((float) $v, 0, ',', '.');
}
function kpi(string $icon, string $color, string $value, string $label, string $sub = ''): string
{
return '<div class="col-6 col-lg-3"><div class="kpi-card">'
. '<div class="kpi-icon" style="background:' . $color . '1f;color:' . $color . ';"><i class="' . $icon . '"></i></div>'
. '<div class="kpi-body"><div class="kpi-value">' . $value . '</div>'
. '<div class="kpi-label">' . $label . '</div>'
. ($sub !== '' ? '<div class="kpi-sub">' . $sub . '</div>' : '')
. '</div></div></div>';
}
function fillColor(?float $p): string
{
if ($p === null) return '#d5d9e0';
if ($p >= 75) return '#1ebf73';
if ($p >= 45) return '#f1b40f';
return '#e67e22';
}
/* =========================================================== PERIODO */
$validDate = function ($d): bool {
if (!is_string($d) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $d)) return false;
[$y, $m, $dd] = array_map('intval', explode('-', $d));
return checkdate($m, $dd, $y);
};
$presets = [
'month' => 'Questo mese',
'lastmonth' => 'Mese scorso',
'3m' => 'Ultimi 3 mesi',
'12m' => 'Ultimi 12 mesi',
'year' => "Quest'anno",
];
$preset = $_GET['preset'] ?? '';
$fromIn = $_GET['from'] ?? '';
$toIn = $_GET['to'] ?? '';
$from = $to = null;
if (($preset === 'custom' || $preset === '') && $validDate($fromIn) && $validDate($toIn)) {
$preset = 'custom';
$from = $fromIn;
$to = $toIn;
}
if ($from === null) {
if (!isset($presets[$preset])) $preset = '3m';
$firstThis = new DateTime('first day of this month');
switch ($preset) {
case 'month':
$from = $firstThis->format('Y-m-d');
$to = $firstThis->format('Y-m-t');
break;
case 'lastmonth':
$lm = (clone $firstThis)->modify('-1 month');
$from = $lm->format('Y-m-d');
$to = $lm->format('Y-m-t');
break;
case '12m':
$from = (clone $firstThis)->modify('-11 months')->format('Y-m-d');
$to = $firstThis->format('Y-m-t');
break;
case 'year':
$from = date('Y-01-01');
$to = date('Y-12-31');
break;
default: // 3m
$from = (clone $firstThis)->modify('-2 months')->format('Y-m-d');
$to = $firstThis->format('Y-m-t');
}
}
if ($from > $to) {
[$from, $to] = [$to, $from];
}
$d1 = $from . ' 00:00:00';
$d2 = date('Y-m-d', strtotime($to . ' +1 day')) . ' 00:00:00';
$now = date('Y-m-d H:i:s');
/* =========================================================== FILTRI */
$fService = (int) ($_GET['service'] ?? 0);
$fCat = (int) ($_GET['category'] ?? 0);
$fTeacher = (int) ($_GET['teacher'] ?? 0);
$topLimit = in_array((int) ($_GET['top'] ?? 10), [10, 25, 50], true) ? (int) ($_GET['top'] ?? 10) : 10;
$sortMap = [
'attended' => ['Lezioni frequentate', 'attended DESC, upcoming DESC'],
'lost' => ['Lezioni perse', 'lost DESC, attended DESC'],
'reprog' => ['Riprogrammazioni', 'reprog DESC, attended DESC'],
'cancelled' => ['Cancellazioni', 'cancelled DESC, attended DESC'],
];
$topSort = isset($sortMap[$_GET['sort'] ?? '']) ? $_GET['sort'] : 'attended';
$sf = '';
$sp = [];
if ($fService > 0) {
$sf .= ' AND s.idservice = :f_service';
$sp[':f_service'] = $fService;
}
if ($fCat > 0) {
$sf .= ' AND s.category = :f_cat';
$sp[':f_cat'] = $fCat;
}
if ($fTeacher > 0) {
$sf .= ' AND s.idteacher = :f_teacher';
$sp[':f_teacher'] = $fTeacher;
}
$P = [':d1' => $d1, ':d2' => $d2] + $sp;
/* =========================================================== DATI FILTRI */
$services = fetchAllQ($pdo, "SELECT idservice, servicename, day, time FROM service ORDER BY servicename, day, time");
$categories = fetchAllQ($pdo, "SELECT idservicecategory, namecategory FROM servicecategory ORDER BY namecategory");
$teachers = fetchAllQ($pdo, "SELECT DISTINCT e.idemployee, e.nameemployee, e.surnameemployee
FROM employee e JOIN service s ON s.idteacher = e.idemployee
ORDER BY e.surnameemployee, e.nameemployee");
/* =========================================================== ORDINI */
$orderRows = fetchAllQ($pdo, "SELECT o.order_date_created AS d, o.nticket, o.iduser, o.reprogrammed
FROM orderbook o
LEFT JOIN service s ON o.idservice = s.idservice
WHERE o.order_date_created >= :d1 AND o.order_date_created < :d2 $sf", $P);
$ordersCount = count($orderRows);
$ticketsSold = 0;
$reprogCounter = 0;
$buyers = [];
foreach ($orderRows as $o) {
$ticketsSold += (int) $o['nticket'];
$reprogCounter += (int) $o['reprogrammed'];
if (!empty($o['iduser'])) $buyers[(int) $o['iduser']] = true;
}
$buyersCount = count($buyers);
// Nuovi clienti: persone il cui PRIMO ordine in assoluto cade nel periodo
$newCustomers = (int) (fetchAllQ($pdo, "SELECT COUNT(*) AS c FROM (
SELECT iduser, MIN(order_date_created) AS first_order
FROM orderbook WHERE iduser IS NOT NULL GROUP BY iduser
) t WHERE t.first_order >= :d1 AND t.first_order < :d2", [':d1' => $d1, ':d2' => $d2])[0]['c'] ?? 0);
/* =========================================================== LEZIONI (KPI) */
$lessonBase = "FROM bookingclass bc
JOIN serviceschedule ss ON bc.idserviceschedule = ss.idserviceschedule
JOIN service s ON ss.idservice = s.idservice
WHERE ss.dateschedule >= :d1 AND ss.dateschedule < :d2 $sf";
$k = fetchAllQ($pdo, "SELECT
COALESCE(SUM(bc.status = 'booked' AND bc.lostlesson = 'N'), 0) AS booked,
COALESCE(SUM(bc.status = 'booked' AND bc.lostlesson = 'N' AND ss.dateschedule <= :now), 0) AS attended,
COALESCE(SUM(bc.lostlesson = 'Y' AND bc.status <> 'cancelled'), 0) AS lost,
COALESCE(SUM(bc.status = 'cancelled'), 0) AS cancelled,
COALESCE(SUM(bc.status = 'pending'), 0) AS pending,
COALESCE(SUM(bc.status = 'waiting'), 0) AS waiting,
COALESCE(SUM(bc.is_reprogrammed = 'Y' AND bc.status <> 'cancelled'), 0) AS reprog,
COALESCE(SUM(bc.is_gift = 'Y' AND bc.status = 'booked'), 0) AS gifts,
COUNT(DISTINCT CASE WHEN bc.status = 'booked' THEN bc.iduser END) AS people
$lessonBase", $P + [':now' => $now])[0];
/* =========================================================== SESSIONI (riempimento) */
$sessions = fetchAllQ($pdo, "SELECT ss.idserviceschedule, ss.dateschedule,
s.idservice, s.servicename, s.colorclass, s.maxcapacity, s.day, s.time,
COUNT(CASE WHEN bc.status = 'booked' THEN 1 END) AS booked,
COUNT(CASE WHEN bc.status = 'booked' AND bc.lostlesson = 'N' THEN 1 END) AS present,
COUNT(CASE WHEN bc.lostlesson = 'Y' AND bc.status <> 'cancelled' THEN 1 END) AS lost,
COUNT(CASE WHEN bc.status = 'cancelled' THEN 1 END) AS cancelled,
COUNT(CASE WHEN bc.status = 'waiting' THEN 1 END) AS waiting,
COUNT(CASE WHEN bc.is_reprogrammed = 'Y' AND bc.status <> 'cancelled' THEN 1 END) AS reprog
FROM serviceschedule ss
JOIN service s ON ss.idservice = s.idservice
LEFT JOIN bookingclass bc ON bc.idserviceschedule = ss.idserviceschedule
WHERE ss.dateschedule >= :d1 AND ss.dateschedule < :d2 $sf
GROUP BY ss.idserviceschedule, ss.dateschedule, s.idservice, s.servicename,
s.colorclass, s.maxcapacity, s.day, s.time
ORDER BY ss.dateschedule", $P);
/* =========================================================== BUCKET TEMPORALI */
$rangeDays = (int) round((strtotime($to) - strtotime($from)) / 86400) + 1;
$weekly = $rangeDays <= 62; // periodi brevi: per settimana, altrimenti per mese
$monthsShort = ['', 'Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'];
$bucketKey = function (string $date) use ($weekly): string {
$ts = strtotime($date);
if ($weekly) {
$dow = (int) date('N', $ts);
return date('Y-m-d', strtotime('-' . ($dow - 1) . ' days', $ts));
}
return date('Y-m', $ts);
};
$buckets = [];
$cursor = strtotime($weekly ? $bucketKey($from) : date('Y-m-01', strtotime($from)));
$endTs = strtotime($to);
while ($cursor <= $endTs) {
$key = $weekly ? date('Y-m-d', $cursor) : date('Y-m', $cursor);
$buckets[$key] = [
'label' => $weekly ? date('d/m', $cursor) : $monthsShort[(int) date('n', $cursor)] . ' ' . date('y', $cursor),
'orders' => 0,
'tickets' => 0,
'present' => 0,
'lost' => 0,
'cancelled' => 0,
'reprog' => 0,
'cap_past' => 0,
'booked_past' => 0,
];
$cursor = strtotime($weekly ? '+1 week' : '+1 month', $cursor);
}
foreach ($orderRows as $o) {
$bk = $bucketKey($o['d']);
if (isset($buckets[$bk])) {
$buckets[$bk]['orders']++;
$buckets[$bk]['tickets'] += (int) $o['nticket'];
}
}
/* =========================================================== AGGREGAZIONE PER CLASSE */
$byClass = [];
$totCapPast = $totBookedPast = $totSessions = $totPast = $totFull = 0;
foreach ($sessions as $s) {
$sid = (int) $s['idservice'];
$isPast = $s['dateschedule'] <= $now;
$cap = (int) $s['maxcapacity'];
$booked = (int) $s['booked'];
if (!isset($byClass[$sid])) {
$byClass[$sid] = [
'name' => $s['servicename'],
'color' => $s['colorclass'] ?: '#1ebf73',
'day' => $s['day'],
'time' => $s['time'],
'sessions' => 0,
'past' => 0,
'booked' => 0,
'booked_past' => 0,
'present_past' => 0,
'cap_past' => 0,
'full' => 0,
'lost' => 0,
'cancelled' => 0,
'reprog' => 0,
'waiting' => 0,
];
}
$c = &$byClass[$sid];
$c['sessions']++;
$c['booked'] += $booked;
$c['lost'] += (int) $s['lost'];
$c['cancelled'] += (int) $s['cancelled'];
$c['reprog'] += (int) $s['reprog'];
$c['waiting'] += (int) $s['waiting'];
$totSessions++;
if ($isPast) {
$c['past']++;
$c['booked_past'] += $booked;
$c['present_past'] += (int) $s['present'];
$totPast++;
if ($cap > 0) {
$c['cap_past'] += $cap;
$totCapPast += $cap;
$totBookedPast += $booked;
if ($booked >= $cap) {
$c['full']++;
$totFull++;
}
}
}
unset($c);
$bk = $bucketKey($s['dateschedule']);
if (isset($buckets[$bk])) {
$buckets[$bk]['present'] += (int) $s['present'];
$buckets[$bk]['lost'] += (int) $s['lost'];
$buckets[$bk]['cancelled'] += (int) $s['cancelled'];
$buckets[$bk]['reprog'] += (int) $s['reprog'];
if ($isPast && $cap > 0) {
$buckets[$bk]['cap_past'] += $cap;
$buckets[$bk]['booked_past'] += $booked;
}
}
}
foreach ($byClass as &$c) {
$c['fill'] = pct($c['booked_past'], $c['cap_past']);
$c['avg'] = $c['past'] > 0 ? round($c['present_past'] / $c['past'], 1) : null;
}
unset($c);
uasort($byClass, function ($a, $b) {
return ($b['fill'] ?? -1) <=> ($a['fill'] ?? -1);
});
$fillTotal = pct($totBookedPast, $totCapPast);
$lostRate = pct((int) $k['lost'], (int) $k['booked'] + (int) $k['lost']);
/* =========================================================== CLASSIFICA UTENTI */
$topUsers = fetchAllQ($pdo, "SELECT u.id, u.first_name, u.last_name, u.email,
COALESCE(SUM(bc.status = 'booked' AND bc.lostlesson = 'N' AND ss.dateschedule <= :now1), 0) AS attended,
COALESCE(SUM(bc.status = 'booked' AND bc.lostlesson = 'N' AND ss.dateschedule > :now2), 0) AS upcoming,
COALESCE(SUM(bc.lostlesson = 'Y' AND bc.status <> 'cancelled'), 0) AS lost,
COALESCE(SUM(bc.is_reprogrammed = 'Y' AND bc.status <> 'cancelled'), 0) AS reprog,
COALESCE(SUM(bc.status = 'cancelled'), 0) AS cancelled,
MAX(CASE WHEN bc.status = 'booked' AND bc.lostlesson = 'N' AND ss.dateschedule <= :now3 THEN ss.dateschedule END) AS last_lesson
FROM bookingclass bc
JOIN serviceschedule ss ON bc.idserviceschedule = ss.idserviceschedule
JOIN service s ON ss.idservice = s.idservice
JOIN auth_users u ON u.id = bc.iduser
WHERE ss.dateschedule >= :d1 AND ss.dateschedule < :d2 $sf
GROUP BY u.id, u.first_name, u.last_name, u.email
HAVING (attended + upcoming + lost + cancelled) > 0
ORDER BY " . $sortMap[$topSort][1] . ", u.last_name, u.first_name
LIMIT " . $topLimit, $P + [':now1' => $now, ':now2' => $now, ':now3' => $now]);
/* =========================================================== DATI GRAFICI */
$chart = [
'labels' => [],
'orders' => [],
'tickets' => [],
'present' => [],
'lost' => [],
'cancelled' => [],
'reprog' => [],
'fill' => [],
'classLabels' => [],
'classFill' => [],
'classColors' => [],
];
foreach ($buckets as $b) {
$chart['labels'][] = $b['label'];
$chart['orders'][] = $b['orders'];
$chart['tickets'][] = $b['tickets'];
$chart['present'][] = $b['present'];
$chart['lost'][] = $b['lost'];
$chart['cancelled'][] = $b['cancelled'];
$chart['reprog'][] = $b['reprog'];
$chart['fill'][] = pct($b['booked_past'], $b['cap_past']);
}
foreach ($byClass as $c) {
if ($c['fill'] === null) continue;
$chart['classLabels'][] = $c['name'] . ($c['day'] ? ' · ' . $c['day'] . ' ' . $c['time'] : '');
$chart['classFill'][] = $c['fill'];
$chart['classColors'][] = $c['color'];
}
/* =========================================================== LINK FILTRI */
$baseFilters = array_filter([
'service' => $fService ?: null,
'category' => $fCat ?: null,
'teacher' => $fTeacher ?: null,
'top' => $topLimit !== 10 ? $topLimit : null,
'sort' => $topSort !== 'attended' ? $topSort : null,
]);
$periodLabel = date('d/m/Y', strtotime($from)) . ' – ' . date('d/m/Y', strtotime($to));
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8" />
<title>YogiBook - Statistiche</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta content="YogiBook - Prenotazione facile YogaSoul" name="description" />
<meta content="Advanced Creative Solutions" name="author" />
<link rel="shortcut icon" href="assets/images/favicon.ico">
<link href="assets/css/bootstrap.min.css" id="bootstrap-style" rel="stylesheet" type="text/css" />
<link href="assets/css/icons.min.css" rel="stylesheet" type="text/css" />
<link href="assets/css/app.min.css" id="app-style" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<style>
.stats-card {
background: #fff;
border-radius: 14px;
box-shadow: 0 3px 16px rgba(0, 0, 0, 0.06);
padding: 20px 22px;
margin-bottom: 20px;
}
.stats-card h5 {
font-size: 15px;
font-weight: 700;
color: #374151;
margin: 0 0 14px;
}
.stats-card h5 small {
font-weight: 500;
color: #9ca3af;
font-size: 12px;
margin-left: 6px;
}
.preset-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.preset-chip {
border: 1px solid #e2e4e9;
background: #fff;
color: #374151;
font-size: 13px;
font-weight: 600;
padding: 6px 14px;
border-radius: 20px;
text-decoration: none;
transition: all 0.15s;
}
.preset-chip:hover {
border-color: #b08d57;
color: #374151;
}
.preset-chip.active {
background: #E1D0B8;
border-color: #E1D0B8;
color: #5a4630;
}
.filters label {
font-size: 12px;
font-weight: 600;
color: #6b7280;
margin-bottom: 3px;
}
.period-label {
font-size: 13px;
color: #6b7280;
}
.section-title {
font-size: 13px;
font-weight: 700;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.06em;
margin: 6px 0 10px;
}
.kpi-card {
background: #fff;
border-radius: 14px;
box-shadow: 0 3px 16px rgba(0, 0, 0, 0.06);
padding: 16px 18px;
display: flex;
gap: 14px;
align-items: flex-start;
margin-bottom: 18px;
height: calc(100% - 18px);
}
.kpi-icon {
flex: 0 0 42px;
width: 42px;
height: 42px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
}
.kpi-value {
font-size: 26px;
font-weight: 800;
color: #1f2937;
line-height: 1.1;
}
.kpi-label {
font-size: 13px;
font-weight: 600;
color: #4b5563;
margin-top: 2px;
}
.kpi-sub {
font-size: 12px;
color: #9ca3af;
margin-top: 3px;
}
.chart-box {
position: relative;
height: 280px;
}
.class-dot {
display: inline-block;
width: 11px;
height: 11px;
border-radius: 50%;
margin-right: 7px;
vertical-align: middle;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.fill-bar {
background: #eef0f3;
border-radius: 20px;
height: 8px;
width: 110px;
display: inline-block;
vertical-align: middle;
margin-right: 8px;
overflow: hidden;
}
.fill-bar span {
display: block;
height: 100%;
border-radius: 20px;
}
.stats-table th {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.03em;
color: #6b7280;
white-space: nowrap;
}
.stats-table td {
vertical-align: middle;
white-space: nowrap;
}
.stats-table tfoot td {
font-weight: 700;
border-top: 2px solid #e5e7eb;
}
.rank {
display: inline-flex;
width: 26px;
height: 26px;
border-radius: 50%;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 800;
background: #f3f4f6;
color: #374151;
}
.rank.r1 {
background: #f6d365;
}
.rank.r2 {
background: #d7dbe0;
}
.rank.r3 {
background: #e6b98c;
}
.muted-note {
font-size: 12px;
color: #9ca3af;
}
.empty-state {
text-align: center;
color: #9ca3af;
padding: 24px;
}
</style>
</head>
<body>
<div id="layout-wrapper">
<header id="page-topbar" class="isvertical-topbar">
<div class="navbar-header">
<div class="d-flex">
<?php include('include/logoarea.php'); ?>
<button type="button" class="btn btn-sm px-3 font-size-24 header-item waves-effect vertical-menu-btn">
<i class="bx bx-menu align-middle"></i>
</button>
<div class="page-title-box align-self-center d-none d-md-block">
<h4 class="page-title mb-0">Statistiche</h4>
</div>
</div>
<div class="d-flex">
<?php include('include/languageselection.php'); ?>
<?php include('include/profiletopbar.php'); ?>
</div>
</div>
</header>
<?php include('include/sidebar.php'); ?>
<header class="ishorizontal-topbar">
<div class="navbar-header">
<div class="d-flex"></div>
</div>
<div class="topnav">
<div class="container-fluid">
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
</div>
</div>
</header>
<div class="main-content">
<div class="page-content">
<div class="container-fluid">
<!-- ============================== FILTRI -->
<div class="stats-card">
<div class="preset-chips">
<?php foreach ($presets as $pk => $plabel) : ?>
<a class="preset-chip <?php echo $preset === $pk ? 'active' : ''; ?>"
href="?<?php echo e(http_build_query($baseFilters + ['preset' => $pk])); ?>"><?php echo e($plabel); ?></a>
<?php endforeach; ?>
<?php if ($preset === 'custom') : ?>
<span class="preset-chip active">Personalizzato</span>
<?php endif; ?>
</div>
<form method="get" id="statsForm" class="filters">
<input type="hidden" name="preset" id="presetInput" value="<?php echo e($preset); ?>">
<div class="row g-2 align-items-end">
<div class="col-6 col-md-2">
<label>Dal</label>
<input type="date" name="from" class="form-control date-input" value="<?php echo e($from); ?>">
</div>
<div class="col-6 col-md-2">
<label>Al</label>
<input type="date" name="to" class="form-control date-input" value="<?php echo e($to); ?>">
</div>
<div class="col-12 col-md-3">
<label>Classe</label>
<select name="service" class="form-select auto-submit">
<option value="">Tutte le classi</option>
<?php foreach ($services as $sv) : ?>
<option value="<?php echo (int) $sv['idservice']; ?>" <?php echo $fService === (int) $sv['idservice'] ? 'selected' : ''; ?>>
<?php echo e($sv['servicename'] . ($sv['day'] ? ' – ' . $sv['day'] . ' ' . $sv['time'] : '')); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6 col-md-2">
<label>Categoria</label>
<select name="category" class="form-select auto-submit">
<option value="">Tutte</option>
<?php foreach ($categories as $ct) : ?>
<option value="<?php echo (int) $ct['idservicecategory']; ?>" <?php echo $fCat === (int) $ct['idservicecategory'] ? 'selected' : ''; ?>>
<?php echo e($ct['namecategory']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php if (count($teachers) > 1) : ?>
<div class="col-6 col-md-2">
<label>Insegnante</label>
<select name="teacher" class="form-select auto-submit">
<option value="">Tutti</option>
<?php foreach ($teachers as $t) : ?>
<option value="<?php echo (int) $t['idemployee']; ?>" <?php echo $fTeacher === (int) $t['idemployee'] ? 'selected' : ''; ?>>
<?php echo e(trim($t['nameemployee'] . ' ' . $t['surnameemployee'])); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="col-12 col-md-auto d-flex gap-2">
<button type="submit" class="btn btn-primary"><i class="fas fa-filter me-1"></i> Applica</button>
<a href="statistiche.php" class="btn btn-light">Azzera</a>
</div>
</div>
<input type="hidden" name="top" value="<?php echo $topLimit; ?>">
<input type="hidden" name="sort" value="<?php echo e($topSort); ?>">
</form>
<div class="period-label mt-3">
<i class="far fa-calendar me-1"></i> Periodo: <strong><?php echo e($periodLabel); ?></strong>
<?php if ($fService || $fCat || $fTeacher) : ?> · <span style="color:#b08d57;font-weight:600;">filtri attivi</span><?php endif; ?>
</div>
</div>
<!-- ============================== KPI -->
<div class="section-title">Ordini</div>
<div class="row">
<?php
echo kpi('fas fa-receipt', '#8e44ad', n($ordersCount), 'Ordini', 'per data ordine');
echo kpi('fas fa-ticket-alt', '#b08d57', n($ticketsSold), 'Lezioni vendute', 'somma dei ticket');
echo kpi('fas fa-user-tag', '#2980b9', n($buyersCount), 'Clienti che hanno acquistato');
echo kpi('fas fa-user-plus', '#16a085', n($newCustomers), 'Nuovi clienti', 'primo ordine nel periodo');
?>
</div>
<div class="section-title">Lezioni e persone</div>
<div class="row">
<?php
echo kpi('fas fa-users', '#1ebf73', n($k['people']), 'Persone attive', 'con almeno una prenotazione');
echo kpi('fas fa-calendar-check', '#1ebf73', n($k['booked']), 'Lezioni prenotate', n($k['attended']) . ' già svolte' . ((int) $k['gifts'] > 0 ? ' · ' . n($k['gifts']) . ' omaggio' : ''));
echo kpi('fas fa-chart-pie', fillColor($fillTotal), fmtPct($fillTotal), 'Riempimento medio', $totPast > 0 ? 'su ' . n($totPast) . ' lezioni svolte' : 'nessuna lezione svolta');
echo kpi('fas fa-fire', '#c0392b', n($totFull), 'Lezioni al completo', $totPast > 0 ? fmtPct(pct($totFull, $totPast)) . ' delle lezioni svolte' : '');
?>
</div>
<div class="section-title">Riprogrammazioni, perse e cancellate</div>
<div class="row">
<?php
echo kpi('fas fa-calendar-alt', '#2980b9', n($k['reprog']), 'Lezioni riprogrammate', 'contatore ordini del periodo: ' . n($reprogCounter));
echo kpi('fas fa-wind', '#e67e22', n($k['lost']), 'Lezioni perse', $lostRate !== null ? fmtPct($lostRate) . ' delle prenotazioni' : '');
echo kpi('fas fa-ban', '#e74c3c', n($k['cancelled']), 'Lezioni cancellate');
echo kpi('fas fa-list-ol', '#d1698a', n($k['waiting']), 'In lista d\'attesa', n($k['pending']) . ' in attesa di conferma');
?>
</div>
<!-- ============================== GRAFICI -->
<div class="row">
<div class="col-lg-6">
<div class="stats-card">
<h5>Andamento lezioni <small><?php echo $weekly ? 'per settimana' : 'per mese'; ?></small></h5>
<div class="chart-box"><canvas id="chLessons"></canvas></div>
</div>
</div>
<div class="col-lg-6">
<div class="stats-card">
<h5>Andamento ordini <small><?php echo $weekly ? 'per settimana' : 'per mese'; ?></small></h5>
<div class="chart-box"><canvas id="chOrders"></canvas></div>
</div>
</div>
<div class="col-lg-6">
<div class="stats-card">
<h5>Riempimento nel tempo <small>lezioni svolte</small></h5>
<div class="chart-box"><canvas id="chFill"></canvas></div>
</div>
</div>
<div class="col-lg-6">
<div class="stats-card">
<h5>Riempimento per classe <small>lezioni svolte</small></h5>
<?php if (empty($chart['classFill'])) : ?>
<div class="empty-state">Nessuna lezione svolta nel periodo.</div>
<?php else : ?>
<div class="chart-box" style="height:<?php echo max(280, count($chart['classFill']) * 34 + 40); ?>px;"><canvas id="chClassFill"></canvas></div>
<?php endif; ?>
</div>
</div>
</div>
<!-- ============================== TABELLA PER CLASSE -->
<div class="stats-card">
<h5>Dettaglio per classe <small>ordinato per riempimento</small></h5>
<?php if (empty($byClass)) : ?>
<div class="empty-state">Nessuna classe programmata nel periodo.</div>
<?php else : ?>
<div class="table-responsive">
<table class="table stats-table mb-0">
<thead>
<tr>
<th>Classe</th>
<th class="text-center">Lezioni<br><small>svolte / tot.</small></th>
<th class="text-center">Prenotazioni</th>
<th class="text-center">Media presenti</th>
<th>Riempimento</th>
<th class="text-center">Al completo</th>
<th class="text-center">Perse</th>
<th class="text-center">Cancellate</th>
<th class="text-center">Riprogr.</th>
<th class="text-center">In coda</th>
</tr>
</thead>
<tbody>
<?php
$sum = ['past' => 0, 'sessions' => 0, 'booked' => 0, 'present_past' => 0, 'full' => 0, 'lost' => 0, 'cancelled' => 0, 'reprog' => 0, 'waiting' => 0];
foreach ($byClass as $c) :
foreach ($sum as $sk => $_) $sum[$sk] += $c[$sk];
?>
<tr>
<td>
<span class="class-dot" style="background:<?php echo e($c['color']); ?>;"></span>
<strong><?php echo e($c['name']); ?></strong>
<?php if ($c['day']) : ?><br><small class="text-muted" style="margin-left:18px;"><?php echo e($c['day'] . ' ' . $c['time']); ?></small><?php endif; ?>
</td>
<td class="text-center"><?php echo n($c['past']); ?> / <?php echo n($c['sessions']); ?></td>
<td class="text-center"><?php echo n($c['booked']); ?></td>
<td class="text-center"><?php echo $c['avg'] === null ? '–' : number_format($c['avg'], 1, ',', '.'); ?></td>
<td>
<span class="fill-bar"><span style="width:<?php echo min(100, (float) ($c['fill'] ?? 0)); ?>%;background:<?php echo fillColor($c['fill']); ?>;"></span></span>
<strong><?php echo fmtPct($c['fill']); ?></strong>
</td>
<td class="text-center"><?php echo n($c['full']); ?></td>
<td class="text-center"><?php echo n($c['lost']); ?></td>
<td class="text-center"><?php echo n($c['cancelled']); ?></td>
<td class="text-center"><?php echo n($c['reprog']); ?></td>
<td class="text-center"><?php echo n($c['waiting']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<td>Totale</td>
<td class="text-center"><?php echo n($sum['past']); ?> / <?php echo n($sum['sessions']); ?></td>
<td class="text-center"><?php echo n($sum['booked']); ?></td>
<td class="text-center"><?php echo $sum['past'] > 0 ? number_format($sum['present_past'] / $sum['past'], 1, ',', '.') : '–'; ?></td>
<td><strong><?php echo fmtPct($fillTotal); ?></strong></td>
<td class="text-center"><?php echo n($sum['full']); ?></td>
<td class="text-center"><?php echo n($sum['lost']); ?></td>
<td class="text-center"><?php echo n($sum['cancelled']); ?></td>
<td class="text-center"><?php echo n($sum['reprog']); ?></td>
<td class="text-center"><?php echo n($sum['waiting']); ?></td>
</tr>
</tfoot>
</table>
</div>
<div class="muted-note mt-2">Prenotazioni = posti occupati (incluse le perse). Media presenti e riempimento considerano solo le lezioni già svolte.</div>
<?php endif; ?>
</div>
<!-- ============================== CLASSIFICA UTENTI -->
<div class="stats-card">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
<h5 class="mb-0">Classifica utenti <small>nel periodo</small></h5>
<div class="d-flex gap-2 align-items-center">
<label class="muted-note mb-0">Ordina per</label>
<select class="form-select form-select-sm" style="width:auto;" onchange="setTop('sort', this.value)">
<?php foreach ($sortMap as $sk => $sv) : ?>
<option value="<?php echo e($sk); ?>" <?php echo $topSort === $sk ? 'selected' : ''; ?>><?php echo e($sv[0]); ?></option>
<?php endforeach; ?>
</select>
<select class="form-select form-select-sm" style="width:auto;" onchange="setTop('top', this.value)">
<?php foreach ([10, 25, 50] as $tl) : ?>
<option value="<?php echo $tl; ?>" <?php echo $topLimit === $tl ? 'selected' : ''; ?>>Primi <?php echo $tl; ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<?php if (empty($topUsers)) : ?>
<div class="empty-state">Nessuna prenotazione nel periodo.</div>
<?php else : ?>
<div class="table-responsive">
<table class="table stats-table mb-0">
<thead>
<tr>
<th>#</th>
<th>Utente</th>
<th class="text-center">Frequentate</th>
<th class="text-center">Prenotate future</th>
<th class="text-center">Perse</th>
<th class="text-center">Riprogr.</th>
<th class="text-center">Cancellate</th>
<th class="text-center">Presenza</th>
<th>Ultima lezione</th>
</tr>
</thead>
<tbody>
<?php foreach ($topUsers as $i => $u) :
$pos = $i + 1;
$presence = pct((int) $u['attended'], (int) $u['attended'] + (int) $u['lost']);
?>
<tr>
<td><span class="rank <?php echo $pos <= 3 ? 'r' . $pos : ''; ?>"><?php echo $pos; ?></span></td>
<td>
<strong><?php echo e(trim($u['first_name'] . ' ' . $u['last_name'])); ?></strong><br>
<small class="text-muted"><?php echo e($u['email']); ?></small>
</td>
<td class="text-center"><strong><?php echo n($u['attended']); ?></strong></td>
<td class="text-center"><?php echo n($u['upcoming']); ?></td>
<td class="text-center"><?php echo (int) $u['lost'] > 0 ? '<span style="color:#e67e22;font-weight:700;">' . n($u['lost']) . '</span>' : '0'; ?></td>
<td class="text-center"><?php echo n($u['reprog']); ?></td>
<td class="text-center"><?php echo n($u['cancelled']); ?></td>
<td class="text-center">
<span class="fill-bar" style="width:60px;"><span style="width:<?php echo (float) ($presence ?? 0); ?>%;background:<?php echo fillColor($presence); ?>;"></span></span><?php echo fmtPct($presence); ?>
</td>
<td><?php echo $u['last_lesson'] ? e(date('d/m/Y', strtotime($u['last_lesson']))) : '–'; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="muted-note mt-2">Presenza = lezioni frequentate / (frequentate + perse).</div>
<?php endif; ?>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
</div>
</div>
<script src="assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="assets/libs/metismenujs/metismenujs.min.js"></script>
<script src="assets/libs/simplebar/simplebar.min.js"></script>
<script src="assets/libs/eva-icons/eva.min.js"></script>
<script src="assets/js/app.js"></script>
<script>
// ---- filtri
document.querySelectorAll('.date-input').forEach(function(el) {
el.addEventListener('change', function() {
document.getElementById('presetInput').value = 'custom';
});
});
document.querySelectorAll('.auto-submit').forEach(function(el) {
el.addEventListener('change', function() {
document.getElementById('statsForm').submit();
});
});
function setTop(name, value) {
var form = document.getElementById('statsForm');
form.querySelector('input[name="' + name + '"]').value = value;
form.submit();
}
// ---- grafici
const D = <?php echo json_encode($chart, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?>;
Chart.defaults.font.family = getComputedStyle(document.body).fontFamily;
Chart.defaults.color = '#6b7280';
Chart.defaults.plugins.legend.position = 'bottom';
Chart.defaults.plugins.legend.labels.boxWidth = 12;
const baseOpts = {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
}
};
new Chart(document.getElementById('chLessons'), {
type: 'bar',
data: {
labels: D.labels,
datasets: [{
label: 'Prenotate',
data: D.present,
backgroundColor: '#1ebf73',
stack: 'l',
borderRadius: 4
},
{
label: 'Perse',
data: D.lost,
backgroundColor: '#e67e22',
stack: 'l'
},
{
label: 'Cancellate',
data: D.cancelled,
backgroundColor: '#e74c3c',
stack: 'l'
},
{
label: 'Riprogrammate',
data: D.reprog,
type: 'line',
borderColor: '#2980b9',
backgroundColor: '#2980b9',
tension: 0.3,
stack: 'r'
}
]
},
options: Object.assign({}, baseOpts, {
scales: {
x: {
stacked: true,
grid: {
display: false
}
},
y: {
stacked: true,
beginAtZero: true,
ticks: {
precision: 0
}
}
}
})
});
new Chart(document.getElementById('chOrders'), {
type: 'bar',
data: {
labels: D.labels,
datasets: [{
label: 'Ordini',
data: D.orders,
backgroundColor: '#8e44ad',
borderRadius: 4
},
{
label: 'Lezioni vendute',
data: D.tickets,
type: 'line',
borderColor: '#b08d57',
backgroundColor: '#b08d57',
tension: 0.3
}
]
},
options: Object.assign({}, baseOpts, {
scales: {
x: {
grid: {
display: false
}
},
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
})
});
new Chart(document.getElementById('chFill'), {
type: 'line',
data: {
labels: D.labels,
datasets: [{
label: 'Riempimento %',
data: D.fill,
borderColor: '#1ebf73',
backgroundColor: 'rgba(30,191,115,0.12)',
fill: true,
tension: 0.3,
spanGaps: true
}]
},
options: Object.assign({}, baseOpts, {
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (c) => c.parsed.y === null ? 'nessuna lezione svolta' : c.parsed.y + '%'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
min: 0,
max: 100,
ticks: {
callback: (v) => v + '%'
}
}
}
})
});
if (document.getElementById('chClassFill')) {
new Chart(document.getElementById('chClassFill'), {
type: 'bar',
data: {
labels: D.classLabels,
datasets: [{
label: 'Riempimento %',
data: D.classFill,
backgroundColor: D.classColors,
borderColor: 'rgba(0,0,0,0.08)',
borderWidth: 1,
borderRadius: 4
}]
},
options: Object.assign({}, baseOpts, {
indexAxis: 'y',
interaction: {
mode: 'nearest',
intersect: true
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (c) => c.parsed.x + '%'
}
}
},
scales: {
x: {
min: 0,
max: 100,
ticks: {
callback: (v) => v + '%'
}
},
y: {
grid: {
display: false
}
}
}
})
});
}
</script>
</body>
</html>