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'); ?>