classi di oggi * php cron/class_report.php 2026-09-25 -> classi di una data specifica * php cron/class_report.php --dry-run -> NON invia, salva le anteprime HTML in cron/preview/ */ use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\Exception as MailException; if (PHP_SAPI !== 'cli') { http_response_code(403); exit('Accesso consentito solo da CLI'); } date_default_timezone_set('Europe/Rome'); $root = dirname(__DIR__); chdir($root); require_once $root . '/public/class/db-functions.php'; // include anche vendor/autoload.php $pdo = DBHandlerSelect::getInstance()->getConnection(); // carica anche il .env /* ---------------------------------------------------------------- helper */ function cfg(string $key, $default = null) { $v = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key); return ($v === false || $v === null || $v === '') ? $default : $v; } function h($v): string { return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); } function logmsg(string $m): void { echo '[' . date('Y-m-d H:i:s') . '] ' . $m . PHP_EOL; } function chip(string $label, string $bg, string $fg): string { return '' . h($label) . ''; } function fullName(array $p): string { $n = trim(($p['first_name'] ?? '') . ' ' . ($p['last_name'] ?? '')); return $n !== '' ? $n : 'Utente #' . (int) ($p['iduser'] ?? 0); } function italianDate(DateTime $d): string { $days = ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato']; $months = ['', 'Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre']; return $days[(int) $d->format('w')] . ' ' . (int) $d->format('j') . ' ' . $months[(int) $d->format('n')] . ' ' . $d->format('Y'); } /* ----------------------------------------------------------- argomenti */ $dryRun = in_array('--dry-run', $argv, true); $day = date('Y-m-d'); foreach (array_slice($argv, 1) as $a) { if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $a)) { $day = $a; } } $recipients = array_values(array_filter(array_map('trim', explode(',', (string) cfg('TEACHER_EMAIL', ''))))); if (!$recipients && !$dryRun) { logmsg('ERRORE: TEACHER_EMAIL non impostato nel .env'); exit(1); } $location = cfg('CLASS_LOCATION', 'via Valassina 62/B Seregno - Sala Contesto Yoga'); /* ------------------------------------------------------ classi del giorno */ $stmt = $pdo->prepare( "SELECT ss.idserviceschedule, ss.dateschedule, s.servicename, s.colorclass, s.maxcapacity FROM serviceschedule ss LEFT JOIN service s ON ss.idservice = s.idservice WHERE ss.dateschedule >= :d1 AND ss.dateschedule < :d2 ORDER BY ss.dateschedule" ); $stmt->execute([ ':d1' => $day . ' 00:00:00', ':d2' => date('Y-m-d', strtotime($day . ' +1 day')) . ' 00:00:00', ]); $classes = $stmt->fetchAll(); if (!$classes) { logmsg("Nessuna classe il $day. Nessuna mail inviata."); exit(0); } $ids = array_map(fn($c) => (int) $c['idserviceschedule'], $classes); $ph = implode(',', array_fill(0, count($ids), '?')); $stmtB = $pdo->prepare( "SELECT bc.idbookingclass, bc.idserviceschedule, bc.lostlesson, bc.status, u.id AS iduser, u.first_name, u.last_name, u.email FROM bookingclass bc LEFT JOIN auth_users u ON bc.iduser = u.id WHERE bc.idserviceschedule IN ($ph) AND bc.status IN ('booked', 'pending', 'waiting') ORDER BY bc.created_at, u.last_name, u.first_name" ); $stmtB->execute($ids); $bookingsBySchedule = []; foreach ($stmtB->fetchAll() as $b) { $bookingsBySchedule[(int) $b['idserviceschedule']][] = $b; } /* ------------------------------------------------------------ template */ function personRows(array $list, string $type): string { if (!$list) { return 'Nessun partecipante'; } $numColor = ['booked' => '#1ebf73', 'pending' => '#8a6d00', 'waiting' => '#d1698a'][$type]; $html = ''; $i = 1; foreach ($list as $p) { $isLost = ($type === 'booked' && ($p['lostlesson'] ?? '') === 'Y'); if ($type === 'booked') { $chipHtml = $isLost ? chip('Persa', '#1f2937', '#ffffff') : chip('Confermata', '#e3f2ec', '#1a7f52'); } elseif ($type === 'pending') { $chipHtml = chip('In attesa', '#fff4d6', '#8a6d00'); } else { $chipHtml = chip('In coda', '#fbeef3', '#d1698a'); } $nameColor = $isLost ? '#9ca3af' : '#1f2937'; $email = !empty($p['email']) ? '
' . h($p['email']) . '' : ''; $html .= '' . '' . '' . '' . '' . '
' . $i . ($type === 'waiting' ? '°' : '.') . '' . h(fullName($p)) . $email . '' . $chipHtml . '
'; $i++; } return $html; } function section(string $title, string $color, string $rows): string { return '' . '
' . $title . '
' . '' . $rows . '
' . ''; } function statBox(string $value, string $label, string $color): string { return '' . '
' . $value . '
' . '
' . $label . '
' . ''; } function renderEmail(array $cls, array $booked, array $pending, array $waiting, string $location): string { $d = new DateTime($cls['dateschedule']); $accent = $cls['colorclass'] ?: '#1ebf73'; $max = (int) $cls['maxcapacity']; $nBooked = count($booked); $nLost = count(array_filter($booked, fn($b) => ($b['lostlesson'] ?? '') === 'Y')); $free = $max > 0 ? max(0, $max - $nBooked) : '–'; $isFull = $max > 0 && $nBooked >= $max; $stats = '' . statBox($nBooked . '/' . $max, 'Iscritti', $isFull ? '#c0392b' : '#1a7f52') . statBox((string) $free, 'Posti liberi', '#374151') . statBox((string) count($pending), 'In attesa', '#8a6d00') . statBox((string) count($waiting), 'In coda', '#d1698a') . '
'; $lostNote = $nLost > 0 ? '
di cui ' . $nLost . ' segnat' . ($nLost === 1 ? 'a' : 'e') . ' come pers' . ($nLost === 1 ? 'a' : 'e') . '
' : ''; $body = section('Partecipanti confermati (' . $nBooked . ')', '#374151', personRows($booked, 'booked')); if ($pending) { $body .= section('In attesa di conferma (' . count($pending) . ')', '#8a6d00', personRows($pending, 'pending')); } if ($waiting) { $body .= section('Lista d\'attesa (' . count($waiting) . ')', '#d1698a', personRows($waiting, 'waiting')); } return '' . h($cls['servicename']) . '' . '' . '
' . '' // barra colore classe . '' // intestazione . '' // statistiche . '' // elenchi . $body // footer . '' . '
 
' . '
YogiBook · Riepilogo classe
' . '

' . h($cls['servicename']) . '

' . '
' . h(italianDate($d)) . ' · ore ' . $d->format('H:i') . '
' . '
' . h($location) . '
' . '
' . $stats . '
' . $lostNote . '
Generato automaticamente da YogiBook il ' . date('d/m/Y \a\l\l\e H:i') . '
'; } function renderText(array $cls, array $booked, array $pending, array $waiting, string $location): string { $d = new DateTime($cls['dateschedule']); $max = (int) $cls['maxcapacity']; $t = $cls['servicename'] . "\n" . italianDate($d) . ' ore ' . $d->format('H:i') . "\n" . $location . "\n\n"; $t .= 'Iscritti: ' . count($booked) . '/' . $max . ' | In attesa: ' . count($pending) . ' | In coda: ' . count($waiting) . "\n\n"; $blocks = ['CONFERMATI' => $booked, 'IN ATTESA DI CONFERMA' => $pending, "LISTA D'ATTESA" => $waiting]; foreach ($blocks as $title => $list) { if ($title !== 'CONFERMATI' && !$list) { continue; } $t .= $title . "\n"; if (!$list) { $t .= " Nessun partecipante\n"; } $i = 1; foreach ($list as $p) { $lost = ($p['status'] === 'booked' && ($p['lostlesson'] ?? '') === 'Y') ? ' (PERSA)' : ''; $t .= ' ' . $i++ . '. ' . fullName($p) . $lost . "\n"; } $t .= "\n"; } return $t; } /* -------------------------------------------------------------- mailer */ function makeMailer(): PHPMailer { $m = new PHPMailer(true); $m->isSMTP(); $m->Host = cfg('MAIL_HOST'); $m->Port = (int) cfg('MAIL_PORT', 465); $m->SMTPAuth = true; $m->Username = cfg('MAIL_USERNAME'); $m->Password = cfg('MAIL_PASSWORD'); $enc = strtolower((string) cfg('MAIL_ENCRYPTION', 'ssl')); $m->SMTPSecure = $enc === 'ssl' ? PHPMailer::ENCRYPTION_SMTPS : ($enc === 'tls' ? PHPMailer::ENCRYPTION_STARTTLS : ''); $m->SMTPKeepAlive = true; $m->CharSet = 'UTF-8'; $m->Encoding = 'base64'; $m->setFrom(cfg('MAIL_FROM_ADDRESS'), cfg('MAIL_FROM_NAME', 'YogiBook')); $m->isHTML(true); return $m; } /* --------------------------------------------------------------- invio */ $mailer = $dryRun ? null : makeMailer(); $sent = 0; $failed = 0; foreach ($classes as $cls) { $sid = (int) $cls['idserviceschedule']; $all = $bookingsBySchedule[$sid] ?? []; $booked = array_values(array_filter($all, fn($b) => $b['status'] === 'booked')); $pending = array_values(array_filter($all, fn($b) => $b['status'] === 'pending')); $waiting = array_values(array_filter($all, fn($b) => $b['status'] === 'waiting')); $d = new DateTime($cls['dateschedule']); $subject = $cls['servicename'] . ' · ' . $d->format('d/m') . ' ore ' . $d->format('H:i') . ' · ' . count($booked) . '/' . (int) $cls['maxcapacity'] . ' iscritti'; if ($pending) { $subject .= ' · ' . count($pending) . ' in attesa'; } if ($waiting) { $subject .= ' · ' . count($waiting) . ' in coda'; } $html = renderEmail($cls, $booked, $pending, $waiting, $location); $text = renderText($cls, $booked, $pending, $waiting, $location); if ($dryRun) { $dir = __DIR__ . '/preview'; if (!is_dir($dir)) { mkdir($dir, 0775, true); } $file = $dir . '/classe_' . $sid . '.html'; file_put_contents($file, $html); logmsg("[DRY-RUN] $subject -> $file"); continue; } try { $mailer->clearAddresses(); foreach ($recipients as $r) { $mailer->addAddress($r, cfg('TEACHER_NAME', '')); } $mailer->Subject = $subject; $mailer->Body = $html; $mailer->AltBody = $text; $mailer->send(); $sent++; logmsg("OK: $subject"); } catch (MailException $e) { $failed++; logmsg("ERRORE classe #$sid: " . $mailer->ErrorInfo); } sleep(1); // evita limiti di invio del server SMTP } if ($mailer) { $mailer->smtpClose(); } logmsg("Fine: $sent inviate, $failed errori (" . count($classes) . " classi il $day)."); exit($failed > 0 ? 1 : 0);