sommario update

This commit is contained in:
2026-10-16 15:54:23 +02:00
parent 0d3cf5aeaa
commit 3038d75045
+68 -25
View File
@@ -26,6 +26,7 @@ $SOGLIE = [
'classi_deboli' => ['verde' => 0, 'giallo' => 2], // n. classi sotto il 40% di riempimento 'classi_deboli' => ['verde' => 0, 'giallo' => 2], // n. classi sotto il 40% di riempimento
'trend' => ['verde' => -5, 'giallo' => -20], // variazione % rispetto al periodo prima 'trend' => ['verde' => -5, 'giallo' => -20], // variazione % rispetto al periodo prima
'soglia_debole' => 40, // sotto questo riempimento una classe è "debole" 'soglia_debole' => 40, // sotto questo riempimento una classe è "debole"
'giorni_rinnovo' => 30, // giorni concessi dopo la scadenza per rinnovare
]; ];
if (!function_exists('e')) { if (!function_exists('e')) {
@@ -98,7 +99,7 @@ $periodi = [
'180' => 'Ultimi 6 mesi', '180' => 'Ultimi 6 mesi',
'365' => 'Ultimi 12 mesi', '365' => 'Ultimi 12 mesi',
]; ];
$giorni = isset($periodi[$_GET['p'] ?? '']) ? (int) $_GET['p'] : 30; $giorni = isset($periodi[$_GET['p'] ?? '']) ? (int) $_GET['p'] : 90;
$oggi = new DateTime('today'); $oggi = new DateTime('today');
$fine = (clone $oggi)->modify('+1 day'); // escluso $fine = (clone $oggi)->modify('+1 day'); // escluso
@@ -112,7 +113,7 @@ $now = date('Y-m-d H:i:s');
* Tutte le metriche di un periodo [$d1, $d2). * Tutte le metriche di un periodo [$d1, $d2).
* Le lezioni considerano solo quelle già svolte (fino ad adesso). * Le lezioni considerano solo quelle già svolte (fino ad adesso).
*/ */
function metriche(PDO $pdo, string $d1, string $d2, string $now, int $sogliaDebole): array function metriche(PDO $pdo, string $d1, string $d2, string $now, int $sogliaDebole, int $grazia = 30): array
{ {
$m = []; $m = [];
$fineLezioni = min($d2, $now); $fineLezioni = min($d2, $now);
@@ -194,14 +195,22 @@ function metriche(PDO $pdo, string $d1, string $d2, string $now, int $sogliaDebo
$m['last_minute_pct'] = null; $m['last_minute_pct'] = null;
} }
// --- Riacquisto: chi ha un pacchetto scaduto nel periodo, ha comprato di nuovo? // --- 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, $scad = q($pdo, "SELECT o.iduser,
MAX(EXISTS(SELECT 1 FROM orderbook o2 MAX(EXISTS(SELECT 1 FROM orderbook o2
WHERE o2.iduser = o.iduser AND o2.idorderbook <> o.idorderbook WHERE o2.iduser = o.iduser AND o2.idorderbook <> o.idorderbook
AND o2.order_date_created > o.order_date_created)) AS riacquistato 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 FROM orderbook o
WHERE o.iduser IS NOT NULL AND o.expireon >= ? AND o.expireon < ? AND o.expireon < CURDATE() WHERE o.iduser IS NOT NULL AND o.expireon >= ? AND o.expireon < ?
GROUP BY o.iduser", [substr($d1, 0, 10), substr($d2, 0, 10)]); AND o.expireon <= DATE_SUB(CURDATE(), INTERVAL ? DAY)
GROUP BY o.iduser", [$grazia, $g1, $g2, $grazia]);
$m['scaduti'] = count($scad); $m['scaduti'] = count($scad);
$m['riacquisto'] = pctv(count(array_filter($scad, fn($r) => (int) $r['riacquistato'] === 1)), count($scad)); $m['riacquisto'] = pctv(count(array_filter($scad, fn($r) => (int) $r['riacquistato'] === 1)), count($scad));
@@ -226,8 +235,26 @@ $d1 = $inizio->format('Y-m-d 00:00:00');
$d2 = $fine->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'); $d1p = $inizioP->format('Y-m-d 00:00:00');
$A = metriche($pdo, $d1, $d2, $now, $SOGLIE['soglia_debole']); $A = metriche($pdo, $d1, $d2, $now, $SOGLIE['soglia_debole'], $SOGLIE['giorni_rinnovo']);
$P = metriche($pdo, $d1p, $d1, $now, $SOGLIE['soglia_debole']); $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'));
$abbPrima = $haAnnoPrima ? abbonati($pdo, date('Y-m-d', strtotime('-1 year'))) : abbonati($pdo, substr($d1, 0, 10));
// --- Certificati (situazione di oggi) // --- Certificati (situazione di oggi)
$cert = q($pdo, "SELECT COUNT(*) AS attivi, $cert = q($pdo, "SELECT COUNT(*) AS attivi,
@@ -257,17 +284,18 @@ $add = function (string $titolo, string $icona, string $valore, string $stato, ?
}; };
/** Testo del confronto con il periodo precedente. */ /** Testo del confronto con il periodo precedente. */
$conf = function ($cur, $prev, string $unita = '', bool $punti = false): ?string { $conf = function ($cur, $prev, string $unita = '', bool $punti = false, string $rif = 'prima'): ?string {
if ($prev === null || $cur === null) return null; if ($prev === null || $cur === null) return null;
$tail = ' (' . $rif . ': ' . n0($prev) . $unita . ')';
if ($punti) { if ($punti) {
$d = round((float) $cur - (float) $prev, 1); $d = round((float) $cur - (float) $prev, 1);
if (abs($d) < 0.5) return '= stabile (prima ' . n0($prev) . $unita . ')'; if (abs($d) < 0.5) return '= stabile' . $tail;
return ($d > 0 ? '▲ +' : '▼ ') . number_format($d, 0, ',', '.') . ' punti (prima ' . n0($prev) . $unita . ')'; return ($d > 0 ? '▲ +' : '▼ ') . number_format($d, 0, ',', '.') . ' punti' . $tail;
} }
$d = delta($cur, $prev); $d = delta($cur, $prev);
if ($d === null) return 'prima: ' . n0($prev); if ($d === null) return $rif . ': ' . n0($prev);
if (abs($d) < 1) return '= stabile (prima ' . n0($prev) . ')'; if (abs($d) < 1) return '= stabile' . $tail;
return ($d > 0 ? '▲ +' : '▼ ') . number_format($d, 0, ',', '.') . '% (prima ' . n0($prev) . ')'; return ($d > 0 ? '▲ +' : '▼ ') . number_format($d, 0, ',', '.') . '%' . $tail;
}; };
$trendStato = function ($cur, $prev) use ($SOGLIE): string { $trendStato = function ($cur, $prev) use ($SOGLIE): string {
@@ -290,15 +318,30 @@ $add(
n0($A['lezioni_svolte']) . ' lezioni svolte · ' . n0($A['piene_pct'] ?? 0) . '% al completo' n0($A['lezioni_svolte']) . ' lezioni svolte · ' . n0($A['piene_pct'] ?? 0) . '% al completo'
); );
// 2. Lezioni vendute // 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'
);
// 3. Lezioni vendute
$add( $add(
'Lezioni vendute', 'Lezioni vendute',
'fa-ticket-alt', 'fa-ticket-alt',
n0($A['vendute']), n0($A['vendute']),
$trendStato($A['vendute'], $P['vendute']), $trendStato($A['vendute'], $Y['vendute']),
$conf($A['vendute'], $P['vendute']), $conf($A['vendute'], $Y['vendute'], '', false, $etichettaY),
[ [
'Le vendite tengono o crescono rispetto al periodo prima.', 'Le vendite tengono o crescono rispetto al ' . $etichettaY . '.',
'Vendite in calo: una comunicazione o un\'offerta a chi ha il pacchetto in scadenza può aiutare.', 'Vendite in calo: una comunicazione o un\'offerta a chi ha il pacchetto in scadenza può aiutare.',
'Vendite in forte calo: serve un\'azione commerciale (promozione, evento, newsletter).', 'Vendite in forte calo: serve un\'azione commerciale (promozione, evento, newsletter).',
], ],
@@ -310,8 +353,8 @@ $add(
'Nuovi clienti', 'Nuovi clienti',
'fa-user-plus', 'fa-user-plus',
n0($A['nuovi']), n0($A['nuovi']),
$trendStato($A['nuovi'], $P['nuovi']), $trendStato($A['nuovi'], $Y['nuovi']),
$conf($A['nuovi'], $P['nuovi']), $conf($A['nuovi'], $Y['nuovi'], '', false, $etichettaY),
[ [
'Arrivano persone nuove: la scuola si fa conoscere.', 'Arrivano persone nuove: la scuola si fa conoscere.',
'Meno nuovi ingressi del solito: spingi lezioni di prova e passaparola.', 'Meno nuovi ingressi del solito: spingi lezioni di prova e passaparola.',
@@ -325,11 +368,11 @@ $add(
'Persone che praticano', 'Persone che praticano',
'fa-users', 'fa-users',
n0($A['persone']), n0($A['persone']),
$trendStato($A['persone'], $P['persone']), $trendStato($A['persone'], $Y['persone']),
$conf($A['persone'], $P['persone']), $conf($A['persone'], $Y['persone'], '', false, $etichettaY),
[ [
'La comunità che pratica è stabile o in crescita.', 'La comunità che pratica è stabile o in crescita.',
'Meno persone in sala del periodo prima: contatta chi non viene da un po\'.', 'Meno persone in sala rispetto al ' . $etichettaY . ': contatta chi non viene da un po\'.',
'Calo forte di persone attive: verifica abbandoni e pacchetti non rinnovati.', 'Calo forte di persone attive: verifica abbandoni e pacchetti non rinnovati.',
], ],
'almeno una lezione frequentata' 'almeno una lezione frequentata'
@@ -347,7 +390,7 @@ $add(
'Una parte non rinnova: un promemoria prima della scadenza può fare la differenza.', '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.', 'Molti non rinnovano: chiedi un riscontro a chi non torna e prepara un\'offerta di rinnovo.',
], ],
n0($A['scaduti']) . ' clienti con pacchetto scaduto nel periodo' 'entro ' . $SOGLIE['giorni_rinnovo'] . ' giorni dalla scadenza · ' . n0($A['scaduti']) . ' pacchetti finiti, valutati solo dopo ' . $SOGLIE['giorni_rinnovo'] . ' giorni'
); );
// 6. Lezioni perse // 6. Lezioni perse
@@ -971,7 +1014,7 @@ $prevTxt = $inizioP->format('d/m/Y') . ' – ' . (clone $inizio)->modify('-1
</div> </div>
<div class="sm-period-txt"> <div class="sm-period-txt">
<i class="far fa-calendar me-1"></i> <strong><?php echo e($periodoTxt); ?></strong> <i class="far fa-calendar me-1"></i> <strong><?php echo e($periodoTxt); ?></strong>
· confronto con <?php echo e($prevTxt); ?> · confronto con <?php echo e($prevTxt); ?><?php if ($haAnnoPrima) : ?> (vendite e presenze: <?php echo e($etichettaY); ?>)<?php endif; ?>
</div> </div>
</div> </div>