From 434eb8bc48627a58d7c8023ebe086f257b991dd9 Mon Sep 17 00:00:00 2001 From: solocla Date: Wed, 16 Sep 2026 16:27:47 +0200 Subject: [PATCH 01/18] branch waiting list with update --- ...0260916124349_add_waiting_list_support.php | 20 ++ ...403_add_waiting_list_enabled_to_option.php | 20 ++ public/adminpanel.php | 44 +++- public/bookandgo.php | 21 +- public/bookingpanel.php | 12 +- public/bookingpanelorder.php | 2 +- public/deleteorder.php | 43 ++++ public/include/sidebar.php | 6 + public/include/waitinglist.php | 111 ++++++++++ public/leavewaiting.php | 26 +++ public/options_save.php | 33 +++ public/optionsschool.php | 190 ++++++++++++++++++ public/ordersummary.php | 41 +++- public/rebookandgo.php | 16 +- public/userpanel.php | 124 ++++++++++++ 15 files changed, 687 insertions(+), 22 deletions(-) create mode 100644 db/migrations/20260916124349_add_waiting_list_support.php create mode 100644 db/migrations/20260916131403_add_waiting_list_enabled_to_option.php create mode 100644 public/deleteorder.php create mode 100644 public/include/waitinglist.php create mode 100644 public/leavewaiting.php create mode 100644 public/options_save.php create mode 100644 public/optionsschool.php diff --git a/db/migrations/20260916124349_add_waiting_list_support.php b/db/migrations/20260916124349_add_waiting_list_support.php new file mode 100644 index 0000000..8060c84 --- /dev/null +++ b/db/migrations/20260916124349_add_waiting_list_support.php @@ -0,0 +1,20 @@ +table('bookingclass'); + $table + ->addColumn('created_at', 'timestamp', [ + 'default' => 'CURRENT_TIMESTAMP', + 'null' => false, + 'after' => 'cancellation_token', + ]) + ->update(); + } +} diff --git a/db/migrations/20260916131403_add_waiting_list_enabled_to_option.php b/db/migrations/20260916131403_add_waiting_list_enabled_to_option.php new file mode 100644 index 0000000..d89337b --- /dev/null +++ b/db/migrations/20260916131403_add_waiting_list_enabled_to_option.php @@ -0,0 +1,20 @@ +table('option') + ->addColumn('waitinglist_enabled', 'char', [ + 'limit' => 1, + 'default' => 'N', + 'null' => false, + 'after' => 'maxbeforetimecancell', + ]) + ->update(); + } +} diff --git a/public/adminpanel.php b/public/adminpanel.php index 30f262f..0eceb1b 100644 --- a/public/adminpanel.php +++ b/public/adminpanel.php @@ -73,9 +73,9 @@ if (!empty($scheduleIds)) { auth_users.first_name, auth_users.last_name, auth_users.id AS iduser FROM bookingclass LEFT JOIN auth_users ON bookingclass.iduser = auth_users.id - WHERE bookingclass.idserviceschedule IN ($ph) - AND bookingclass.status IN ('booked', 'pending') - ORDER BY auth_users.last_name, auth_users.first_name"; + WHERE bookingclass.idserviceschedule IN ($ph) + AND bookingclass.status IN ('booked', 'pending', 'waiting') + ORDER BY bookingclass.created_at, auth_users.last_name, auth_users.first_name"; $stmtBookings = $pdo->prepare($sqlBookings); $stmtBookings->execute(array_values($scheduleIds)); foreach ($stmtBookings->fetchAll() as $b) { @@ -575,8 +575,10 @@ $italianMonths = [ // separa confermati (booked) da in attesa (pending) $bookings = array_filter($allRows, fn($b) => $b['status'] === 'booked'); $pendings = array_filter($allRows, fn($b) => $b['status'] === 'pending'); + $waitings = array_filter($allRows, fn($b) => $b['status'] === 'waiting'); $countPersons = count($bookings); // contatore = solo confermati $countPending = count($pendings); + $countWaiting = count($waitings); $maxcap = (int) $sch['maxcapacity']; $isFull = $maxcap > 0 && $countPersons >= $maxcap; $colorclass = $sch['colorclass'] ?: '#e6f3ff'; @@ -599,6 +601,12 @@ $italianMonths = [ in attesa + 0) : ?> + + + in coda + +

via Valassina 62/B Seregno - Sala Contesto Yoga

@@ -676,6 +684,36 @@ $italianMonths = [ + 0) : ?> +
+ Lista d'attesa () +
+
+ +
+
+
+ + ° + + + In coda +
+
+ +
+
+ +
+ +
Aggiungi partecipante
diff --git a/public/bookandgo.php b/public/bookandgo.php index ed71dd1..8ec5eb4 100644 --- a/public/bookandgo.php +++ b/public/bookandgo.php @@ -1,6 +1,7 @@ fetch_assoc()) { } $stmtUser->close(); +// Decide lo stato: pending se c'è posto, waiting se la classe è piena +$bookingStatus = decideBookingStatus($conn, $idnewbooking); + +if ($bookingStatus === 'full') { + // Classe piena e lista d'attesa disattivata: non inseriamo nulla + header("Location: userpanel.php?full=Y"); + exit(); +} + // Inserisce la nuova prenotazione in bookingclass $insertQuery = "INSERT INTO bookingclass (idserviceschedule, iduser, prevbookingstart, idprevserviceschedule, idservice, idorder, bookingstart, status) - VALUES (?, ?, NULL, '0', ?, ?, ?, 'pending')"; + VALUES (?, ?, NULL, '0', ?, ?, ?, ?)"; $stmt = $conn->prepare($insertQuery); -$stmt->bind_param("iiiis", $idnewbooking, $iduser, $idservicenew, $idorder, $newtime); +$stmt->bind_param("iiiiss", $idnewbooking, $iduser, $idservicenew, $idorder, $newtime, $bookingStatus); if (!$stmt->execute()) { $stmt->close(); die("Errore durante l'inserimento della nuova prenotazione: " . $conn->error); @@ -78,9 +88,12 @@ $stmt->close(); // Connessione $bkngstm gestita globalmente: non la chiudiamo qui +// Parametro di ritorno diverso se finito in lista d'attesa +$redirectFlag = ($bookingStatus === 'waiting') ? 'waiting=Y' : 'reprogram=Y'; + // Se non abbiamo un'email valida, salta l'invio ma completa comunque il flusso if ($emailuser === '') { - header("Location: userpanel.php?reprogram=Y"); + header("Location: userpanel.php?$redirectFlag"); exit(); } @@ -168,5 +181,5 @@ try { } // Esegui il reindirizzamento -header("Location: userpanel.php?reprogram=Y"); +header("Location: userpanel.php?$redirectFlag"); exit(); diff --git a/public/bookingpanel.php b/public/bookingpanel.php index 6858ef3..20947e7 100644 --- a/public/bookingpanel.php +++ b/public/bookingpanel.php @@ -507,7 +507,7 @@ $conn->close(); $mysqli = new mysqli($servername, $username, $password, $dbname); $eventId = $bookedclass->getColumnVal("idserviceschedule"); - $bookingQuery = "SELECT iduser FROM bookingclass WHERE bookingclass.idserviceschedule = ? AND bookingclass.status='booked'"; + $bookingQuery = "SELECT iduser FROM bookingclass WHERE bookingclass.idserviceschedule = ? AND bookingclass.status IN ('booked','pending')"; $stmt = $mysqli->prepare($bookingQuery); $stmt->bind_param("i", $eventId); $stmt->execute(); @@ -538,7 +538,7 @@ $conn->close(); 0) { ?>
-
+
@@ -548,7 +548,7 @@ $conn->close(); 0) { ?>
"> -
+

getColumnVal("servicename")); ?> -/getColumnVal("maxcapacity")); ?>

Quando:

@@ -561,7 +561,11 @@ $conn->close(); 0) { ?> - + getColumnVal("waitinglist_enabled") === 'Y') { ?> + + + +
diff --git a/public/bookingpanelorder.php b/public/bookingpanelorder.php index 4a248d0..77a9c94 100644 --- a/public/bookingpanelorder.php +++ b/public/bookingpanelorder.php @@ -742,7 +742,7 @@ $conn->close(); ?> - +
diff --git a/public/deleteorder.php b/public/deleteorder.php new file mode 100644 index 0000000..d9d34aa --- /dev/null +++ b/public/deleteorder.php @@ -0,0 +1,43 @@ +connect_error) { + die("Connessione fallita: " . $conn->connect_error); +} + +if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { + header("Location: riepilogo.php?message=error_delete"); + exit(); +} + +$idorder = (int) $_GET['id']; + +$conn->begin_transaction(); + +try { + // 1. Cancella tutte le prenotazioni di questo ordine + $stmt1 = $conn->prepare("DELETE FROM bookingclass WHERE idorder = ?"); + $stmt1->bind_param("i", $idorder); + $stmt1->execute(); + $stmt1->close(); + + // 2. Cancella l'ordine + $stmt2 = $conn->prepare("DELETE FROM orderbook WHERE idorderbook = ?"); + $stmt2->bind_param("i", $idorder); + $stmt2->execute(); + $stmt2->close(); + + $conn->commit(); + header("Location: riepilogo.php?message=success_delete"); + exit(); +} catch (Exception $e) { + $conn->rollback(); + header("Location: riepilogo.php?message=error_delete"); + exit(); +} + +$conn->close(); diff --git a/public/include/sidebar.php b/public/include/sidebar.php index 6d71f8e..fb428d5 100644 --- a/public/include/sidebar.php +++ b/public/include/sidebar.php @@ -120,6 +120,12 @@ Sommario Ordini +
  • + + + Opzioni Scuola + +
  • diff --git a/public/include/waitinglist.php b/public/include/waitinglist.php new file mode 100644 index 0000000..418feae --- /dev/null +++ b/public/include/waitinglist.php @@ -0,0 +1,111 @@ +query("SELECT waitinglist_enabled FROM `option` LIMIT 1"); + if ($res && ($row = $res->fetch_assoc())) { + return $row['waitinglist_enabled'] === 'Y'; + } + return false; +} + +/** + * Capienza, posti impegnati (booked + pending) e posti liberi di una classe. + * + * @return array{maxcapacity:int, occupied:int, free:int} + */ +function getClassOccupancy(mysqli $conn, int $idserviceschedule): array +{ + // Capienza (da service via serviceschedule) + $cap = 0; + $stmt = $conn->prepare( + "SELECT s.maxcapacity + FROM serviceschedule ss + JOIN service s ON ss.idservice = s.idservice + WHERE ss.idserviceschedule = ?" + ); + $stmt->bind_param("i", $idserviceschedule); + $stmt->execute(); + $res = $stmt->get_result(); + if ($rowCap = $res->fetch_assoc()) { + $cap = (int) $rowCap['maxcapacity']; + } + $stmt->close(); + + // Posti impegnati: booked + pending + $occupied = 0; + $stmt = $conn->prepare( + "SELECT COUNT(*) AS occupied + FROM bookingclass + WHERE idserviceschedule = ? + AND status IN ('booked', 'pending')" + ); + $stmt->bind_param("i", $idserviceschedule); + $stmt->execute(); + $res = $stmt->get_result(); + if ($rowOcc = $res->fetch_assoc()) { + $occupied = (int) $rowOcc['occupied']; + } + $stmt->close(); + + $free = $cap - $occupied; + + return [ + 'maxcapacity' => $cap, + 'occupied' => $occupied, + 'free' => $free < 0 ? 0 : $free, + ]; +} + +/** + * Decide lo stato di una nuova prenotazione: + * - 'pending' -> c'è posto (flusso normale, insegnante approva) + * - 'waiting' -> classe piena E waiting list attiva + * - 'full' -> classe piena E waiting list disattivata (NON prenotabile) + */ +function decideBookingStatus(mysqli $conn, int $idserviceschedule): string +{ + $occ = getClassOccupancy($conn, $idserviceschedule); + + if ($occ['free'] > 0) { + return 'pending'; + } + + return isWaitingListEnabled($conn) ? 'waiting' : 'full'; +} + +/** + * Posizione (1-based) di un utente nella coda waiting di una classe. + * 0 se non è in coda. + */ +function getWaitingPosition(mysqli $conn, int $idserviceschedule, int $iduser): int +{ + $stmt = $conn->prepare( + "SELECT COUNT(*) AS pos + FROM bookingclass + WHERE idserviceschedule = ? + AND status = 'waiting' + AND created_at <= ( + SELECT created_at FROM bookingclass + WHERE idserviceschedule = ? AND iduser = ? AND status = 'waiting' + ORDER BY created_at ASC LIMIT 1 + )" + ); + $stmt->bind_param("iii", $idserviceschedule, $idserviceschedule, $iduser); + $stmt->execute(); + $res = $stmt->get_result(); + $pos = 0; + if ($row = $res->fetch_assoc()) { + $pos = (int) $row['pos']; + } + $stmt->close(); + return $pos; +} diff --git a/public/leavewaiting.php b/public/leavewaiting.php new file mode 100644 index 0000000..0eb2c41 --- /dev/null +++ b/public/leavewaiting.php @@ -0,0 +1,26 @@ +connect_error) { + header("Location: userpanel.php"); + exit(); +} + +$iduserlogin = (int) $iduserlogin; +$idbookingclass = isset($_GET['id']) ? (int) $_GET['id'] : 0; + +if ($idbookingclass > 0) { + // Cancella solo se è una riga waiting DELL'utente loggato (sicurezza) + $stmt = $conn->prepare( + "DELETE FROM bookingclass + WHERE idbookingclass = ? AND iduser = ? AND status = 'waiting'" + ); + $stmt->bind_param("ii", $idbookingclass, $iduserlogin); + $stmt->execute(); + $stmt->close(); +} + +$conn->close(); +header("Location: userpanel.php?leftwaiting=Y"); +exit(); diff --git a/public/options_save.php b/public/options_save.php new file mode 100644 index 0000000..a00e691 --- /dev/null +++ b/public/options_save.php @@ -0,0 +1,33 @@ +connect_error) { + header("Location: options.php?message=error"); + exit(); +} + +// Checkbox non spuntata = nessun invio -> default 'N' +$waitingEnabled = (isset($_POST['waitinglist_enabled']) && $_POST['waitinglist_enabled'] === 'Y') ? 'Y' : 'N'; +$maxCancel = isset($_POST['maxbeforetimecancell']) ? (string) (int) $_POST['maxbeforetimecancell'] : '0'; + +// Aggiorna l'unico record di option (idption = 1) +$stmt = $conn->prepare("UPDATE `option` SET waitinglist_enabled = ?, maxbeforetimecancell = ? WHERE idption = 1"); +$stmt->bind_param("ss", $waitingEnabled, $maxCancel); + +if ($stmt->execute()) { + $stmt->close(); + $conn->close(); + header("Location: optionsschool.php?message=success"); + exit(); +} else { + $stmt->close(); + $conn->close(); + header("Location: optionsschool.php?message=error"); + exit(); +} diff --git a/public/optionsschool.php b/public/optionsschool.php new file mode 100644 index 0000000..2c707bd --- /dev/null +++ b/public/optionsschool.php @@ -0,0 +1,190 @@ +connect_error) { + die("Connessione fallita: " . $conn->connect_error); +} + +function e($value): string +{ + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} + +$message = $_GET['message'] ?? 'n'; + +// Carica l'unico record di option +$opt = $conn->query("SELECT * FROM `option` LIMIT 1")->fetch_assoc(); +$conn->close(); + +$waitingEnabled = ($opt['waitinglist_enabled'] ?? 'N') === 'Y'; +$maxCancel = $opt['maxbeforetimecancell'] ?? '0'; +?> + + + + + + YogiBook - Opzioni + + + + + + + + + + + + +
    +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + + + + + + +
    +
    +
    +
    +
    Impostazioni prenotazioni
    +

    Configura il comportamento del sistema di prenotazione.

    + + + +
    +
    +
    Lista d'attesa
    +
    Se attiva, quando una classe è piena gli utenti possono mettersi in coda. Alla liberazione di un posto verranno gestiti in ordine di arrivo.
    +
    +
    + > +
    +
    + +
    +
    +
    Ore minime per l'annullamento
    +
    Quante ore prima dell'inizio di una lezione è ancora possibile annullarla.
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + + \ No newline at end of file diff --git a/public/ordersummary.php b/public/ordersummary.php index 78b4085..bfa4886 100644 --- a/public/ordersummary.php +++ b/public/ordersummary.php @@ -172,23 +172,38 @@ if ($result_dayoff->num_rows > 0) { }); } - // Funzione per confermare la cancellazione - function confirmDelete(id, deletePageUrl) { + // Funzione per confermare la cancellazione di un ordine e delle sue prenotazioni + window.confirmDeleteOrder = function(id) { Swal.fire({ title: "Sei sicuro?", - text: "Questa prenotazione verrà cancellata definitivamente! Ricordati poi di riprogrammare la tua lezione!", + text: "Verranno cancellati l'ordine e TUTTE le prenotazioni collegate a questa persona. L'azione è irreversibile!", icon: "warning", showCancelButton: true, confirmButtonColor: "#d33", cancelButtonColor: "#3085d6", - confirmButtonText: "Sì, cancella!", + confirmButtonText: "Sì, cancella tutto!", cancelButtonText: "Annulla" }).then((result) => { if (result.isConfirmed) { - window.location.href = `deleteclass.php?id=${id}`; + window.location.href = `deleteorder.php?id=${id}`; } }); } + Swal.fire({ + title: "Sei sicuro?", + text: "Questa prenotazione verrà cancellata definitivamente! Ricordati poi di riprogrammare la tua lezione!", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#d33", + cancelButtonColor: "#3085d6", + confirmButtonText: "Sì, cancella!", + cancelButtonText: "Annulla" + }).then((result) => { + if (result.isConfirmed) { + window.location.href = `deleteclass.php?id=${id}`; + } + }); + } }); + + + +
    +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +

    Compila i dati dell'evento. I campi con * sono obbligatori.

    + +
    + + +
    +
    + + +
    +
    + + +
    URL pubblico del widget. Vuoto = generato dal titolo.
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + + +
    +
    + +
    + + +
    +
    Per un indirizzo personalizzato, premi "Calcola" per ottenere le coordinate mappa.
    +
    + +
    + +
    + + +
    +
    Premi "Calcola coordinate" per ottenere lat/lng dall'indirizzo.
    +
    + +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + +
    + +
    Testo formattato, elenchi, link e immagini.
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +

    Questi campi sono facoltativi e verranno usati anche nella card pubblica e nelle email.

    + + +
    + +
    +
    + +

    Trascina qui l'immagine oppure sfoglia

    + JPG, PNG, WEBP o GIF · max 5 MB +
    +
    + Copertina +
    +
    + + +
    + +
    Immagine principale mostrata nella card.
    +
    + + +
    + + + + +
    + +
    +
    + + +
    0 = gratuito.
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    + Indietro + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + + + + \ No newline at end of file diff --git a/public/event_geocode.php b/public/event_geocode.php new file mode 100644 index 0000000..e108585 --- /dev/null +++ b/public/event_geocode.php @@ -0,0 +1,61 @@ + coordinate. Proxy server-side verso OpenStreetMap / + * Nominatim (gratuito, senza API key). Ritorna JSON: + * { ok:true, lat:45.66, lng:8.79, display:"..." } oppure { ok:false, error:"..." } + * + * Nota: Nominatim chiede max 1 richiesta/secondo e uno User-Agent identificativo + * (impostato sotto). Quando avrai la chiave Google Maps si può passare alla + * Geocoding API di Google per maggiore precisione, cambiando solo questo file. + */ +if (function_exists('ob_get_level') && ob_get_level()) { + ob_clean(); +} +header('Content-Type: application/json; charset=utf-8'); + +$q = trim($_GET['q'] ?? ''); +if ($q === '') { + echo json_encode(['ok' => false, 'error' => 'Indirizzo mancante.']); + exit(); +} + +$url = 'https://nominatim.openstreetmap.org/search?format=json&limit=1&addressdetails=0&q=' . rawurlencode($q); +$ua = 'YogiBook/1.0 (info@yogasoul.it)'; +$json = false; + +if (function_exists('curl_init')) { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => $ua, + CURLOPT_TIMEOUT => 8, + CURLOPT_FOLLOWLOCATION => true, + ]); + $json = curl_exec($ch); + curl_close($ch); +} + +if ($json === false) { + $ctx = stream_context_create(['http' => [ + 'method' => 'GET', + 'header' => "User-Agent: {$ua}\r\n", + 'timeout' => 8, + ]]); + $json = @file_get_contents($url, false, $ctx); +} + +$data = $json ? json_decode($json, true) : null; + +if (!empty($data[0]['lat']) && !empty($data[0]['lon'])) { + echo json_encode([ + 'ok' => true, + 'lat' => round((float) $data[0]['lat'], 7), + 'lng' => round((float) $data[0]['lon'], 7), + 'display' => $data[0]['display_name'] ?? '', + ]); +} else { + echo json_encode(['ok' => false, 'error' => 'Indirizzo non trovato. Prova a essere più specifico.']); +} +exit(); diff --git a/public/event_locations.php b/public/event_locations.php new file mode 100644 index 0000000..a2ac5a9 --- /dev/null +++ b/public/event_locations.php @@ -0,0 +1,471 @@ + elenco + form nuovo + * - event_locations.php?edit=ID -> form in modifica + * - event_locations.php?delete=ID -> elimina (con conferma via modale) + * - event_locations.php?makedefault=ID -> imposta predefinita + * Il salvataggio (insert/update) passa da event_locations_save.php. + */ +$pdo = DBHandlerSelect::getInstance()->getConnection(); + +function e($value): string +{ + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} + +// Azioni inline (prima di ogni output) +if (isset($_GET['delete'])) { + $delId = (int) $_GET['delete']; + $pdo->prepare("DELETE FROM event_locations WHERE id = ?")->execute([$delId]); + header('Location: event_locations.php?message=deleted'); + exit(); +} +if (isset($_GET['makedefault'])) { + $defId = (int) $_GET['makedefault']; + // Una sola predefinita: Y su questa, N su tutte le altre + $pdo->prepare("UPDATE event_locations SET is_default = IF(id = ?, 'Y', 'N'), updated_at = NOW()")->execute([$defId]); + header('Location: event_locations.php?message=default'); + exit(); +} + +$editId = isset($_GET['edit']) ? (int) $_GET['edit'] : 0; +$edit = null; +if ($editId > 0) { + $st = $pdo->prepare("SELECT * FROM event_locations WHERE id = ?"); + $st->execute([$editId]); + $edit = $st->fetch(PDO::FETCH_ASSOC); +} + +$tableMissing = false; +$locations = []; +try { + $locations = $pdo->query("SELECT * FROM event_locations ORDER BY is_default DESC, name ASC")->fetchAll(PDO::FETCH_ASSOC); +} catch (Throwable $ex) { + $tableMissing = true; +} + +$message = $_GET['message'] ?? ''; + +$val = [ + 'id' => $edit['id'] ?? 0, + 'name' => $edit['name'] ?? '', + 'address' => $edit['address'] ?? '', + 'lat' => $edit['lat'] ?? '', + 'lng' => $edit['lng'] ?? '', + 'is_default' => ($edit['is_default'] ?? 'N') === 'Y', +]; +$isEdit = $editId > 0 && $edit; +?> + + + + + + YogiBook - Sedi + + + + + + + + + + + + + +
    +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + + + + + + + + + + + + + + + +
    + +
    +
    +
    +
    +

    Le sedi salvate compaiono nella tendina "Sede" del form evento.

    + +
    + + +
    + + +
    + +
    + +
    + + +
    +
    Premi "Calcola" per ottenere le coordinate dall'indirizzo.
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    + > + +
    + +
    + + + Annulla + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    Sedi salvate
    +

    Rubrica delle sedi riutilizzabili.

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    SedeIndirizzoCoordinateAzioni
    +
    + + Nessuna sede salvata. Aggiungine una dal form a sinistra. +
    +
    + + + Predefinita + + —'; ?> + Modifica + + Predefinita + + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    + + + + + + + + + + + \ No newline at end of file diff --git a/public/event_locations_save.php b/public/event_locations_save.php new file mode 100644 index 0000000..4c61683 --- /dev/null +++ b/public/event_locations_save.php @@ -0,0 +1,67 @@ +getConnection(); + +$id = isset($_POST['id']) ? (int) $_POST['id'] : 0; +$name = trim($_POST['name'] ?? ''); +$address = trim($_POST['address'] ?? ''); +$isDefault = (isset($_POST['is_default']) && $_POST['is_default'] === 'Y') ? 'Y' : 'N'; + +$lat = ($_POST['lat'] ?? '') !== '' ? (float) $_POST['lat'] : null; +$lng = ($_POST['lng'] ?? '') !== '' ? (float) $_POST['lng'] : null; + +if ($name === '' || $address === '') { + header('Location: event_locations.php?message=error'); + exit(); +} + +try { + if ($id > 0) { + $stmt = $pdo->prepare("UPDATE event_locations + SET name = :name, address = :address, lat = :lat, lng = :lng, + is_default = :is_default, updated_at = NOW() + WHERE id = :id"); + $stmt->execute([ + ':name' => $name, + ':address' => $address, + ':lat' => $lat, + ':lng' => $lng, + ':is_default' => $isDefault, + ':id' => $id, + ]); + $savedId = $id; + } else { + $stmt = $pdo->prepare("INSERT INTO event_locations (name, address, lat, lng, is_default, created_at, updated_at) + VALUES (:name, :address, :lat, :lng, :is_default, NOW(), NOW())"); + $stmt->execute([ + ':name' => $name, + ':address' => $address, + ':lat' => $lat, + ':lng' => $lng, + ':is_default' => $isDefault, + ]); + $savedId = (int) $pdo->lastInsertId(); + } + + // Una sola predefinita + if ($isDefault === 'Y') { + $pdo->prepare("UPDATE event_locations SET is_default = IF(id = ?, 'Y', 'N'), updated_at = NOW()")->execute([$savedId]); + } + + header('Location: event_locations.php?message=success'); + exit(); +} catch (Throwable $ex) { + error_log('[event_locations_save] ' . $ex->getMessage()); + header('Location: event_locations.php?message=error'); + exit(); +} diff --git a/public/event_participants.php b/public/event_participants.php new file mode 100644 index 0000000..c4fb0dc --- /dev/null +++ b/public/event_participants.php @@ -0,0 +1,435 @@ +getConnection(); + +function e($value): string +{ + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} + +function initials(string $first, string $last): string +{ + $ini = strtoupper(mb_substr(trim($first), 0, 1) . mb_substr(trim($last), 0, 1)); + return $ini !== '' ? $ini : '?'; +} + +function avatarColor(string $seed): string +{ + $palette = ['#1ebf73', '#2980b9', '#8e44ad', '#e67e22', '#16a085', '#c0392b', '#2c3e50', '#d35400']; + return $palette[abs(crc32($seed)) % count($palette)]; +} + +$eventId = isset($_GET['id']) ? (int) $_GET['id'] : 0; +if ($eventId <= 0) { + header('Location: events.php'); + exit(); +} + +// Annullamento singola iscrizione (deve avvenire prima di ogni output) +if (isset($_GET['cancel_booking'])) { + $bookingId = (int) $_GET['cancel_booking']; + $upd = $pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ? AND event_id = ?"); + $upd->execute([$bookingId, $eventId]); + header('Location: event_participants.php?id=' . $eventId . '&message=booking_cancelled'); + exit(); +} + +$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ?"); +$stmt->execute([$eventId]); +$event = $stmt->fetch(PDO::FETCH_ASSOC); +if (!$event) { + header('Location: events.php?message=error'); + exit(); +} + +$bStmt = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? ORDER BY created_at DESC, id DESC"); +$bStmt->execute([$eventId]); +$bookings = $bStmt->fetchAll(PDO::FETCH_ASSOC); + +// Riepilogo (solo prenotazioni non annullate) +$bookedSeats = 0; +$activeCount = 0; +$revenue = 0.0; +foreach ($bookings as $b) { + if ($b['status'] !== 'cancelled') { + $bookedSeats += (int) $b['seats']; + $activeCount++; + $revenue += (float) $b['amount']; + } +} +$maxSeats = (int) $event['max_seats']; +$message = $_GET['message'] ?? ''; + +function bookingBadge(string $status): array +{ + switch ($status) { + case 'confirmed': + return ['Confermata', '#e7f7ee', '#1a7f52']; + case 'cancelled': + return ['Annullata', '#fdecea', '#c0392b']; + default: + return ['In attesa', '#fff4e5', '#b26a00']; + } +} +?> + + + + + + YogiBook - Partecipanti + + + + + + + + + + + + + +
    +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +
    Partecipanti
    +

    + + · +

    +
    + Tutti gli eventi +
    + +
    +
    +
    +
    /
    +
    Posti prenotati
    +
    +
    +
    +
    +
    +
    Iscrizioni attive
    +
    +
    +
    +
    +
    +
    Incasso stimato
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PartecipanteEmailTelefonoPostiMarketingIscritto ilStatoAzioni
    +
    + + Ancora nessun iscritto a questo evento. +
    +
    +
    +
    + +
    + + ospite + +
    +
    + + Sì + + No + + + + + + — + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    + + + + + + + + + + + \ No newline at end of file diff --git a/public/event_preview.php b/public/event_preview.php new file mode 100644 index 0000000..1ff0a61 --- /dev/null +++ b/public/event_preview.php @@ -0,0 +1,995 @@ +getConnection(); + +function e($value): string +{ + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} + +$id = isset($_GET['id']) ? (int) $_GET['id'] : 0; +if ($id <= 0) { + http_response_code(404); + echo 'Evento non trovato'; + exit(); +} + +$st = $pdo->prepare("SELECT e.*, u.first_name AS cf, u.last_name AS cl + FROM events e LEFT JOIN auth_users u ON u.id = e.user_id + WHERE e.id = ?"); +$st->execute([$id]); +$ev = $st->fetch(PDO::FETCH_ASSOC); +if (!$ev) { + http_response_code(404); + echo 'Evento non trovato'; + exit(); +} + +$bs = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings WHERE event_id = ? AND status <> 'cancelled'"); +$bs->execute([$id]); +$booked = (int) $bs->fetchColumn(); +$maxSeats = (int) $ev['max_seats']; +$remaining = max(0, $maxSeats - $booked); +$soldOut = $remaining <= 0; +$maxPer = max(1, (int) $ev['max_per_booking']); +$maxQty = $soldOut ? 0 : max(1, min($maxPer, $remaining)); + +$cost = (float) $ev['cost']; +$curr = $ev['currency'] ?: 'EUR'; +$symbols = ['EUR' => '€', 'USD' => '$', 'GBP' => '£', 'CHF' => 'CHF']; +$sym = $symbols[$curr] ?? $curr; +$priceLabel = $cost > 0 ? $sym . number_format($cost, 2, ',', '.') : 'Gratuito'; + +$color = (isset($ev['color']) && preg_match('/^#[0-9a-fA-F]{6}$/', $ev['color'])) ? $ev['color'] : '#4CAF50'; + +$gallery = []; +if (!empty($ev['gallery'])) { + $g = json_decode($ev['gallery'], true); + if (is_array($g)) { + $gallery = $g; + } +} + +$mesiAbbr = [1 => 'GEN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAG', 6 => 'GIU', 7 => 'LUG', 8 => 'AGO', 9 => 'SET', 10 => 'OTT', 11 => 'NOV', 12 => 'DIC']; +$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']; + +$startTs = strtotime($ev['starts_at']); +$dayNum = (int) date('j', $startTs); +$monAbbr = $mesiAbbr[(int) date('n', $startTs)]; +$dateLong = $dayNum . ' ' . $mesi[(int) date('n', $startTs)] . ' ' . date('Y', $startTs); +$timeLabel = date('H:i', $startTs); +if (!empty($ev['ends_at'])) { + $timeLabel .= '–' . date('H:i', strtotime($ev['ends_at'])); +} + +$organizer = trim((string) $ev['organizer_name']) !== '' + ? $ev['organizer_name'] + : trim(($ev['cf'] ?? '') . ' ' . ($ev['cl'] ?? '')); + +$mapQuery = trim((string) $ev['address']) !== '' + ? $ev['address'] + : (($ev['lat'] !== null && $ev['lng'] !== null) ? ($ev['lat'] . ',' . $ev['lng']) : ''); + +$hasDetails = trim((string) $ev['description']) !== '' + || trim((string) $ev['suitable_for']) !== '' + || trim((string) $ev['what_to_bring']) !== '' + || trim((string) $ev['instructions']) !== '' + || !empty($ev['cover_image']) + || !empty($gallery) + || $mapQuery !== ''; + +function textBlock(?string $t): string +{ + return nl2br(e((string) $t)); +} +?> + + + + + + <?php echo e($ev['title']); ?> — Anteprima + + + + + + + + + +
    Anteprima — la card parte compatta; “Dettagli” apre la scheda completa. Il pulsante non invia nulla.
    + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +

    + + + +
    +
    +
    + + + + + Capacità / +
    + +
    + + + + + +
    + +
    + + + + + · +
    +
    +
    +
    +
    + + + +
    +
    + + <?php echo e($ev['title']); ?> + +
    + + + +
    + +
    + +
    + +
    +
    +
    + + + +
    +
    + +
    +
    + + Per chi è adatto
    +
    +
    + + +
    +
    + + + Cosa portare
    +
    +
    + + +
    +
    + + + + Istruzioni
    +
    +
    + +
    +
    + + + +
    +

    + + + + Galleria

    + +
    + + + +
    +

    + + + Dove

    + +
    + +
    +
    + + + +
    + +
    Evento esaurito — non ci sono più posti disponibili.
    + +
    +
    Prenota questo evento
    +
    +
    Persone
    +
    + + 1 + +
    +
    +
    +
    Totale
    +
    +
    + +
    + +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    + + + +
    +
    + +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/public/event_save.php b/public/event_save.php new file mode 100644 index 0000000..6e40759 --- /dev/null +++ b/public/event_save.php @@ -0,0 +1,187 @@ +getConnection(); + +// --- ID insegnante loggato ------------------------------------------------- +// headscript.php popola $iduserlogin da Auth::user()->present()->id +$currentUserId = $iduserlogin ?? ($_SESSION['iduserlogin'] ?? null); + +// --- Helper ---------------------------------------------------------------- +function slugify(string $text): string +{ + $text = trim($text); + if (function_exists('iconv')) { + $conv = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text); + if ($conv !== false) { + $text = $conv; + } + } + $text = strtolower($text); + $text = preg_replace('/[^a-z0-9]+/', '-', $text); + $text = trim($text, '-'); + return $text !== '' ? $text : 'evento'; +} + +function uniqueSlug(PDO $pdo, string $base, int $excludeId): string +{ + $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]); + } + if ((int) $st->fetchColumn() === 0) { + return $slug; + } + $slug = $base . '-' . $i; + $i++; + } +} + +function toDateTime(?string $v): ?string +{ + if (!$v) { + return null; + } + $v = str_replace('T', ' ', $v); + $ts = strtotime($v); + return $ts ? date('Y-m-d H:i:s', $ts) : null; +} + +// --- Lettura e validazione input ------------------------------------------- +$id = isset($_POST['id']) ? (int) $_POST['id'] : 0; +$title = trim($_POST['title'] ?? ''); + +$startsAt = toDateTime($_POST['starts_at'] ?? null); +$maxSeats = (int) ($_POST['max_seats'] ?? 0); + +$redirectOnError = $id > 0 ? "event_form.php?id={$id}&message=error" : "event_form.php?message=error"; +if ($title === '' || $startsAt === null || $maxSeats < 1) { + header("Location: {$redirectOnError}"); + exit(); +} + +$slugInput = trim($_POST['slug'] ?? ''); +$slugBase = slugify($slugInput !== '' ? $slugInput : $title); +$slug = uniqueSlug($pdo, $slugBase, $id); + +$endsAt = toDateTime($_POST['ends_at'] ?? null); +$address = trim($_POST['address'] ?? ''); +$organizer = trim($_POST['organizer_name'] ?? ''); +$description = $_POST['description'] ?? ''; +$cost = round((float) ($_POST['cost'] ?? 0), 2); +$maxPerBooking = max(1, (int) ($_POST['max_per_booking'] ?? 1)); +$coverImage = trim($_POST['cover_image'] ?? ''); + +$lat = ($_POST['lat'] ?? '') !== '' ? (float) $_POST['lat'] : null; +$lng = ($_POST['lng'] ?? '') !== '' ? (float) $_POST['lng'] : null; + +$currency = strtoupper(trim($_POST['currency'] ?? 'EUR')); +if (!in_array($currency, ['EUR', 'USD', 'GBP', 'CHF'], true)) { + $currency = 'EUR'; +} + +$color = trim($_POST['color'] ?? '#4CAF50'); +if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) { + $color = '#4CAF50'; +} + +$status = $_POST['status'] ?? 'draft'; +if (!in_array($status, ['draft', 'published', 'cancelled'], true)) { + $status = 'draft'; +} + +// Campi facoltativi (testo) +$instructions = trim($_POST['instructions'] ?? ''); +$whatToBring = trim($_POST['what_to_bring'] ?? ''); +$suitableFor = trim($_POST['suitable_for'] ?? ''); + +// Galleria: array JSON di path -> validazione e re-encode pulito +$gallery = null; +$galleryRaw = $_POST['gallery'] ?? ''; +if ($galleryRaw !== '') { + $decoded = json_decode($galleryRaw, true); + if (is_array($decoded)) { + $clean = []; + foreach ($decoded as $p) { + if (is_string($p) && $p !== '') { + $clean[] = $p; + } + } + $gallery = !empty($clean) ? json_encode(array_values($clean), JSON_UNESCAPED_SLASHES) : null; + } +} + +// --- Insert / Update ------------------------------------------------------- +try { + if ($id > 0) { + $sql = "UPDATE events SET + title = :title, slug = :slug, description = :description, + instructions = :instructions, what_to_bring = :what_to_bring, suitable_for = :suitable_for, + organizer_name = :organizer_name, starts_at = :starts_at, ends_at = :ends_at, + address = :address, lat = :lat, lng = :lng, cost = :cost, currency = :currency, + max_seats = :max_seats, max_per_booking = :max_per_booking, color = :color, + cover_image = :cover_image, gallery = :gallery, status = :status, updated_at = NOW() + WHERE id = :id"; + $params = [':id' => $id]; + } else { + $sql = "INSERT INTO events + (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, + cover_image, gallery, status, created_at, updated_at) + VALUES + (: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, + :cover_image, :gallery, :status, NOW(), NOW())"; + $params = [':user_id' => $currentUserId]; + } + + $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, + ':color' => $color, + ':cover_image' => $coverImage !== '' ? $coverImage : null, + ':gallery' => $gallery, + ':status' => $status, + ]; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + + header('Location: events.php?message=success'); + exit(); +} catch (Throwable $ex) { + error_log('[event_save] ' . $ex->getMessage()); + header("Location: {$redirectOnError}"); + exit(); +} diff --git a/public/event_upload_cover.php b/public/event_upload_cover.php new file mode 100644 index 0000000..b1c436d --- /dev/null +++ b/public/event_upload_cover.php @@ -0,0 +1,62 @@ + false, 'error' => $msg]); + exit(); +}; + +if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['cover'])) { + $fail('Richiesta non valida.'); +} + +$file = $_FILES['cover']; +if ($file['error'] !== UPLOAD_ERR_OK) { + $fail('Errore nel caricamento (codice ' . (int) $file['error'] . ').'); +} +if ($file['size'] > 5 * 1024 * 1024) { + $fail("L'immagine supera i 5 MB."); +} + +// Validazione MIME reale (non ci si fida dell'estensione) +$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif']; +$info = @getimagesize($file['tmp_name']); +$mime = $info['mime'] ?? ''; +if (!isset($allowed[$mime])) { + $fail('Formato non supportato. Usa JPG, PNG, WEBP o GIF.'); +} +$ext = $allowed[$mime]; + +$relDir = 'assets/uploads/events'; +$absDir = __DIR__ . '/' . $relDir; +if (!is_dir($absDir)) { + @mkdir($absDir, 0775, true); +} +if (!is_dir($absDir) || !is_writable($absDir)) { + $fail('Cartella non scrivibile: ' . $relDir . ' (crea la cartella e dai permessi di scrittura).'); +} + +try { + $rand = bin2hex(random_bytes(6)); +} catch (Throwable $e) { + $rand = uniqid('', true); +} +$name = date('Ymd_His') . '_' . $rand . '.' . $ext; +$dest = $absDir . '/' . $name; + +if (!move_uploaded_file($file['tmp_name'], $dest)) { + $fail('Impossibile salvare il file.'); +} + +echo json_encode(['ok' => true, 'path' => $relDir . '/' . $name]); +exit(); diff --git a/public/events.php b/public/events.php new file mode 100644 index 0000000..6943f5b --- /dev/null +++ b/public/events.php @@ -0,0 +1,463 @@ + event_participants.php + * - Modifica -> event_form.php?id= + * - Annulla -> modale (SweetAlert2) che chiede se inviare la mail di + * annullamento a tutti i partecipanti, poi event_delete.php + */ +$pdo = DBHandlerSelect::getInstance()->getConnection(); + +function e($value): string +{ + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} + +$message = $_GET['message'] ?? ''; + +// Posti prenotati = SOMMA dei posti delle prenotazioni non annullate. +$sql = "SELECT e.*, + COALESCE(b.booked, 0) AS booked, + COALESCE(b.bookings_count, 0) AS bookings_count, + u.first_name AS creator_first, + u.last_name AS creator_last + FROM events e + LEFT JOIN ( + SELECT event_id, + SUM(seats) AS booked, + COUNT(*) AS bookings_count + FROM event_bookings + WHERE status <> 'cancelled' + GROUP BY event_id + ) b ON b.event_id = e.id + LEFT JOIN auth_users u ON u.id = e.user_id + ORDER BY e.starts_at ASC"; +$events = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC); + +$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' +]; + +function dataItaliana(string $dt, array $mesi): string +{ + $t = strtotime($dt); + return date('j', $t) . ' ' . $mesi[(int) date('n', $t)] . ' ' . date('Y', $t); +} + +/** @return array [label, bgColor, textColor] */ +function statusBadge(string $status): array +{ + switch ($status) { + case 'published': + return ['Aperto', '#e7f7ee', '#1a7f52']; + case 'cancelled': + return ['Annullato', '#fdecea', '#c0392b']; + default: + return ['Bozza', '#eef0f3', '#6b7280']; + } +} +?> + + + + + + YogiBook - Eventi + + + + + + + + + + + + + + +
    +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    Eventi
    +

    Ciao , gestisci qui i tuoi eventi: crea, modifica, vedi i partecipanti o annulla.

    +
    + Nuovo evento +
    + +
    + + + + + + + + + + + + + + + + + + + + + 0 && $booked >= $maxSeats; + [$stLabel, $stBg, $stColor] = statusBadge($ev['status']); + + $timeLabel = date('H:i', strtotime($ev['starts_at'])); + if (!empty($ev['ends_at'])) { + $timeLabel .= ' - ' . date('H:i', strtotime($ev['ends_at'])); + } + ?> + + + + + + + + + + + +
    OrarioNomeOrganizzatorePrenotatoStatoAzioni
    +
    + + Nessun evento ancora. Crea il primo con "Nuovo evento". +
    +
    +
    + + + + + + + (ID: ) +
    +
    + + / + + + + + Anteprima + Partecipanti + Modifica + + + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    + + + + + + + + + + + \ No newline at end of file diff --git a/public/include/event_mailer.php b/public/include/event_mailer.php new file mode 100644 index 0000000..f2d3ec7 --- /dev/null +++ b/public/include/event_mailer.php @@ -0,0 +1,58 @@ +isSMTP(); + // $mail->Host = getenv('MAIL_HOST'); + // $mail->SMTPAuth = true; + // $mail->Username = getenv('MAIL_USERNAME'); + // $mail->Password = getenv('MAIL_PASSWORD'); + // $mail->SMTPSecure = getenv('MAIL_ENCRYPTION'); // 'ssl' + // $mail->Port = (int) getenv('MAIL_PORT'); // 465 + // $mail->setFrom(getenv('MAIL_FROM_ADDRESS'), getenv('MAIL_FROM_NAME')); + // $mail->addAddress($toEmail, $toName); + // $mail->isHTML(true); + // $mail->Subject = 'Evento annullato: ' . $event['title']; + // $mail->Body = '...TEMPLATE HTML...'; + // return $mail->send(); + // ------------------------------------------------------------------ + + error_log(sprintf( + '[event mail] STUB annullamento -> %s (%s) per evento #%d "%s"', + $toEmail, + $toName, + (int) $event['id'], + $event['title'] ?? '' + )); + + return true; + } +} diff --git a/public/include/sidebar.php b/public/include/sidebar.php index fb428d5..176b103 100644 --- a/public/include/sidebar.php +++ b/public/include/sidebar.php @@ -70,63 +70,93 @@ hasRole('Admin')): ?> - -
  • - - - Servizi - -
  • + + +
  • - Classi + Classi + +
  • +
  • + + + Servizi
  • - Pending/Waiting + Pending/Waiting
  • - Day Off + Day Off
  • + + + + +
  • + + + Eventi + +
  • +
  • + + + Sedi + +
  • + + +
  • - Order Pending - -
  • -
  • - - - Certificati - -
  • -
  • - - - Situazione + Order Pending
  • - Sommario Ordini - -
  • -
  • - - - Opzioni Scuola + Sommario Ordini
  • + + + +
  • + + + Situazione + +
  • +
  • + + + Certificati + +
  • + + + + +
  • + + + Opzioni Scuola + +
  • + +
  • From dea86cb6dfb1533cd45b745a3d6974f180e4b33b Mon Sep 17 00:00:00 2001 From: solocla Date: Thu, 17 Sep 2026 16:53:51 +0200 Subject: [PATCH 03/18] create events api wp --- public/apiwp/cancel.php | 257 +++++++++ public/event_form.php | 10 +- public/event_locations.php | 4 +- public/event_participants.php | 60 ++- public/event_preview.php | 45 +- public/event_save.php | 6 + public/event_waitlist.php | 59 +++ wp plugin/yogasoul-eventi.zip | Bin 0 -> 9818 bytes .../assets/yogasoul-eventi.css | 497 ++++++++++++++++++ .../yogasoul-eventi/assets/yogasoul-eventi.js | 246 +++++++++ wp plugin/yogasoul-eventi/yogasoul-eventi.php | 275 ++++++++++ 11 files changed, 1433 insertions(+), 26 deletions(-) create mode 100644 public/apiwp/cancel.php create mode 100644 public/event_waitlist.php create mode 100644 wp plugin/yogasoul-eventi.zip create mode 100644 wp plugin/yogasoul-eventi/assets/yogasoul-eventi.css create mode 100644 wp plugin/yogasoul-eventi/assets/yogasoul-eventi.js create mode 100644 wp plugin/yogasoul-eventi/yogasoul-eventi.php diff --git a/public/apiwp/cancel.php b/public/apiwp/cancel.php new file mode 100644 index 0000000..21c084a --- /dev/null +++ b/public/apiwp/cancel.php @@ -0,0 +1,257 @@ + mostra pagina di conferma (NON annulla: + * evita che gli scanner email prefetchino il link e annullino da soli) + * POST (token + confirm=1) -> annulla e, se la prenotazione occupava posti, + * promuove automaticamente la prima in lista d'attesa. + */ +$pdo = pdo_conn(); +$token = trim($_POST['token'] ?? $_GET['token'] ?? ''); +$isPost = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST'; + +$booking = null; +if ($token !== '') { + $q = $pdo->prepare("SELECT b.*, e.title AS ev_title, e.starts_at AS ev_starts, e.id AS eid + FROM event_bookings b JOIN events e ON e.id = b.event_id + WHERE b.token = ?"); + $q->execute([$token]); + $booking = $q->fetch(PDO::FETCH_ASSOC); +} + +$state = 'form'; // form | notfound | already | toolate | done +$promotedCount = 0; + +if (!$booking) { + $state = 'notfound'; +} elseif ($booking['status'] === 'cancelled') { + $state = 'already'; +} elseif (strtotime($booking['ev_starts']) <= time()) { + $state = 'toolate'; +} elseif ($isPost && !empty($_POST['confirm'])) { + try { + $pdo->beginTransaction(); + // Lock evento per serializzare con eventuali prenotazioni concorrenti + $pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([(int) $booking['eid']]); + + // Rileggo lo stato aggiornato della prenotazione + $bl = $pdo->prepare("SELECT * FROM event_bookings WHERE id = ?"); + $bl->execute([(int) $booking['id']]); + $fresh = $bl->fetch(PDO::FETCH_ASSOC); + + if ($fresh && $fresh['status'] !== 'cancelled') { + $wasOccupying = in_array($fresh['status'], ['confirmed', 'pending'], true); + $pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ?") + ->execute([(int) $fresh['id']]); + + if ($wasOccupying) { + $promoted = promoteFromWaitlist($pdo, (int) $booking['eid']); + $promotedCount = count($promoted); + // TODO Step 4: email "sei stato promosso" ai $promoted + } + } + $pdo->commit(); + $state = 'done'; + } catch (Throwable $ex) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + error_log('[apiwp/cancel] ' . $ex->getMessage()); + $state = 'error'; + } +} + +function e($v): string +{ + return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); +} +?> + + + + + + Disdetta prenotazione — YogaSoul + + + + + + + + +
    + +
    ✕
    +

    Link non valido

    +

    Questa prenotazione non esiste o il link è errato.

    + + +
    ✓
    +

    Già annullata

    +

    Questa prenotazione risulta già annullata.

    + + +
    ⏱
    +

    Non più annullabile

    +

    L'evento è già iniziato: non è più possibile annullare online. Contattaci per assistenza.

    + + +
    !
    +

    Si è verificato un errore

    +

    Riprova tra poco. Se il problema persiste, contattaci.

    + + +
    ✓
    +

    Prenotazione annullata

    +

    La tua prenotazione per è stata annullata.

    + 0) : ?> +

    Un posto è stato assegnato alla prima persona in lista d'attesa.

    + + + +
    ?
    +

    Annullare la prenotazione?

    +
    +
    +
    · post
    +
    +

    Confermi di voler annullare la prenotazione a nome di ?

    +
    + + + +
    +

    Se hai cambiato idea, chiudi pure questa pagina.

    + +
    + + + \ No newline at end of file diff --git a/public/event_form.php b/public/event_form.php index 9612e1c..f4975ff 100644 --- a/public/event_form.php +++ b/public/event_form.php @@ -67,6 +67,7 @@ $val = [ 'what_to_bring' => $ev['what_to_bring'] ?? '', 'suitable_for' => $ev['suitable_for'] ?? '', 'gallery' => $ev['gallery'] ?? '', + 'waitlist_enabled' => ($ev['waitlist_enabled'] ?? 'Y') === 'Y', ]; $isEdit = $id > 0; @@ -531,7 +532,7 @@ $pageTitle = $isEdit ? 'Modifica evento' : 'Nuovo evento';
  • -
    +
    +
    + +
    + > + +
    +
    diff --git a/public/event_locations.php b/public/event_locations.php index a2ac5a9..d7dbac9 100644 --- a/public/event_locations.php +++ b/public/event_locations.php @@ -274,7 +274,7 @@ $isEdit = $editId > 0 && $edit;
    -
    +
    @@ -327,7 +327,7 @@ $isEdit = $editId > 0 && $edit;
    -
    +
    Sedi salvate
    diff --git a/public/event_participants.php b/public/event_participants.php index c4fb0dc..4004be0 100644 --- a/public/event_participants.php +++ b/public/event_participants.php @@ -1,5 +1,6 @@ prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ? AND event_id = ?"); - $upd->execute([$bookingId, $eventId]); + try { + $pdo->beginTransaction(); + $pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([$eventId]); + + $bl = $pdo->prepare("SELECT status FROM event_bookings WHERE id = ? AND event_id = ?"); + $bl->execute([$bookingId, $eventId]); + $bStatus = $bl->fetchColumn(); + + if ($bStatus !== false && $bStatus !== 'cancelled') { + $wasOccupying = in_array($bStatus, ['confirmed', 'pending'], true); + $pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ? AND event_id = ?") + ->execute([$bookingId, $eventId]); + if ($wasOccupying) { + // libera posti -> promuove la prima in lista d'attesa + promoteFromWaitlist($pdo, $eventId); + } + } + $pdo->commit(); + } catch (Throwable $ex) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + error_log('[participants cancel] ' . $ex->getMessage()); + } header('Location: event_participants.php?id=' . $eventId . '&message=booking_cancelled'); exit(); } @@ -52,15 +75,18 @@ $bStmt = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? ORDER BY $bStmt->execute([$eventId]); $bookings = $bStmt->fetchAll(PDO::FETCH_ASSOC); -// Riepilogo (solo prenotazioni non annullate) -$bookedSeats = 0; -$activeCount = 0; -$revenue = 0.0; +// Riepilogo: i posti occupati sono solo confermate/pending; la lista d'attesa a parte +$bookedSeats = 0; +$waitingSeats = 0; +$activeCount = 0; +$revenue = 0.0; foreach ($bookings as $b) { - if ($b['status'] !== 'cancelled') { + if (in_array($b['status'], ['confirmed', 'pending'], true)) { $bookedSeats += (int) $b['seats']; $activeCount++; $revenue += (float) $b['amount']; + } elseif ($b['status'] === 'waiting') { + $waitingSeats += (int) $b['seats']; } } $maxSeats = (int) $event['max_seats']; @@ -70,11 +96,13 @@ function bookingBadge(string $status): array { switch ($status) { case 'confirmed': - return ['Confermata', '#e7f7ee', '#1a7f52']; + return ['Confermata', '#e7f7ee', '#1a7f52']; case 'cancelled': - return ['Annullata', '#fdecea', '#c0392b']; + return ['Annullata', '#fdecea', '#c0392b']; + case 'waiting': + return ["Lista d'attesa", '#eef2fb', '#3a5bb8']; default: - return ['In attesa', '#fff4e5', '#b26a00']; + return ['In attesa', '#fff4e5', '#b26a00']; } } ?> @@ -290,19 +318,25 @@ function bookingBadge(string $status): array
    -
    +
    /
    Posti prenotati
    -
    +
    +
    +
    +
    In lista d'attesa
    +
    +
    +
    Iscrizioni attive
    -
    +
    Incasso stimato
    diff --git a/public/event_preview.php b/public/event_preview.php index 1ff0a61..adddb45 100644 --- a/public/event_preview.php +++ b/public/event_preview.php @@ -35,14 +35,17 @@ if (!$ev) { exit(); } -$bs = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings WHERE event_id = ? AND status <> 'cancelled'"); +$bs = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings WHERE event_id = ? AND status IN ('confirmed','pending')"); $bs->execute([$id]); $booked = (int) $bs->fetchColumn(); $maxSeats = (int) $ev['max_seats']; $remaining = max(0, $maxSeats - $booked); $soldOut = $remaining <= 0; $maxPer = max(1, (int) $ev['max_per_booking']); -$maxQty = $soldOut ? 0 : max(1, min($maxPer, $remaining)); +$waitlistOn = ($ev['waitlist_enabled'] ?? 'N') === 'Y'; +$waitMode = $soldOut && $waitlistOn; // pieno ma con lista d'attesa +$hardFull = $soldOut && !$waitlistOn; // pieno senza lista d'attesa +$maxQty = $soldOut ? ($waitMode ? $maxPer : 0) : max(1, min($maxPer, $remaining)); $cost = (float) $ev['cost']; $curr = $ev['currency'] ?: 'EUR'; @@ -573,6 +576,15 @@ function textBlock(?string $t): string color: #c0392b; } + .waitlist-banner { + background: color-mix(in srgb, var(--accent) 12%, white); + color: var(--accent-ink); + border-radius: 12px; + padding: 12px 14px; + font-size: 13.5px; + margin-bottom: 14px; + } + .form-error { color: #c0392b; font-size: 13px; @@ -817,11 +829,14 @@ function textBlock(?string $t): string
    - +
    Evento esaurito — non ci sono più posti disponibili.
    + +
    Posti esauriti — puoi iscriverti alla lista d'attesa: ti avviseremo se si libera un posto.
    +
    -
    Prenota questo evento
    +
    Persone
    @@ -834,7 +849,7 @@ function textBlock(?string $t): string
    Totale
    - +
    @@ -860,12 +875,15 @@ function textBlock(?string $t): string Desidero ricevere aggiornamenti e novità via email. - +
    +
    + mostra pagina di conferma (NON annulla: + * evita che gli scanner email prefetchino il link e annullino da soli) + * POST (token + confirm=1) -> annulla e, se la prenotazione occupava posti, + * promuove automaticamente la prima in lista d'attesa. + */ + $pdo = pdo_conn(); + $token = trim($_POST['token'] ?? $_GET['token'] ?? ''); + $isPost = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST'; + + $booking = null; + if ($token !== '') { + $q = $pdo->prepare("SELECT b.*, e.title AS ev_title, e.starts_at AS ev_starts, e.id AS eid + FROM event_bookings b JOIN events e ON e.id = b.event_id + WHERE b.token = ?"); + $q->execute([$token]); + $booking = $q->fetch(PDO::FETCH_ASSOC); + } + + $state = 'form'; // form | notfound | already | toolate | done + $promotedCount = 0; + $didCancel = false; + $promoted = []; + + if (!$booking) { + $state = 'notfound'; + } elseif ($booking['status'] === 'cancelled') { + $state = 'already'; + } elseif (strtotime($booking['ev_starts']) <= time()) { + $state = 'toolate'; + } elseif ($isPost && !empty($_POST['confirm'])) { + try { + $pdo->beginTransaction(); + // Lock evento per serializzare con eventuali prenotazioni concorrenti + $pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([(int) $booking['eid']]); + + // Rileggo lo stato aggiornato della prenotazione + $bl = $pdo->prepare("SELECT * FROM event_bookings WHERE id = ?"); + $bl->execute([(int) $booking['id']]); + $fresh = $bl->fetch(PDO::FETCH_ASSOC); + + if ($fresh && $fresh['status'] !== 'cancelled') { + $wasOccupying = in_array($fresh['status'], ['confirmed', 'pending', 'offered'], true); + $pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ?") + ->execute([(int) $fresh['id']]); + + if ($wasOccupying) { + $promoted = promoteFromWaitlist($pdo, (int) $booking['eid']); + $promotedCount = count($promoted); + } + $didCancel = true; + } + $pdo->commit(); + $state = 'done'; + + // Email dopo il commit (best-effort) + if ($didCancel) { + try { + $evStmt = pdo_conn()->prepare("SELECT * FROM events WHERE id = ?"); + $evStmt->execute([(int) $booking['eid']]); + $evRow = $evStmt->fetch(PDO::FETCH_ASSOC); + if ($evRow) { + yse_mail_user_cancel($booking, $evRow); // B: conferma disdetta + foreach ($promoted as $pr) { // E: promozione con riconferma + try { + yse_mail_promotion($pr, $evRow); + } catch (Throwable $pe) { + error_log('[promo mail] ' . $pe->getMessage()); + } + } + } + } catch (Throwable $mailEx) { + error_log('[cancel mail] ' . $mailEx->getMessage()); + } + } + } catch (Throwable $ex) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + error_log('[apiwp/cancel] ' . $ex->getMessage()); + $state = 'error'; + } + } + + function e($v): string + { + return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); + } + ?> + + + + + + Disdetta prenotazione — YogaSoul + + + + + + + + +
    + +
    ✕
    +

    Link non valido

    +

    Questa prenotazione non esiste o il link è errato.

    + + +
    ✓
    +

    Già annullata

    +

    Questa prenotazione risulta già annullata.

    + + +
    ⏱
    +

    Non più annullabile

    +

    L'evento è già iniziato: non è più possibile annullare online. Contattaci per assistenza.

    + + +
    !
    +

    Si è verificato un errore

    +

    Riprova tra poco. Se il problema persiste, contattaci.

    + + +
    ✓
    +

    Prenotazione annullata

    +

    La tua prenotazione per è stata annullata.

    + 0) : ?> +

    Un posto è stato assegnato alla prima persona in lista d'attesa.

    + + + +
    ?
    +

    Annullare la prenotazione?

    +
    +
    +
    · post
    +
    +

    Confermi di voler annullare la prenotazione a nome di ?

    +
    + + + +
    +

    Se hai cambiato idea, chiudi pure questa pagina.

    + +
    + + \ No newline at end of file diff --git a/public/apiwp/confirm.php b/public/apiwp/confirm.php new file mode 100644 index 0000000..bf0d467 --- /dev/null +++ b/public/apiwp/confirm.php @@ -0,0 +1,247 @@ + 'confirmed'). + * GET confirm.php?token=... -> pagina di conferma (non modifica nulla) + * POST (token + confirm=1) -> conferma il posto e invia l'email di conferma. + */ +$pdo = pdo_conn(); +$token = trim($_POST['token'] ?? $_GET['token'] ?? ''); +$isPost = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST'; + +$booking = null; +if ($token !== '') { + $q = $pdo->prepare("SELECT b.*, e.title AS ev_title, e.starts_at AS ev_starts, e.id AS eid + FROM event_bookings b JOIN events e ON e.id = b.event_id + WHERE b.token = ?"); + $q->execute([$token]); + $booking = $q->fetch(PDO::FETCH_ASSOC); +} + +$state = 'form'; // form | notfound | already | invalid | toolate | done | error + +if (!$booking) { + $state = 'notfound'; +} elseif ($booking['status'] === 'confirmed') { + $state = 'already'; +} elseif ($booking['status'] !== 'offered') { + // waiting / cancelled / ecc.: non c'è un'offerta da confermare + $state = 'invalid'; +} elseif (strtotime($booking['ev_starts']) <= time()) { + $state = 'toolate'; +} elseif ($isPost && !empty($_POST['confirm'])) { + try { + $upd = $pdo->prepare("UPDATE event_bookings SET status = 'confirmed', updated_at = NOW() WHERE id = ? AND status = 'offered'"); + $upd->execute([(int) $booking['id']]); + + if ($upd->rowCount() > 0) { + $state = 'done'; + // Email di conferma definitiva (best-effort) + try { + $ev = $pdo->prepare("SELECT * FROM events WHERE id = ?"); + $ev->execute([(int) $booking['eid']]); + $evRow = $ev->fetch(PDO::FETCH_ASSOC); + if ($evRow) { + $booking['status'] = 'confirmed'; + yse_mail_booking($booking, $evRow); + } + } catch (Throwable $me) { + error_log('[confirm mail] ' . $me->getMessage()); + } + } else { + $state = 'invalid'; + } + } catch (Throwable $ex) { + error_log('[apiwp/confirm] ' . $ex->getMessage()); + $state = 'error'; + } +} + +function e($v): string +{ + return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); +} +?> + + + + + + Conferma posto — YogaSoul + + + + + + + + +
    + +
    ✕
    +

    Link non valido

    +

    Questo link non è valido o è scaduto.

    + + +
    ✓
    +

    Posto già confermato

    +

    Il tuo posto per è già confermato. Ci vediamo lì!

    + + +
    i
    +

    Offerta non più disponibile

    +

    Questa offerta non è più valida. Se pensi ci sia un errore, contattaci.

    + + +
    ⏱
    +

    Evento già iniziato

    +

    Non è più possibile confermare online. Contattaci per assistenza.

    + + +
    !
    +

    Si è verificato un errore

    +

    Riprova tra poco. Se il problema persiste, contattaci.

    + + +
    ✓
    +

    Posto confermato!

    +

    Grazie , il tuo posto per è confermato. Ti abbiamo inviato l'email con tutti i dettagli.

    + + +
    🎉
    +

    Si è liberato un posto!

    +
    +
    +
    · post
    +
    +

    Confermi la tua presenza, ?

    +
    + + + +
    +

    Se non puoi partecipare, ignora pure questa email: il posto passerà a un'altra persona.

    + +
    + + + \ No newline at end of file diff --git a/public/apiwp/events.php b/public/apiwp/events.php index 413e657..cc9c5e2 100644 --- a/public/apiwp/events.php +++ b/public/apiwp/events.php @@ -49,7 +49,7 @@ try { LEFT JOIN auth_users u ON u.id = e.user_id LEFT JOIN ( SELECT event_id, SUM(seats) AS occupied - FROM event_bookings WHERE status IN ('confirmed','pending') + FROM event_bookings WHERE status IN ('confirmed','pending','offered') GROUP BY event_id ) b ON b.event_id = e.id LEFT JOIN ( diff --git a/public/event_delete.php b/public/event_delete.php index 54b9084..54a0509 100644 --- a/public/event_delete.php +++ b/public/event_delete.php @@ -41,7 +41,11 @@ try { // 3) Notifica (opzionale). Il template vero arriva nello step successivo. if ($notify && !empty($participants)) { foreach ($participants as $p) { - sendEventCancellationMail($p['email'], $p['name'], $event); + try { + yse_mail_event_cancel($p, $event); + } catch (Throwable $mailEx) { + error_log('[event_delete mail] ' . $mailEx->getMessage()); + } } } diff --git a/public/event_form.php b/public/event_form.php index f4975ff..e455dcf 100644 --- a/public/event_form.php +++ b/public/event_form.php @@ -37,6 +37,9 @@ if ($id > 0) { $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); // Sedi salvate (se la tabella non esiste ancora, si degrada su indirizzo libero) $locations = []; @@ -68,6 +71,9 @@ $val = [ 'suitable_for' => $ev['suitable_for'] ?? '', 'gallery' => $ev['gallery'] ?? '', 'waitlist_enabled' => ($ev['waitlist_enabled'] ?? 'Y') === 'Y', + 'reminder_enabled' => $ev['reminder_enabled'] ?? null, + 'reminder_hours' => $ev['reminder_hours'] ?? '', + 'offer_expiry_hours' => $ev['offer_expiry_hours'] ?? '', ]; $isEdit = $id > 0; @@ -549,6 +555,27 @@ $pageTitle = $isEdit ? 'Modifica evento' : 'Nuovo evento';
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +

    Lascia vuoto o "Predefinito" per usare le impostazioni generali (Opzioni Eventi).

    +
    Indietro +
    +

    Opzioni Eventi

    +
    +
    +
    + + +
    +
    + + +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + + + + + + +
    +
    +
    +
    +
    +
    Copie alla scuola
    +

    Scegli su quali email l'insegnante/scuola riceve una copia (CC).

    + +
    + + +
    Se vuota, non viene inviata nessuna copia anche se le voci sotto sono attive.
    +
    + + +
    +
    +
    +
    +
    +
    + > +
    +
    + +
    +
    + +
    +
    +
    Promemoria
    +

    Invio automatico del promemoria agli iscritti prima dell'evento (richiede il cron, vedi setup).

    + +
    +
    +
    Attiva promemoria
    +
    Invia un'email di promemoria agli iscritti confermati.
    +
    +
    + > +
    +
    +
    +
    +
    Ore prima dell'evento
    +
    Quante ore prima dell'inizio inviare il promemoria.
    +
    +
    + +
    +
    +
    +
    +
    Scadenza offerta lista d'attesa (ore)
    +
    Default: quanto tempo una persona promossa ha per riconfermare prima che il posto passi al prossimo. Modificabile per singolo evento.
    +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + + \ No newline at end of file diff --git a/public/event_options_save.php b/public/event_options_save.php new file mode 100644 index 0000000..e7b33f7 --- /dev/null +++ b/public/event_options_save.php @@ -0,0 +1,57 @@ +getConnection(); + +function yn(string $k): string +{ + return (isset($_POST[$k]) && $_POST[$k] === 'Y') ? 'Y' : 'N'; +} + +$ccEmail = trim($_POST['event_notify_email'] ?? ''); +$reminderH = max(1, min(168, (int) ($_POST['event_reminder_hours'] ?? 24))); +$offerH = max(1, min(336, (int) ($_POST['event_offer_expiry_hours'] ?? 24))); + +try { + // Individua la riga singola di configurazione + $idption = (int) $pdo->query("SELECT idption FROM `option` ORDER BY idption ASC LIMIT 1")->fetchColumn(); + + $sql = "UPDATE `option` SET + event_notify_email = :cc_email, + event_cc_booking = :cc_booking, + event_cc_promotion = :cc_promotion, + event_cc_user_cancel = :cc_user_cancel, + event_cc_event_cancel = :cc_event_cancel, + event_cc_reminder = :cc_reminder, + event_reminder_enabled = :rem_enabled, + event_reminder_hours = :rem_hours, + event_offer_expiry_hours = :offer_hours + WHERE idption = :idption"; + $pdo->prepare($sql)->execute([ + ':cc_email' => $ccEmail !== '' ? $ccEmail : null, + ':cc_booking' => yn('event_cc_booking'), + ':cc_promotion' => yn('event_cc_promotion'), + ':cc_user_cancel' => yn('event_cc_user_cancel'), + ':cc_event_cancel' => yn('event_cc_event_cancel'), + ':cc_reminder' => yn('event_cc_reminder'), + ':rem_enabled' => yn('event_reminder_enabled'), + ':rem_hours' => $reminderH, + ':offer_hours' => $offerH, + ':idption' => $idption, + ]); + + header('Location: event_options.php?message=success'); + exit(); +} catch (Throwable $ex) { + error_log('[event_options_save] ' . $ex->getMessage()); + header('Location: event_options.php?message=error'); + exit(); +} diff --git a/public/event_participants.php b/public/event_participants.php index 4004be0..e581972 100644 --- a/public/event_participants.php +++ b/public/event_participants.php @@ -1,6 +1,7 @@ beginTransaction(); $pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([$eventId]); @@ -44,15 +46,31 @@ if (isset($_GET['cancel_booking'])) { $bStatus = $bl->fetchColumn(); if ($bStatus !== false && $bStatus !== 'cancelled') { - $wasOccupying = in_array($bStatus, ['confirmed', 'pending'], true); + $wasOccupying = in_array($bStatus, ['confirmed', 'pending', 'offered'], true); $pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ? AND event_id = ?") ->execute([$bookingId, $eventId]); if ($wasOccupying) { - // libera posti -> promuove la prima in lista d'attesa - promoteFromWaitlist($pdo, $eventId); + // libera posti -> offre alla prima in lista d'attesa + $promoted = promoteFromWaitlist($pdo, $eventId); } } $pdo->commit(); + + // Email di promozione (E, con riconferma) ai nuovi "offered" + if (!empty($promoted)) { + $ev = $pdo->prepare("SELECT * FROM events WHERE id = ?"); + $ev->execute([$eventId]); + $evRow = $ev->fetch(PDO::FETCH_ASSOC); + if ($evRow) { + foreach ($promoted as $pr) { + try { + yse_mail_promotion($pr, $evRow); + } catch (Throwable $me) { + error_log('[promo mail] ' . $me->getMessage()); + } + } + } + } } catch (Throwable $ex) { if ($pdo->inTransaction()) { $pdo->rollBack(); @@ -75,13 +93,13 @@ $bStmt = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? ORDER BY $bStmt->execute([$eventId]); $bookings = $bStmt->fetchAll(PDO::FETCH_ASSOC); -// Riepilogo: i posti occupati sono solo confermate/pending; la lista d'attesa a parte +// Riepilogo: posti occupati = confermate/pending/offerte (in riconferma); lista d'attesa a parte $bookedSeats = 0; $waitingSeats = 0; $activeCount = 0; $revenue = 0.0; foreach ($bookings as $b) { - if (in_array($b['status'], ['confirmed', 'pending'], true)) { + if (in_array($b['status'], ['confirmed', 'pending', 'offered'], true)) { $bookedSeats += (int) $b['seats']; $activeCount++; $revenue += (float) $b['amount']; @@ -96,13 +114,15 @@ function bookingBadge(string $status): array { switch ($status) { case 'confirmed': - return ['Confermata', '#e7f7ee', '#1a7f52']; + return ['Confermata', '#e7f7ee', '#1a7f52']; case 'cancelled': - return ['Annullata', '#fdecea', '#c0392b']; + return ['Annullata', '#fdecea', '#c0392b']; case 'waiting': - return ["Lista d'attesa", '#eef2fb', '#3a5bb8']; + return ["Lista d'attesa", '#eef2fb', '#3a5bb8']; + case 'offered': + return ['Da riconfermare', '#fff4e5', '#b26a00']; default: - return ['In attesa', '#fff4e5', '#b26a00']; + return ['In attesa', '#fff4e5', '#b26a00']; } } ?> diff --git a/public/event_preview.php b/public/event_preview.php index adddb45..f320dcd 100644 --- a/public/event_preview.php +++ b/public/event_preview.php @@ -35,7 +35,7 @@ if (!$ev) { exit(); } -$bs = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings WHERE event_id = ? AND status IN ('confirmed','pending')"); +$bs = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings WHERE event_id = ? AND status IN ('confirmed','pending','offered')"); $bs->execute([$id]); $booked = (int) $bs->fetchColumn(); $maxSeats = (int) $ev['max_seats']; @@ -186,6 +186,14 @@ function textBlock(?string $t): string line-height: 1; } + .chead-thumb { + width: 76px; + height: 76px; + border-radius: 12px; + object-fit: cover; + flex-shrink: 0; + } + .chead .main { flex: 1; min-width: 0; @@ -702,6 +710,9 @@ function textBlock(?string $t): string
    + + <?php echo e($ev['title']); ?> +

    diff --git a/public/event_reminder.php b/public/event_reminder.php new file mode 100644 index 0000000..002885e --- /dev/null +++ b/public/event_reminder.php @@ -0,0 +1,222 @@ +getConnection(); + +function e($value): string +{ + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} + +$eventId = isset($_GET['id']) ? (int) $_GET['id'] : 0; +if ($eventId <= 0) { + header('Location: events.php'); + exit(); +} + +$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ?"); +$stmt->execute([$eventId]); +$event = $stmt->fetch(PDO::FETCH_ASSOC); +if (!$event) { + header('Location: events.php?message=error'); + exit(); +} + +$cnt = $pdo->prepare("SELECT COUNT(*) FROM event_bookings WHERE event_id = ? AND status = 'confirmed'"); +$cnt->execute([$eventId]); +$recipients = (int) $cnt->fetchColumn(); + +$defaultMsg = "Ti ricordiamo il tuo prossimo appuntamento. Ti aspettiamo, a presto!"; +$meetingLink = yse_maps_link($event); // prefill: link mappa derivato dall'indirizzo/coordinate +$message = $_GET['message'] ?? ''; +?> + + + + + + YogiBook - Promemoria + + + + + + + + + + + + +
    +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + + + + + + +
    +
    +
    +
    +
    Invia promemoria
    +

    Controlla il testo e il punto di ritrovo, poi invia agli iscritti confermati.

    + +
    + ·
    + Destinatari confermati: +
    + +
    + + +
    + + +
    Data, ora e luogo dell'evento vengono aggiunti automaticamente sotto al messaggio.
    +
    + +
    + + +
    Prefissato dal luogo dell'evento. Puoi incollare un link di Google Maps più preciso (es. la condivisione della posizione). Lascia vuoto per non includerlo.
    +
    + +
    + Indietro + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + + + \ No newline at end of file diff --git a/public/event_reminder_send.php b/public/event_reminder_send.php new file mode 100644 index 0000000..3987943 --- /dev/null +++ b/public/event_reminder_send.php @@ -0,0 +1,58 @@ +getConnection(); + +$eventId = (int) ($_POST['id'] ?? 0); +$message = trim($_POST['message'] ?? ''); +$meetingLink = trim($_POST['meeting_link'] ?? ''); + +if ($eventId <= 0) { + header('Location: events.php'); + exit(); +} + +// Accetta solo link http/https come punto di ritrovo +if ($meetingLink !== '' && !preg_match('#^https?://#i', $meetingLink)) { + $meetingLink = ''; +} + +try { + $es = $pdo->prepare("SELECT * FROM events WHERE id = ?"); + $es->execute([$eventId]); + $event = $es->fetch(PDO::FETCH_ASSOC); + if (!$event) { + header('Location: events.php?message=error'); + exit(); + } + + $ps = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? AND status = 'confirmed'"); + $ps->execute([$eventId]); + $participants = $ps->fetchAll(PDO::FETCH_ASSOC); + + foreach ($participants as $p) { + try { + yse_mail_reminder($p, $event, $message, $meetingLink); + } catch (Throwable $me) { + error_log('[reminder send] ' . $me->getMessage()); + } + } + + $pdo->prepare("UPDATE events SET reminder_sent_at = NOW() WHERE id = ?")->execute([$eventId]); + + header('Location: event_reminder.php?id=' . $eventId . '&message=sent'); + exit(); +} catch (Throwable $ex) { + error_log('[event_reminder_send] ' . $ex->getMessage()); + header('Location: event_reminder.php?id=' . $eventId . '&message=error'); + exit(); +} diff --git a/public/event_save.php b/public/event_save.php index 4389b2c..8279617 100644 --- a/public/event_save.php +++ b/public/event_save.php @@ -112,6 +112,11 @@ $suitableFor = trim($_POST['suitable_for'] ?? ''); $waitlistEnabled = (isset($_POST['waitlist_enabled']) && $_POST['waitlist_enabled'] === 'Y') ? 'Y' : 'N'; +// 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; + // Galleria: array JSON di path -> validazione e re-encode pulito $gallery = null; $galleryRaw = $_POST['gallery'] ?? ''; @@ -138,6 +143,7 @@ try { address = :address, lat = :lat, lng = :lng, cost = :cost, currency = :currency, max_seats = :max_seats, max_per_booking = :max_per_booking, color = :color, 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, updated_at = NOW() WHERE id = :id"; $params = [':id' => $id]; @@ -147,12 +153,14 @@ try { 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, created_at, updated_at) VALUES (: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, NOW(), NOW())"; $params = [':user_id' => $currentUserId]; } @@ -175,6 +183,9 @@ try { ':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, diff --git a/public/events.php b/public/events.php index 6943f5b..c17a791 100644 --- a/public/events.php +++ b/public/events.php @@ -30,7 +30,7 @@ $sql = "SELECT e.*, SUM(seats) AS booked, COUNT(*) AS bookings_count FROM event_bookings - WHERE status <> 'cancelled' + WHERE status IN ('confirmed','pending','offered') GROUP BY event_id ) b ON b.event_id = e.id LEFT JOIN auth_users u ON u.id = e.user_id @@ -391,6 +391,9 @@ function statusBadge(string $status): array Anteprima Partecipanti Modifica + time()) : ?> + Promemoria +
    '], "\n", $html)) + )); + + return $mail->send(); + } catch (Throwable $e) { + error_log('[event mail] ' . $e->getMessage()); + return false; + } +} + +/* ---------- 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 = 'Quando' . $when . ''; + if ($addr !== '') { + $mapsLink = $maps !== '' ? '  apri mappa' : ''; + $rows .= 'Dove' . yse_h($addr) . $mapsLink . ''; + } + + $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 .= '

    ' . $f[1] . ':
    ' . nl2br(yse_h($val)) . '

    '; + } + } + + $html = '' . $rows . '
    '; + if ($info !== '') { + $html .= '
    ' . $info . '
    '; + } + 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 ' +
    +
    + + + + + +
    +
    ' . yse_h($from) . '
    +

    ' . $heading . '

    +
    ' . $title . '
    +
    ' . $bodyHtml . '
    Questa email è stata inviata da ' . yse_h($from) . '.
    +
    +
    '; +} + +function yse_button(string $url, string $label, string $accent): string +{ + return '' . yse_h($label) . ''; +} + +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 = '

    Ciao ' . yse_h($name) . ", ti abbiamo messo in lista d'attesa per questo evento. Ti avviseremo appena si libera un posto.

    "; + } else { + $heading = 'Prenotazione confermata'; + $intro = '

    Ciao ' . yse_h($name) . ', la tua prenotazione è confermata. Ecco i dettagli:

    '; + } + + $body = $intro; + $body .= yse_event_info_block($event); + $body .= '

    Posti prenotati: ' . (int) $booking['seats'] . '

    '; + $body .= '

    Se non puoi più partecipare, puoi annullare qui:

    '; + $body .= '

    ' . yse_button(yse_cancel_url($booking), 'Annulla prenotazione', $accent) . '

    '; + + $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 = '

    Ciao ' . yse_h($name) . ', la tua prenotazione per questo evento è stata annullata.

    '; + $body .= yse_event_info_block($event); + $body .= '

    Ci dispiace non vederti questa volta. A presto!

    '; + + $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 = '

    Ciao ' . yse_h($name) . ", ci dispiace: l'evento è stato annullato.

    "; + $body .= yse_event_info_block($event); + $body .= '

    La tua prenotazione è stata annullata automaticamente. Per informazioni, rispondi pure a questa email.

    '; + + $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 = '

    Ciao ' . yse_h($name) . ", buone notizie: si è liberato un posto e tocca a te!

    "; + $body .= yse_event_info_block($event); + $body .= '

    Conferma entro breve per tenere il posto:

    '; + $body .= '

    ' . yse_button(yse_confirm_url($booking), 'Confermo, ci sarò', $accent) . '

    '; + $body .= '

    Se non puoi partecipare, annulla qui così offriamo il posto a un\'altra persona.

    '; + + $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 = '

    Ciao ' . yse_h($name) . ',
    ' . $note . '

    '; + $body .= yse_event_info_block($event); + if ($meetingLink !== '') { + $body .= '

    Punto di ritrovo: apri la posizione sulla mappa

    '; + } + $body .= '

    Se non puoi più partecipare, avvisaci annullando:

    '; + $body .= '

    ' . yse_button(yse_cancel_url($booking), 'Annulla prenotazione', $accent) . '

    '; + + $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']); +} diff --git a/public/include/event_waitlist.php b/public/include/event_waitlist.php new file mode 100644 index 0000000..2258334 --- /dev/null +++ b/public/include/event_waitlist.php @@ -0,0 +1,60 @@ + le prenotazioni promosse (per inviare le email nello Step 4) + */ +if (!function_exists('promoteFromWaitlist')) { + function promoteFromWaitlist(PDO $pdo, int $eventId): array + { + $ev = $pdo->prepare("SELECT max_seats FROM events WHERE id = ?"); + $ev->execute([$eventId]); + $maxSeats = (int) $ev->fetchColumn(); + if ($maxSeats <= 0) { + return []; + } + + $os = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings + WHERE event_id = ? AND status IN ('confirmed','pending','offered')"); + $os->execute([$eventId]); + $free = $maxSeats - (int) $os->fetchColumn(); + if ($free <= 0) { + return []; + } + + $ws = $pdo->prepare("SELECT * FROM event_bookings + WHERE event_id = ? AND status = 'waiting' + ORDER BY created_at ASC, id ASC"); + $ws->execute([$eventId]); + $waiting = $ws->fetchAll(PDO::FETCH_ASSOC); + + $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 = ?"); + foreach ($waiting as $w) { + $need = (int) $w['seats']; + if ($need <= $free) { + $upd->execute([$w['id']]); + $free -= $need; + $w['status'] = 'offered'; + $promoted[] = $w; + if ($free <= 0) { + break; + } + } else { + break; // rispetta la coda + } + } + return $promoted; + } +} diff --git a/public/include/sidebar.php b/public/include/sidebar.php index 176b103..d144a9f 100644 --- a/public/include/sidebar.php +++ b/public/include/sidebar.php @@ -114,6 +114,12 @@ Sedi +
  • + + + Opzione Eventi + +
  • From f396954be43acd6ecaeb28099e25cc3f99455ba3 Mon Sep 17 00:00:00 2001 From: solocla Date: Fri, 18 Sep 2026 10:05:37 +0200 Subject: [PATCH 05/18] error with cancel --- public/apiwp/cancel.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/apiwp/cancel.php b/public/apiwp/cancel.php index 85ffe57..0a0646f 100644 --- a/public/apiwp/cancel.php +++ b/public/apiwp/cancel.php @@ -1,4 +1,6 @@ Date: Fri, 18 Sep 2026 10:11:31 +0200 Subject: [PATCH 06/18] fixed cancel error --- public/apiwp/cancel.php | 10 +++++----- public/apiwp/confirm.php | 8 +++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/public/apiwp/cancel.php b/public/apiwp/cancel.php index 0a0646f..af56c02 100644 --- a/public/apiwp/cancel.php +++ b/public/apiwp/cancel.php @@ -1,6 +1,4 @@ diff --git a/public/apiwp/confirm.php b/public/apiwp/confirm.php index bf0d467..7bbb995 100644 --- a/public/apiwp/confirm.php +++ b/public/apiwp/confirm.php @@ -59,9 +59,11 @@ if (!$booking) { } } -function e($v): string -{ - return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); +if (!function_exists('e')) { + function e($v): string + { + return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); + } } ?> From c9a0cc6034538e6c6c805dba1f01e1d467b03dae Mon Sep 17 00:00:00 2001 From: solocla Date: Fri, 18 Sep 2026 10:13:36 +0200 Subject: [PATCH 07/18] fixed cancel 2 --- public/apiwp/cancel.php | 295 ++------------------------------------- public/apiwp/confirm.php | 8 +- 2 files changed, 14 insertions(+), 289 deletions(-) diff --git a/public/apiwp/cancel.php b/public/apiwp/cancel.php index af56c02..c48fd3c 100644 --- a/public/apiwp/cancel.php +++ b/public/apiwp/cancel.php @@ -27,6 +27,7 @@ if ($token !== '') { $state = 'form'; // form | notfound | already | toolate | done $promotedCount = 0; $didCancel = false; +$promoted = []; if (!$booking) { $state = 'notfound'; @@ -46,28 +47,34 @@ if (!$booking) { $fresh = $bl->fetch(PDO::FETCH_ASSOC); if ($fresh && $fresh['status'] !== 'cancelled') { - $wasOccupying = in_array($fresh['status'], ['confirmed', 'pending'], true); + $wasOccupying = in_array($fresh['status'], ['confirmed', 'pending', 'offered'], true); $pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ?") ->execute([(int) $fresh['id']]); if ($wasOccupying) { $promoted = promoteFromWaitlist($pdo, (int) $booking['eid']); $promotedCount = count($promoted); - // Nota: l'email di promozione (E, con riconferma) si attiva nel secondo tempo. } $didCancel = true; } $pdo->commit(); $state = 'done'; - // Email di conferma disdetta all'utente (best-effort) + // Email dopo il commit (best-effort) if ($didCancel) { try { $evStmt = pdo_conn()->prepare("SELECT * FROM events WHERE id = ?"); $evStmt->execute([(int) $booking['eid']]); $evRow = $evStmt->fetch(PDO::FETCH_ASSOC); if ($evRow) { - yse_mail_user_cancel($booking, $evRow); + yse_mail_user_cancel($booking, $evRow); // B: conferma disdetta + foreach ($promoted as $pr) { // E: promozione con riconferma + try { + yse_mail_promotion($pr, $evRow); + } catch (Throwable $pe) { + error_log('[promo mail] ' . $pe->getMessage()); + } + } } } catch (Throwable $mailEx) { error_log('[cancel mail] ' . $mailEx->getMessage()); @@ -273,284 +280,4 @@ if (!function_exists('e')) {
    - mostra pagina di conferma (NON annulla: - * evita che gli scanner email prefetchino il link e annullino da soli) - * POST (token + confirm=1) -> annulla e, se la prenotazione occupava posti, - * promuove automaticamente la prima in lista d'attesa. - */ - $pdo = pdo_conn(); - $token = trim($_POST['token'] ?? $_GET['token'] ?? ''); - $isPost = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST'; - - $booking = null; - if ($token !== '') { - $q = $pdo->prepare("SELECT b.*, e.title AS ev_title, e.starts_at AS ev_starts, e.id AS eid - FROM event_bookings b JOIN events e ON e.id = b.event_id - WHERE b.token = ?"); - $q->execute([$token]); - $booking = $q->fetch(PDO::FETCH_ASSOC); - } - - $state = 'form'; // form | notfound | already | toolate | done - $promotedCount = 0; - $didCancel = false; - $promoted = []; - - if (!$booking) { - $state = 'notfound'; - } elseif ($booking['status'] === 'cancelled') { - $state = 'already'; - } elseif (strtotime($booking['ev_starts']) <= time()) { - $state = 'toolate'; - } elseif ($isPost && !empty($_POST['confirm'])) { - try { - $pdo->beginTransaction(); - // Lock evento per serializzare con eventuali prenotazioni concorrenti - $pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([(int) $booking['eid']]); - - // Rileggo lo stato aggiornato della prenotazione - $bl = $pdo->prepare("SELECT * FROM event_bookings WHERE id = ?"); - $bl->execute([(int) $booking['id']]); - $fresh = $bl->fetch(PDO::FETCH_ASSOC); - - if ($fresh && $fresh['status'] !== 'cancelled') { - $wasOccupying = in_array($fresh['status'], ['confirmed', 'pending', 'offered'], true); - $pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ?") - ->execute([(int) $fresh['id']]); - - if ($wasOccupying) { - $promoted = promoteFromWaitlist($pdo, (int) $booking['eid']); - $promotedCount = count($promoted); - } - $didCancel = true; - } - $pdo->commit(); - $state = 'done'; - - // Email dopo il commit (best-effort) - if ($didCancel) { - try { - $evStmt = pdo_conn()->prepare("SELECT * FROM events WHERE id = ?"); - $evStmt->execute([(int) $booking['eid']]); - $evRow = $evStmt->fetch(PDO::FETCH_ASSOC); - if ($evRow) { - yse_mail_user_cancel($booking, $evRow); // B: conferma disdetta - foreach ($promoted as $pr) { // E: promozione con riconferma - try { - yse_mail_promotion($pr, $evRow); - } catch (Throwable $pe) { - error_log('[promo mail] ' . $pe->getMessage()); - } - } - } - } catch (Throwable $mailEx) { - error_log('[cancel mail] ' . $mailEx->getMessage()); - } - } - } catch (Throwable $ex) { - if ($pdo->inTransaction()) { - $pdo->rollBack(); - } - error_log('[apiwp/cancel] ' . $ex->getMessage()); - $state = 'error'; - } - } - - function e($v): string - { - return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); - } - ?> - - - - - - Disdetta prenotazione — YogaSoul - - - - - - - - -
    - -
    ✕
    -

    Link non valido

    -

    Questa prenotazione non esiste o il link è errato.

    - - -
    ✓
    -

    Già annullata

    -

    Questa prenotazione risulta già annullata.

    - - -
    ⏱
    -

    Non più annullabile

    -

    L'evento è già iniziato: non è più possibile annullare online. Contattaci per assistenza.

    - - -
    !
    -

    Si è verificato un errore

    -

    Riprova tra poco. Se il problema persiste, contattaci.

    - - -
    ✓
    -

    Prenotazione annullata

    -

    La tua prenotazione per è stata annullata.

    - 0) : ?> -

    Un posto è stato assegnato alla prima persona in lista d'attesa.

    - - - -
    ?
    -

    Annullare la prenotazione?

    -
    -
    -
    · post
    -
    -

    Confermi di voler annullare la prenotazione a nome di ?

    -
    - - - -
    -

    Se hai cambiato idea, chiudi pure questa pagina.

    - -
    - - \ No newline at end of file diff --git a/public/apiwp/confirm.php b/public/apiwp/confirm.php index 7bbb995..bf0d467 100644 --- a/public/apiwp/confirm.php +++ b/public/apiwp/confirm.php @@ -59,11 +59,9 @@ if (!$booking) { } } -if (!function_exists('e')) { - function e($v): string - { - return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); - } +function e($v): string +{ + return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); } ?> From 9a423414a75f46e0ae32e4016f0546c0e02ae42e Mon Sep 17 00:00:00 2001 From: solocla Date: Fri, 18 Sep 2026 15:36:44 +0200 Subject: [PATCH 08/18] added cron and waiting --- ...0260918133221_add_publish_at_to_events.php | 15 + public/apiwp/cron_reminders.php | 127 ++++ public/apiwp/events.php | 6 +- public/event_form.php | 22 + public/event_save.php | 16 +- public/events.php | 65 +- .../assets/yogasoul-eventi.css | 600 ++++-------------- wp plugin/yogasoul-eventi/yogasoul-eventi.php | 69 +- 8 files changed, 406 insertions(+), 514 deletions(-) create mode 100644 db/migrations/20260918133221_add_publish_at_to_events.php create mode 100644 public/apiwp/cron_reminders.php diff --git a/db/migrations/20260918133221_add_publish_at_to_events.php b/db/migrations/20260918133221_add_publish_at_to_events.php new file mode 100644 index 0000000..7736bf1 --- /dev/null +++ b/db/migrations/20260918133221_add_publish_at_to_events.php @@ -0,0 +1,15 @@ +table('events') + ->addColumn('publish_at', 'datetime', ['null' => true, 'comment' => 'apertura prenotazioni programmata', 'after' => 'status']) + ->update(); + } +} diff --git a/public/apiwp/cron_reminders.php b/public/apiwp/cron_reminders.php new file mode 100644 index 0000000..4a5c7e1 --- /dev/null +++ b/public/apiwp/cron_reminders.php @@ -0,0 +1,127 @@ + false, 'error' => 'Forbidden'], 403); +} + +$report = ['published' => 0, 'reminders' => 0, 'offers_expired' => 0, 'promoted' => 0]; + +// Default scuola +$opt = []; +try { + $opt = $pdo->query("SELECT * FROM `option` LIMIT 1")->fetch(PDO::FETCH_ASSOC) ?: []; +} catch (Throwable $e) { + $opt = []; +} +$defRemEnabled = ($opt['event_reminder_enabled'] ?? 'N') === 'Y'; +$defRemHours = (int) ($opt['event_reminder_hours'] ?? 24); +$defOfferHours = (int) ($opt['event_offer_expiry_hours'] ?? 24); + +/* 1) Pubblica gli eventi programmati arrivati a scadenza */ +try { + $pub = $pdo->prepare("UPDATE events SET status = 'published', updated_at = NOW() + WHERE status = 'scheduled' AND publish_at IS NOT NULL AND publish_at <= NOW()"); + $pub->execute(); + $report['published'] = $pub->rowCount(); +} catch (Throwable $e) { + error_log('[cron publish] ' . $e->getMessage()); +} + +/* 2) Promemoria automatici (una sola volta per evento) */ +try { + $evs = $pdo->query("SELECT * FROM events + WHERE status = 'published' AND reminder_sent_at IS NULL AND starts_at > NOW()") + ->fetchAll(PDO::FETCH_ASSOC); + foreach ($evs as $ev) { + $enabled = $ev['reminder_enabled'] !== null ? ($ev['reminder_enabled'] === 'Y') : $defRemEnabled; + if (!$enabled) { + continue; + } + $hours = $ev['reminder_hours'] !== null ? (int) $ev['reminder_hours'] : $defRemHours; + if (strtotime($ev['starts_at']) - $hours * 3600 > time()) { + continue; // non ancora l'ora del promemoria + } + $ps = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? AND status = 'confirmed'"); + $ps->execute([$ev['id']]); + foreach ($ps->fetchAll(PDO::FETCH_ASSOC) as $p) { + try { + yse_mail_reminder($p, $ev); + $report['reminders']++; + } catch (Throwable $me) { + error_log('[cron reminder] ' . $me->getMessage()); + } + } + $pdo->prepare("UPDATE events SET reminder_sent_at = NOW() WHERE id = ?")->execute([$ev['id']]); + } +} catch (Throwable $e) { + error_log('[cron reminders] ' . $e->getMessage()); +} + +/* 3) Scadenza offerte lista d'attesa */ +try { + $offers = $pdo->query("SELECT b.*, e.offer_expiry_hours AS ev_offer_hours + FROM event_bookings b JOIN events e ON e.id = b.event_id + WHERE b.status = 'offered' AND b.offered_at IS NOT NULL") + ->fetchAll(PDO::FETCH_ASSOC); + foreach ($offers as $o) { + $hours = $o['ev_offer_hours'] !== null ? (int) $o['ev_offer_hours'] : $defOfferHours; + if (strtotime($o['offered_at']) + $hours * 3600 > time()) { + continue; // offerta ancora valida + } + $promoted = []; + try { + $pdo->beginTransaction(); + $pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([(int) $o['event_id']]); + $st = $pdo->prepare("SELECT status FROM event_bookings WHERE id = ?"); + $st->execute([(int) $o['id']]); + if ($st->fetchColumn() === 'offered') { + $pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ?") + ->execute([(int) $o['id']]); + $promoted = promoteFromWaitlist($pdo, (int) $o['event_id']); + $report['offers_expired']++; + } + $pdo->commit(); + + if (!empty($promoted)) { + $ev = $pdo->prepare("SELECT * FROM events WHERE id = ?"); + $ev->execute([(int) $o['event_id']]); + $evRow = $ev->fetch(PDO::FETCH_ASSOC); + foreach ($promoted as $pr) { + try { + yse_mail_promotion($pr, $evRow); + $report['promoted']++; + } catch (Throwable $pe) { + error_log('[cron promo] ' . $pe->getMessage()); + } + } + } + } catch (Throwable $te) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + error_log('[cron offer] ' . $te->getMessage()); + } + } +} catch (Throwable $e) { + error_log('[cron offers] ' . $e->getMessage()); +} + +send_json(['ok' => true, 'report' => $report, 'at' => date('c')]); diff --git a/public/apiwp/events.php b/public/apiwp/events.php index cc9c5e2..85fd31c 100644 --- a/public/apiwp/events.php +++ b/public/apiwp/events.php @@ -22,7 +22,7 @@ try { $idsParam = trim($_GET['ids'] ?? ''); $limit = isset($_GET['limit']) ? max(1, min(50, (int) $_GET['limit'])) : 0; - $where = ["e.status = 'published'", "COALESCE(e.ends_at, e.starts_at) >= NOW()"]; + $where = ["e.status IN ('published','scheduled')", "COALESCE(e.ends_at, e.starts_at) >= NOW()"]; $params = []; if ($id > 0) { @@ -96,6 +96,10 @@ try { 'id' => (int) $r['id'], 'title' => $r['title'], 'slug' => $r['slug'], + 'status' => $r['status'], + 'scheduled' => $r['status'] === 'scheduled', + 'bookable' => $r['status'] === 'published', + 'publish_at' => $r['publish_at'], 'description' => $r['description'], // HTML (sanificare al rendering in WP) 'instructions' => $r['instructions'], 'what_to_bring' => $r['what_to_bring'], diff --git a/public/event_form.php b/public/event_form.php index e455dcf..96d79fb 100644 --- a/public/event_form.php +++ b/public/event_form.php @@ -74,6 +74,8 @@ $val = [ 'reminder_enabled' => $ev['reminder_enabled'] ?? null, 'reminder_hours' => $ev['reminder_hours'] ?? '', 'offer_expiry_hours' => $ev['offer_expiry_hours'] ?? '', + 'schedule_enabled' => (($ev['status'] ?? '') === 'scheduled') || !empty($ev['publish_at']), + 'publish_at' => !empty($ev['publish_at']) ? date('Y-m-d H:i', strtotime($ev['publish_at'])) : '', ]; $isEdit = $id > 0; @@ -555,6 +557,22 @@ $pageTitle = $isEdit ? 'Modifica evento' : 'Nuovo evento';
    + +
    +
    + +
    + > + +
    +
    +
    + + +
    +
    +

    Se attivo, l'evento resta "Programmato" (visibile ma non prenotabile) finché il cron non lo apre alla data indicata.

    +
    @@ -612,6 +630,10 @@ $pageTitle = $isEdit ? 'Modifica evento' : 'Nuovo evento'; }; var endPicker = flatpickr("#ends_at", fpOpts); + flatpickr("#publish_at", fpOpts); + document.getElementById('schedule_enabled').addEventListener('change', function() { + document.getElementById('publish-at-wrap').style.display = this.checked ? '' : 'none'; + }); var startPicker = flatpickr("#starts_at", Object.assign({}, fpOpts, { onChange: function(dates) { if (!dates[0]) return; diff --git a/public/event_save.php b/public/event_save.php index 8279617..7015048 100644 --- a/public/event_save.php +++ b/public/event_save.php @@ -105,6 +105,15 @@ if (!in_array($status, ['draft', 'published', 'cancelled'], true)) { $status = 'draft'; } +// Apertura programmata: se attiva con data futura -> stato 'scheduled' + publish_at +$scheduleEnabled = isset($_POST['schedule_enabled']) && $_POST['schedule_enabled'] === 'Y'; +$publishAt = toDateTime($_POST['publish_at'] ?? null); +if ($scheduleEnabled && $publishAt && strtotime($publishAt) > time()) { + $status = 'scheduled'; +} else { + $publishAt = null; // niente programmazione (o data già passata) +} + // Campi facoltativi (testo) $instructions = trim($_POST['instructions'] ?? ''); $whatToBring = trim($_POST['what_to_bring'] ?? ''); @@ -144,7 +153,7 @@ try { max_seats = :max_seats, max_per_booking = :max_per_booking, color = :color, 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, updated_at = NOW() + cover_image = :cover_image, gallery = :gallery, status = :status, publish_at = :publish_at, updated_at = NOW() WHERE id = :id"; $params = [':id' => $id]; } else { @@ -154,14 +163,14 @@ try { address, lat, lng, cost, currency, max_seats, max_per_booking, color, waitlist_enabled, reminder_enabled, reminder_hours, offer_expiry_hours, - cover_image, gallery, status, created_at, updated_at) + cover_image, gallery, status, publish_at, created_at, updated_at) VALUES (: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, NOW(), NOW())"; + :cover_image, :gallery, :status, :publish_at, NOW(), NOW())"; $params = [':user_id' => $currentUserId]; } @@ -189,6 +198,7 @@ try { ':color' => $color, ':cover_image' => $coverImage !== '' ? $coverImage : null, ':gallery' => $gallery, + ':publish_at' => $publishAt, ':status' => $status, ]; diff --git a/public/events.php b/public/events.php index c17a791..c57ce63 100644 --- a/public/events.php +++ b/public/events.php @@ -22,6 +22,7 @@ $message = $_GET['message'] ?? ''; $sql = "SELECT e.*, COALESCE(b.booked, 0) AS booked, COALESCE(b.bookings_count, 0) AS bookings_count, + COALESCE(w.waiting, 0) AS waiting, u.first_name AS creator_first, u.last_name AS creator_last FROM events e @@ -33,6 +34,12 @@ $sql = "SELECT e.*, WHERE 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' + GROUP BY event_id + ) w ON w.event_id = e.id LEFT JOIN auth_users u ON u.id = e.user_id ORDER BY e.starts_at ASC"; $events = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC); @@ -63,11 +70,13 @@ function statusBadge(string $status): array { switch ($status) { case 'published': - return ['Aperto', '#e7f7ee', '#1a7f52']; + return ['Aperto', '#e7f7ee', '#1a7f52']; + case 'scheduled': + return ['Programmato', '#eef2fb', '#3a5bb8']; case 'cancelled': - return ['Annullato', '#fdecea', '#c0392b']; + return ['Annullato', '#fdecea', '#c0392b']; default: - return ['Bozza', '#eef0f3', '#6b7280']; + return ['Bozza', '#eef0f3', '#6b7280']; } } ?> @@ -209,6 +218,29 @@ function statusBadge(string $status): array color: #c0392b; } + .wait-pill { + display: inline-block; + background: #eef2fb; + color: #3a5bb8; + font-weight: 700; + font-size: 12px; + padding: 2px 9px; + border-radius: 20px; + margin-left: 6px; + } + + .id-chip { + display: inline-block; + background: #eef0f3; + color: #4b5563; + font-weight: 700; + font-size: 11px; + padding: 1px 8px; + border-radius: 6px; + margin-left: 6px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + } + .status-badge { display: inline-block; font-size: 12px; @@ -375,7 +407,7 @@ function statusBadge(string $status): array - (ID: ) + ID
    @@ -383,11 +415,15 @@ function statusBadge(string $status): array / + 0) : ?> + + in lista + + Anteprima Partecipanti Modifica @@ -428,6 +464,27 @@ function statusBadge(string $status): array + - @@ -329,98 +158,118 @@ if ($result_dayoff->num_rows > 0) {
    - -
    -
    - - -
    - -
    -
    - +
    + +
    + -
    - +
    +
    + +
    +
    + -
    -
    -
    -
    -
    -
    -
    -
    Riepilogo Ordini (Amministratore)
    -

    Tutti gli ordini registrati

    -
    - Tutti - Attivi - Scaduti -
    -
    - - - - - - - - - - - - - - - - $today); - $badgeClass = $isActive ? 'badge bg-success' : 'badge bg-danger'; - $badgeText = $isActive ? 'Attivo' : 'Scaduto'; - ?> +
    +
    +
    +
    +
    +
    +
    +
    Riepilogo Ordini (Amministratore)
    +

    Tutti gli ordini registrati

    + +
    + Tutti + Attivi + Scaduti + + +
    + +
    +
    Clicca su scadenza, max riprogr. o riprogrammate per modificarle.
    + +
    +
    StatoNumero OrdineNomeCognomeNome ClasseNumero TicketScadenzaStatusAzioni
    + - - - - - - - - - + + + + + + + + + + + - - -
    - - StatoN. OrdineNomeCognomeClasseTicketScadenzaMax riprogr.RiprogrammateStatusAzioni
    + + + $today); + $max = $order['maxreschedule']; + $done = (int) $order['reprogrammed']; + $search = implode(' ', [ + $order['first_name'], + $order['last_name'], + $order['order_billing_email'], + $id, + $order['order_id'], + $order['servicename'], + ]); + ?> + + + + + + + + + + + + –' : (int) $max; ?> + + + + + + + + + + + + Nessun ordine trovato. + + + +
    +
    -
    @@ -429,11 +278,261 @@ if ($result_dayoff->num_rows > 0) { - + + + + - + diff --git a/public/php_errors.log b/public/php_errors.log index 24e3684..5c97b12 100644 --- a/public/php_errors.log +++ b/public/php_errors.log @@ -1,3 +1,4 @@ [15-Oct-2025 15:13:11 UTC] Tentativo di aggiornamento AJAX - idorderbook: 1, new_expiry: 2025-10-16 [15-Oct-2025 15:13:19 UTC] Tentativo di aggiornamento AJAX - idorderbook: 1, new_expiry: 2025-10-15 [15-Oct-2025 15:13:58 UTC] Tentativo di aggiornamento AJAX - idorderbook: 1, new_expiry: 2025-10-30 +[24-Sep-2026 09:19:47 UTC] Tentativo di aggiornamento AJAX - idorderbook: 7, new_expiry: 2026-11-02 diff --git a/public/update_reschedule.php b/public/update_reschedule.php new file mode 100644 index 0000000..86c047d --- /dev/null +++ b/public/update_reschedule.php @@ -0,0 +1,65 @@ + 'error', 'message' => 'Metodo non consentito.'], 405); +} + +$allowedFields = ['maxreschedule', 'reprogrammed']; +$field = $_POST['field'] ?? ''; +$orderId = (int) ($_POST['order_id'] ?? 0); +$value = (string) ($_POST['value'] ?? ''); + +if (!in_array($field, $allowedFields, true)) { + jsonOut(['status' => 'error', 'message' => 'Campo non valido.'], 400); +} +if ($orderId <= 0) { + jsonOut(['status' => 'error', 'message' => 'ID ordine non valido.'], 400); +} +if (!preg_match('/^\d{1,3}$/', $value)) { + jsonOut(['status' => 'error', 'message' => 'Inserisci un numero intero da 0 a 999.'], 400); +} + +try { + $pdo = DBHandlerSelect::getInstance()->getConnection(); + + $check = $pdo->prepare("SELECT idorderbook FROM orderbook WHERE idorderbook = :id"); + $check->execute([':id' => $orderId]); + if (!$check->fetch()) { + jsonOut(['status' => 'error', 'message' => 'Ordine non trovato.'], 404); + } + + // $field è da whitelist, quindi sicuro da inserire nella query + $upd = $pdo->prepare("UPDATE orderbook SET `$field` = :v WHERE idorderbook = :id"); + $upd->execute([':v' => (int) $value, ':id' => $orderId]); + + $row = $pdo->prepare("SELECT maxreschedule, reprogrammed FROM orderbook WHERE idorderbook = :id"); + $row->execute([':id' => $orderId]); + $r = $row->fetch(); + + jsonOut([ + 'status' => 'success', + 'message' => 'Valore aggiornato.', + 'maxreschedule' => $r['maxreschedule'] === null ? null : (int) $r['maxreschedule'], + 'reprogrammed' => (int) $r['reprogrammed'], + ]); +} catch (Throwable $e) { + error_log('update_reschedule: ' . $e->getMessage()); + jsonOut(['status' => 'error', 'message' => 'Errore durante il salvataggio.'], 500); +} From b605119dec740cd1bc778d7757af0f8049fddeaa Mon Sep 17 00:00:00 2001 From: solocla Date: Thu, 24 Sep 2026 11:46:07 +0200 Subject: [PATCH 13/18] added stats and reodrder order --- public/include/sidebar.php | 7 +- public/ordersummary.php | 96 ++- public/statistiche.php | 1165 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1257 insertions(+), 11 deletions(-) create mode 100644 public/statistiche.php diff --git a/public/include/sidebar.php b/public/include/sidebar.php index d144a9f..0f07c03 100644 --- a/public/include/sidebar.php +++ b/public/include/sidebar.php @@ -151,7 +151,12 @@ Certificati - +
  • + + + Statistiche + +
  • diff --git a/public/ordersummary.php b/public/ordersummary.php index 5ec547c..2bfdf95 100644 --- a/public/ordersummary.php +++ b/public/ordersummary.php @@ -120,6 +120,33 @@ $qParam = $q !== '' ? '&q=' . urlencode($q) : ''; box-shadow: inset 0 0 0 1px #cfe8db; } + #ordersTable th.sortable { + cursor: pointer; + user-select: none; + white-space: nowrap; + } + + #ordersTable th.sortable:hover { + color: #1ebf73; + } + + #ordersTable th.sortable::after { + content: "\2195"; + margin-left: 5px; + font-size: 11px; + opacity: 0.35; + } + + #ordersTable th.sortable.asc::after { + content: "\25B2"; + opacity: 1; + } + + #ordersTable th.sortable.desc::after { + content: "\25BC"; + opacity: 1; + } + .num-input { width: 80px; } @@ -203,16 +230,16 @@ $qParam = $q !== '' ? '&q=' . urlencode($q) : ''; - - - - - - - - - - + + + + + + + + + + @@ -341,6 +368,55 @@ $qParam = $q !== '' ? '&q=' . urlencode($q) : ''; $('#orderSearch').on('input', applySearch); applySearch(); + /* ===================== ORDINAMENTO ===================== */ + var sortCol = 1, + sortDir = -1; // di partenza: N. Ordine decrescente, come la query + + function cellValue(row, idx, type) { + var td = $(row).children().eq(idx); + if (td.find('input').length) { + // cella in modifica: usa il valore originale + var orig = td.data('original'); + if (typeof orig === 'undefined') orig = td.attr('data-value'); + td = $(''; @@ -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']) ? '
    ' . h($p['email']) . '' : ''; + $email = ($full && !empty($p['email'])) + ? '
    ' . h($p['email']) . '' + : ''; $html .= ''; $i++; @@ -174,7 +235,12 @@ function statBox(string $value, string $label, string $color): string . ''; } -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, ? '
    di cui ' . $nLost . ' segnat' . ($nLost === 1 ? 'a' : 'e') . ' come pers' . ($nLost === 1 ? 'a' : 'e') . '
    ' : ''; - $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 + ? '' + : ''; + + $teacherLine = $teacherFull !== '' + ? '
    Insegnante: ' . h($teacherFull) . '
    ' + : ''; + return '' . h($cls['servicename']) . '' . '' . '
    StatoN. OrdineNomeCognomeClasseTicketScadenzaMax riprogr.RiprogrammateStatusStatoN. OrdineNomeCognomeClasseTicketScadenzaMax riprogr.RiprogrammateStatus Azioni
    ').text(orig || ''); + } + var v = td.attr('data-value'); + if (typeof v === 'undefined') v = td.text(); + v = String(v).trim(); + if (v === '' || v === '–') return null; + if (type === 'num') { + var num = parseFloat(v.replace(',', '.')); + return isNaN(num) ? null : num; + } + if (type === 'date') return v; // formato AAAA-MM-GG: ordinabile come testo + return norm(v); + } + + $('#ordersTable thead').on('click', 'th.sortable', function() { + var th = $(this); + var idx = th.index(); + var type = th.data('type'); + + sortDir = (sortCol === idx) ? -sortDir : 1; + sortCol = idx; + + $('#ordersTable thead th.sortable').removeClass('asc desc'); + th.addClass(sortDir === 1 ? 'asc' : 'desc'); + + var rows = $('#ordersTable tbody tr.order-row').get(); + rows.sort(function(a, b) { + var va = cellValue(a, idx, type); + var vb = cellValue(b, idx, type); + if (va === null && vb === null) return 0; + if (va === null) return 1; // vuoti sempre in fondo + if (vb === null) return -1; + if (va < vb) return -sortDir; + if (va > vb) return sortDir; + return 0; + }); + $('#noResults').before(rows); + }); + /* ===================== SCADENZA ===================== */ $(document).on('click', 'td.expiry-date', function() { var cell = $(this); diff --git a/public/statistiche.php b/public/statistiche.php new file mode 100644 index 0000000..68dc5e4 --- /dev/null +++ b/public/statistiche.php @@ -0,0 +1,1165 @@ + +getConnection(); + +if (!function_exists('e')) { + function e($value): string + { + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); + } +} + +function fetchAllQ(PDO $pdo, string $sql, array $params = []): array +{ + $st = $pdo->prepare($sql); + $st->execute($params); + return $st->fetchAll(); +} + +function pct($num, $den): ?float +{ + return $den > 0 ? round($num / $den * 100, 1) : null; +} + +function fmtPct(?float $v): string +{ + return $v === null ? '–' : number_format($v, 0, ',', '.') . '%'; +} + +function n($v): string +{ + return number_format((float) $v, 0, ',', '.'); +} + +function kpi(string $icon, string $color, string $value, string $label, string $sub = ''): string +{ + return '
    ' + . '
    ' + . '
    ' . $value . '
    ' + . '
    ' . $label . '
    ' + . ($sub !== '' ? '
    ' . $sub . '
    ' : '') + . '
    '; +} + +function fillColor(?float $p): string +{ + if ($p === null) return '#d5d9e0'; + if ($p >= 75) return '#1ebf73'; + if ($p >= 45) return '#f1b40f'; + return '#e67e22'; +} + +/* =========================================================== PERIODO */ + +$validDate = function ($d): bool { + if (!is_string($d) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $d)) return false; + [$y, $m, $dd] = array_map('intval', explode('-', $d)); + return checkdate($m, $dd, $y); +}; + +$presets = [ + 'month' => 'Questo mese', + 'lastmonth' => 'Mese scorso', + '3m' => 'Ultimi 3 mesi', + '12m' => 'Ultimi 12 mesi', + 'year' => "Quest'anno", +]; + +$preset = $_GET['preset'] ?? ''; +$fromIn = $_GET['from'] ?? ''; +$toIn = $_GET['to'] ?? ''; +$from = $to = null; + +if (($preset === 'custom' || $preset === '') && $validDate($fromIn) && $validDate($toIn)) { + $preset = 'custom'; + $from = $fromIn; + $to = $toIn; +} + +if ($from === null) { + if (!isset($presets[$preset])) $preset = '3m'; + $firstThis = new DateTime('first day of this month'); + switch ($preset) { + case 'month': + $from = $firstThis->format('Y-m-d'); + $to = $firstThis->format('Y-m-t'); + break; + case 'lastmonth': + $lm = (clone $firstThis)->modify('-1 month'); + $from = $lm->format('Y-m-d'); + $to = $lm->format('Y-m-t'); + break; + case '12m': + $from = (clone $firstThis)->modify('-11 months')->format('Y-m-d'); + $to = $firstThis->format('Y-m-t'); + break; + case 'year': + $from = date('Y-01-01'); + $to = date('Y-12-31'); + break; + default: // 3m + $from = (clone $firstThis)->modify('-2 months')->format('Y-m-d'); + $to = $firstThis->format('Y-m-t'); + } +} +if ($from > $to) { + [$from, $to] = [$to, $from]; +} + +$d1 = $from . ' 00:00:00'; +$d2 = date('Y-m-d', strtotime($to . ' +1 day')) . ' 00:00:00'; +$now = date('Y-m-d H:i:s'); + +/* =========================================================== FILTRI */ + +$fService = (int) ($_GET['service'] ?? 0); +$fCat = (int) ($_GET['category'] ?? 0); +$fTeacher = (int) ($_GET['teacher'] ?? 0); +$topLimit = in_array((int) ($_GET['top'] ?? 10), [10, 25, 50], true) ? (int) ($_GET['top'] ?? 10) : 10; + +$sortMap = [ + 'attended' => ['Lezioni frequentate', 'attended DESC, upcoming DESC'], + 'lost' => ['Lezioni perse', 'lost DESC, attended DESC'], + 'reprog' => ['Riprogrammazioni', 'reprog DESC, attended DESC'], + 'cancelled' => ['Cancellazioni', 'cancelled DESC, attended DESC'], +]; +$topSort = isset($sortMap[$_GET['sort'] ?? '']) ? $_GET['sort'] : 'attended'; + +$sf = ''; +$sp = []; +if ($fService > 0) { + $sf .= ' AND s.idservice = :f_service'; + $sp[':f_service'] = $fService; +} +if ($fCat > 0) { + $sf .= ' AND s.category = :f_cat'; + $sp[':f_cat'] = $fCat; +} +if ($fTeacher > 0) { + $sf .= ' AND s.idteacher = :f_teacher'; + $sp[':f_teacher'] = $fTeacher; +} +$P = [':d1' => $d1, ':d2' => $d2] + $sp; + +/* =========================================================== DATI FILTRI */ + +$services = fetchAllQ($pdo, "SELECT idservice, servicename, day, time FROM service ORDER BY servicename, day, time"); +$categories = fetchAllQ($pdo, "SELECT idservicecategory, namecategory FROM servicecategory ORDER BY namecategory"); +$teachers = fetchAllQ($pdo, "SELECT DISTINCT e.idemployee, e.nameemployee, e.surnameemployee + FROM employee e JOIN service s ON s.idteacher = e.idemployee + ORDER BY e.surnameemployee, e.nameemployee"); + +/* =========================================================== ORDINI */ + +$orderRows = fetchAllQ($pdo, "SELECT o.order_date_created AS d, o.nticket, o.iduser, o.reprogrammed + FROM orderbook o + LEFT JOIN service s ON o.idservice = s.idservice + WHERE o.order_date_created >= :d1 AND o.order_date_created < :d2 $sf", $P); + +$ordersCount = count($orderRows); +$ticketsSold = 0; +$reprogCounter = 0; +$buyers = []; +foreach ($orderRows as $o) { + $ticketsSold += (int) $o['nticket']; + $reprogCounter += (int) $o['reprogrammed']; + if (!empty($o['iduser'])) $buyers[(int) $o['iduser']] = true; +} +$buyersCount = count($buyers); + +// Nuovi clienti: persone il cui PRIMO ordine in assoluto cade nel periodo +$newCustomers = (int) (fetchAllQ($pdo, "SELECT COUNT(*) AS c FROM ( + SELECT iduser, MIN(order_date_created) AS first_order + FROM orderbook WHERE iduser IS NOT NULL GROUP BY iduser + ) t WHERE t.first_order >= :d1 AND t.first_order < :d2", [':d1' => $d1, ':d2' => $d2])[0]['c'] ?? 0); + +/* =========================================================== LEZIONI (KPI) */ + +$lessonBase = "FROM bookingclass bc + JOIN serviceschedule ss ON bc.idserviceschedule = ss.idserviceschedule + JOIN service s ON ss.idservice = s.idservice + WHERE ss.dateschedule >= :d1 AND ss.dateschedule < :d2 $sf"; + +$k = fetchAllQ($pdo, "SELECT + COALESCE(SUM(bc.status = 'booked' AND bc.lostlesson = 'N'), 0) AS booked, + COALESCE(SUM(bc.status = 'booked' AND bc.lostlesson = 'N' AND ss.dateschedule <= :now), 0) AS attended, + COALESCE(SUM(bc.lostlesson = 'Y' AND bc.status <> 'cancelled'), 0) AS lost, + COALESCE(SUM(bc.status = 'cancelled'), 0) AS cancelled, + COALESCE(SUM(bc.status = 'pending'), 0) AS pending, + COALESCE(SUM(bc.status = 'waiting'), 0) AS waiting, + COALESCE(SUM(bc.is_reprogrammed = 'Y' AND bc.status <> 'cancelled'), 0) AS reprog, + COALESCE(SUM(bc.is_gift = 'Y' AND bc.status = 'booked'), 0) AS gifts, + COUNT(DISTINCT CASE WHEN bc.status = 'booked' THEN bc.iduser END) AS people + $lessonBase", $P + [':now' => $now])[0]; + +/* =========================================================== SESSIONI (riempimento) */ + +$sessions = fetchAllQ($pdo, "SELECT ss.idserviceschedule, ss.dateschedule, + s.idservice, s.servicename, s.colorclass, s.maxcapacity, s.day, s.time, + COUNT(CASE WHEN bc.status = 'booked' THEN 1 END) AS booked, + COUNT(CASE WHEN bc.status = 'booked' AND bc.lostlesson = 'N' THEN 1 END) AS present, + COUNT(CASE WHEN bc.lostlesson = 'Y' AND bc.status <> 'cancelled' THEN 1 END) AS lost, + COUNT(CASE WHEN bc.status = 'cancelled' THEN 1 END) AS cancelled, + COUNT(CASE WHEN bc.status = 'waiting' THEN 1 END) AS waiting, + COUNT(CASE WHEN bc.is_reprogrammed = 'Y' AND bc.status <> 'cancelled' THEN 1 END) AS reprog + FROM serviceschedule ss + JOIN service s ON ss.idservice = s.idservice + LEFT JOIN bookingclass bc ON bc.idserviceschedule = ss.idserviceschedule + WHERE ss.dateschedule >= :d1 AND ss.dateschedule < :d2 $sf + GROUP BY ss.idserviceschedule, ss.dateschedule, s.idservice, s.servicename, + s.colorclass, s.maxcapacity, s.day, s.time + ORDER BY ss.dateschedule", $P); + +/* =========================================================== BUCKET TEMPORALI */ + +$rangeDays = (int) round((strtotime($to) - strtotime($from)) / 86400) + 1; +$weekly = $rangeDays <= 62; // periodi brevi: per settimana, altrimenti per mese +$monthsShort = ['', 'Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic']; + +$bucketKey = function (string $date) use ($weekly): string { + $ts = strtotime($date); + if ($weekly) { + $dow = (int) date('N', $ts); + return date('Y-m-d', strtotime('-' . ($dow - 1) . ' days', $ts)); + } + return date('Y-m', $ts); +}; + +$buckets = []; +$cursor = strtotime($weekly ? $bucketKey($from) : date('Y-m-01', strtotime($from))); +$endTs = strtotime($to); +while ($cursor <= $endTs) { + $key = $weekly ? date('Y-m-d', $cursor) : date('Y-m', $cursor); + $buckets[$key] = [ + 'label' => $weekly ? date('d/m', $cursor) : $monthsShort[(int) date('n', $cursor)] . ' ' . date('y', $cursor), + 'orders' => 0, + 'tickets' => 0, + 'present' => 0, + 'lost' => 0, + 'cancelled' => 0, + 'reprog' => 0, + 'cap_past' => 0, + 'booked_past' => 0, + ]; + $cursor = strtotime($weekly ? '+1 week' : '+1 month', $cursor); +} + +foreach ($orderRows as $o) { + $bk = $bucketKey($o['d']); + if (isset($buckets[$bk])) { + $buckets[$bk]['orders']++; + $buckets[$bk]['tickets'] += (int) $o['nticket']; + } +} + +/* =========================================================== AGGREGAZIONE PER CLASSE */ + +$byClass = []; +$totCapPast = $totBookedPast = $totSessions = $totPast = $totFull = 0; + +foreach ($sessions as $s) { + $sid = (int) $s['idservice']; + $isPast = $s['dateschedule'] <= $now; + $cap = (int) $s['maxcapacity']; + $booked = (int) $s['booked']; + + if (!isset($byClass[$sid])) { + $byClass[$sid] = [ + 'name' => $s['servicename'], + 'color' => $s['colorclass'] ?: '#1ebf73', + 'day' => $s['day'], + 'time' => $s['time'], + 'sessions' => 0, + 'past' => 0, + 'booked' => 0, + 'booked_past' => 0, + 'present_past' => 0, + 'cap_past' => 0, + 'full' => 0, + 'lost' => 0, + 'cancelled' => 0, + 'reprog' => 0, + 'waiting' => 0, + ]; + } + $c = &$byClass[$sid]; + $c['sessions']++; + $c['booked'] += $booked; + $c['lost'] += (int) $s['lost']; + $c['cancelled'] += (int) $s['cancelled']; + $c['reprog'] += (int) $s['reprog']; + $c['waiting'] += (int) $s['waiting']; + $totSessions++; + + if ($isPast) { + $c['past']++; + $c['booked_past'] += $booked; + $c['present_past'] += (int) $s['present']; + $totPast++; + if ($cap > 0) { + $c['cap_past'] += $cap; + $totCapPast += $cap; + $totBookedPast += $booked; + if ($booked >= $cap) { + $c['full']++; + $totFull++; + } + } + } + unset($c); + + $bk = $bucketKey($s['dateschedule']); + if (isset($buckets[$bk])) { + $buckets[$bk]['present'] += (int) $s['present']; + $buckets[$bk]['lost'] += (int) $s['lost']; + $buckets[$bk]['cancelled'] += (int) $s['cancelled']; + $buckets[$bk]['reprog'] += (int) $s['reprog']; + if ($isPast && $cap > 0) { + $buckets[$bk]['cap_past'] += $cap; + $buckets[$bk]['booked_past'] += $booked; + } + } +} + +foreach ($byClass as &$c) { + $c['fill'] = pct($c['booked_past'], $c['cap_past']); + $c['avg'] = $c['past'] > 0 ? round($c['present_past'] / $c['past'], 1) : null; +} +unset($c); + +uasort($byClass, function ($a, $b) { + return ($b['fill'] ?? -1) <=> ($a['fill'] ?? -1); +}); + +$fillTotal = pct($totBookedPast, $totCapPast); +$lostRate = pct((int) $k['lost'], (int) $k['booked'] + (int) $k['lost']); + +/* =========================================================== CLASSIFICA UTENTI */ + +$topUsers = fetchAllQ($pdo, "SELECT u.id, u.first_name, u.last_name, u.email, + COALESCE(SUM(bc.status = 'booked' AND bc.lostlesson = 'N' AND ss.dateschedule <= :now1), 0) AS attended, + COALESCE(SUM(bc.status = 'booked' AND bc.lostlesson = 'N' AND ss.dateschedule > :now2), 0) AS upcoming, + COALESCE(SUM(bc.lostlesson = 'Y' AND bc.status <> 'cancelled'), 0) AS lost, + COALESCE(SUM(bc.is_reprogrammed = 'Y' AND bc.status <> 'cancelled'), 0) AS reprog, + COALESCE(SUM(bc.status = 'cancelled'), 0) AS cancelled, + MAX(CASE WHEN bc.status = 'booked' AND bc.lostlesson = 'N' AND ss.dateschedule <= :now3 THEN ss.dateschedule END) AS last_lesson + FROM bookingclass bc + JOIN serviceschedule ss ON bc.idserviceschedule = ss.idserviceschedule + JOIN service s ON ss.idservice = s.idservice + JOIN auth_users u ON u.id = bc.iduser + WHERE ss.dateschedule >= :d1 AND ss.dateschedule < :d2 $sf + GROUP BY u.id, u.first_name, u.last_name, u.email + HAVING (attended + upcoming + lost + cancelled) > 0 + ORDER BY " . $sortMap[$topSort][1] . ", u.last_name, u.first_name + LIMIT " . $topLimit, $P + [':now1' => $now, ':now2' => $now, ':now3' => $now]); + +/* =========================================================== DATI GRAFICI */ + +$chart = [ + 'labels' => [], + 'orders' => [], + 'tickets' => [], + 'present' => [], + 'lost' => [], + 'cancelled' => [], + 'reprog' => [], + 'fill' => [], + 'classLabels' => [], + 'classFill' => [], + 'classColors' => [], +]; +foreach ($buckets as $b) { + $chart['labels'][] = $b['label']; + $chart['orders'][] = $b['orders']; + $chart['tickets'][] = $b['tickets']; + $chart['present'][] = $b['present']; + $chart['lost'][] = $b['lost']; + $chart['cancelled'][] = $b['cancelled']; + $chart['reprog'][] = $b['reprog']; + $chart['fill'][] = pct($b['booked_past'], $b['cap_past']); +} +foreach ($byClass as $c) { + if ($c['fill'] === null) continue; + $chart['classLabels'][] = $c['name'] . ($c['day'] ? ' · ' . $c['day'] . ' ' . $c['time'] : ''); + $chart['classFill'][] = $c['fill']; + $chart['classColors'][] = $c['color']; +} + +/* =========================================================== LINK FILTRI */ + +$baseFilters = array_filter([ + 'service' => $fService ?: null, + 'category' => $fCat ?: null, + 'teacher' => $fTeacher ?: null, + 'top' => $topLimit !== 10 ? $topLimit : null, + 'sort' => $topSort !== 'attended' ? $topSort : null, +]); +$periodLabel = date('d/m/Y', strtotime($from)) . ' – ' . date('d/m/Y', strtotime($to)); +?> + + + + + + YogiBook - Statistiche + + + + + + + + + + + + + + +
    +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + + +
    +
    + $plabel) : ?> + + + + Personalizzato + +
    + +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + 1) : ?> +
    + + +
    + +
    + + Azzera +
    +
    + + +
    +
    + Periodo: + · filtri attivi +
    +
    + + +
    Ordini
    +
    + +
    + +
    Lezioni e persone
    +
    + 0 ? ' · ' . n($k['gifts']) . ' omaggio' : '')); + echo kpi('fas fa-chart-pie', fillColor($fillTotal), fmtPct($fillTotal), 'Riempimento medio', $totPast > 0 ? 'su ' . n($totPast) . ' lezioni svolte' : 'nessuna lezione svolta'); + echo kpi('fas fa-fire', '#c0392b', n($totFull), 'Lezioni al completo', $totPast > 0 ? fmtPct(pct($totFull, $totPast)) . ' delle lezioni svolte' : ''); + ?> +
    + +
    Riprogrammazioni, perse e cancellate
    +
    + +
    + + +
    +
    +
    +
    Andamento lezioni
    +
    +
    +
    +
    +
    +
    Andamento ordini
    +
    +
    +
    +
    +
    +
    Riempimento nel tempo lezioni svolte
    +
    +
    +
    +
    +
    +
    Riempimento per classe lezioni svolte
    + +
    Nessuna lezione svolta nel periodo.
    + +
    + +
    +
    +
    + + +
    +
    Dettaglio per classe ordinato per riempimento
    + +
    Nessuna classe programmata nel periodo.
    + +
    + + + + + + + + + + + + + + + + + 0, 'sessions' => 0, 'booked' => 0, 'present_past' => 0, 'full' => 0, 'lost' => 0, 'cancelled' => 0, 'reprog' => 0, 'waiting' => 0]; + foreach ($byClass as $c) : + foreach ($sum as $sk => $_) $sum[$sk] += $c[$sk]; + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ClasseLezioni
    svolte / tot.
    PrenotazioniMedia presentiRiempimentoAl completoPerseCancellateRiprogr.In coda
    + + +
    +
    / + + +
    Totale / 0 ? number_format($sum['present_past'] / $sum['past'], 1, ',', '.') : '–'; ?>
    +
    +
    Prenotazioni = posti occupati (incluse le perse). Media presenti e riempimento considerano solo le lezioni già svolte.
    + +
    + + +
    +
    +
    Classifica utenti nel periodo
    +
    + + + +
    +
    + +
    Nessuna prenotazione nel periodo.
    + +
    + + + + + + + + + + + + + + + + $u) : + $pos = $i + 1; + $presence = pct((int) $u['attended'], (int) $u['attended'] + (int) $u['lost']); + ?> + + + + + + + + + + + + + +
    #UtenteFrequentatePrenotate futurePerseRiprogr.CancellatePresenzaUltima lezione
    +
    + +
    0 ? '' . n($u['lost']) . '' : '0'; ?> + +
    +
    +
    Presenza = lezioni frequentate / (frequentate + perse).
    + +
    + +
    + +
    +
    +
    + + + + + + + + + + + \ No newline at end of file From 20287e96422292fb1e861c0fa5c60677afcd8feb Mon Sep 17 00:00:00 2001 From: solocla Date: Thu, 24 Sep 2026 11:51:57 +0200 Subject: [PATCH 14/18] coloro wihite text --- public/adminpanel.php | 2 +- public/userpanel.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/adminpanel.php b/public/adminpanel.php index 9392250..4b7d836 100644 --- a/public/adminpanel.php +++ b/public/adminpanel.php @@ -199,7 +199,7 @@ $italianMonths = [ .class-date { flex: 0 0 110px; background: #E1D0B8; - color: #5a4630; + color: #ffffff; display: flex; flex-direction: column; justify-content: center; diff --git a/public/userpanel.php b/public/userpanel.php index b25960e..6883f78 100644 --- a/public/userpanel.php +++ b/public/userpanel.php @@ -268,7 +268,7 @@ $italianMonths = [ .custom-date-box { flex: 0 0 110px; background: #E1D0B8; - color: #5a4630; + color: #ffffff; display: flex; flex-direction: column; justify-content: center; From c337806cdbc33d8f2c76d852851fb2c3b428e15e Mon Sep 17 00:00:00 2001 From: solocla Date: Thu, 24 Sep 2026 17:30:19 +0200 Subject: [PATCH 15/18] fixed order propagation --- public/propagatesingleorder.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/propagatesingleorder.php b/public/propagatesingleorder.php index f43c827..eb49390 100644 --- a/public/propagatesingleorder.php +++ b/public/propagatesingleorder.php @@ -119,7 +119,10 @@ if ($is_openflexy || $service_schedule_result->num_rows > 0) { $maxcapacity = $row['maxcapacity']; $stmt_maxcapacity->close(); - $bookingclass_count_query = "SELECT COUNT(*) as total_records FROM bookingclass WHERE idserviceschedule = ?"; + $bookingclass_count_query = "SELECT COUNT(*) AS total_records + FROM bookingclass + WHERE idserviceschedule = ? + AND status = 'booked'"; $stmt_count = $conn->prepare($bookingclass_count_query); $stmt_count->bind_param("i", $idserviceschedule); $stmt_count->execute(); From 38b7aca148681f7190b89b191166d82fa5b460e4 Mon Sep 17 00:00:00 2001 From: solocla Date: Thu, 24 Sep 2026 17:41:19 +0200 Subject: [PATCH 16/18] fixed propagate order --- public/propagatesingleorder.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/propagatesingleorder.php b/public/propagatesingleorder.php index eb49390..b8e29f2 100644 --- a/public/propagatesingleorder.php +++ b/public/propagatesingleorder.php @@ -103,7 +103,11 @@ if ($is_openflexy || $service_schedule_result->num_rows > 0) { $idservice = $schedule_row["idservice"]; $idserviceschedule = $schedule_row["idserviceschedule"]; - $querychk = "SELECT * FROM bookingclass WHERE iduser = ? AND idserviceschedule = ?"; + $querychk = "SELECT 1 FROM bookingclass + WHERE iduser = ? + AND idserviceschedule = ? + AND status <> 'cancelled' + LIMIT 1"; $stmt_chk = $conn->prepare($querychk); $stmt_chk->bind_param("ii", $selected_user_id, $idserviceschedule); $stmt_chk->execute(); From 76fcfb2ba3a2090a1c4833cb8df7cfbb65550db6 Mon Sep 17 00:00:00 2001 From: solocla Date: Thu, 1 Oct 2026 11:34:07 +0200 Subject: [PATCH 17/18] added teacher and option teacher in school --- cron/class_report.php | 219 +++-- cron/preview/classe_307.html | 1 + cron/preview/classe_307_insegnante.html | 1 + cron/preview/classe_307_titolare.html | 1 + ...001073831_extend_employee_for_teachers.php | 51 ++ ...61001075259_add_show_teacher_to_option.php | 21 + public/admin-services.php | 179 +++- public/adminpanel.php | 60 +- .../teachers/teacher_9b5422fa1b0622aa.jpg | Bin 0 -> 134854 bytes public/include/sidebar.php | 6 + public/insegnanti.php | 783 ++++++++++++++++++ public/options_save.php | 46 +- public/optionsschool.php | 10 + public/userpanel.php | 62 +- 14 files changed, 1354 insertions(+), 86 deletions(-) create mode 100644 cron/preview/classe_307.html create mode 100644 cron/preview/classe_307_insegnante.html create mode 100644 cron/preview/classe_307_titolare.html create mode 100644 db/migrations/20261001073831_extend_employee_for_teachers.php create mode 100644 db/migrations/20261001075259_add_show_teacher_to_option.php create mode 100644 public/assets/images/teachers/teacher_9b5422fa1b0622aa.jpg create mode 100644 public/insegnanti.php diff --git a/cron/class_report.php b/cron/class_report.php index 92d4cce..00f7384 100644 --- a/cron/class_report.php +++ b/cron/class_report.php @@ -1,13 +1,19 @@ 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 '' . h($label) . ''; } +/** 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 '
    Nessun partecipante
    ' . '' . '' - . '' + . '' . '' . '
    ' . $i . ($type === 'waiting' ? '°' : '.') . '' . h(fullName($p)) . $email . '' . h(personName($p, $full)) . $email . '' . $chipHtml . '
    ' . $copyNote . '
    ' . '' - // barra colore classe . '' - // intestazione + . $copyBanner . '' - // statistiche . '' - // elenchi . $body - // footer . '' . '
     
    ' . '
    YogiBook · Riepilogo classe
    ' . '

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

    ' . '
    ' . h(italianDate($d)) . ' · ore ' . $d->format('H:i') . '
    ' . '
    ' . h($location) . '
    ' + . $teacherLine . '
    ' . $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 +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 !== '' + ? 'Copia per il titolare · classe di ' . h($tName) + : 'Copia per il titolare · 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); diff --git a/cron/preview/classe_307.html b/cron/preview/classe_307.html new file mode 100644 index 0000000..94ae61f --- /dev/null +++ b/cron/preview/classe_307.html @@ -0,0 +1 @@ +Hatha Yoga
     
    YogiBook · Riepilogo classe

    Hatha Yoga

    Lunedì 5 Ottobre 2026 · ore 19:30
    via Valassina 62/B Seregno - Sala Contesto Yoga
    1/1
    Iscritti
    0
    Posti liberi
    0
    In attesa
    0
    In coda
    Partecipanti confermati (1)
    1.Amelia Benucci
    info@claudiosironi.com
    Confermata
    Generato automaticamente da YogiBook il 01/10/2026 alle 11:15
    \ No newline at end of file diff --git a/cron/preview/classe_307_insegnante.html b/cron/preview/classe_307_insegnante.html new file mode 100644 index 0000000..8a936cc --- /dev/null +++ b/cron/preview/classe_307_insegnante.html @@ -0,0 +1 @@ +Hatha Yoga
     
    YogiBook · Riepilogo classe

    Hatha Yoga

    Lunedì 5 Ottobre 2026 · ore 19:30
    via Valassina 62/B Seregno - Sala Contesto Yoga
    Insegnante: Francesca -
    1/1
    Iscritti
    0
    Posti liberi
    0
    In attesa
    0
    In coda
    Partecipanti confermati (1)
    1.Amelia B.Confermata
    Generato automaticamente da YogiBook il 01/10/2026 alle 11:30
    \ No newline at end of file diff --git a/cron/preview/classe_307_titolare.html b/cron/preview/classe_307_titolare.html new file mode 100644 index 0000000..d9f04e1 --- /dev/null +++ b/cron/preview/classe_307_titolare.html @@ -0,0 +1 @@ +Hatha Yoga
     
    Copia per il titolare · classe di Francesca -
    YogiBook · Riepilogo classe

    Hatha Yoga

    Lunedì 5 Ottobre 2026 · ore 19:30
    via Valassina 62/B Seregno - Sala Contesto Yoga
    Insegnante: Francesca -
    1/1
    Iscritti
    0
    Posti liberi
    0
    In attesa
    0
    In coda
    Partecipanti confermati (1)
    1.Amelia Benucci
    info@claudiosironi.com
    Confermata
    Generato automaticamente da YogiBook il 01/10/2026 alle 11:30
    \ No newline at end of file diff --git a/db/migrations/20261001073831_extend_employee_for_teachers.php b/db/migrations/20261001073831_extend_employee_for_teachers.php new file mode 100644 index 0000000..3b69d99 --- /dev/null +++ b/db/migrations/20261001073831_extend_employee_for_teachers.php @@ -0,0 +1,51 @@ +table('employee') + ->addColumn('email', 'string', ['limit' => 150, 'null' => true, 'after' => 'surnameemployee']) + ->addColumn('phone', 'string', ['limit' => 40, 'null' => true, 'after' => 'email']) + ->addColumn('photo', 'string', ['limit' => 191, 'null' => true, 'after' => 'phone']) + ->addColumn('is_owner', 'char', [ + 'limit' => 1, + 'null' => false, + 'default' => 'N', + 'comment' => 'Y = titolare della scuola (uno solo)', + 'after' => 'photo', + ]) + ->addColumn('notifications', 'char', [ + 'limit' => 1, + 'null' => false, + 'default' => 'Y', + 'comment' => 'Y = riceve le email', + 'after' => 'is_owner', + ]) + ->addColumn('active', 'char', ['limit' => 1, 'null' => false, 'default' => 'Y', 'after' => 'notifications']) + ->addColumn('created_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP']) + ->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null, 'update' => 'CURRENT_TIMESTAMP']) + ->update(); + + // L'insegnante già presente (id più basso) diventa il titolare + $this->execute("UPDATE `employee` SET `is_owner` = 'Y' ORDER BY `idemployee` LIMIT 1"); + } + + public function down(): void + { + $this->table('employee') + ->removeColumn('email') + ->removeColumn('phone') + ->removeColumn('photo') + ->removeColumn('is_owner') + ->removeColumn('notifications') + ->removeColumn('active') + ->removeColumn('created_at') + ->removeColumn('updated_at') + ->update(); + } +} diff --git a/db/migrations/20261001075259_add_show_teacher_to_option.php b/db/migrations/20261001075259_add_show_teacher_to_option.php new file mode 100644 index 0000000..2d0e12d --- /dev/null +++ b/db/migrations/20261001075259_add_show_teacher_to_option.php @@ -0,0 +1,21 @@ +table('option') + ->addColumn('show_teacher', 'char', [ + 'limit' => 1, + 'null' => false, + 'default' => 'N', + 'comment' => 'Y = mostra l\'insegnante nelle card delle classi', + 'after' => 'waitinglist_enabled', + ]) + ->update(); + } +} diff --git a/public/admin-services.php b/public/admin-services.php index 5fa0656..605e3fe 100644 --- a/public/admin-services.php +++ b/public/admin-services.php @@ -11,9 +11,56 @@ function e($value): string return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); } +function teacherInitials(string $first, string $last): string +{ + $ini = mb_strtoupper(mb_substr(trim($first), 0, 1) . mb_substr(trim($last), 0, 1)); + return $ini !== '' ? $ini : '?'; +} + +function teacherColor(string $seed): string +{ + $palette = ['#1ebf73', '#2980b9', '#8e44ad', '#e67e22', '#16a085', '#c0392b', '#2c3e50', '#b08d57']; + return $palette[abs(crc32($seed)) % count($palette)]; +} + $message = $_GET['message'] ?? 'N'; $insertFeedback = null; +/* ------------------------------------------------------------------------- + * Insegnanti (titolare per primo, poi attivi, poi alfabetico) + * ---------------------------------------------------------------------- */ +$teachers = $pdo->query( + "SELECT idemployee, nameemployee, surnameemployee, photo, is_owner, active + FROM employee + ORDER BY is_owner DESC, active DESC, surnameemployee, nameemployee" +)->fetchAll(); + +$teachersById = []; +$ownerId = 0; +foreach ($teachers as $t) { + $teachersById[(int) $t['idemployee']] = $t; + if ($t['is_owner'] === 'Y' && !$ownerId) { + $ownerId = (int) $t['idemployee']; + } +} + +/* ------------------------------------------------------------------------- + * Assegnazione insegnante a una classe esistente (POST) + * ---------------------------------------------------------------------- */ +if (($_POST['action'] ?? '') === 'assignteacher') { + $idservice = (int) ($_POST['idservice'] ?? 0); + $idteacher = (int) ($_POST['idteacher'] ?? 0); + + if ($idservice > 0 && isset($teachersById[$idteacher])) { + $stmt = $pdo->prepare("UPDATE service SET idteacher = :t WHERE idservice = :s"); + $stmt->execute([':t' => $idteacher, ':s' => $idservice]); + header('Location: ' . $_SERVER['PHP_SELF'] . '?message=teacherassigned'); + } else { + header('Location: ' . $_SERVER['PHP_SELF'] . '?message=teachererror'); + } + exit; +} + /* ------------------------------------------------------------------------- * Inserimento nuova classe (POST) con prepared statement * ---------------------------------------------------------------------- */ @@ -26,14 +73,17 @@ if (isset($_POST['classinsert']) && $_POST['classinsert'] === 'Y') { $maxcapacity = $_POST['maxcapacity'] ?? ''; $colorclass = trim($_POST['colorclass'] ?? ''); $classduration = $_POST['classduration'] ?? ''; + $idteacher = (int) ($_POST['idteacher'] ?? 0); if ($servicename === '') { $insertFeedback = ['type' => 'error', 'text' => 'La descrizione della classe è obbligatoria.']; + } elseif (!isset($teachersById[$idteacher])) { + $insertFeedback = ['type' => 'error', 'text' => 'Seleziona l\'insegnante della classe.']; } else { $sql = "INSERT INTO service - (servicename, wpcatalognumber, day, time, category, maxcapacity, colorclass, classduration) + (servicename, wpcatalognumber, day, time, category, maxcapacity, colorclass, classduration, idteacher) VALUES - (:servicename, :wpcatalog, :day, :time, :category, :maxcapacity, :colorclass, :classduration)"; + (:servicename, :wpcatalog, :day, :time, :category, :maxcapacity, :colorclass, :classduration, :idteacher)"; $stmt = $pdo->prepare($sql); $ok = $stmt->execute([ ':servicename' => $servicename, @@ -44,6 +94,7 @@ if (isset($_POST['classinsert']) && $_POST['classinsert'] === 'Y') { ':maxcapacity' => $maxcapacity !== '' ? (int) $maxcapacity : null, ':colorclass' => $colorclass, ':classduration' => $classduration !== '' ? (float) $classduration : null, + ':idteacher' => $idteacher, ]); if ($ok) { @@ -83,6 +134,32 @@ $propRows = $pdo->query( foreach ($propRows as $pr) { $propagationsByService[$pr['idservice']][] = $pr; } + +/** + * Opzioni del select insegnanti. + * Gli insegnanti non attivi compaiono solo se già assegnati a quella classe. + */ +function teacherOptions(array $teachers, int $selectedId): string +{ + $html = ''; + $found = false; + foreach ($teachers as $t) { + $tid = (int) $t['idemployee']; + $isSel = $tid === $selectedId; + if ($t['active'] !== 'Y' && !$isSel) { + continue; + } + if ($isSel) $found = true; + $label = trim($t['nameemployee'] . ' ' . $t['surnameemployee']) + . ($t['is_owner'] === 'Y' ? ' (titolare)' : '') + . ($t['active'] !== 'Y' ? ' (non attivo)' : ''); + $html .= ''; + } + if (!$found) { + $html = '' . $html; + } + return $html; +} ?> @@ -197,6 +274,43 @@ foreach ($propRows as $pr) { .action-btns .btn { margin: 2px; } + + .teacher-cell { + display: flex; + align-items: center; + gap: 8px; + min-width: 210px; + } + + .teacher-mini { + flex: 0 0 30px; + width: 30px; + height: 30px; + border-radius: 50%; + object-fit: cover; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 11px; + font-weight: 800; + } + + .teacher-mini.none { + background: #f3f4f6; + color: #c0c4cc; + } + + .teacher-select { + padding: 5px 30px 5px 10px !important; + font-size: 13px; + } + + .teacher-select.unassigned { + border-color: #f0b27a; + background-color: #fff8f1; + color: #b9770e; + } @@ -238,6 +352,12 @@ foreach ($propRows as $pr) { + + + + + + @@ -261,6 +381,11 @@ foreach ($propRows as $pr) {
    + + +
    @@ -343,6 +468,14 @@ foreach ($propRows as $pr) {
    +
    +
    + + +
    +
    @@ -358,7 +491,7 @@ foreach ($propRows as $pr) {
    Classi esistenti
    -

    Propaga una classe sul calendario, associala o modificane i dettagli.

    +

    Propaga una classe sul calendario, associala, assegna l'insegnante o modificane i dettagli.

    @@ -369,16 +502,20 @@ foreach ($propRows as $pr) { + - + - + + -
    Orario Durata CategoriaInsegnante Azioni
    Nessuna classe presente.Nessuna classe presente.
    @@ -391,6 +528,25 @@ foreach ($propRows as $pr) { +
    + + + + + + + + + + +
    +
    @@ -407,7 +563,7 @@ foreach ($propRows as $pr) {
    +
    @@ -514,6 +670,17 @@ foreach ($propRows as $pr) { $('#colorclass').val(col.toHexString()); } }); + + // Cambio insegnante dalla tabella: conferma e salva + $('.teacher-select').on('change', function() { + var sel = $(this); + var teacherName = sel.find('option:selected').text(); + if (confirm('Assegnare ' + teacherName + ' alla classe ' + sel.data('classname') + '?')) { + sel.closest('form')[0].submit(); + } else { + sel.val(String(sel.data('current')) === '0' ? '' : String(sel.data('current'))); + } + }); }); // Espansione riga propagazione diff --git a/public/adminpanel.php b/public/adminpanel.php index 4b7d836..4c9d4e1 100644 --- a/public/adminpanel.php +++ b/public/adminpanel.php @@ -28,8 +28,16 @@ function avatarColor(string $seed): string return $palette[$idx]; } -$optRow = $pdo->query("SELECT maxbeforetimecancell FROM `option` LIMIT 1")->fetch(); +$optRow = $pdo->query("SELECT maxbeforetimecancell, show_teacher FROM `option` LIMIT 1")->fetch(); $timetocancel = $optRow ? (int) $optRow['maxbeforetimecancell'] : 24; +$showTeacher = ($optRow['show_teacher'] ?? 'N') === 'Y'; + +$teachersById = []; +if ($showTeacher) { + foreach ($pdo->query("SELECT idemployee, nameemployee, surnameemployee, photo FROM employee")->fetchAll() as $t) { + $teachersById[(int) $t['idemployee']] = $t; + } +} if (isset($_GET['prev_month'])) { $currentMonthStart = preg_replace('/[^0-9\-]/', '', $_GET['prev_month']) . '-01'; @@ -44,7 +52,7 @@ if (strtotime($currentMonthStart) === false) { $currentMonthEnd = date("Y-m-t", strtotime($currentMonthStart)); $sqlSchedule = "SELECT serviceschedule.*, service.servicename, service.colorclass, - service.maxcapacity, service.idservice + service.maxcapacity, service.idservice, service.idteacher FROM serviceschedule LEFT JOIN service ON serviceschedule.idservice = service.idservice WHERE serviceschedule.dateschedule BETWEEN :start AND DATE_ADD(:end, INTERVAL 1 DAY) @@ -231,6 +239,38 @@ $italianMonths = [ margin: 0 0 4px; } + .class-teacher { + display: flex; + align-items: center; + gap: 8px; + margin-top: 4px; + } + + .class-teacher strong { + color: #374151; + font-weight: 600; + } + + .teacher-avatar-sm { + flex: 0 0 26px; + width: 26px; + height: 26px; + border-radius: 50%; + object-fit: cover; + background: #E1D0B8; + color: #5a4630; + font-size: 11px; + font-weight: 800; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .teacher-avatar-sm.none { + background: #fdf0e3; + color: #e67e22; + } + .class-meta { color: #6b7280; font-size: 14px; @@ -610,6 +650,22 @@ $italianMonths = [

    via Valassina 62/B Seregno - Sala Contesto Yoga

    + +

    + + + + + + + Insegnante: + + + Insegnante non assegnato + +

    +
    diff --git a/public/assets/images/teachers/teacher_9b5422fa1b0622aa.jpg b/public/assets/images/teachers/teacher_9b5422fa1b0622aa.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d09f13ae4c78c5532822d5044e8bb87eb14bb1c0 GIT binary patch literal 134854 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 literal 0 HcmV?d00001 diff --git a/public/include/sidebar.php b/public/include/sidebar.php index 0f07c03..4c2eb0a 100644 --- a/public/include/sidebar.php +++ b/public/include/sidebar.php @@ -166,6 +166,12 @@ Opzioni Scuola +
  • + + + Insegnanti + +
  • diff --git a/public/insegnanti.php b/public/insegnanti.php new file mode 100644 index 0000000..471bdbe --- /dev/null +++ b/public/insegnanti.php @@ -0,0 +1,783 @@ +getConnection(); + +if (!function_exists('e')) { + function e($value): string + { + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); + } +} + +if (session_status() !== PHP_SESSION_ACTIVE) { + session_start(); +} +if (empty($_SESSION['csrf_teachers'])) { + $_SESSION['csrf_teachers'] = bin2hex(random_bytes(16)); +} +$csrf = $_SESSION['csrf_teachers']; + +define('PHOTO_DIR_REL', 'assets/images/teachers/'); +$photoDirAbs = __DIR__ . '/' . PHOTO_DIR_REL; + +/* ================================================================ helper */ + +function flashRedirect(string $type, string $msg): void +{ + $_SESSION['teachers_flash'] = [$type, $msg]; + header('Location: insegnanti.php'); + exit; +} + +function teacherInitials(string $first, string $last): string +{ + $ini = mb_strtoupper(mb_substr(trim($first), 0, 1) . mb_substr(trim($last), 0, 1)); + return $ini !== '' ? $ini : '?'; +} + +function teacherColor(string $seed): string +{ + $palette = ['#1ebf73', '#2980b9', '#8e44ad', '#e67e22', '#16a085', '#c0392b', '#2c3e50', '#b08d57']; + return $palette[abs(crc32($seed)) % count($palette)]; +} + +function deletePhotoFile(string $dir, ?string $name): void +{ + if ($name && preg_match('/^teacher_[a-f0-9]{16}\.(jpg|png|webp)$/', $name) && is_file($dir . $name)) { + @unlink($dir . $name); + } +} + +/** Ritorna il nome file salvato, null se nessun file caricato. Lancia RuntimeException se non valido. */ +function handlePhotoUpload(array $file, string $dir): ?string +{ + if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) { + return null; + } + if ($file['error'] !== UPLOAD_ERR_OK) { + throw new RuntimeException('Errore nel caricamento della foto.'); + } + if ($file['size'] > 3 * 1024 * 1024) { + throw new RuntimeException('La foto supera i 3 MB.'); + } + $mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']); + $ext = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'][$mime] ?? null; + if (!$ext || @getimagesize($file['tmp_name']) === false) { + throw new RuntimeException('Formato foto non valido: usa JPG, PNG o WEBP.'); + } + if (!is_dir($dir) && !@mkdir($dir, 0755, true)) { + throw new RuntimeException('La cartella delle foto non è scrivibile.'); + } + $name = 'teacher_' . bin2hex(random_bytes(8)) . '.' . $ext; + if (!move_uploaded_file($file['tmp_name'], $dir . $name)) { + throw new RuntimeException('Impossibile salvare la foto.'); + } + return $name; +} + +function findTeacher(PDO $pdo, int $id): ?array +{ + $st = $pdo->prepare("SELECT * FROM employee WHERE idemployee = :id"); + $st->execute([':id' => $id]); + $row = $st->fetch(); + return $row ?: null; +} + +/* ================================================================ azioni POST */ + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!hash_equals($csrf, (string) ($_POST['csrf'] ?? ''))) { + flashRedirect('danger', 'Sessione scaduta, riprova.'); + } + + $action = $_POST['action'] ?? ''; + $id = (int) ($_POST['id'] ?? 0); + + try { + /* ---------- crea / modifica */ + if ($action === 'save') { + $name = trim((string) ($_POST['nameemployee'] ?? '')); + $surname = trim((string) ($_POST['surnameemployee'] ?? '')); + $email = trim((string) ($_POST['email'] ?? '')); + $phone = trim((string) ($_POST['phone'] ?? '')); + $notify = isset($_POST['notifications']) ? 'Y' : 'N'; + $active = isset($_POST['active']) ? 'Y' : 'N'; + + if ($name === '' || $surname === '') { + flashRedirect('danger', 'Nome e cognome sono obbligatori.'); + } + if (mb_strlen($name) > 100 || mb_strlen($surname) > 100) { + flashRedirect('danger', 'Nome o cognome troppo lunghi.'); + } + if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) { + flashRedirect('danger', 'Indirizzo email non valido.'); + } + if ($phone !== '' && !preg_match('/^[0-9 +().\/-]{5,40}$/', $phone)) { + flashRedirect('danger', 'Numero di telefono non valido.'); + } + if ($email !== '') { + $dup = $pdo->prepare("SELECT COUNT(*) AS c FROM employee WHERE email = :email AND idemployee <> :id"); + $dup->execute([':email' => $email, ':id' => $id]); + if ((int) $dup->fetch()['c'] > 0) { + flashRedirect('danger', 'Questa email è già usata da un altro insegnante.'); + } + } + + if ($id > 0) { + $current = findTeacher($pdo, $id); + if (!$current) { + flashRedirect('danger', 'Insegnante non trovato.'); + } + if ($current['is_owner'] === 'Y' && $active === 'N') { + flashRedirect('danger', 'Il titolare non può essere disattivato.'); + } + + $photo = $current['photo']; + $newPhoto = handlePhotoUpload($_FILES['photo'] ?? [], $photoDirAbs); + if ($newPhoto !== null) { + deletePhotoFile($photoDirAbs, $photo); + $photo = $newPhoto; + } elseif (isset($_POST['remove_photo'])) { + deletePhotoFile($photoDirAbs, $photo); + $photo = null; + } + + $st = $pdo->prepare("UPDATE employee SET nameemployee = :n, surnameemployee = :s, email = :e, + phone = :p, photo = :ph, notifications = :nt, active = :a + WHERE idemployee = :id"); + $st->execute([ + ':n' => $name, + ':s' => $surname, + ':e' => $email ?: null, + ':p' => $phone ?: null, + ':ph' => $photo, + ':nt' => $notify, + ':a' => $active, + ':id' => $id, + ]); + flashRedirect('success', 'Insegnante aggiornato.'); + } + + // nuovo + $photo = handlePhotoUpload($_FILES['photo'] ?? [], $photoDirAbs); + $hasOwner = (int) $pdo->query("SELECT COUNT(*) AS c FROM employee WHERE is_owner = 'Y'")->fetch()['c'] > 0; + + $st = $pdo->prepare("INSERT INTO employee + (nameemployee, surnameemployee, email, phone, photo, is_owner, notifications, active, idlocation) + VALUES (:n, :s, :e, :p, :ph, :o, :nt, :a, 0)"); + $st->execute([ + ':n' => $name, + ':s' => $surname, + ':e' => $email ?: null, + ':p' => $phone ?: null, + ':ph' => $photo, + ':o' => $hasOwner ? 'N' : 'Y', + ':nt' => $notify, + ':a' => $hasOwner ? $active : 'Y', + ]); + flashRedirect('success', 'Insegnante aggiunto' . ($hasOwner ? '.' : ' e impostato come titolare.')); + } + + /* ---------- imposta titolare */ + if ($action === 'set_owner' && $id > 0) { + if (!findTeacher($pdo, $id)) { + flashRedirect('danger', 'Insegnante non trovato.'); + } + $pdo->beginTransaction(); + $pdo->exec("UPDATE employee SET is_owner = 'N' WHERE is_owner = 'Y'"); + $st = $pdo->prepare("UPDATE employee SET is_owner = 'Y', active = 'Y' WHERE idemployee = :id"); + $st->execute([':id' => $id]); + $pdo->commit(); + flashRedirect('success', 'Titolare aggiornato.'); + } + + /* ---------- notifiche on/off */ + if ($action === 'toggle_notifications' && $id > 0) { + $st = $pdo->prepare("UPDATE employee SET notifications = IF(notifications = 'Y', 'N', 'Y') WHERE idemployee = :id"); + $st->execute([':id' => $id]); + flashRedirect('success', 'Notifiche aggiornate.'); + } + + /* ---------- elimina */ + if ($action === 'delete' && $id > 0) { + $t = findTeacher($pdo, $id); + if (!$t) { + flashRedirect('danger', 'Insegnante non trovato.'); + } + if ($t['is_owner'] === 'Y') { + flashRedirect('danger', 'Il titolare non può essere eliminato. Imposta prima un altro titolare.'); + } + $cnt = $pdo->prepare("SELECT COUNT(*) AS c FROM service WHERE idteacher = :id"); + $cnt->execute([':id' => $id]); + $nServices = (int) $cnt->fetch()['c']; + if ($nServices > 0) { + flashRedirect('danger', 'Impossibile eliminare: ha ' . $nServices . ' classi assegnate. Riassegnale oppure disattiva l\'insegnante.'); + } + $pdo->beginTransaction(); + $pdo->prepare("DELETE FROM dayoff WHERE idteacher = :id")->execute([':id' => $id]); + $pdo->prepare("DELETE FROM employee WHERE idemployee = :id")->execute([':id' => $id]); + $pdo->commit(); + deletePhotoFile($photoDirAbs, $t['photo']); + flashRedirect('success', 'Insegnante eliminato.'); + } + + flashRedirect('danger', 'Azione non valida.'); + } catch (RuntimeException $ex) { + flashRedirect('danger', $ex->getMessage()); + } catch (Throwable $ex) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + error_log('insegnanti.php: ' . $ex->getMessage()); + flashRedirect('danger', 'Errore durante il salvataggio. Riprova.'); + } +} + +/* ================================================================ lettura */ + +$flash = $_SESSION['teachers_flash'] ?? null; +unset($_SESSION['teachers_flash']); + +$teachers = $pdo->query("SELECT e.*, + (SELECT COUNT(*) FROM service s WHERE s.idteacher = e.idemployee) AS nservices + FROM employee e + ORDER BY e.is_owner DESC, e.active DESC, e.surnameemployee, e.nameemployee")->fetchAll(); +?> + + + + + + YogiBook - Insegnanti + + + + + + + + + + + + + + +
    +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    +
    Insegnanti della scuola
    + insegnant +
    + +
    + + +
    + +

    Nessun insegnante. Aggiungi il primo: diventerà automaticamente il titolare.

    +
    + +
    + $tid, + 'nameemployee' => $t['nameemployee'], + 'surnameemployee' => $t['surnameemployee'], + 'email' => $t['email'], + 'phone' => $t['phone'], + 'photo' => $photoUrl, + 'notifications' => $t['notifications'] === 'Y', + 'active' => $isActive, + 'is_owner' => $isOwner, + ]; + ?> +
    +
    +
    + + <?php echo e($fullName); ?> + +
    + +
    +
    + Titolare + Non attivo +
    +
    + +
    + ' . e($t['email']) . '' : 'email non inserita'; ?> +
    +
    + ' . e($t['phone']) . '' : 'telefono non inserito'; ?> +
    +
    + class assegnat +
    + +
    + + + + +
    + onchange="this.form.submit()"> + +
    + +
    manca l'email
    + + + +
    + + +
    + + + + +
    +
    + + + + +
    + +
    +
    +
    +
    + +
    + + +
    + +
    +
    +
    + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/options_save.php b/public/options_save.php index a00e691..1863883 100644 --- a/public/options_save.php +++ b/public/options_save.php @@ -1,33 +1,33 @@ getConnection(); + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - header("Location: options.php"); - exit(); + header('Location: options.php'); + exit; } -$conn = new mysqli($servername, $username, $password, $dbname); -if ($conn->connect_error) { - header("Location: options.php?message=error"); - exit(); -} +$waiting = isset($_POST['waitinglist_enabled']) ? 'Y' : 'N'; +$showTeacher = isset($_POST['show_teacher']) ? 'Y' : 'N'; +$maxCancel = max(0, (int) ($_POST['maxbeforetimecancell'] ?? 0)); -// Checkbox non spuntata = nessun invio -> default 'N' -$waitingEnabled = (isset($_POST['waitinglist_enabled']) && $_POST['waitinglist_enabled'] === 'Y') ? 'Y' : 'N'; -$maxCancel = isset($_POST['maxbeforetimecancell']) ? (string) (int) $_POST['maxbeforetimecancell'] : '0'; +try { + $exists = (int) $pdo->query("SELECT COUNT(*) AS c FROM `option`")->fetch()['c'] > 0; -// Aggiorna l'unico record di option (idption = 1) -$stmt = $conn->prepare("UPDATE `option` SET waitinglist_enabled = ?, maxbeforetimecancell = ? WHERE idption = 1"); -$stmt->bind_param("ss", $waitingEnabled, $maxCancel); + if ($exists) { + $st = $pdo->prepare("UPDATE `option` + SET waitinglist_enabled = :w, show_teacher = :s, maxbeforetimecancell = :m + LIMIT 1"); + } else { + $st = $pdo->prepare("INSERT INTO `option` (waitinglist_enabled, show_teacher, maxbeforetimecancell) + VALUES (:w, :s, :m)"); + } + $st->execute([':w' => $waiting, ':s' => $showTeacher, ':m' => (string) $maxCancel]); -if ($stmt->execute()) { - $stmt->close(); - $conn->close(); - header("Location: optionsschool.php?message=success"); - exit(); -} else { - $stmt->close(); - $conn->close(); - header("Location: optionsschool.php?message=error"); - exit(); + header('Location: optionsschool.php?message=success'); +} catch (Throwable $e) { + error_log('options_save: ' . $e->getMessage()); + header('Location: options.php?message=error'); } +exit; diff --git a/public/optionsschool.php b/public/optionsschool.php index 2c707bd..53ab9b4 100644 --- a/public/optionsschool.php +++ b/public/optionsschool.php @@ -19,6 +19,7 @@ $conn->close(); $waitingEnabled = ($opt['waitinglist_enabled'] ?? 'N') === 'Y'; $maxCancel = $opt['maxbeforetimecancell'] ?? '0'; +$showTeacher = ($opt['show_teacher'] ?? 'N') === 'Y'; ?> @@ -155,6 +156,15 @@ $maxCancel = $opt['maxbeforetimecancell'] ?? '0'; >
    +
    +
    +
    Mostra insegnante nelle classi
    +
    Se attiva, nelle card delle classi viene indicato il nome (e la foto) dell'insegnante.
    +
    +
    + > +
    +
    diff --git a/public/userpanel.php b/public/userpanel.php index 6883f78..b62e1c7 100644 --- a/public/userpanel.php +++ b/public/userpanel.php @@ -59,7 +59,7 @@ $stmtBooked->execute([ $bookedRows = $stmtBooked->fetchAll(); // Lezioni in attesa di conferma (pending) dell'utente, nel mese visualizzato -$sqlPending = "SELECT bookingclass.*, service.servicename, service.colorclass, serviceschedule.dateschedule +$sqlPending = "SELECT bookingclass.*, service.servicename, service.colorclass, service.idteacher, serviceschedule.dateschedule FROM bookingclass LEFT JOIN service ON bookingclass.idservice = service.idservice LEFT JOIN serviceschedule ON bookingclass.idserviceschedule = serviceschedule.idserviceschedule @@ -76,7 +76,7 @@ $stmtPending->execute([ $pendingRows = $stmtPending->fetchAll(); // Lezioni in lista d'attesa (waiting) dell'utente, nel mese visualizzato -$sqlWaiting = "SELECT bookingclass.*, service.servicename, service.colorclass, service.maxcapacity, serviceschedule.dateschedule +$sqlWaiting = "SELECT bookingclass.*, service.servicename, service.colorclass, service.maxcapacity, service.idteacher, serviceschedule.dateschedule FROM bookingclass LEFT JOIN service ON bookingclass.idservice = service.idservice LEFT JOIN serviceschedule ON bookingclass.idserviceschedule = serviceschedule.idserviceschedule @@ -157,6 +157,18 @@ $pending = (int) ($rowCounts['pending'] ?? 0); $toprogram = $totalTickets - $passedRecords - $futureRecords - $pending - $lost; +/* ------------------------------------------------------------------------- + * 4b) Insegnanti nelle card (solo se attivo nelle Opzioni) + * ---------------------------------------------------------------------- */ +$optShow = $pdo->query("SELECT show_teacher FROM `option` LIMIT 1")->fetch(); +$showTeacher = ($optShow['show_teacher'] ?? 'N') === 'Y'; + +$teachersById = []; +if ($showTeacher) { + foreach ($pdo->query("SELECT idemployee, nameemployee, surnameemployee, photo FROM employee")->fetchAll() as $t) { + $teachersById[(int) $t['idemployee']] = $t; + } +} /* ------------------------------------------------------------------------- * 5) Pre-caricamento limiti di riprogrammazione per gli ordini coinvolti * (una sola query invece di una per ogni card, dentro il loop) @@ -191,6 +203,22 @@ function e($value): string return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); } +function teacherLine(array $row, bool $show, array $teachersById): string +{ + if (!$show) return ''; + $t = $teachersById[(int) ($row['idteacher'] ?? 0)] ?? null; + if (!$t) return ''; + + $name = trim($t['nameemployee'] . ' ' . $t['surnameemployee']); + if (!empty($t['photo'])) { + $avatar = ''; + } else { + $ini = mb_strtoupper(mb_substr($t['nameemployee'], 0, 1) . mb_substr($t['surnameemployee'], 0, 1)); + $avatar = '' . e($ini) . ''; + } + return '

    ' . $avatar . 'Insegnante: ' . e($name) . '

    '; +} + $italianMonths = [ "January" => "Gennaio", "February" => "Febbraio", @@ -305,6 +333,33 @@ $italianMonths = [ margin: 0 0 4px; } + .teacher-line { + display: flex; + align-items: center; + gap: 8px; + margin: 4px 0 6px; + } + + .teacher-line strong { + color: #374151; + font-weight: 600; + } + + .teacher-avatar-sm { + flex: 0 0 26px; + width: 26px; + height: 26px; + border-radius: 50%; + object-fit: cover; + background: #E1D0B8; + color: #5a4630; + font-size: 11px; + font-weight: 800; + display: inline-flex; + align-items: center; + justify-content: center; + } + .custom-paragraph { color: #6b7280; font-size: 14px; @@ -681,6 +736,7 @@ $italianMonths = [ In attesa

    Quando:

    +

    Luogo: via Valassina 62/B Seregno - Sala Contesto Yoga

    @@ -719,6 +775,7 @@ $italianMonths = [ In coda

    Quando:

    +

    Luogo: via Valassina 62/B Seregno - Sala Contesto Yoga

    0) : ?>

    @@ -801,6 +858,7 @@ $italianMonths = [

    Quando:

    +

    Luogo: via Valassina 62/B Seregno - Sala Contesto Yoga