1156 lines
45 KiB
PHP
1156 lines
45 KiB
PHP
<?php require_once('include/headscript.php'); ?>
|
||
<?php
|
||
/**
|
||
* YogiBook - Sommario per la direzione
|
||
*
|
||
* Un colpo d'occhio sullo stato della scuola: ogni indicatore ha un semaforo
|
||
* (verde / giallo / rosso), il confronto con il periodo precedente e un consiglio.
|
||
* Le soglie si cambiano nell'array $SOGLIE qui sotto.
|
||
*
|
||
* Il periodo finisce sempre OGGI e viene confrontato con lo stesso numero di giorni
|
||
* immediatamente precedente (es. ultimi 30 giorni vs i 30 prima).
|
||
*/
|
||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
|
||
/* =========================================================== SOGLIE (modificabili) */
|
||
// [verde se ..., giallo se ..., altrimenti rosso]
|
||
$SOGLIE = [
|
||
'riempimento' => ['verde' => 70, 'giallo' => 45], // % posti occupati: più alto è meglio
|
||
'perse' => ['verde' => 5, 'giallo' => 10], // % lezioni perse: più basso è meglio
|
||
'cancellazioni' => ['verde' => 8, 'giallo' => 15], // % cancellazioni sulle prenotazioni
|
||
'last_minute' => ['verde' => 20, 'giallo' => 40], // % cancellazioni con meno di 24 ore di preavviso
|
||
'riacquisto' => ['verde' => 60, 'giallo' => 40], // % clienti che riacquistano dopo la scadenza
|
||
'non_usate' => ['verde' => 5, 'giallo' => 15], // % lezioni rimaste inutilizzate nei pacchetti scaduti
|
||
'certificati' => ['verde' => 95, 'giallo' => 80], // % abbonati attivi con certificato valido
|
||
'classi_deboli' => ['verde' => 0, 'giallo' => 2], // n. classi sotto il 40% di riempimento
|
||
'trend' => ['verde' => -5, 'giallo' => -20], // variazione % rispetto al periodo prima
|
||
'soglia_debole' => 40, // sotto questo riempimento una classe è "debole"
|
||
'giorni_rinnovo' => 30, // giorni concessi dopo la scadenza per rinnovare
|
||
];
|
||
|
||
if (!function_exists('e')) {
|
||
function e($value): string
|
||
{
|
||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
}
|
||
|
||
function q(PDO $pdo, string $sql, array $p = []): array
|
||
{
|
||
$st = $pdo->prepare($sql);
|
||
$st->execute($p);
|
||
return $st->fetchAll(PDO::FETCH_ASSOC);
|
||
}
|
||
|
||
function q1(PDO $pdo, string $sql, array $p = [])
|
||
{
|
||
$r = q($pdo, $sql, $p);
|
||
return $r ? reset($r[0]) : null;
|
||
}
|
||
|
||
function pctv($num, $den): ?float
|
||
{
|
||
return $den > 0 ? round($num / $den * 100, 1) : null;
|
||
}
|
||
|
||
/** Giorno della settimana in italiano (in service.day è salvato in inglese). */
|
||
function giornoIt(string $s): string
|
||
{
|
||
return strtr($s, [
|
||
'Monday' => 'Lunedì',
|
||
'Tuesday' => 'Martedì',
|
||
'Wednesday' => 'Mercoledì',
|
||
'Thursday' => 'Giovedì',
|
||
'Friday' => 'Venerdì',
|
||
'Saturday' => 'Sabato',
|
||
'Sunday' => 'Domenica'
|
||
]);
|
||
}
|
||
|
||
function n0($v): string
|
||
{
|
||
return number_format((float) $v, 0, ',', '.');
|
||
}
|
||
|
||
/** Semaforo: 'verde' | 'giallo' | 'rosso' | 'grigio' (dati insufficienti). */
|
||
function semaforo(?float $v, array $s, bool $piuAltoMeglio): string
|
||
{
|
||
if ($v === null) return 'grigio';
|
||
if ($piuAltoMeglio) {
|
||
return $v >= $s['verde'] ? 'verde' : ($v >= $s['giallo'] ? 'giallo' : 'rosso');
|
||
}
|
||
return $v <= $s['verde'] ? 'verde' : ($v <= $s['giallo'] ? 'giallo' : 'rosso');
|
||
}
|
||
|
||
/** Variazione % tra periodo attuale e precedente (null se non confrontabile). */
|
||
function delta($cur, $prev): ?float
|
||
{
|
||
if ($prev === null || $cur === null) return null;
|
||
if ((float) $prev == 0.0) return null;
|
||
return round(((float) $cur - (float) $prev) / (float) $prev * 100, 1);
|
||
}
|
||
|
||
/* =========================================================== PERIODO */
|
||
|
||
$periodi = [
|
||
'30' => 'Ultimi 30 giorni',
|
||
'90' => 'Ultimi 3 mesi',
|
||
'180' => 'Ultimi 6 mesi',
|
||
'365' => 'Ultimi 12 mesi',
|
||
];
|
||
$giorni = isset($periodi[$_GET['p'] ?? '']) ? (int) $_GET['p'] : 90;
|
||
|
||
$oggi = new DateTime('today');
|
||
$fine = (clone $oggi)->modify('+1 day'); // escluso
|
||
$inizio = (clone $fine)->modify("-{$giorni} days");
|
||
$inizioP = (clone $inizio)->modify("-{$giorni} days"); // periodo precedente
|
||
$now = date('Y-m-d H:i:s');
|
||
|
||
/* =========================================================== CALCOLO METRICHE */
|
||
|
||
/**
|
||
* Tutte le metriche di un periodo [$d1, $d2).
|
||
* Le lezioni considerano solo quelle già svolte (fino ad adesso).
|
||
*/
|
||
function metriche(PDO $pdo, string $d1, string $d2, string $now, int $sogliaDebole, int $grazia = 30): array
|
||
{
|
||
$m = [];
|
||
$fineLezioni = min($d2, $now);
|
||
|
||
// --- Vendite
|
||
$o = q($pdo, "SELECT COUNT(*) AS ordini, COALESCE(SUM(nticket), 0) AS lezioni, COUNT(DISTINCT iduser) AS clienti
|
||
FROM orderbook WHERE order_date_created >= ? AND order_date_created < ?", [$d1, $d2])[0];
|
||
$m['ordini'] = (int) $o['ordini'];
|
||
$m['vendute'] = (int) $o['lezioni'];
|
||
$m['clienti'] = (int) $o['clienti'];
|
||
|
||
$m['nuovi'] = (int) q1($pdo, "SELECT COUNT(*) FROM (
|
||
SELECT iduser, MIN(order_date_created) AS primo FROM orderbook WHERE iduser IS NOT NULL GROUP BY iduser
|
||
) t WHERE t.primo >= ? AND t.primo < ?", [$d1, $d2]);
|
||
|
||
// --- Lezioni svolte: presenze, perse, persone
|
||
$l = q($pdo, "SELECT
|
||
COALESCE(SUM(bc.status = 'booked' AND bc.lostlesson = 'N'), 0) AS presenti,
|
||
COALESCE(SUM(bc.lostlesson = 'Y' AND bc.status <> 'cancelled'), 0) AS perse,
|
||
COUNT(DISTINCT CASE WHEN bc.status = 'booked' AND bc.lostlesson = 'N' THEN bc.iduser END) AS persone
|
||
FROM bookingclass bc
|
||
JOIN serviceschedule ss ON ss.idserviceschedule = bc.idserviceschedule
|
||
WHERE ss.dateschedule >= ? AND ss.dateschedule < ?", [$d1, $fineLezioni])[0];
|
||
$m['presenze'] = (int) $l['presenti'];
|
||
$m['perse'] = (int) $l['perse'];
|
||
$m['persone'] = (int) $l['persone'];
|
||
$m['perse_pct'] = pctv($m['perse'], $m['presenze'] + $m['perse']);
|
||
|
||
// --- Riempimento (lezioni svolte con capienza)
|
||
$sess = q(
|
||
$pdo,
|
||
"SELECT ss.idserviceschedule, s.idservice, s.servicename, s.day, s.time, s.colorclass, s.maxcapacity,
|
||
COUNT(CASE WHEN bc.status = 'booked' THEN 1 END) AS occupati
|
||
FROM serviceschedule ss
|
||
JOIN service s ON s.idservice = ss.idservice
|
||
LEFT JOIN bookingclass bc ON bc.idserviceschedule = ss.idserviceschedule
|
||
WHERE ss.dateschedule >= ? AND ss.dateschedule < ? AND s.maxcapacity > 0
|
||
GROUP BY ss.idserviceschedule, s.idservice, s.servicename, s.day, s.time, s.colorclass, s.maxcapacity",
|
||
[$d1, $fineLezioni]
|
||
);
|
||
|
||
$cap = $occ = $piene = 0;
|
||
$perClasse = [];
|
||
foreach ($sess as $s) {
|
||
$c = (int) $s['maxcapacity'];
|
||
$b = (int) $s['occupati'];
|
||
$cap += $c;
|
||
$occ += min($b, $c);
|
||
if ($b >= $c) $piene++;
|
||
$id = (int) $s['idservice'];
|
||
$perClasse[$id] ??= ['nome' => $s['servicename'], 'quando' => giornoIt(trim($s['day'] . ' ' . $s['time'])), 'colore' => $s['colorclass'] ?: '#1ebf73', 'cap' => 0, 'occ' => 0, 'lezioni' => 0];
|
||
$perClasse[$id]['cap'] += $c;
|
||
$perClasse[$id]['occ'] += min($b, $c);
|
||
$perClasse[$id]['lezioni']++;
|
||
}
|
||
foreach ($perClasse as &$pc) $pc['fill'] = pctv($pc['occ'], $pc['cap']);
|
||
unset($pc);
|
||
|
||
$m['lezioni_svolte'] = count($sess);
|
||
$m['riempimento'] = pctv($occ, $cap);
|
||
$m['piene_pct'] = pctv($piene, count($sess));
|
||
$m['classi'] = $perClasse;
|
||
// Classi "deboli": almeno 2 lezioni svolte e riempimento sotto soglia
|
||
$m['classi_deboli'] = array_values(array_filter($perClasse, fn($c) => $c['lezioni'] >= 2 && $c['fill'] !== null && $c['fill'] < $sogliaDebole));
|
||
|
||
// --- Cancellazioni (storico) — la tabella esiste solo dopo la migrazione
|
||
try {
|
||
$c = q($pdo, "SELECT COUNT(*) AS tot,
|
||
COALESCE(SUM(TIMESTAMPDIFF(MINUTE, c.cancelled_at, COALESCE(ss.dateschedule, c.bookingstart)) < 1440), 0) AS last_minute
|
||
FROM bookingclass_cancellations c
|
||
LEFT JOIN serviceschedule ss ON ss.idserviceschedule = c.idserviceschedule
|
||
WHERE c.cancelled_at >= ? AND c.cancelled_at < ?", [$d1, $d2])[0];
|
||
$m['canc'] = (int) $c['tot'];
|
||
$m['canc_pct'] = pctv($m['canc'], $m['presenze'] + $m['perse'] + $m['canc']);
|
||
$m['last_minute_pct'] = pctv((int) $c['last_minute'], $m['canc']);
|
||
} catch (PDOException $e) {
|
||
$m['canc'] = null;
|
||
$m['canc_pct'] = null;
|
||
$m['last_minute_pct'] = null;
|
||
}
|
||
|
||
// --- Rinnovo: chi ha finito un pacchetto ne ha comprato un altro?
|
||
// Contano i pacchetti scaduti da almeno $grazia giorni (prima non è ancora "mancato rinnovo"),
|
||
// quindi la finestra è spostata indietro di $grazia giorni. Così un pacchetto da 12 lezioni
|
||
// (circa 3 mesi) viene valutato solo quando è davvero finito, qualunque sia il periodo scelto.
|
||
// Rinnovato = un nuovo ordine dopo quello scaduto, entro $grazia giorni dalla scadenza.
|
||
$g1 = date('Y-m-d', strtotime(substr($d1, 0, 10) . " -{$grazia} days"));
|
||
$g2 = date('Y-m-d', strtotime(substr($d2, 0, 10) . " -{$grazia} days"));
|
||
$scad = q($pdo, "SELECT o.iduser,
|
||
MAX(EXISTS(SELECT 1 FROM orderbook o2
|
||
WHERE o2.iduser = o.iduser AND o2.idorderbook <> o.idorderbook
|
||
AND o2.order_date_created > o.order_date_created
|
||
AND o2.order_date_created <= DATE_ADD(o.expireon, INTERVAL ? DAY))) AS riacquistato
|
||
FROM orderbook o
|
||
WHERE o.iduser IS NOT NULL AND o.expireon >= ? AND o.expireon < ?
|
||
AND o.expireon <= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||
GROUP BY o.iduser", [$grazia, $g1, $g2, $grazia]);
|
||
$m['scaduti'] = count($scad);
|
||
$m['riacquisto'] = pctv(count(array_filter($scad, fn($r) => (int) $r['riacquistato'] === 1)), count($scad));
|
||
|
||
// --- Lezioni rimaste inutilizzate nei pacchetti scaduti
|
||
$u = q(
|
||
$pdo,
|
||
"SELECT COALESCE(SUM(o.nticket), 0) AS comprate,
|
||
COALESCE(SUM(LEAST(o.nticket, (SELECT COUNT(*) FROM bookingclass bc
|
||
WHERE bc.idorder = o.idorderbook AND bc.status NOT IN ('cancelled', 'waiting')))), 0) AS usate
|
||
FROM orderbook o
|
||
WHERE o.expireon >= ? AND o.expireon < ? AND o.expireon < CURDATE() AND o.nticket > 0",
|
||
[substr($d1, 0, 10), substr($d2, 0, 10)]
|
||
)[0];
|
||
$m['pacchetti_lezioni'] = (int) $u['comprate'];
|
||
$m['non_usate'] = (int) $u['comprate'] - (int) $u['usate'];
|
||
$m['non_usate_pct'] = pctv($m['non_usate'], (int) $u['comprate']);
|
||
|
||
return $m;
|
||
}
|
||
|
||
$d1 = $inizio->format('Y-m-d 00:00:00');
|
||
$d2 = $fine->format('Y-m-d 00:00:00');
|
||
$d1p = $inizioP->format('Y-m-d 00:00:00');
|
||
|
||
$A = metriche($pdo, $d1, $d2, $now, $SOGLIE['soglia_debole'], $SOGLIE['giorni_rinnovo']);
|
||
$P = metriche($pdo, $d1p, $d1, $now, $SOGLIE['soglia_debole'], $SOGLIE['giorni_rinnovo']);
|
||
|
||
// Vendite e presenze sono stagionali (settembre pieno, estate vuota): se ci sono dati,
|
||
// le confrontiamo con lo STESSO periodo dell'anno scorso invece che con quello appena prima.
|
||
$d1y = date('Y-m-d H:i:s', strtotime($d1 . ' -1 year'));
|
||
$d2y = date('Y-m-d H:i:s', strtotime($d2 . ' -1 year'));
|
||
$primoOrdine = q1($pdo, "SELECT MIN(order_date_created) FROM orderbook");
|
||
$haAnnoPrima = $primoOrdine && $primoOrdine <= substr($d1y, 0, 10);
|
||
$Y = $haAnnoPrima ? metriche($pdo, $d1y, $d2y, $d2y, $SOGLIE['soglia_debole'], $SOGLIE['giorni_rinnovo']) : $P;
|
||
$etichettaY = $haAnnoPrima ? 'stesso periodo ' . date('Y', strtotime($d1y)) : 'periodo prima';
|
||
|
||
/** Abbonati con un pacchetto valido in una certa data (non dipende dalla durata dei pacchetti). */
|
||
function abbonati(PDO $pdo, string $data): int
|
||
{
|
||
return (int) q1($pdo, "SELECT COUNT(DISTINCT iduser) FROM orderbook
|
||
WHERE iduser IS NOT NULL AND order_date_created <= ? AND expireon >= ?", [$data, $data]);
|
||
}
|
||
$abbOggi = abbonati($pdo, date('Y-m-d'));
|
||
|
||
// Numeri di andamento (senza giudizio)
|
||
$ultimoAnno = q($pdo, "SELECT COALESCE(SUM(nticket), 0) AS lezioni, COUNT(*) AS ordini
|
||
FROM orderbook WHERE order_date_created > DATE_SUB(CURDATE(), INTERVAL 12 MONTH)")[0];
|
||
$nuoviAnno = (int) q1($pdo, "SELECT COUNT(*) FROM (SELECT iduser, MIN(order_date_created) AS primo FROM orderbook
|
||
WHERE iduser IS NOT NULL GROUP BY iduser) t WHERE t.primo > DATE_SUB(CURDATE(), INTERVAL 12 MONTH)");
|
||
$mesiDati = max(1, min(12, (int) ceil((time() - strtotime((string) (q1($pdo, "SELECT MIN(order_date_created) FROM orderbook") ?: date('Y-m-d')))) / (86400 * 30.4))));
|
||
$mesiPeriodo = $giorni / 30.4;
|
||
|
||
// Lezioni ancora da fare nei pacchetti validi oggi = riempimento "già venduto" delle prossime settimane
|
||
$portafoglio = (int) q1($pdo, "SELECT COALESCE(SUM(GREATEST(o.nticket - (SELECT COUNT(*) FROM bookingclass bc
|
||
WHERE bc.idorder = o.idorderbook AND bc.status NOT IN ('cancelled', 'waiting')
|
||
AND bc.bookingstart <= NOW()), 0)), 0)
|
||
FROM orderbook o WHERE o.expireon >= CURDATE() AND o.nticket > 0");
|
||
|
||
$info = [
|
||
['fa-ticket-alt', 'Lezioni vendute', n0($A['vendute']), n0($A['ordini']) . ' ordini · media ultimo anno: ' . n0((int) $ultimoAnno['lezioni'] / $mesiDati * $mesiPeriodo) . ' in un periodo così'],
|
||
['fa-user-plus', 'Nuovi clienti', n0($A['nuovi']), 'media ultimo anno: ' . number_format($nuoviAnno / $mesiDati, 1, ',', '.') . ' al mese'],
|
||
['fa-users', 'Persone in sala', n0($A['persone']), 'almeno una lezione frequentata nel periodo'],
|
||
['fa-layer-group', 'Lezioni già vendute da fare', n0($portafoglio), 'nei pacchetti validi oggi: è il riempimento delle prossime settimane'],
|
||
];
|
||
$abbPrima = $haAnnoPrima ? abbonati($pdo, date('Y-m-d', strtotime('-1 year'))) : abbonati($pdo, substr($d1, 0, 10));
|
||
|
||
// --- Certificati (situazione di oggi)
|
||
$cert = q($pdo, "SELECT COUNT(*) AS attivi,
|
||
COALESCE(SUM(c.scad >= CURDATE()), 0) AS validi
|
||
FROM (SELECT DISTINCT iduser FROM orderbook WHERE expireon >= CURDATE() AND iduser IS NOT NULL) a
|
||
LEFT JOIN (SELECT iduser, MAX(expirydatedocument) AS scad FROM certificateuserprofile GROUP BY iduser) c
|
||
ON c.iduser = a.iduser")[0];
|
||
$certPct = pctv((int) $cert['validi'], (int) $cert['attivi']);
|
||
$certMancanti = (int) $cert['attivi'] - (int) $cert['validi'];
|
||
|
||
/* =========================================================== INDICATORI */
|
||
|
||
$ind = [];
|
||
|
||
/** Aggiunge un indicatore. $testi = [verde, giallo, rosso] con il consiglio per ogni stato. */
|
||
$add = function (string $titolo, string $icona, string $valore, string $stato, ?string $confronto, array $testi, string $dettaglio = '') use (&$ind) {
|
||
$i = ['verde' => 0, 'giallo' => 1, 'rosso' => 2][$stato] ?? null;
|
||
$ind[] = [
|
||
'titolo' => $titolo,
|
||
'icona' => $icona,
|
||
'valore' => $valore,
|
||
'stato' => $stato,
|
||
'confronto' => $confronto,
|
||
'testo' => $i === null ? 'Dati non ancora sufficienti in questo periodo.' : $testi[$i],
|
||
'dettaglio' => $dettaglio,
|
||
];
|
||
};
|
||
|
||
/** Testo del confronto con il periodo precedente. */
|
||
$conf = function ($cur, $prev, string $unita = '', bool $punti = false, string $rif = 'prima'): ?string {
|
||
if ($prev === null || $cur === null) return null;
|
||
$tail = ' (' . $rif . ': ' . n0($prev) . $unita . ')';
|
||
if ($punti) {
|
||
$d = round((float) $cur - (float) $prev, 1);
|
||
if (abs($d) < 0.5) return '= stabile' . $tail;
|
||
return ($d > 0 ? '▲ +' : '▼ ') . number_format($d, 0, ',', '.') . ' punti' . $tail;
|
||
}
|
||
$d = delta($cur, $prev);
|
||
if ($d === null) return $rif . ': ' . n0($prev);
|
||
if (abs($d) < 1) return '= stabile' . $tail;
|
||
return ($d > 0 ? '▲ +' : '▼ ') . number_format($d, 0, ',', '.') . '%' . $tail;
|
||
};
|
||
|
||
$trendStato = function ($cur, $prev) use ($SOGLIE): string {
|
||
if ($prev === null || (float) $prev == 0.0) return $cur > 0 ? 'verde' : 'grigio';
|
||
return semaforo(delta($cur, $prev), $SOGLIE['trend'], true);
|
||
};
|
||
|
||
// 1. Riempimento
|
||
$add(
|
||
'Riempimento delle lezioni',
|
||
'fa-chart-pie',
|
||
$A['riempimento'] === null ? '–' : n0($A['riempimento']) . '%',
|
||
semaforo($A['riempimento'], $SOGLIE['riempimento'], true),
|
||
$conf($A['riempimento'], $P['riempimento'], '%', true),
|
||
[
|
||
'Le sale sono ben piene: ottimo equilibrio tra orari e domanda.',
|
||
'C\'è spazio libero: promuovi le classi meno piene o valuta di accorparle.',
|
||
'Troppi posti vuoti: rivedi orari e numero di classi, o lancia una promozione mirata.',
|
||
],
|
||
n0($A['lezioni_svolte']) . ' lezioni svolte · ' . n0($A['piene_pct'] ?? 0) . '% al completo'
|
||
);
|
||
|
||
// 2. Abbonati attivi oggi (indipendente dalla durata dei pacchetti)
|
||
$add(
|
||
'Abbonati attivi',
|
||
'fa-id-card',
|
||
n0($abbOggi),
|
||
$trendStato($abbOggi, $abbPrima),
|
||
$conf($abbOggi, $abbPrima, '', false, $haAnnoPrima ? 'un anno fa' : 'inizio periodo'),
|
||
[
|
||
'Il numero di persone con un pacchetto valido tiene o cresce.',
|
||
'Ci sono meno abbonati attivi: punta sui rinnovi di chi è in scadenza.',
|
||
'Gli abbonati attivi sono in forte calo: è l\'indicatore più importante da recuperare.',
|
||
],
|
||
'con un pacchetto valido oggi'
|
||
);
|
||
|
||
/*
|
||
* Vendite, nuovi clienti e persone in sala NON hanno il semaforo:
|
||
* con pacchetti da 12 lezioni (circa 3 mesi) chi compra non riordina per mesi,
|
||
* quindi le vendite di un periodo salgono e scendono a ondate anche se la scuola va bene,
|
||
* e in un bacino di paese i nuovi clienti sono pochi per natura.
|
||
* Li mostriamo come numeri di andamento (con la media mensile dell'ultimo anno come riferimento).
|
||
* Lo stato di salute lo danno "Abbonati attivi" e "Clienti che rinnovano".
|
||
*/
|
||
|
||
// 5. Riacquisto
|
||
$add(
|
||
'Clienti che rinnovano',
|
||
'fa-rotate',
|
||
$A['riacquisto'] === null ? '–' : n0($A['riacquisto']) . '%',
|
||
semaforo($A['riacquisto'], $SOGLIE['riacquisto'], true),
|
||
$conf($A['riacquisto'], $P['riacquisto'], '%', true),
|
||
[
|
||
'La maggior parte di chi finisce un pacchetto ne compra un altro: ottima fidelizzazione.',
|
||
'Una parte non rinnova: un promemoria prima della scadenza può fare la differenza.',
|
||
'Molti non rinnovano: chiedi un riscontro a chi non torna e prepara un\'offerta di rinnovo.',
|
||
],
|
||
'entro ' . $SOGLIE['giorni_rinnovo'] . ' giorni dalla scadenza · ' . n0($A['scaduti']) . ' pacchetti finiti, valutati solo dopo ' . $SOGLIE['giorni_rinnovo'] . ' giorni'
|
||
);
|
||
|
||
// 6. Lezioni perse
|
||
$add(
|
||
'Lezioni perse',
|
||
'fa-wind',
|
||
$A['perse_pct'] === null ? '–' : n0($A['perse_pct']) . '%',
|
||
semaforo($A['perse_pct'], $SOGLIE['perse'], false),
|
||
$conf($A['perse_pct'], $P['perse_pct'], '%', true),
|
||
[
|
||
'Quasi tutti si presentano alle lezioni prenotate.',
|
||
'Qualche assenza di troppo: un promemoria il giorno prima aiuta.',
|
||
'Molte assenze: posti bloccati e clienti scontenti. Valuta promemoria e regole più chiare.',
|
||
],
|
||
n0($A['perse']) . ' perse su ' . n0($A['presenze'] + $A['perse']) . ' prenotazioni'
|
||
);
|
||
|
||
// 7. Cancellazioni
|
||
if ($A['canc'] !== null) {
|
||
$add(
|
||
'Cancellazioni',
|
||
'fa-ban',
|
||
n0($A['canc_pct'] ?? 0) . '%',
|
||
semaforo($A['canc_pct'], $SOGLIE['cancellazioni'], false),
|
||
$conf($A['canc_pct'], $P['canc_pct'], '%', true),
|
||
[
|
||
'Poche cancellazioni: le prenotazioni sono affidabili.',
|
||
'Una parte delle prenotazioni viene cancellata: controlla se riguarda sempre le stesse classi o persone.',
|
||
'Troppe cancellazioni: i posti si liberano tardi e restano vuoti.',
|
||
],
|
||
n0($A['canc']) . ' cancellazioni nel periodo'
|
||
);
|
||
|
||
$add(
|
||
'Cancellazioni all\'ultimo',
|
||
'fa-stopwatch',
|
||
$A['last_minute_pct'] === null ? '–' : n0($A['last_minute_pct']) . '%',
|
||
semaforo($A['last_minute_pct'], $SOGLIE['last_minute'], false),
|
||
$conf($A['last_minute_pct'], $P['last_minute_pct'], '%', true),
|
||
[
|
||
'Chi cancella lo fa con anticipo: il posto si può riassegnare.',
|
||
'Una parte cancella a ridosso della lezione: ricorda le regole di cancellazione.',
|
||
'Molte cancellazioni con meno di 24 ore: posti persi. Valuta regole più rigide.',
|
||
],
|
||
'meno di 24 ore prima della lezione'
|
||
);
|
||
}
|
||
|
||
// 8. Lezioni non usate nei pacchetti scaduti
|
||
$add(
|
||
'Lezioni non usate',
|
||
'fa-hourglass-end',
|
||
$A['non_usate_pct'] === null ? '–' : n0($A['non_usate_pct']) . '%',
|
||
semaforo($A['non_usate_pct'], $SOGLIE['non_usate'], false),
|
||
$conf($A['non_usate_pct'], $P['non_usate_pct'], '%', true),
|
||
[
|
||
'I clienti usano quasi tutte le lezioni comprate prima della scadenza.',
|
||
'Alcune lezioni scadono senza essere usate: avvisa chi è vicino alla scadenza.',
|
||
'Molte lezioni scadono inutilizzate: rischio di clienti scontenti che non rinnovano.',
|
||
],
|
||
n0($A['non_usate']) . ' lezioni su ' . n0($A['pacchetti_lezioni']) . ' nei pacchetti scaduti'
|
||
);
|
||
|
||
// 9. Classi deboli
|
||
$nDeboli = count($A['classi_deboli']);
|
||
$add(
|
||
'Classi poco frequentate',
|
||
'fa-person-chalkboard',
|
||
n0($nDeboli),
|
||
$A['lezioni_svolte'] > 0 ? semaforo($nDeboli, $SOGLIE['classi_deboli'], false) : 'grigio',
|
||
$conf($nDeboli, count($P['classi_deboli'])),
|
||
[
|
||
'Tutte le classi hanno una buona partecipazione.',
|
||
'Qualche classe fatica: valuta cambio orario, promozione o accorpamento.',
|
||
'Diverse classi sono sotto il ' . $SOGLIE['soglia_debole'] . '%: conviene rivedere il calendario.',
|
||
],
|
||
$nDeboli ? implode(', ', array_map(fn($c) => $c['nome'] . ($c['quando'] ? ' (' . $c['quando'] . ')' : ''), array_slice($A['classi_deboli'], 0, 3))) . ($nDeboli > 3 ? '…' : '') : 'sotto il ' . $SOGLIE['soglia_debole'] . '% di riempimento'
|
||
);
|
||
|
||
// 10. Certificati
|
||
$add(
|
||
'Certificati medici',
|
||
'fa-file-medical',
|
||
$certPct === null ? '–' : n0($certPct) . '%',
|
||
semaforo($certPct, $SOGLIE['certificati'], true),
|
||
null,
|
||
[
|
||
'Quasi tutti gli abbonati sono in regola.',
|
||
'Alcuni abbonati hanno il certificato scaduto o mancante: manda il promemoria.',
|
||
'Troppi abbonati senza certificato valido: è un rischio per la scuola.',
|
||
],
|
||
$certMancanti > 0 ? n0($certMancanti) . ' abbonati attivi da sistemare' : 'abbonati attivi oggi'
|
||
);
|
||
|
||
/* =========================================================== GIUDIZIO COMPLESSIVO */
|
||
|
||
$conta = ['verde' => 0, 'giallo' => 0, 'rosso' => 0, 'grigio' => 0];
|
||
foreach ($ind as $i) $conta[$i['stato']]++;
|
||
$valutati = $conta['verde'] + $conta['giallo'] + $conta['rosso'];
|
||
$punteggio = $valutati ? (int) round(($conta['verde'] * 100 + $conta['giallo'] * 50) / $valutati) : null;
|
||
|
||
if ($punteggio === null) {
|
||
$statoGen = 'grigio';
|
||
$frase = 'Non ci sono ancora abbastanza dati in questo periodo.';
|
||
} elseif ($conta['rosso'] >= 3 || $punteggio < 50) {
|
||
$statoGen = 'rosso';
|
||
$frase = 'Ci sono diversi punti critici: conviene intervenire subito su quelli in rosso.';
|
||
} elseif ($conta['rosso'] > 0 || $punteggio < 75) {
|
||
$statoGen = 'giallo';
|
||
$frase = 'La scuola va abbastanza bene, ma alcuni aspetti meritano attenzione.';
|
||
} else {
|
||
$statoGen = 'verde';
|
||
$frase = 'La scuola è in salute: la maggior parte degli indicatori è positiva.';
|
||
}
|
||
|
||
$gruppi = [
|
||
'verde' => ['Cosa va bene', 'fa-circle-check'],
|
||
'giallo' => ['Da migliorare', 'fa-circle-exclamation'],
|
||
'rosso' => ['Cosa non va', 'fa-circle-xmark'],
|
||
];
|
||
|
||
// Classi migliori / peggiori
|
||
$classiOrd = array_values(array_filter($A['classi'], fn($c) => $c['lezioni'] >= 2 && $c['fill'] !== null));
|
||
usort($classiOrd, fn($a, $b) => $b['fill'] <=> $a['fill']);
|
||
$migliori = array_slice($classiOrd, 0, 3);
|
||
$peggiori = array_slice(array_reverse($classiOrd), 0, 3);
|
||
|
||
$periodoTxt = $inizio->format('d/m/Y') . ' – ' . $oggi->format('d/m/Y');
|
||
$prevTxt = $inizioP->format('d/m/Y') . ' – ' . (clone $inizio)->modify('-1 day')->format('d/m/Y');
|
||
?>
|
||
<!doctype html>
|
||
<html lang="it">
|
||
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>YogiBook - Sommario</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta content="YogiBook - Prenotazione facile YogaSoul" name="description" />
|
||
<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">
|
||
|
||
<style>
|
||
:root {
|
||
--verde: #1a9e5f;
|
||
--verde-bg: #e7f7ef;
|
||
--giallo: #d49a00;
|
||
--giallo-bg: #fff6dc;
|
||
--rosso: #d64545;
|
||
--rosso-bg: #fdecec;
|
||
--grigio: #9ca3af;
|
||
--grigio-bg: #f3f4f6;
|
||
--ink: #1f2937;
|
||
--muted: #6b7280;
|
||
--sabbia: #E1D0B8;
|
||
}
|
||
|
||
.sm-card {
|
||
background: #fff;
|
||
border-radius: 16px;
|
||
box-shadow: 0 4px 22px rgba(0, 0, 0, .06);
|
||
}
|
||
|
||
/* ---- Periodo */
|
||
.sm-period {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.sm-chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.sm-chip {
|
||
border: 1px solid #e2e4e9;
|
||
background: #fff;
|
||
color: #374151;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
padding: 6px 14px;
|
||
border-radius: 20px;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.sm-chip:hover {
|
||
border-color: #b08d57;
|
||
color: #374151;
|
||
}
|
||
|
||
.sm-chip.active {
|
||
background: var(--sabbia);
|
||
border-color: var(--sabbia);
|
||
color: #5a4630;
|
||
}
|
||
|
||
.sm-period-txt {
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* ---- Hero */
|
||
.sm-hero {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto;
|
||
gap: 28px;
|
||
align-items: center;
|
||
padding: 26px 30px;
|
||
margin-bottom: 22px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sm-hero::after {
|
||
content: '';
|
||
position: absolute;
|
||
right: -60px;
|
||
top: -60px;
|
||
width: 240px;
|
||
height: 240px;
|
||
border-radius: 50%;
|
||
opacity: .08;
|
||
background: var(--c);
|
||
}
|
||
|
||
.sm-light {
|
||
background: #1f2937;
|
||
border-radius: 22px;
|
||
padding: 12px 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 9px;
|
||
box-shadow: inset 0 0 0 3px #374151;
|
||
}
|
||
|
||
.sm-light span {
|
||
width: 38px;
|
||
height: 38px;
|
||
border-radius: 50%;
|
||
background: #374151;
|
||
transition: all .3s;
|
||
}
|
||
|
||
.sm-light .r.on {
|
||
background: #ff5b5b;
|
||
box-shadow: 0 0 18px #ff5b5b;
|
||
}
|
||
|
||
.sm-light .g.on {
|
||
background: #ffc531;
|
||
box-shadow: 0 0 18px #ffc531;
|
||
}
|
||
|
||
.sm-light .v.on {
|
||
background: #33d17a;
|
||
box-shadow: 0 0 18px #33d17a;
|
||
}
|
||
|
||
.sm-kicker {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
letter-spacing: .08em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.sm-hero h2 {
|
||
font-size: 26px;
|
||
font-weight: 800;
|
||
color: var(--ink);
|
||
margin: 4px 0 6px;
|
||
}
|
||
|
||
.sm-hero p {
|
||
color: #4b5563;
|
||
margin: 0;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.sm-score {
|
||
text-align: center;
|
||
}
|
||
|
||
.sm-ring {
|
||
--p: 0;
|
||
margin: 0 auto;
|
||
width: 120px;
|
||
height: 120px;
|
||
border-radius: 50%;
|
||
display: grid;
|
||
place-items: center;
|
||
background: conic-gradient(var(--c) calc(var(--p) * 1%), #eef0f3 0);
|
||
}
|
||
|
||
.sm-ring div {
|
||
width: 92px;
|
||
height: 92px;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
display: grid;
|
||
place-items: center;
|
||
}
|
||
|
||
.sm-ring strong {
|
||
font-size: 30px;
|
||
font-weight: 800;
|
||
color: var(--ink);
|
||
line-height: 1;
|
||
}
|
||
|
||
.sm-ring small {
|
||
display: block;
|
||
font-size: 11px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.sm-counts {
|
||
display: flex;
|
||
gap: 14px;
|
||
justify-content: center;
|
||
margin-top: 10px;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.sm-counts span::before {
|
||
content: '';
|
||
display: inline-block;
|
||
width: 9px;
|
||
height: 9px;
|
||
border-radius: 50%;
|
||
margin-right: 5px;
|
||
background: var(--c);
|
||
}
|
||
|
||
/* ---- Colonne */
|
||
.sm-col-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 15px;
|
||
font-weight: 800;
|
||
color: var(--c);
|
||
margin: 4px 0 12px;
|
||
}
|
||
|
||
.sm-col-head .n {
|
||
background: var(--bg);
|
||
color: var(--c);
|
||
border-radius: 20px;
|
||
padding: 1px 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.sm-empty {
|
||
border: 2px dashed #e5e7eb;
|
||
border-radius: 14px;
|
||
padding: 18px;
|
||
color: var(--grigio);
|
||
text-align: center;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.sm-ind {
|
||
padding: 16px 18px;
|
||
margin-bottom: 14px;
|
||
border-left: 5px solid var(--c);
|
||
}
|
||
|
||
.sm-ind-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.sm-ind-ico {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 12px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: var(--bg);
|
||
color: var(--c);
|
||
font-size: 16px;
|
||
flex: 0 0 40px;
|
||
}
|
||
|
||
.sm-ind-title {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: #4b5563;
|
||
}
|
||
|
||
.sm-ind-val {
|
||
font-size: 26px;
|
||
font-weight: 800;
|
||
color: var(--ink);
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.sm-dot {
|
||
margin-left: auto;
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 50%;
|
||
background: var(--c);
|
||
box-shadow: 0 0 0 4px var(--bg);
|
||
flex: 0 0 14px;
|
||
}
|
||
|
||
.sm-ind-det {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.sm-ind-cmp {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
margin-top: 4px;
|
||
color: #4b5563;
|
||
}
|
||
|
||
.sm-ind-txt {
|
||
font-size: 13px;
|
||
color: #374151;
|
||
margin-top: 8px;
|
||
padding-top: 8px;
|
||
border-top: 1px solid #f1f2f4;
|
||
}
|
||
|
||
.st-verde {
|
||
--c: var(--verde);
|
||
--bg: var(--verde-bg);
|
||
}
|
||
|
||
.st-giallo {
|
||
--c: var(--giallo);
|
||
--bg: var(--giallo-bg);
|
||
}
|
||
|
||
.st-rosso {
|
||
--c: var(--rosso);
|
||
--bg: var(--rosso-bg);
|
||
}
|
||
|
||
.st-grigio {
|
||
--c: var(--grigio);
|
||
--bg: var(--grigio-bg);
|
||
}
|
||
|
||
/* ---- Classi */
|
||
.sm-classes {
|
||
padding: 20px 22px;
|
||
margin-bottom: 22px;
|
||
}
|
||
|
||
.sm-classes h5 {
|
||
font-size: 15px;
|
||
font-weight: 800;
|
||
color: var(--ink);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.sm-cls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
}
|
||
|
||
.sm-cls:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.sm-cls .dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
flex: 0 0 10px;
|
||
}
|
||
|
||
.sm-cls .nm {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
color: #374151;
|
||
}
|
||
|
||
.sm-cls .nm small {
|
||
display: block;
|
||
font-weight: 400;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.sm-bar {
|
||
width: 120px;
|
||
height: 8px;
|
||
border-radius: 10px;
|
||
background: #eef0f3;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sm-bar span {
|
||
display: block;
|
||
height: 100%;
|
||
border-radius: 10px;
|
||
background: var(--c);
|
||
}
|
||
|
||
.sm-cls .pc {
|
||
width: 44px;
|
||
text-align: right;
|
||
font-weight: 800;
|
||
font-size: 14px;
|
||
color: var(--ink);
|
||
}
|
||
|
||
.sm-info-head {
|
||
font-size: 15px;
|
||
font-weight: 800;
|
||
color: #1f2937;
|
||
margin: 4px 0 12px;
|
||
}
|
||
|
||
.sm-info-head small {
|
||
display: block;
|
||
font-weight: 400;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.sm-info {
|
||
padding: 14px 16px;
|
||
display: flex;
|
||
gap: 12px;
|
||
height: 100%;
|
||
--c: #2980b9;
|
||
--bg: #e8f1fa;
|
||
}
|
||
|
||
.sm-info .sm-ind-val {
|
||
font-size: 22px;
|
||
}
|
||
|
||
.sm-info .sm-ind-det {
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.sm-foot {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
@media (max-width: 767px) {
|
||
.sm-hero {
|
||
grid-template-columns: 1fr;
|
||
text-align: center;
|
||
padding: 22px 18px;
|
||
}
|
||
|
||
.sm-light {
|
||
flex-direction: row;
|
||
justify-self: center;
|
||
}
|
||
|
||
.sm-bar {
|
||
width: 70px;
|
||
}
|
||
}
|
||
|
||
@media print {
|
||
|
||
.vertical-menu,
|
||
#page-topbar,
|
||
.ishorizontal-topbar,
|
||
.sm-chips,
|
||
footer {
|
||
display: none !important;
|
||
}
|
||
|
||
.main-content {
|
||
margin-left: 0 !important;
|
||
}
|
||
|
||
.page-content {
|
||
padding: 0 !important;
|
||
}
|
||
|
||
.sm-card {
|
||
box-shadow: none;
|
||
border: 1px solid #e5e7eb;
|
||
break-inside: avoid;
|
||
}
|
||
}
|
||
</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">Sommario</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">
|
||
|
||
<!-- Periodo -->
|
||
<div class="sm-period">
|
||
<div class="sm-chips">
|
||
<?php foreach ($periodi as $k => $lbl) : ?>
|
||
<a class="sm-chip <?php echo (int) $k === $giorni ? 'active' : ''; ?>" href="?p=<?php echo e($k); ?>"><?php echo e($lbl); ?></a>
|
||
<?php endforeach; ?>
|
||
<a class="sm-chip" href="#" onclick="window.print();return false;"><i class="fas fa-print me-1"></i> Stampa</a>
|
||
</div>
|
||
<div class="sm-period-txt">
|
||
<i class="far fa-calendar me-1"></i> <strong><?php echo e($periodoTxt); ?></strong>
|
||
· confronto con <?php echo e($prevTxt); ?><?php if ($haAnnoPrima) : ?> (vendite e presenze: <?php echo e($etichettaY); ?>)<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Giudizio complessivo -->
|
||
<div class="sm-card sm-hero st-<?php echo $statoGen; ?>">
|
||
<div class="sm-light" aria-hidden="true">
|
||
<span class="r <?php echo $statoGen === 'rosso' ? 'on' : ''; ?>"></span>
|
||
<span class="g <?php echo $statoGen === 'giallo' ? 'on' : ''; ?>"></span>
|
||
<span class="v <?php echo $statoGen === 'verde' ? 'on' : ''; ?>"></span>
|
||
</div>
|
||
<div>
|
||
<div class="sm-kicker">Stato della scuola · <?php echo e($periodi[(string) $giorni]); ?></div>
|
||
<h2><?php echo e(['verde' => 'Tutto bene', 'giallo' => 'Bene, con margini di miglioramento', 'rosso' => 'Attenzione', 'grigio' => 'Dati insufficienti'][$statoGen]); ?></h2>
|
||
<p><?php echo e($frase); ?></p>
|
||
</div>
|
||
<div class="sm-score">
|
||
<div class="sm-ring" style="--p: <?php echo (int) ($punteggio ?? 0); ?>;">
|
||
<div><span><strong><?php echo $punteggio === null ? '–' : $punteggio; ?></strong><small>su 100</small></span></div>
|
||
</div>
|
||
<div class="sm-counts">
|
||
<span class="st-verde"><?php echo $conta['verde']; ?></span>
|
||
<span class="st-giallo"><?php echo $conta['giallo']; ?></span>
|
||
<span class="st-rosso"><?php echo $conta['rosso']; ?></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Numeri di andamento: senza semaforo -->
|
||
<div class="sm-info-head"><i class="fas fa-chart-line me-1"></i> Andamento del periodo <small>numeri da leggere, senza giudizio: con pacchetti lunghi e un bacino di paese le vendite vanno a ondate</small></div>
|
||
<div class="row g-3 mb-4">
|
||
<?php foreach ($info as [$ic, $tit, $val, $sub]) : ?>
|
||
<div class="col-6 col-lg-3">
|
||
<div class="sm-card sm-info">
|
||
<div class="sm-ind-ico"><i class="fas <?php echo $ic; ?>"></i></div>
|
||
<div>
|
||
<div class="sm-ind-title"><?php echo e($tit); ?></div>
|
||
<div class="sm-ind-val"><?php echo e($val); ?></div>
|
||
<div class="sm-ind-det"><?php echo e($sub); ?></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<!-- Indicatori raggruppati per semaforo -->
|
||
<div class="row">
|
||
<?php foreach ($gruppi as $stato => [$titolo, $icona]) :
|
||
$lista = array_values(array_filter($ind, fn($i) => $i['stato'] === $stato));
|
||
?>
|
||
<div class="col-lg-4">
|
||
<div class="sm-col-head st-<?php echo $stato; ?>">
|
||
<i class="fas <?php echo $icona; ?>"></i> <?php echo e($titolo); ?>
|
||
<span class="n"><?php echo count($lista); ?></span>
|
||
</div>
|
||
<?php if (!$lista) : ?>
|
||
<div class="sm-empty"><?php echo $stato === 'verde' ? 'Nessun indicatore positivo in questo periodo.' : 'Niente da segnalare qui.'; ?></div>
|
||
<?php endif; ?>
|
||
<?php foreach ($lista as $i) : ?>
|
||
<div class="sm-card sm-ind st-<?php echo $i['stato']; ?>">
|
||
<div class="sm-ind-top">
|
||
<div class="sm-ind-ico"><i class="fas <?php echo e($i['icona']); ?>"></i></div>
|
||
<div>
|
||
<div class="sm-ind-title"><?php echo e($i['titolo']); ?></div>
|
||
<div class="sm-ind-val"><?php echo e($i['valore']); ?></div>
|
||
</div>
|
||
<span class="sm-dot" title="<?php echo e($i['stato']); ?>"></span>
|
||
</div>
|
||
<?php if ($i['dettaglio'] !== '') : ?><div class="sm-ind-det"><?php echo e($i['dettaglio']); ?></div><?php endif; ?>
|
||
<?php if ($i['confronto']) : ?><div class="sm-ind-cmp"><?php echo e($i['confronto']); ?></div><?php endif; ?>
|
||
<div class="sm-ind-txt"><i class="far fa-lightbulb me-1"></i> <?php echo e($i['testo']); ?></div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<?php
|
||
$grigi = array_values(array_filter($ind, fn($i) => $i['stato'] === 'grigio'));
|
||
if ($grigi) : ?>
|
||
<div class="sm-foot"><i class="fas fa-circle-info me-1"></i> Senza dati sufficienti nel periodo: <?php echo e(implode(', ', array_column($grigi, 'titolo'))); ?>.</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Classi migliori e peggiori -->
|
||
<?php if ($classiOrd) : ?>
|
||
<div class="row">
|
||
<?php foreach ([['Le classi più piene', $migliori], ['Le classi più vuote', $peggiori]] as [$tit, $lista]) : ?>
|
||
<div class="col-lg-6">
|
||
<div class="sm-card sm-classes">
|
||
<h5><?php echo e($tit); ?></h5>
|
||
<?php foreach ($lista as $c) :
|
||
$st = semaforo($c['fill'], $SOGLIE['riempimento'], true); ?>
|
||
<div class="sm-cls st-<?php echo $st; ?>">
|
||
<span class="dot" style="background:<?php echo e($c['colore']); ?>"></span>
|
||
<div class="nm"><?php echo e($c['nome']); ?><small><?php echo e($c['quando']); ?> · <?php echo n0($c['lezioni']); ?> lezioni</small></div>
|
||
<div class="sm-bar"><span style="width:<?php echo min(100, (float) $c['fill']); ?>%"></span></div>
|
||
<div class="pc"><?php echo n0($c['fill']); ?>%</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="sm-foot">
|
||
Il riempimento e le lezioni perse considerano solo le lezioni già svolte. I rinnovi vengono valutati solo a pacchetto finito.
|
||
Per tutti i dettagli apri le <a href="statistiche.php">Statistiche</a>.
|
||
</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>
|
||
</body>
|
||
|
||
</html>
|