added teacher and option teacher in school

This commit is contained in:
2026-10-01 11:34:07 +02:00
parent 38b7aca148
commit 76fcfb2ba3
14 changed files with 1354 additions and 86 deletions
+166 -53
View File
@@ -1,13 +1,19 @@
<?php
/**
* YogiBook - Riepilogo classi giornaliero per l'insegnante.
* Invia UNA mail per ogni classe del giorno.
* YogiBook - Riepilogo classi giornaliero.
* Invia UNA mail per ogni classe del giorno:
* - all'insegnante della classe (se notifiche attive, attivo e con email)
* -> versione riservata: nomi "Mario R.", senza email dei partecipanti
* - al titolare, se diverso dall'insegnante (se notifiche attive e con email)
* -> copia separata con i dati completi
* - se l'insegnante È il titolare: una sola mail completa
*
* Uso:
* php cron/class_report.php -> 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/
* php cron/class_report.php --only-at=17 -> esce senza fare nulla se in Italia non sono le 17
*/
use PHPMailer\PHPMailer\PHPMailer;
@@ -56,12 +62,35 @@ function chip(string $label, string $bg, string $fg): string
return '<span style="display:inline-block;background:' . $bg . ';color:' . $fg . ';font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">' . h($label) . '</span>';
}
/** Nome completo (versione titolare) */
function fullName(array $p): string
{
$n = trim(($p['first_name'] ?? '') . ' ' . ($p['last_name'] ?? ''));
return $n !== '' ? $n : 'Utente #' . (int) ($p['iduser'] ?? 0);
}
/** Nome + iniziale del cognome (versione insegnante): "Mario R." */
function shortName(array $p): string
{
$first = trim((string) ($p['first_name'] ?? ''));
$last = trim((string) ($p['last_name'] ?? ''));
if ($first === '' && $last === '') {
return 'Utente #' . (int) ($p['iduser'] ?? 0);
}
$ini = $last !== '' ? ' ' . mb_strtoupper(mb_substr($last, 0, 1)) . '.' : '';
return ($first !== '' ? $first : '') . $ini;
}
function personName(array $p, bool $full): string
{
return $full ? fullName($p) : shortName($p);
}
function teacherName(?array $t): string
{
return $t ? trim($t['nameemployee'] . ' ' . $t['surnameemployee']) : '';
}
function italianDate(DateTime $d): string
{
$days = ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'];
@@ -69,6 +98,27 @@ function italianDate(DateTime $d): string
return $days[(int) $d->format('w')] . ' ' . (int) $d->format('j') . ' ' . $months[(int) $d->format('n')] . ' ' . $d->format('Y');
}
/** Motivo per cui non può ricevere ('' = può ricevere) */
function whyNot(?array $t): string
{
if (!$t) return 'nessun insegnante assegnato';
if (($t['active'] ?? 'N') !== 'Y') return 'insegnante non attivo';
if (($t['notifications'] ?? 'N') !== 'Y') return 'notifiche disattivate';
if (empty($t['email'])) return 'email mancante';
if (!filter_var($t['email'], FILTER_VALIDATE_EMAIL)) return 'email non valida';
return '';
}
/** Può ricevere mail? notifiche attive, attivo, email valida */
function canReceive(?array $t): bool
{
return $t
&& ($t['notifications'] ?? 'N') === 'Y'
&& ($t['active'] ?? 'N') === 'Y'
&& !empty($t['email'])
&& filter_var($t['email'], FILTER_VALIDATE_EMAIL);
}
/* ----------------------------------------------------------- argomenti */
$dryRun = in_array('--dry-run', $argv, true);
@@ -79,18 +129,27 @@ foreach (array_slice($argv, 1) as $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');
// Le mail agli insegnanti (non titolari) partono solo se l'opzione "Mostra insegnante" è attiva
$optRow = $pdo->query("SELECT show_teacher FROM `option` LIMIT 1")->fetch();
$teacherMailsEnabled = ($optRow['show_teacher'] ?? 'N') === 'Y';
/* ---------------------------------------------------------- insegnanti */
$employees = [];
$owner = null;
foreach ($pdo->query("SELECT idemployee, nameemployee, surnameemployee, email, is_owner, notifications, active FROM employee")->fetchAll() as $t) {
$employees[(int) $t['idemployee']] = $t;
if ($t['is_owner'] === 'Y' && $owner === null) {
$owner = $t;
}
}
/* ------------------------------------------------------ classi del giorno */
$stmt = $pdo->prepare(
"SELECT ss.idserviceschedule, ss.dateschedule, s.servicename, s.colorclass, s.maxcapacity
"SELECT ss.idserviceschedule, ss.dateschedule, s.servicename, s.colorclass, s.maxcapacity, s.idteacher
FROM serviceschedule ss
LEFT JOIN service s ON ss.idservice = s.idservice
WHERE ss.dateschedule >= :d1 AND ss.dateschedule < :d2
@@ -127,7 +186,7 @@ foreach ($stmtB->fetchAll() as $b) {
/* ------------------------------------------------------------ template */
function personRows(array $list, string $type): string
function personRows(array $list, string $type, bool $full): string
{
if (!$list) {
return '<tr><td style="padding:16px;color:#9ca3af;font-size:14px;text-align:center;">Nessun partecipante</td></tr>';
@@ -145,12 +204,14 @@ function personRows(array $list, string $type): string
$chipHtml = chip('In coda', '#fbeef3', '#d1698a');
}
$nameColor = $isLost ? '#9ca3af' : '#1f2937';
$email = !empty($p['email']) ? '<br><span style="font-size:12px;color:#6b7280;font-weight:400;">' . h($p['email']) . '</span>' : '';
$email = ($full && !empty($p['email']))
? '<br><span style="font-size:12px;color:#6b7280;font-weight:400;">' . h($p['email']) . '</span>'
: '';
$html .= '<tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;">'
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>'
. '<td width="34" valign="top" style="font-weight:800;color:' . $numColor . ';font-size:14px;padding-top:1px;">' . $i . ($type === 'waiting' ? '°' : '.') . '</td>'
. '<td valign="top" style="font-size:15px;color:' . $nameColor . ';font-weight:600;">' . h(fullName($p)) . $email . '</td>'
. '<td valign="top" style="font-size:15px;color:' . $nameColor . ';font-weight:600;">' . h(personName($p, $full)) . $email . '</td>'
. '<td valign="top" align="right" width="100">' . $chipHtml . '</td>'
. '</tr></table></td></tr>';
$i++;
@@ -174,7 +235,12 @@ function statBox(string $value, string $label, string $color): string
. '</td>';
}
function renderEmail(array $cls, array $booked, array $pending, array $waiting, string $location): string
/**
* @param bool $full true = versione titolare (nomi completi + email)
* @param string $teacherFull nome insegnante da mostrare in testata
* @param string|null $copyNote nota "copia per il titolare" (solo se il titolare non è l'insegnante)
*/
function renderEmail(array $cls, array $booked, array $pending, array $waiting, string $location, bool $full, string $teacherFull, ?string $copyNote): string
{
$d = new DateTime($cls['dateschedule']);
$accent = $cls['colorclass'] ?: '#1ebf73';
@@ -195,42 +261,54 @@ function renderEmail(array $cls, array $booked, array $pending, array $waiting,
? '<div style="font-size:13px;color:#6b7280;margin-top:6px;">di cui ' . $nLost . ' segnat' . ($nLost === 1 ? 'a' : 'e') . ' come pers' . ($nLost === 1 ? 'a' : 'e') . '</div>'
: '';
$body = section('Partecipanti confermati (' . $nBooked . ')', '#374151', personRows($booked, 'booked'));
$body = section('Partecipanti confermati (' . $nBooked . ')', '#374151', personRows($booked, 'booked', $full));
if ($pending) {
$body .= section('In attesa di conferma (' . count($pending) . ')', '#8a6d00', personRows($pending, 'pending'));
$body .= section('In attesa di conferma (' . count($pending) . ')', '#8a6d00', personRows($pending, 'pending', $full));
}
if ($waiting) {
$body .= section('Lista d\'attesa (' . count($waiting) . ')', '#d1698a', personRows($waiting, 'waiting'));
$body .= section('Lista d\'attesa (' . count($waiting) . ')', '#d1698a', personRows($waiting, 'waiting', $full));
}
$copyBanner = $copyNote
? '<tr><td style="padding:14px 28px 0;"><div style="background:#f5eee4;color:#5a4630;border-radius:10px;padding:10px 14px;font-size:13px;">' . $copyNote . '</div></td></tr>'
: '';
$teacherLine = $teacherFull !== ''
? '<div style="font-size:13px;color:#6b7280;margin-top:4px;">Insegnante: <strong style="color:#374151;">' . h($teacherFull) . '</strong></div>'
: '';
return '<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>' . h($cls['servicename']) . '</title></head>'
. '<body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;">'
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center">'
. '<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);">'
// barra colore classe
. '<tr><td style="height:8px;background:' . h($accent) . ';font-size:0;line-height:0;">&nbsp;</td></tr>'
// intestazione
. $copyBanner
. '<tr><td style="padding:26px 28px 6px;">'
. '<div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div>'
. '<h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">' . h($cls['servicename']) . '</h1>'
. '<div style="font-size:15px;color:#374151;font-weight:600;">' . h(italianDate($d)) . ' · ore ' . $d->format('H:i') . '</div>'
. '<div style="font-size:13px;color:#6b7280;margin-top:4px;">' . h($location) . '</div>'
. $teacherLine
. '</td></tr>'
// statistiche
. '<tr><td style="padding:16px 22px 0;">' . $stats . '<div style="padding:0 6px;">' . $lostNote . '</div></td></tr>'
// elenchi
. $body
// footer
. '<tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il ' . date('d/m/Y \a\l\l\e H:i') . '</td></tr>'
. '</table></td></tr></table></body></html>';
}
function renderText(array $cls, array $booked, array $pending, array $waiting, string $location): string
function renderText(array $cls, array $booked, array $pending, array $waiting, string $location, bool $full, string $teacherFull, ?string $copyNote): 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";
$t = '';
if ($copyNote) {
$t .= strip_tags($copyNote) . "\n\n";
}
$t .= $cls['servicename'] . "\n" . italianDate($d) . ' ore ' . $d->format('H:i') . "\n" . $location . "\n";
if ($teacherFull !== '') {
$t .= 'Insegnante: ' . $teacherFull . "\n";
}
$t .= "\nIscritti: " . 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) {
@@ -243,7 +321,7 @@ function renderText(array $cls, array $booked, array $pending, array $waiting, s
$i = 1;
foreach ($list as $p) {
$lost = ($p['status'] === 'booked' && ($p['lostlesson'] ?? '') === 'Y') ? ' (PERSA)' : '';
$t .= ' ' . $i++ . '. ' . fullName($p) . $lost . "\n";
$t .= ' ' . $i++ . '. ' . personName($p, $full) . $lost . "\n";
}
$t .= "\n";
}
@@ -276,6 +354,7 @@ function makeMailer(): PHPMailer
$mailer = $dryRun ? null : makeMailer();
$sent = 0;
$failed = 0;
$skipped = 0;
foreach ($classes as $cls) {
$sid = (int) $cls['idserviceschedule'];
@@ -284,51 +363,85 @@ foreach ($classes as $cls) {
$pending = array_values(array_filter($all, fn($b) => $b['status'] === 'pending'));
$waiting = array_values(array_filter($all, fn($b) => $b['status'] === 'waiting'));
$teacher = $employees[(int) $cls['idteacher']] ?? null;
$teacherIsOwner = $teacher && $owner && (int) $teacher['idemployee'] === (int) $owner['idemployee'];
$tName = teacherName($teacher);
$d = new DateTime($cls['dateschedule']);
$subject = $cls['servicename'] . ' · ' . $d->format('d/m') . ' ore ' . $d->format('H:i')
$baseSubject = $cls['servicename'] . ' · ' . $d->format('d/m') . ' ore ' . $d->format('H:i')
. ' · ' . count($booked) . '/' . (int) $cls['maxcapacity'] . ' iscritti';
if ($pending) {
$subject .= ' · ' . count($pending) . ' in attesa';
$baseSubject .= ' · ' . count($pending) . ' in attesa';
}
if ($waiting) {
$subject .= ' · ' . count($waiting) . ' in coda';
$baseSubject .= ' · ' . 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);
// Chi riceve cosa
$deliveries = [];
if ($teacherIsOwner) {
if (canReceive($owner)) {
$deliveries[] = ['to' => $owner, 'full' => true, 'copy' => null, 'tag' => 'titolare'];
}
$file = $dir . '/classe_' . $sid . '.html';
file_put_contents($file, $html);
logmsg("[DRY-RUN] $subject -> $file");
} else {
if (!$teacherMailsEnabled) {
logmsg("Classe #$sid ({$cls['servicename']}): mail all'insegnante non inviata (opzione insegnanti disattivata)");
} elseif (canReceive($teacher)) {
$deliveries[] = ['to' => $teacher, 'full' => false, 'copy' => null, 'tag' => 'insegnante'];
} else {
logmsg("Classe #$sid ({$cls['servicename']}): insegnante " . ($tName ?: '-') . " escluso: " . whyNot($teacher));
}
if (canReceive($owner)) {
$note = $tName !== ''
? '<strong>Copia per il titolare</strong> · classe di ' . h($tName)
: '<strong>Copia per il titolare</strong> · classe senza insegnante assegnato';
$deliveries[] = ['to' => $owner, 'full' => true, 'copy' => $note, 'tag' => 'titolare'];
}
}
if (!$deliveries) {
$skipped++;
logmsg("SALTATA classe #$sid ({$cls['servicename']}): nessun destinatario (notifiche disattivate o email mancante).");
continue;
}
try {
$mailer->clearAddresses();
foreach ($recipients as $r) {
$mailer->addAddress($r, cfg('TEACHER_NAME', ''));
foreach ($deliveries as $dv) {
$subject = ($dv['copy'] ? '[Copia] ' : '') . $baseSubject;
$html = renderEmail($cls, $booked, $pending, $waiting, $location, $dv['full'], $tName, $dv['copy']);
$text = renderText($cls, $booked, $pending, $waiting, $location, $dv['full'], $tName, $dv['copy']);
$toName = teacherName($dv['to']);
if ($dryRun) {
$dir = __DIR__ . '/preview';
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
$file = $dir . '/classe_' . $sid . '_' . $dv['tag'] . '.html';
file_put_contents($file, $html);
logmsg("[DRY-RUN] a {$toName} <{$dv['to']['email']}> ({$dv['tag']}): $subject -> $file");
continue;
}
$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);
try {
$mailer->clearAddresses();
$mailer->addAddress($dv['to']['email'], $toName);
$mailer->Subject = $subject;
$mailer->Body = $html;
$mailer->AltBody = $text;
$mailer->send();
$sent++;
logmsg("OK a {$toName} ({$dv['tag']}): $subject");
} catch (MailException $e) {
$failed++;
logmsg("ERRORE classe #$sid a {$dv['to']['email']}: " . $mailer->ErrorInfo);
}
sleep(1); // evita limiti di invio del server SMTP
}
sleep(1); // evita limiti di invio del server SMTP
}
if ($mailer) {
$mailer->smtpClose();
}
logmsg("Fine: $sent inviate, $failed errori (" . count($classes) . " classi il $day).");
logmsg("Fine: $sent inviate, $failed errori, $skipped classi senza destinatari (" . count($classes) . " classi il $day).");
exit($failed > 0 ? 1 : 0);
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hatha Yoga</title></head><body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);"><tr><td style="height:8px;background:#fa9b9b;font-size:0;line-height:0;">&nbsp;</td></tr><tr><td style="padding:26px 28px 6px;"><div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div><h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">Hatha Yoga</h1><div style="font-size:15px;color:#374151;font-weight:600;">Lunedì 5 Ottobre 2026 · ore 19:30</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">via Valassina 62/B Seregno - Sala Contesto Yoga</div></td></tr><tr><td style="padding:16px 22px 0;"><table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#c0392b;line-height:1.1;">1/1</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Iscritti</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#374151;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Posti liberi</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#8a6d00;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In attesa</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#d1698a;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In coda</div></td></tr></table><div style="padding:0 6px;"></div></td></tr><tr><td style="padding:22px 28px 0;"><div style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">Partecipanti confermati (1)</div><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;"><tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td width="34" valign="top" style="font-weight:800;color:#1ebf73;font-size:14px;padding-top:1px;">1.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Amelia Benucci<br><span style="font-size:12px;color:#6b7280;font-weight:400;">info@claudiosironi.com</span></td><td valign="top" align="right" width="100"><span style="display:inline-block;background:#e3f2ec;color:#1a7f52;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">Confermata</span></td></tr></table></td></tr></table></td></tr><tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il 01/10/2026 alle 11:15</td></tr></table></td></tr></table></body></html>
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hatha Yoga</title></head><body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);"><tr><td style="height:8px;background:#fa9b9b;font-size:0;line-height:0;">&nbsp;</td></tr><tr><td style="padding:26px 28px 6px;"><div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div><h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">Hatha Yoga</h1><div style="font-size:15px;color:#374151;font-weight:600;">Lunedì 5 Ottobre 2026 · ore 19:30</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">via Valassina 62/B Seregno - Sala Contesto Yoga</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">Insegnante: <strong style="color:#374151;">Francesca -</strong></div></td></tr><tr><td style="padding:16px 22px 0;"><table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#c0392b;line-height:1.1;">1/1</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Iscritti</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#374151;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Posti liberi</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#8a6d00;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In attesa</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#d1698a;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In coda</div></td></tr></table><div style="padding:0 6px;"></div></td></tr><tr><td style="padding:22px 28px 0;"><div style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">Partecipanti confermati (1)</div><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;"><tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td width="34" valign="top" style="font-weight:800;color:#1ebf73;font-size:14px;padding-top:1px;">1.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Amelia B.</td><td valign="top" align="right" width="100"><span style="display:inline-block;background:#e3f2ec;color:#1a7f52;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">Confermata</span></td></tr></table></td></tr></table></td></tr><tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il 01/10/2026 alle 11:30</td></tr></table></td></tr></table></body></html>
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hatha Yoga</title></head><body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);"><tr><td style="height:8px;background:#fa9b9b;font-size:0;line-height:0;">&nbsp;</td></tr><tr><td style="padding:14px 28px 0;"><div style="background:#f5eee4;color:#5a4630;border-radius:10px;padding:10px 14px;font-size:13px;"><strong>Copia per il titolare</strong> · classe di Francesca -</div></td></tr><tr><td style="padding:26px 28px 6px;"><div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div><h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">Hatha Yoga</h1><div style="font-size:15px;color:#374151;font-weight:600;">Lunedì 5 Ottobre 2026 · ore 19:30</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">via Valassina 62/B Seregno - Sala Contesto Yoga</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">Insegnante: <strong style="color:#374151;">Francesca -</strong></div></td></tr><tr><td style="padding:16px 22px 0;"><table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#c0392b;line-height:1.1;">1/1</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Iscritti</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#374151;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Posti liberi</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#8a6d00;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In attesa</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#d1698a;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In coda</div></td></tr></table><div style="padding:0 6px;"></div></td></tr><tr><td style="padding:22px 28px 0;"><div style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">Partecipanti confermati (1)</div><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;"><tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td width="34" valign="top" style="font-weight:800;color:#1ebf73;font-size:14px;padding-top:1px;">1.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Amelia Benucci<br><span style="font-size:12px;color:#6b7280;font-weight:400;">info@claudiosironi.com</span></td><td valign="top" align="right" width="100"><span style="display:inline-block;background:#e3f2ec;color:#1a7f52;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">Confermata</span></td></tr></table></td></tr></table></td></tr><tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il 01/10/2026 alle 11:30</td></tr></table></td></tr></table></body></html>