diff --git a/db/migrations/20261014084133_create_bookingclass_cancellations.php b/db/migrations/20261014084133_create_bookingclass_cancellations.php
new file mode 100644
index 0000000..f85fd5b
--- /dev/null
+++ b/db/migrations/20261014084133_create_bookingclass_cancellations.php
@@ -0,0 +1,44 @@
+table('bookingclass_cancellations', ['id' => 'idcancellation'])
+ ->addColumn('idbookingclass', 'integer')
+ ->addColumn('idserviceschedule', 'integer', ['null' => true])
+ ->addColumn('bookingstart', 'datetime')
+ ->addColumn('status', 'string', ['limit' => 30])
+ ->addColumn('idservice', 'integer')
+ ->addColumn('iduser', 'integer', ['signed' => false])
+ ->addColumn('idorder', 'integer', ['null' => true])
+ ->addColumn('prevbookingstart', 'datetime', ['null' => true])
+ ->addColumn('is_reprogrammed', 'char', ['limit' => 1, 'default' => 'N'])
+ ->addColumn('is_gift', 'char', ['limit' => 1, 'default' => 'N'])
+ ->addColumn('booking_created_at', 'timestamp', ['null' => true])
+ ->addColumn('cancelled_at', 'datetime')
+ ->addColumn('cancelled_by', 'integer', ['signed' => false])
+ ->addColumn('reprogrammed_incremented', 'char', ['limit' => 1, 'default' => 'N'])
+ ->addColumn('idschool', 'integer', ['signed' => false, 'default' => 1])
+ ->addIndex(['iduser'])
+ ->addIndex(['idorder'])
+ ->create();
+ }
+}
diff --git a/public/cancellazioni.php b/public/cancellazioni.php
new file mode 100644
index 0000000..4050c24
--- /dev/null
+++ b/public/cancellazioni.php
@@ -0,0 +1,304 @@
+getConnection();
+
+function e($value): string
+{
+ return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
+}
+
+// Preavviso leggibile: minuti, ore o giorni
+function formatPreavviso(?int $minutes): string
+{
+ if ($minutes === null) return '-';
+ if ($minutes < 60) return $minutes . ' min';
+ if ($minutes < 48 * 60) return floor($minutes / 60) . ' ore';
+ return floor($minutes / 1440) . ' giorni';
+}
+
+$sql = "
+SELECT
+ c.idcancellation,
+ c.iduser,
+ c.idorder,
+ c.is_gift,
+ c.reprogrammed_incremented,
+ c.cancelled_at,
+ COALESCE(ss.dateschedule, c.bookingstart) AS lesson_date,
+ TIMESTAMPDIFF(MINUTE, c.cancelled_at, COALESCE(ss.dateschedule, c.bookingstart)) AS preavviso_min,
+ u.first_name,
+ u.last_name,
+ u.email,
+ s.servicename,
+ o.reprogrammed,
+ o.maxreschedule
+FROM bookingclass_cancellations c
+LEFT JOIN auth_users u ON u.id = c.iduser
+LEFT JOIN service s ON s.idservice = c.idservice
+LEFT JOIN serviceschedule ss ON ss.idserviceschedule = c.idserviceschedule
+LEFT JOIN orderbook o ON o.idorderbook = c.idorder
+ORDER BY c.cancelled_at DESC";
+
+$rows = $pdo->query($sql)->fetchAll();
+
+// Contatori ultimi 7 / 30 giorni
+$limit7 = strtotime('-7 days');
+$limit30 = strtotime('-30 days');
+$count7 = 0;
+$count30 = 0;
+foreach ($rows as $r) {
+ $t = strtotime($r['cancelled_at']);
+ if ($t >= $limit7) $count7++;
+ if ($t >= $limit30) $count30++;
+}
+?>
+
+
+
+
+
+ YogiBook - Cancellazioni
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Lezioni cancellate dagli allievi
+
Storico delle cancellazioni, dalla più recente. Usa la ricerca per filtrare per nome, lezione o data.
+
+
+ Ultimi 7 giorni:
+ Ultimi 30 giorni:
+ Totale:
+
+
+
+
+
+
+ Allievo
+ Lezione
+ Data lezione
+ Cancellata il
+ Preavviso
+ Ordine
+ Conta come riprog.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Omaggio
+
+
+ Sì (/)
+
+
+ No
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/delete_lesson.php b/public/delete_lesson.php
index 49973f3..2361c5a 100644
--- a/public/delete_lesson.php
+++ b/public/delete_lesson.php
@@ -51,6 +51,9 @@ $userName = trim(($u['first_name'] ?? '') . ' ' . ($u['last_name'] ?? ''));
$stmt = $conn->prepare("
SELECT
bc.idbookingclass,
+ bc.idorder,
+ bc.status,
+ bc.bookingstart,
s.servicename,
ss.dateschedule
FROM bookingclass bc
@@ -77,18 +80,116 @@ $classDateTxt = $classDateRaw ? date("d-m-Y H:i", strtotime($classDateRaw)) : '
$cancelDateTxt = date("d-m-Y H:i");
/***********************
- * 3) DELETE (safe)
+ * 3) CONTROLLI (stesse regole di userpanel.php)
***********************/
-$stmt = $conn->prepare("DELETE FROM bookingclass WHERE idbookingclass = ? AND iduser = ?");
-$stmt->bind_param("ii", $id, $userId);
-
-if (!$stmt->execute()) {
- $err = $stmt->error;
- $stmt->close();
- die("Errore durante la cancellazione del record: " . htmlspecialchars($err));
+// Solo prenotazioni confermate: pending e lista d'attesa hanno i loro percorsi
+if (($bk['status'] ?? '') !== 'booked') {
+ header("Location: userpanel.php?deleted=0&err=status");
+ exit();
}
-$stmt->close();
+// Finestra oraria
+$classTime = new DateTime($classDateRaw ?: $bk['bookingstart']);
+$currentTime = new DateTime();
+
+// Lezione già iniziata o passata: non si cancella
+if ($classTime <= $currentTime) {
+ header("Location: userpanel.php?deleted=0&err=window");
+ exit();
+}
+
+// Lezione di oggi: fino alle 17:00 entro le 00:01, dopo le 17:00 entro le 12:00
+if ($classTime->format('Y-m-d') === $currentTime->format('Y-m-d')) {
+ $classHour = (int) $classTime->format('H');
+ $classMinute = (int) $classTime->format('i');
+ $isBefore1700 = ($classHour < 17) || ($classHour === 17 && $classMinute === 0);
+ $deadline = new DateTime($classTime->format($isBefore1700 ? 'Y-m-d 00:01:00' : 'Y-m-d 12:00:00'));
+ if ($currentTime > $deadline) {
+ header("Location: userpanel.php?deleted=0&err=window");
+ exit();
+ }
+}
+
+/***********************
+ * 3b) STORICO + RIPROGRAMMAZIONE + CANCELLAZIONE
+ * Tutto in una transazione: o riesce tutto o niente.
+ ***********************/
+$idOrder = $bk['idorder'] !== null ? (int) $bk['idorder'] : null;
+$ord = null;
+$incremented = 'N';
+
+try {
+ $conn->begin_transaction();
+
+ // Limite riprogrammazioni (stessa regola del pulsante Riprogramma).
+ // FOR UPDATE: blocca la riga dell'ordine contro doppi click.
+ if ($idOrder !== null) {
+ $stmt = $conn->prepare("SELECT reprogrammed, maxreschedule FROM orderbook WHERE idorderbook = ? FOR UPDATE");
+ $stmt->bind_param("i", $idOrder);
+ $stmt->execute();
+ $ord = $stmt->get_result()->fetch_assoc();
+ $stmt->close();
+
+ if ($ord) {
+ if ((int) $ord['reprogrammed'] >= (int) $ord['maxreschedule']) {
+ throw new RuntimeException('limit');
+ }
+ $incremented = 'Y';
+ }
+ }
+
+ // Copia nello storico (prima di eliminare)
+ $stmt = $conn->prepare("
+ INSERT INTO bookingclass_cancellations
+ (idbookingclass, idserviceschedule, bookingstart, status, idservice, iduser, idorder,
+ prevbookingstart, is_reprogrammed, is_gift, booking_created_at,
+ cancelled_at, cancelled_by, reprogrammed_incremented)
+ SELECT idbookingclass, idserviceschedule, bookingstart, status, idservice, iduser, idorder,
+ prevbookingstart, is_reprogrammed, is_gift, created_at,
+ NOW(), ?, ?
+ FROM bookingclass
+ WHERE idbookingclass = ? AND iduser = ?
+ ");
+ $stmt->bind_param("isii", $userId, $incremented, $id, $userId);
+ $stmt->execute();
+ $copied = $stmt->affected_rows;
+ $stmt->close();
+ if ($copied !== 1) {
+ throw new RuntimeException('notfound');
+ }
+
+ // La cancellazione conta come una riprogrammazione
+ if ($incremented === 'Y') {
+ $stmt = $conn->prepare("UPDATE orderbook SET reprogrammed = reprogrammed + 1 WHERE idorderbook = ?");
+ $stmt->bind_param("i", $idOrder);
+ $stmt->execute();
+ $stmt->close();
+ }
+
+ // Eliminazione della prenotazione
+ $stmt = $conn->prepare("DELETE FROM bookingclass WHERE idbookingclass = ? AND iduser = ?");
+ $stmt->bind_param("ii", $id, $userId);
+ $stmt->execute();
+ $deleted = $stmt->affected_rows;
+ $stmt->close();
+ if ($deleted !== 1) {
+ throw new RuntimeException('notfound');
+ }
+
+ $conn->commit();
+} catch (Throwable $e) {
+ try {
+ $conn->rollback();
+ } catch (Throwable $ignored) {
+ }
+
+ $code = in_array($e->getMessage(), ['limit', 'notfound'], true) ? $e->getMessage() : 'db';
+ if ($code === 'db') {
+ error_log("delete_lesson #{$id} utente #{$userId}: " . $e->getMessage());
+ }
+ header("Location: userpanel.php?deleted=0&err=" . $code);
+ exit();
+}
// Connessione $bkngstm gestita globalmente: non la chiudiamo qui
/***********************
diff --git a/public/include/sidebar.php b/public/include/sidebar.php
index 3212ccf..fa7302f 100644
--- a/public/include/sidebar.php
+++ b/public/include/sidebar.php
@@ -178,6 +178,12 @@
+
+
+
+
+
+
diff --git a/public/logs/mail_cancel.log b/public/logs/mail_cancel.log
index e1e68cf..6d6f3b8 100644
--- a/public/logs/mail_cancel.log
+++ b/public/logs/mail_cancel.log
@@ -1,2 +1,6 @@
[2026-02-18 13:43:57] ERROR sending. booking_id=29 user_id=2 to=info@claudiosironi.com :: SMTP Error: Could not connect to SMTP host. Failed to connect to server
[2026-02-18 13:46:10] ERROR sending. booking_id=30 user_id=2 to=info@claudiosironi.com :: SMTP Error: Could not connect to SMTP host. Failed to connect to server
+[2026-10-14 08:44:28] SMTP in uso host=mail.yogasoul.it porta=465 enc=ssl user=info@yogasoul.it
+[2026-10-14 08:44:30] OK inviata. booking_id=70 user_id=2 to=info@claudiosironi.com lezione='Aerial Yoga - base' prevista='21-10-2026 18:30' cancellata='14-10-2026 08:44'
+[2026-10-14 08:46:52] SMTP in uso host=mail.yogasoul.it porta=465 enc=ssl user=info@yogasoul.it
+[2026-10-14 08:46:54] OK inviata. booking_id=60 user_id=2 to=info@claudiosironi.com lezione='Aerial Yoga - base' prevista='29-10-2026 20:15' cancellata='14-10-2026 08:46'
diff --git a/public/userpanel.php b/public/userpanel.php
index b62e1c7..5299a68 100644
--- a/public/userpanel.php
+++ b/public/userpanel.php
@@ -504,6 +504,7 @@ $italianMonths = [
html: `
Confermi di voler cancellare questa lezione?
RICORDA: dovrai riprogrammarla entro la scadenza del tuo abbonamento per non perderla!
+ La cancellazione conta come una delle riprogrammazioni disponibili per il tuo pacchetto.
Scadenza: ${expirydate}
@@ -651,7 +652,22 @@ $italianMonths = [
-
+ 'Non è più possibile cancellare questa lezione: il tempo per la cancellazione è scaduto.',
+ 'limit' => 'Hai raggiunto il numero massimo di riprogrammazioni per questo pacchetto.',
+ 'status' => 'Questa prenotazione non può essere cancellata da qui.',
+ 'notfound' => 'Prenotazione non trovata.',
+ ];
+ $cancelMsg = $cancelErrors[$_GET['err'] ?? ''] ?? 'Si è verificato un errore: la lezione non è stata cancellata. Riprova o contattaci.';
+ ?>
+
+
+
+
+
+
+
Lezione cancellata con successo! Grazie per aver liberato il posto!