1165 lines
49 KiB
PHP
1165 lines
49 KiB
PHP
<?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/schoolswitcher.php'); ?><?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>
|