cron report
This commit is contained in:
@@ -0,0 +1,327 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YogiBook - Riepilogo classi giornaliero per l'insegnante.
|
||||||
|
* Invia UNA mail per ogni classe del giorno.
|
||||||
|
*
|
||||||
|
* 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/
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 '<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>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 '<tr><td style="padding:16px;color:#9ca3af;font-size:14px;text-align:center;">Nessun partecipante</td></tr>';
|
||||||
|
}
|
||||||
|
$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']) ? '<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" align="right" width="100">' . $chipHtml . '</td>'
|
||||||
|
. '</tr></table></td></tr>';
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function section(string $title, string $color, string $rows): string
|
||||||
|
{
|
||||||
|
return '<tr><td style="padding:22px 28px 0;">'
|
||||||
|
. '<div style="font-size:13px;font-weight:700;color:' . $color . ';text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">' . $title . '</div>'
|
||||||
|
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;">' . $rows . '</table>'
|
||||||
|
. '</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function statBox(string $value, string $label, string $color): string
|
||||||
|
{
|
||||||
|
return '<td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;">'
|
||||||
|
. '<div style="font-size:22px;font-weight:800;color:' . $color . ';line-height:1.1;">' . $value . '</div>'
|
||||||
|
. '<div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">' . $label . '</div>'
|
||||||
|
. '</td>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr>'
|
||||||
|
. 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')
|
||||||
|
. '</tr></table>';
|
||||||
|
|
||||||
|
$lostNote = $nLost > 0
|
||||||
|
? '<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'));
|
||||||
|
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 '<!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;"> </td></tr>'
|
||||||
|
// intestazione
|
||||||
|
. '<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>'
|
||||||
|
. '</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
|
||||||
|
{
|
||||||
|
$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);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Aerial Yoga - mix</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:#6ED4E4;font-size:0;line-height:0;"> </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;">Aerial Yoga - mix</h1><div style="font-size:15px;color:#374151;font-weight:600;">Giovedì 24 Settembre 2026 · ore 19:00</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:#1a7f52;line-height:1.1;">2/8</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;">6</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 (2)</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><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;">2.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Aurora Masiero<br><span style="font-size:12px;color:#6b7280;font-weight:400;">info@yogasoul.it</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 24/09/2026 alle 10:55</td></tr></table></td></tr></table></body></html>
|
||||||
Reference in New Issue
Block a user