Eventi admin: elenco, form e salvataggio per scuola; mailer e lista d'attesa multi-scuola

This commit is contained in:
2026-10-05 09:31:18 +02:00
parent 1c2ee7d646
commit 450abc0cf9
5 changed files with 176 additions and 355 deletions
+41
View File
@@ -8,6 +8,7 @@
* SchoolMail::toStudent($email, $firstname, 'Oggetto', ['riga 1', 'riga 2']);
* SchoolMail::toStudent($email, $firstname, 'Oggetto', $lines, '', $schoolRow, true); // copia alla scuola
* SchoolMail::toMany([['email'=>..,'first_name'=>..], ...], 'Oggetto', $lines); // invio di massa
* SchoolMail::sendHtml($email, $nome, 'Oggetto', $htmlCompleto, $schoolRow, $ccEmail); // HTML già pronto
*
* Le righe sono HTML: i dati variabili vanno passati già con htmlspecialchars().
*/
@@ -86,8 +87,48 @@ class SchoolMail
return ['sent' => $sent, 'failed' => $failed];
}
/**
* Invio di un'email con HTML già composto (es. layout degli eventi).
* Funziona anche senza sessione se si passa $school (endpoint pubblici, cron).
* $cc: indirizzo in copia (ignorato se non valido o uguale al destinatario).
*/
public static function sendHtml(
string $to,
string $toName,
string $subject,
string $html,
?array $school = null,
string $cc = ''
): bool {
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
return false;
}
try {
$school = $school ?? SchoolContext::get();
$mail = self::mailer($school);
$mail->addAddress($to, $toName);
if ($cc !== '' && filter_var($cc, FILTER_VALIDATE_EMAIL) && strcasecmp($cc, $to) !== 0) {
$mail->addCC($cc, (string) ($school['name'] ?? ''));
}
$mail->Subject = $subject;
$mail->Body = $html;
$mail->AltBody = self::htmlToText($html);
$mail->send();
return true;
} catch (\Throwable $e) {
error_log('SchoolMail sendHtml error (' . $to . '): ' . $e->getMessage());
return false;
}
}
/* ------------------------------------------------------------------ */
private static function htmlToText(string $html): string
{
$text = str_replace(['</p>', '<br>', '<br/>', '<br />', '</div>', '</tr>'], "\n", $html);
return trim(preg_replace('/\n{3,}/', "\n\n", html_entity_decode(strip_tags($text), ENT_QUOTES, 'UTF-8')));
}
private static function mailer(array $school): PHPMailer
{
$schoolName = (string) ($school['name'] ?? 'YogiBook');
+19 -17
View File
@@ -1,17 +1,17 @@
<?php
require_once('include/headscript.php');
SchoolContext::requireStaff();
/**
* Modulo Eventi - form creazione/modifica (una sola pagina).
* Modulo Eventi - form creazione/modifica (una sola pagina), scuola corrente.
* - event_form.php -> nuovo evento
* - event_form.php?id=123 -> modifica evento esistente
* - event_form.php?id=123 -> modifica evento esistente (solo se della scuola)
* Invia a event_save.php (insert/update + redirect).
*
* Extra rispetto alla v1:
* - flatpickr per data/ora (fine = inizio + 1h automatica)
* - sede da event_locations (default scuola) o indirizzo personalizzato
* - sede da event_locations della scuola (default) o indirizzo personalizzato
* - "Calcola coordinate" da indirizzo (geocoding via event_geocode.php)
* - copertina drag&drop -> event_upload_cover.php -> assets/uploads/events/
* - copertina drag&drop -> event_upload_cover.php
*/
$pdo = DBHandlerSelect::getInstance()->getConnection();
@@ -24,8 +24,8 @@ $id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$ev = null;
if ($id > 0) {
$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ?");
$stmt->execute([$id]);
$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ? AND idschool = ?");
$stmt->execute([$id, $idschool]);
$ev = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$ev) {
header('Location: events.php?message=error');
@@ -33,19 +33,21 @@ if ($id > 0) {
}
}
// Default dal record unico di `option`
$opt = $pdo->query("SELECT * FROM `option` LIMIT 1")->fetch(PDO::FETCH_ASSOC);
$defaultColor = $opt['event_default_color'] ?? '#4CAF50';
$defaultMaxPerBooking = (int) ($opt['event_default_max_per_booking'] ?? 1);
$defReminderEnabled = ($opt['event_reminder_enabled'] ?? 'N') === 'Y';
$defReminderHours = (int) ($opt['event_reminder_hours'] ?? 24);
$defOfferHours = (int) ($opt['event_offer_expiry_hours'] ?? 24);
// Default dalle impostazioni della scuola
$defaultColor = (string) SchoolSettings::get('event_default_color', '#4CAF50');
$defaultMaxPerBooking = (int) SchoolSettings::get('event_default_max_per_booking', 1);
$defReminderEnabled = SchoolSettings::get('event_reminder_enabled', 'N') === 'Y';
$defReminderHours = (int) SchoolSettings::get('event_reminder_hours', 24);
$defOfferHours = (int) SchoolSettings::get('event_offer_expiry_hours', 24);
// Sedi salvate (se la tabella non esiste ancora, si degrada su indirizzo libero)
// Sedi salvate della scuola (se la query fallisce si degrada su indirizzo libero)
$locations = [];
try {
$locations = $pdo->query("SELECT * FROM event_locations ORDER BY is_default DESC, name ASC")->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
$st = $pdo->prepare("SELECT * FROM event_locations WHERE idschool = ? ORDER BY is_default DESC, name ASC");
$st->execute([$idschool]);
$locations = $st->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $ex) {
error_log('[event_form] sedi: ' . $ex->getMessage());
$locations = [];
}
+48 -48
View File
@@ -1,9 +1,11 @@
<?php
require_once('include/headscript.php');
SchoolContext::requireStaff();
/**
* Handler salvataggio evento (insert + update). Riceve il POST di event_form.php,
* genera lo slug se mancante, valida i campi e fa redirect con ?message=.
* Salvataggio evento (insert + update) della scuola corrente.
* Riceve il POST di event_form.php, genera lo slug se mancante (unico per scuola),
* valida i campi e fa redirect con ?message=.
*/
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: events.php');
@@ -12,10 +14,6 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$pdo = DBHandlerSelect::getInstance()->getConnection();
// --- ID insegnante loggato -------------------------------------------------
// headscript.php popola $iduserlogin da Auth::user()->present()->id
$currentUserId = $iduserlogin ?? ($_SESSION['iduserlogin'] ?? null);
// --- Helper ----------------------------------------------------------------
function slugify(string $text): string
{
@@ -32,18 +30,14 @@ function slugify(string $text): string
return $text !== '' ? $text : 'evento';
}
function uniqueSlug(PDO $pdo, string $base, int $excludeId): string
/** Slug unico all'interno della scuola (esclude l'evento in modifica) */
function uniqueSlug(PDO $pdo, string $base, int $excludeId, int $idschool): string
{
$st = $pdo->prepare("SELECT COUNT(*) FROM events WHERE slug = ? AND idschool = ? AND id <> ?");
$slug = $base;
$i = 2;
while (true) {
if ($excludeId > 0) {
$st = $pdo->prepare("SELECT COUNT(*) FROM events WHERE slug = ? AND id <> ?");
$st->execute([$slug, $excludeId]);
} else {
$st = $pdo->prepare("SELECT COUNT(*) FROM events WHERE slug = ?");
$st->execute([$slug]);
}
$st->execute([$slug, $idschool, $excludeId]);
if ((int) $st->fetchColumn() === 0) {
return $slug;
}
@@ -63,9 +57,15 @@ function toDateTime(?string $v): ?string
}
// --- Lettura e validazione input -------------------------------------------
$id = isset($_POST['id']) ? (int) $_POST['id'] : 0;
$title = trim($_POST['title'] ?? '');
$id = isset($_POST['id']) ? (int) $_POST['id'] : 0;
// In modifica l'evento deve appartenere alla scuola corrente
if ($id > 0 && !SchoolScope::belongs('events', $id)) {
header('Location: events.php?message=error');
exit();
}
$title = trim($_POST['title'] ?? '');
$startsAt = toDateTime($_POST['starts_at'] ?? null);
$maxSeats = (int) ($_POST['max_seats'] ?? 0);
@@ -77,7 +77,7 @@ if ($title === '' || $startsAt === null || $maxSeats < 1) {
$slugInput = trim($_POST['slug'] ?? '');
$slugBase = slugify($slugInput !== '' ? $slugInput : $title);
$slug = uniqueSlug($pdo, $slugBase, $id);
$slug = uniqueSlug($pdo, $slugBase, $id, $idschool);
$endsAt = toDateTime($_POST['ends_at'] ?? null);
$address = trim($_POST['address'] ?? '');
@@ -121,7 +121,7 @@ $suitableFor = trim($_POST['suitable_for'] ?? '');
$waitlistEnabled = (isset($_POST['waitlist_enabled']) && $_POST['waitlist_enabled'] === 'Y') ? 'Y' : 'N';
// Override per-evento (NULL = usa default scuola)
// Override per evento (NULL = usa default scuola)
$reminderEnabled = (isset($_POST['reminder_enabled']) && in_array($_POST['reminder_enabled'], ['Y', 'N'], true)) ? $_POST['reminder_enabled'] : null;
$reminderHours = (($_POST['reminder_hours'] ?? '') !== '') ? max(1, min(168, (int) $_POST['reminder_hours'])) : null;
$offerHours = (($_POST['offer_expiry_hours'] ?? '') !== '') ? max(1, min(336, (int) $_POST['offer_expiry_hours'])) : null;
@@ -154,52 +154,52 @@ try {
waitlist_enabled = :waitlist_enabled,
reminder_enabled = :reminder_enabled, reminder_hours = :reminder_hours, offer_expiry_hours = :offer_expiry_hours,
cover_image = :cover_image, gallery = :gallery, status = :status, publish_at = :publish_at, updated_at = NOW()
WHERE id = :id";
$params = [':id' => $id];
WHERE id = :id AND idschool = :idschool";
$params = [':id' => $id, ':idschool' => $idschool];
} else {
$sql = "INSERT INTO events
(user_id, title, slug, description, instructions, what_to_bring, suitable_for,
(idschool, user_id, title, slug, description, instructions, what_to_bring, suitable_for,
organizer_name, starts_at, ends_at,
address, lat, lng, cost, currency, max_seats, max_per_booking, color,
waitlist_enabled,
reminder_enabled, reminder_hours, offer_expiry_hours,
cover_image, gallery, status, publish_at, created_at, updated_at)
VALUES
(:user_id, :title, :slug, :description, :instructions, :what_to_bring, :suitable_for,
(:idschool, :user_id, :title, :slug, :description, :instructions, :what_to_bring, :suitable_for,
:organizer_name, :starts_at, :ends_at,
:address, :lat, :lng, :cost, :currency, :max_seats, :max_per_booking, :color,
:waitlist_enabled,
:reminder_enabled, :reminder_hours, :offer_expiry_hours,
:cover_image, :gallery, :status, :publish_at, NOW(), NOW())";
$params = [':user_id' => $currentUserId];
$params = [':idschool' => $idschool, ':user_id' => (int) $iduserlogin];
}
$params += [
':title' => $title,
':slug' => $slug,
':description' => $description !== '' ? $description : null,
':instructions' => $instructions !== '' ? $instructions : null,
':what_to_bring' => $whatToBring !== '' ? $whatToBring : null,
':suitable_for' => $suitableFor !== '' ? $suitableFor : null,
':organizer_name' => $organizer !== '' ? $organizer : null,
':starts_at' => $startsAt,
':ends_at' => $endsAt,
':address' => $address !== '' ? $address : null,
':lat' => $lat,
':lng' => $lng,
':cost' => $cost,
':currency' => $currency,
':max_seats' => $maxSeats,
':max_per_booking' => $maxPerBooking,
':waitlist_enabled' => $waitlistEnabled,
':reminder_enabled' => $reminderEnabled,
':reminder_hours' => $reminderHours,
':title' => $title,
':slug' => $slug,
':description' => $description !== '' ? $description : null,
':instructions' => $instructions !== '' ? $instructions : null,
':what_to_bring' => $whatToBring !== '' ? $whatToBring : null,
':suitable_for' => $suitableFor !== '' ? $suitableFor : null,
':organizer_name' => $organizer !== '' ? $organizer : null,
':starts_at' => $startsAt,
':ends_at' => $endsAt,
':address' => $address !== '' ? $address : null,
':lat' => $lat,
':lng' => $lng,
':cost' => $cost,
':currency' => $currency,
':max_seats' => $maxSeats,
':max_per_booking' => $maxPerBooking,
':waitlist_enabled' => $waitlistEnabled,
':reminder_enabled' => $reminderEnabled,
':reminder_hours' => $reminderHours,
':offer_expiry_hours' => $offerHours,
':color' => $color,
':cover_image' => $coverImage !== '' ? $coverImage : null,
':gallery' => $gallery,
':publish_at' => $publishAt,
':status' => $status,
':color' => $color,
':cover_image' => $coverImage !== '' ? $coverImage : null,
':gallery' => $gallery,
':publish_at' => $publishAt,
':status' => $status,
];
$stmt = $pdo->prepare($sql);
@@ -207,7 +207,7 @@ try {
header('Location: events.php?message=success');
exit();
} catch (Throwable $ex) {
} catch (PDOException $ex) {
error_log('[event_save] ' . $ex->getMessage());
header("Location: {$redirectOnError}");
exit();
+11 -5
View File
@@ -1,8 +1,9 @@
<?php
require_once('include/headscript.php');
SchoolContext::requireStaff();
/**
* Modulo Eventi - elenco lato insegnante (stile Amelia).
* Modulo Eventi - elenco lato staff (stile Amelia), solo eventi della scuola corrente.
* Mostra gli eventi raggruppati per data con posti prenotati, stato e azioni:
* - Partecipanti -> event_participants.php
* - Modifica -> event_form.php?id=
@@ -31,18 +32,23 @@ $sql = "SELECT e.*,
SUM(seats) AS booked,
COUNT(*) AS bookings_count
FROM event_bookings
WHERE status IN ('confirmed','pending','offered')
WHERE idschool = ?
AND status IN ('confirmed','pending','offered')
GROUP BY event_id
) b ON b.event_id = e.id
LEFT JOIN (
SELECT event_id, SUM(seats) AS waiting
FROM event_bookings
WHERE status = 'waiting'
WHERE idschool = ?
AND status = 'waiting'
GROUP BY event_id
) w ON w.event_id = e.id
LEFT JOIN auth_users u ON u.id = e.user_id
WHERE e.idschool = ?
ORDER BY e.starts_at ASC";
$events = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
$st = $pdo->prepare($sql);
$st->execute([$idschool, $idschool, $idschool]);
$events = $st->fetchAll(PDO::FETCH_ASSOC);
$mesi = [
1 => 'gennaio',
@@ -87,7 +93,7 @@ function statusBadge(string $status): array
<meta charset="utf-8" />
<title>YogiBook - Eventi</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta content="YogiBook - Prenotazione facile YogaSoul" name="description" />
<meta content="YogiBook" name="description" />
<link rel="shortcut icon" href="assets/images/favicon.ico">
<link href="assets/css/bootstrap.min.css" id="bootstrap-style" rel="stylesheet" type="text/css" />
<link href="assets/css/icons.min.css" rel="stylesheet" type="text/css" />
+57 -285
View File
@@ -1,299 +1,71 @@
<?php
/**
* Motore email del modulo Eventi (PHPMailer, legge le MAIL_* dal .env).
* Lista d'attesa eventi — promozione automatica.
*
* Requisito una tantum: composer require phpmailer/phpmailer
* La scuola si ricava dall'evento stesso: funziona anche senza sessione
* (endpoint pubblici WordPress e cron).
*
* Builder per i 5 tipi:
* A) yse_mail_booking() prenotazione (confermata o lista d'attesa)
* B) yse_mail_user_cancel() disdetta fatta dall'utente
* C) yse_mail_event_cancel() evento annullato dall'insegnante (a ciascun iscritto)
* E) yse_mail_promotion() promozione da lista d'attesa (con RICONFERMA)
* D) yse_mail_reminder() promemoria prima dell'evento
* IMPORTANTE: chiamare DENTRO una transazione in cui la riga dell'evento è già
* bloccata (SELECT ... FOR UPDATE), tipicamente subito dopo aver liberato posti
* (cancellazione di una prenotazione confermata). Così due operazioni simultanee
* non promuovono oltre la capienza.
*
* Le copie alla scuola (CC) sono decise per-tipo dalla pagina "Opzioni Eventi".
* L'indirizzo di copia è option.event_notify_email.
* Promuove in ordine di arrivo (FIFO) le prenotazioni 'waiting' che entrano nei
* posti liberi e si ferma alla prima che non ci sta, per rispettare la coda
* (non "salta" chi è davanti).
*
* @return array<int,array> le prenotazioni promosse (per inviare le email)
*/
if (!function_exists('promoteFromWaitlist')) {
function promoteFromWaitlist(PDO $pdo, int $eventId): array
{
$ev = $pdo->prepare("SELECT max_seats, idschool FROM events WHERE id = ?");
$ev->execute([$eventId]);
$event = $ev->fetch(PDO::FETCH_ASSOC);
if (!$event) {
return [];
}
$maxSeats = (int) $event['max_seats'];
$idschool = (int) $event['idschool'];
if ($maxSeats <= 0 || $idschool <= 0) {
return [];
}
// Assicura l'autoload (di norma già caricato da db-functions.php)
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
$__al = dirname(__DIR__, 2) . '/vendor/autoload.php';
if (is_file($__al)) {
require_once $__al;
}
}
$os = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings
WHERE event_id = ? AND idschool = ?
AND status IN ('confirmed','pending','offered')");
$os->execute([$eventId, $idschool]);
$free = $maxSeats - (int) $os->fetchColumn();
if ($free <= 0) {
return [];
}
if (!defined('YB_PUBLIC_BASE')) {
define('YB_PUBLIC_BASE', 'https://yogibook.yogasoul.it/');
}
$ws = $pdo->prepare("SELECT * FROM event_bookings
WHERE event_id = ? AND idschool = ? AND status = 'waiting'
ORDER BY created_at ASC, id ASC");
$ws->execute([$eventId, $idschool]);
$waiting = $ws->fetchAll(PDO::FETCH_ASSOC);
/* ---------- Opzioni ---------- */
function yse_mail_options(): array
{
static $opt = null;
if ($opt !== null) {
return $opt;
}
$row = [];
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$row = $pdo->query("SELECT * FROM `option` LIMIT 1")->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) {
$row = [];
}
$opt = [
'cc_email' => trim((string) ($row['event_notify_email'] ?? '')),
'cc' => [
'booking' => ($row['event_cc_booking'] ?? 'N') === 'Y',
'user_cancel' => ($row['event_cc_user_cancel'] ?? 'N') === 'Y',
'event_cancel' => ($row['event_cc_event_cancel'] ?? 'N') === 'Y',
'reminder' => ($row['event_cc_reminder'] ?? 'N') === 'Y',
'promotion' => ($row['event_cc_promotion'] ?? 'N') === 'Y',
],
];
return $opt;
}
/* ---------- Invio base ---------- */
function yse_send(string $to, string $toName, string $subject, string $html, bool $ccSchool): bool
{
$env = $_ENV;
try {
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
$mail->isSMTP();
$mail->Host = $env['MAIL_HOST'] ?? '';
$mail->SMTPAuth = true;
$mail->Username = $env['MAIL_USERNAME'] ?? '';
$mail->Password = $env['MAIL_PASSWORD'] ?? '';
$enc = strtolower($env['MAIL_ENCRYPTION'] ?? 'ssl');
$mail->SMTPSecure = ($enc === 'tls')
? \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS
: \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
$mail->Port = (int) ($env['MAIL_PORT'] ?? 465);
$mail->CharSet = 'UTF-8';
$mail->setFrom($env['MAIL_FROM_ADDRESS'] ?? 'info@yogasoul.it', $env['MAIL_FROM_NAME'] ?? 'YogiBook');
$mail->addAddress($to, $toName);
if ($ccSchool) {
$ccEmail = yse_mail_options()['cc_email'];
if ($ccEmail !== '' && strcasecmp($ccEmail, $to) !== 0) {
$mail->addCC($ccEmail);
$promoted = [];
// Promozione = "offerta": tiene il posto ma richiede riconferma (confirm.php)
$upd = $pdo->prepare("UPDATE event_bookings
SET status = 'offered', offered_at = NOW(), updated_at = NOW()
WHERE id = ? AND idschool = ?");
foreach ($waiting as $w) {
$need = (int) $w['seats'];
if ($need <= $free) {
$upd->execute([$w['id'], $idschool]);
$free -= $need;
$w['status'] = 'offered';
$promoted[] = $w;
if ($free <= 0) {
break;
}
} else {
break; // rispetta la coda
}
}
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $html;
$mail->AltBody = trim(preg_replace(
'/\n{3,}/',
"\n\n",
strip_tags(str_replace(['</p>', '<br>', '<br/>', '<br />', '</div>'], "\n", $html))
));
return $mail->send();
} catch (Throwable $e) {
error_log('[event mail] ' . $e->getMessage());
return false;
return $promoted;
}
}
/* ---------- Helper contenuto ---------- */
function yse_h($v): string
{
return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
}
function yse_fmt_datetime(array $event): string
{
$mesi = [
1 => 'gennaio',
2 => 'febbraio',
3 => 'marzo',
4 => 'aprile',
5 => 'maggio',
6 => 'giugno',
7 => 'luglio',
8 => 'agosto',
9 => 'settembre',
10 => 'ottobre',
11 => 'novembre',
12 => 'dicembre'
];
$t = strtotime($event['starts_at']);
$s = (int) date('j', $t) . ' ' . $mesi[(int) date('n', $t)] . ' ' . date('Y', $t) . ' · ' . date('H:i', $t);
if (!empty($event['ends_at'])) {
$s .= '–' . date('H:i', strtotime($event['ends_at']));
}
return $s;
}
function yse_maps_link(array $event): string
{
$q = trim((string) ($event['address'] ?? ''));
if ($q === '' && !empty($event['lat']) && !empty($event['lng'])) {
$q = $event['lat'] . ',' . $event['lng'];
}
return $q !== '' ? 'https://www.google.com/maps/search/?api=1&query=' . rawurlencode($q) : '';
}
function yse_event_info_block(array $event): string
{
$accent = (isset($event['color']) && preg_match('/^#[0-9a-fA-F]{6}$/', $event['color'])) ? $event['color'] : '#4CAF50';
$when = yse_h(yse_fmt_datetime($event));
$addr = trim((string) ($event['address'] ?? ''));
$maps = yse_maps_link($event);
$rows = '<tr><td style="padding:6px 0;color:#8c837c;width:90px;">Quando</td><td style="padding:6px 0;color:#2b2622;font-weight:600;">' . $when . '</td></tr>';
if ($addr !== '') {
$mapsLink = $maps !== '' ? ' &nbsp;<a href="' . yse_h($maps) . '" style="color:' . $accent . ';">apri mappa</a>' : '';
$rows .= '<tr><td style="padding:6px 0;color:#8c837c;">Dove</td><td style="padding:6px 0;color:#2b2622;font-weight:600;">' . yse_h($addr) . $mapsLink . '</td></tr>';
}
$info = '';
foreach ([['suitable_for', 'Per chi è adatto'], ['what_to_bring', 'Cosa portare'], ['instructions', 'Istruzioni']] as $f) {
$val = trim((string) ($event[$f[0]] ?? ''));
if ($val !== '') {
$info .= '<p style="margin:10px 0 0;"><strong style="color:#2b2622;">' . $f[1] . ':</strong><br>' . nl2br(yse_h($val)) . '</p>';
}
}
$html = '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-size:14px;border-collapse:collapse;margin:10px 0;">' . $rows . '</table>';
if ($info !== '') {
$html .= '<div style="background:#faf8f6;border-radius:10px;padding:12px 14px;font-size:14px;color:#4a433d;">' . $info . '</div>';
}
return $html;
}
function yse_email_layout(array $event, string $heading, string $bodyHtml): string
{
$accent = (isset($event['color']) && preg_match('/^#[0-9a-fA-F]{6}$/', $event['color'])) ? $event['color'] : '#4CAF50';
$from = $_ENV['MAIL_FROM_NAME'] ?? 'YogaSoul';
$title = yse_h($event['title'] ?? '');
return '
<div style="background:#f2efec;padding:24px 0;font-family:Arial,Helvetica,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#fff;border-radius:16px;overflow:hidden;">
<tr><td style="background:' . $accent . ';height:6px;"></td></tr>
<tr><td style="padding:28px 30px 6px;">
<div style="font-size:13px;letter-spacing:.05em;text-transform:uppercase;color:#8c837c;">' . yse_h($from) . '</div>
<h1 style="margin:6px 0 2px;font-size:22px;color:#2b2622;">' . $heading . '</h1>
<div style="font-size:17px;color:' . $accent . ';font-weight:600;">' . $title . '</div>
</td></tr>
<tr><td style="padding:10px 30px 26px;color:#4a433d;font-size:15px;line-height:1.6;">' . $bodyHtml . '</td></tr>
<tr><td style="padding:16px 30px;background:#faf8f6;color:#9ca3af;font-size:12px;">Questa email è stata inviata da ' . yse_h($from) . '.</td></tr>
</table>
</td></tr></table>
</div>';
}
function yse_button(string $url, string $label, string $accent): string
{
return '<a href="' . yse_h($url) . '" style="display:inline-block;background:' . $accent . ';color:#fff;text-decoration:none;font-weight:600;padding:12px 22px;border-radius:10px;">' . yse_h($label) . '</a>';
}
function yse_cancel_url(array $booking): string
{
return YB_PUBLIC_BASE . 'apiwp/cancel.php?token=' . rawurlencode($booking['token']);
}
function yse_confirm_url(array $booking): string
{
return YB_PUBLIC_BASE . 'apiwp/confirm.php?token=' . rawurlencode($booking['token']);
}
/* ---------- Builder per tipo ---------- */
/** A) Prenotazione (confermata o lista d'attesa) */
function yse_mail_booking(array $booking, array $event): bool
{
$accent = $event['color'] ?? '#4CAF50';
$waiting = ($booking['status'] ?? '') === 'waiting';
$parts = explode(' ', trim($booking['name']));
$name = $parts[0] ?? '';
if ($waiting) {
$heading = "Sei in lista d'attesa";
$intro = '<p>Ciao ' . yse_h($name) . ", ti abbiamo messo in lista d'attesa per questo evento. Ti avviseremo appena si libera un posto.</p>";
} else {
$heading = 'Prenotazione confermata';
$intro = '<p>Ciao ' . yse_h($name) . ', la tua prenotazione è confermata. Ecco i dettagli:</p>';
}
$body = $intro;
$body .= yse_event_info_block($event);
$body .= '<p style="margin-top:8px;">Posti prenotati: <strong>' . (int) $booking['seats'] . '</strong></p>';
$body .= '<p style="margin:22px 0 6px;">Se non puoi più partecipare, puoi annullare qui:</p>';
$body .= '<p>' . yse_button(yse_cancel_url($booking), 'Annulla prenotazione', $accent) . '</p>';
$subject = ($waiting ? "Lista d'attesa: " : 'Prenotazione confermata: ') . $event['title'];
$html = yse_email_layout($event, $heading, $body);
return yse_send($booking['email'], $booking['name'], $subject, $html, yse_mail_options()['cc']['booking']);
}
/** B) Disdetta fatta dall'utente */
function yse_mail_user_cancel(array $booking, array $event): bool
{
$parts = explode(' ', trim($booking['name']));
$name = $parts[0] ?? '';
$body = '<p>Ciao ' . yse_h($name) . ', la tua prenotazione per questo evento è stata <strong>annullata</strong>.</p>';
$body .= yse_event_info_block($event);
$body .= '<p style="margin-top:14px;">Ci dispiace non vederti questa volta. A presto!</p>';
$subject = 'Prenotazione annullata: ' . $event['title'];
$html = yse_email_layout($event, 'Prenotazione annullata', $body);
return yse_send($booking['email'], $booking['name'], $subject, $html, yse_mail_options()['cc']['user_cancel']);
}
/** C) Evento annullato dall'insegnante (a ciascun iscritto) */
function yse_mail_event_cancel(array $booking, array $event): bool
{
$parts = explode(' ', trim($booking['name']));
$name = $parts[0] ?? '';
$body = '<p>Ciao ' . yse_h($name) . ", ci dispiace: l'evento è stato <strong>annullato</strong>.</p>";
$body .= yse_event_info_block($event);
$body .= '<p style="margin-top:14px;">La tua prenotazione è stata annullata automaticamente. Per informazioni, rispondi pure a questa email.</p>';
$subject = 'Evento annullato: ' . $event['title'];
$html = yse_email_layout($event, 'Evento annullato', $body);
return yse_send($booking['email'], $booking['name'], $subject, $html, yse_mail_options()['cc']['event_cancel']);
}
/** E) Promozione da lista d'attesa — con RICONFERMA */
function yse_mail_promotion(array $booking, array $event): bool
{
$accent = $event['color'] ?? '#4CAF50';
$parts = explode(' ', trim($booking['name']));
$name = $parts[0] ?? '';
$body = '<p>Ciao ' . yse_h($name) . ", buone notizie: si è liberato un posto e tocca a te!</p>";
$body .= yse_event_info_block($event);
$body .= '<p style="margin:22px 0 6px;"><strong>Conferma entro breve</strong> per tenere il posto:</p>';
$body .= '<p>' . yse_button(yse_confirm_url($booking), 'Confermo, ci sarò', $accent) . '</p>';
$body .= '<p style="margin-top:16px;font-size:13px;color:#8c837c;">Se non puoi partecipare, <a href="' . yse_h(yse_cancel_url($booking)) . '" style="color:' . $accent . ';">annulla qui</a> così offriamo il posto a un\'altra persona.</p>';
$subject = 'Si è liberato un posto: ' . $event['title'];
$html = yse_email_layout($event, 'Un posto per te', $body);
return yse_send($booking['email'], $booking['name'], $subject, $html, yse_mail_options()['cc']['promotion']);
}
/** D) Promemoria prima dell'evento (testo e punto di ritrovo opzionali) */
function yse_mail_reminder(array $booking, array $event, string $message = '', string $meetingLink = ''): bool
{
$accent = $event['color'] ?? '#4CAF50';
$parts = explode(' ', trim($booking['name']));
$name = $parts[0] ?? '';
$note = $message !== '' ? nl2br(yse_h($message)) : 'Ti ricordiamo il tuo prossimo appuntamento.';
$body = '<p>Ciao ' . yse_h($name) . ',<br>' . $note . '</p>';
$body .= yse_event_info_block($event);
if ($meetingLink !== '') {
$body .= '<p style="margin:16px 0 6px;"><strong>Punto di ritrovo:</strong> <a href="' . yse_h($meetingLink) . '" style="color:' . $accent . ';">apri la posizione sulla mappa</a></p>';
}
$body .= '<p style="margin:22px 0 6px;">Se non puoi più partecipare, avvisaci annullando:</p>';
$body .= '<p>' . yse_button(yse_cancel_url($booking), 'Annulla prenotazione', $accent) . '</p>';
$subject = 'Promemoria: ' . $event['title'];
$html = yse_email_layout($event, 'Ci vediamo presto', $body);
return yse_send($booking['email'], $booking['name'], $subject, $html, yse_mail_options()['cc']['reminder']);
}