diff --git a/public/admin_reprogram_booking.php b/public/admin_reprogram_booking.php new file mode 100644 index 0000000..4f2f13d --- /dev/null +++ b/public/admin_reprogram_booking.php @@ -0,0 +1,214 @@ + false, 'message' => 'Metodo non consentito.'], 405); +} + +$id = (int) ($_POST['id'] ?? 0); +$idSchedule = (int) ($_POST['idschedule'] ?? 0); +$sendEmail = ($_POST['send_email'] ?? 'N') === 'Y'; +$countReprog = ($_POST['count_reprog'] ?? 'N') === 'Y'; +$staffId = (int) $iduserlogin; + +if ($id <= 0 || $idSchedule <= 0) { + jsonOut(['success' => false, 'message' => 'Seleziona la nuova lezione.'], 400); +} + +$errors = [ + 'notfound' => 'Prenotazione non trovata (forse è stata cancellata).', + 'notarget' => 'La lezione scelta non esiste più.', + 'same' => 'Hai scelto la stessa lezione di adesso.', + 'past' => 'La lezione scelta è già iniziata o passata.', + 'already' => 'L\'allieva è già iscritta alla lezione scelta.', + 'full' => 'La lezione scelta è piena.', +]; + +$pdo = DBHandlerSelect::getInstance()->getConnection(); +$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +$bk = null; +$target = null; +$incremented = 'N'; + +try { + $pdo->beginTransaction(); + + // Prenotazione attuale (bloccata) + $st = $pdo->prepare(" + SELECT bc.idbookingclass, bc.iduser, bc.idorder, bc.idserviceschedule, bc.bookingstart, + s.servicename AS old_servicename, ss.dateschedule AS old_date, + u.email, u.first_name + FROM bookingclass bc + LEFT JOIN service s ON s.idservice = bc.idservice + LEFT JOIN serviceschedule ss ON ss.idserviceschedule = bc.idserviceschedule + LEFT JOIN auth_users u ON u.id = bc.iduser + WHERE bc.idbookingclass = ? + FOR UPDATE + "); + $st->execute([$id]); + $bk = $st->fetch(PDO::FETCH_ASSOC); + if (!$bk) throw new RuntimeException('notfound'); + + if ((int) $bk['idserviceschedule'] === $idSchedule) throw new RuntimeException('same'); + + // Nuova lezione (bloccata: evita due spostamenti contemporanei sull'ultimo posto) + $st = $pdo->prepare(" + SELECT ss.idserviceschedule, ss.idservice, ss.dateschedule, s.servicename, s.maxcapacity + FROM serviceschedule ss + JOIN service s ON s.idservice = ss.idservice + WHERE ss.idserviceschedule = ? + FOR UPDATE + "); + $st->execute([$idSchedule]); + $target = $st->fetch(PDO::FETCH_ASSOC); + if (!$target) throw new RuntimeException('notarget'); + + if (strtotime($target['dateschedule']) <= time()) throw new RuntimeException('past'); + + // L'allieva è già su quella lezione? + $st = $pdo->prepare("SELECT COUNT(*) FROM bookingclass WHERE idserviceschedule = ? AND iduser = ?"); + $st->execute([$idSchedule, (int) $bk['iduser']]); + if ((int) $st->fetchColumn() > 0) throw new RuntimeException('already'); + + // Posti liberi (booked + pending) + if ($target['maxcapacity'] !== null) { + $st = $pdo->prepare("SELECT COUNT(*) FROM bookingclass WHERE idserviceschedule = ? AND status IN ('booked','pending')"); + $st->execute([$idSchedule]); + if ((int) $st->fetchColumn() >= (int) $target['maxcapacity']) throw new RuntimeException('full'); + } + + // Spostamento: la vecchia lezione resta in idprevserviceschedule / prevbookingstart + $up = $pdo->prepare(" + UPDATE bookingclass + SET idprevserviceschedule = :prevsched, + prevbookingstart = :prevstart, + idserviceschedule = :sched, + idservice = :svc, + bookingstart = :start, + is_reprogrammed = 'Y' + WHERE idbookingclass = :id + "); + $up->execute([ + ':prevsched' => $bk['idserviceschedule'] !== null ? (int) $bk['idserviceschedule'] : null, + ':prevstart' => $bk['bookingstart'], + ':sched' => $idSchedule, + ':svc' => (int) $target['idservice'], + ':start' => $target['dateschedule'], + ':id' => $id, + ]); + + // Conta come riprogrammazione + if ($countReprog && $bk['idorder'] !== null) { + $st = $pdo->prepare("UPDATE orderbook SET reprogrammed = reprogrammed + 1 WHERE idorderbook = ?"); + $st->execute([(int) $bk['idorder']]); + if ($st->rowCount() === 1) $incremented = 'Y'; + } + + $pdo->commit(); +} catch (PDOException $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + error_log("admin_reprogram_booking #{$id} -> {$idSchedule} (staff #{$staffId}): " . $e->getMessage()); + jsonOut(['success' => false, 'message' => 'Errore durante la riprogrammazione. Riprova.'], 500); +} catch (RuntimeException $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + $code = $e->getMessage(); + jsonOut(['success' => false, 'message' => $errors[$code] ?? 'Errore durante la riprogrammazione.'], 409); +} + +/* ------------------------------------------------------------------------- + * Email all'allieva (dopo il commit, solo se richiesta) + * ---------------------------------------------------------------------- */ +$fmt = fn($d) => $d ? date('d/m/Y H:i', strtotime($d)) : ''; +$emailNote = ''; + +if ($sendEmail) { + $to = trim((string) ($bk['email'] ?? '')); + + if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { + $emailNote = ' Email non inviata: indirizzo dell\'allieva mancante o non valido.'; + } else { + try { + require_once(__DIR__ . '/class/mailer.php'); + + $clean = fn($v) => trim((string) $v, "\" \t\n\r\0\x0B"); + $enc = strtolower($clean($_ENV['MAIL_ENCRYPTION'] ?? 'ssl')); + + $mail = new \PHPMailer\PHPMailer\PHPMailer(true); + $mail->isSMTP(); + $mail->Host = $clean($_ENV['MAIL_HOST'] ?? ''); + $mail->SMTPAuth = true; + $mail->Username = $_ENV['MAIL_USERNAME'] ?? ''; + $mail->Password = $_ENV['MAIL_PASSWORD'] ?? ''; + $mail->Port = (int) ($_ENV['MAIL_PORT'] ?? 465); + if (in_array($enc, ['tls', 'ssl'], true)) { + $mail->SMTPSecure = $enc; + } + $mail->Timeout = 20; + $mail->CharSet = 'UTF-8'; + + $fromName = $clean($_ENV['MAIL_FROM_NAME'] ?? 'YogiBook'); + $mail->setFrom($clean($_ENV['MAIL_FROM_ADDRESS'] ?? ''), $fromName); + + $h = fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); + $nome = trim((string) ($bk['first_name'] ?? '')); + $oldName = $bk['old_servicename'] ?: 'Lezione'; + $oldDate = $fmt($bk['old_date'] ?: $bk['bookingstart']); + $newName = $target['servicename'] ?: 'Lezione'; + $newDate = $fmt($target['dateschedule']); + + $mail->addAddress($to, $nome); + $mail->isHTML(true); + $mail->Subject = 'Lezione riprogrammata - ' . $newName . ' ' . $newDate; + $mail->Body = " +
+

Ciao " . $h($nome !== '' ? $nome : 'Yogi') . ",

+

la tua lezione è stata riprogrammata.

+

+ Prima: " . $h($oldName) . " – " . $h($oldDate) . "
+ Adesso: " . $h($newName) . " – " . $h($newDate) . " +

+

Se hai domande, rispondi pure a questa email.

+

A presto,
" . $h($fromName) . "

+
"; + $mail->AltBody = "Ciao " . ($nome !== '' ? $nome : 'Yogi') . ",\n" + . "la tua lezione è stata riprogrammata.\n" + . "Prima: {$oldName} - {$oldDate}\nAdesso: {$newName} - {$newDate}\n\n{$fromName}"; + + $mail->send(); + $emailNote = ' Email inviata all\'allieva.'; + } catch (Throwable $e) { + error_log("admin_reprogram_booking #{$id}: email non inviata: " . $e->getMessage()); + $emailNote = ' Attenzione: la lezione è stata spostata ma l\'email non è partita.'; + } + } +} + +$reprogNote = $incremented === 'Y' ? ' Conteggiata come riprogrammazione.' : ''; + +jsonOut([ + 'success' => true, + 'message' => 'Lezione spostata a ' . $fmt($target['dateschedule']) . '.' . $reprogNote . $emailNote, +]); diff --git a/public/admin_reprogram_lessons.php b/public/admin_reprogram_lessons.php new file mode 100644 index 0000000..45c9a29 --- /dev/null +++ b/public/admin_reprogram_lessons.php @@ -0,0 +1,109 @@ + false, 'message' => 'Prenotazione non valida.'], 400); +} + +$pdo = DBHandlerSelect::getInstance()->getConnection(); +$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +try { + // Prenotazione + ordine collegato + $st = $pdo->prepare(" + SELECT bc.idbookingclass, bc.iduser, bc.idservice, bc.idserviceschedule, bc.idorder, + o.expireon, o.reprogrammed, o.maxreschedule + FROM bookingclass bc + LEFT JOIN orderbook o ON o.idorderbook = bc.idorder + WHERE bc.idbookingclass = ? + "); + $st->execute([$id]); + $bk = $st->fetch(PDO::FETCH_ASSOC); + + if (!$bk) { + jsonOut(['success' => false, 'message' => 'Prenotazione non trovata.'], 404); + } + + // Classi ammesse: la stessa + quelle associate + $services = [(int) $bk['idservice']]; + $as = $pdo->prepare("SELECT idassociateservice FROM associateclass WHERE idmainservice = ?"); + $as->execute([(int) $bk['idservice']]); + foreach ($as->fetchAll(PDO::FETCH_COLUMN) as $sid) { + $services[] = (int) $sid; + } + $services = array_values(array_unique($services)); + $ph = implode(',', array_fill(0, count($services), '?')); + + $sql = " + SELECT ss.idserviceschedule, ss.dateschedule, + s.servicename, s.maxcapacity, + (SELECT COUNT(*) FROM bookingclass b + WHERE b.idserviceschedule = ss.idserviceschedule + AND b.status IN ('booked','pending')) AS occupied, + (SELECT COUNT(*) FROM bookingclass b2 + WHERE b2.idserviceschedule = ss.idserviceschedule + AND b2.iduser = ?) AS mine + FROM serviceschedule ss + JOIN service s ON s.idservice = ss.idservice + WHERE ss.idservice IN ($ph) + AND ss.dateschedule > NOW() + AND ss.dateschedule <= DATE_ADD(NOW(), INTERVAL 90 DAY) + ORDER BY ss.dateschedule + LIMIT 200"; + $ls = $pdo->prepare($sql); + $ls->execute(array_merge([(int) $bk['iduser']], $services)); + + $giorni = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']; + $expiry = $bk['expireon'] ? strtotime($bk['expireon'] . ' 23:59:59') : null; + + $lessons = []; + foreach ($ls->fetchAll(PDO::FETCH_ASSOC) as $l) { + $ts = strtotime($l['dateschedule']); + $max = $l['maxcapacity'] !== null ? (int) $l['maxcapacity'] : null; + $occ = (int) $l['occupied']; + + $lessons[] = [ + 'id' => (int) $l['idserviceschedule'], + 'label' => $giorni[(int) date('w', $ts)] . ' ' . date('d/m/Y H:i', $ts), + 'servicename' => $l['servicename'], + 'occupied' => $occ, + 'max' => $max, + 'full' => $max !== null && $occ >= $max, + 'mine' => (int) $l['mine'] > 0, + 'after_expiry' => $expiry !== null && $ts > $expiry, + ]; + } + + jsonOut([ + 'success' => true, + 'lessons' => $lessons, + 'order' => [ + 'has_order' => $bk['idorder'] !== null, + 'reprogrammed' => (int) ($bk['reprogrammed'] ?? 0), + 'maxreschedule' => (int) ($bk['maxreschedule'] ?? 0), + 'expireon' => $bk['expireon'] ? date('d/m/Y', strtotime($bk['expireon'])) : null, + ], + ]); +} catch (PDOException $e) { + error_log("admin_reprogram_lessons #{$id}: " . $e->getMessage()); + jsonOut(['success' => false, 'message' => 'Errore nel caricamento delle lezioni.'], 500); +} diff --git a/public/situationusers.php b/public/situationusers.php index 1c05cb6..bdc3288 100644 --- a/public/situationusers.php +++ b/public/situationusers.php @@ -475,12 +475,18 @@ $expiredCertCount = count(array_filter($users, fn($u) => (int) $u['ha_ordine_att