@@ -755,6 +766,163 @@ $italianMonths = [
});
});
+ // === Intercetta submit "Aggiungi partecipante" ===
+ $(document).on('submit', '.add-booking-form', function(e) {
+ e.preventDefault();
+ const form = $(this);
+ const userid = form.find('.userid').val();
+ const name = form.find('.name').val().trim();
+ const surname = form.find('.surname').val().trim();
+
+ if (!name || !surname) {
+ Swal.fire('Dati mancanti', 'Inserisci nome e cognome.', 'warning');
+ return;
+ }
+
+ const hasUser = userid && parseInt(userid) > 0;
+
+ // Controllo anti-duplicato PRIMA di aprire il modale:
+ // l'utente è già presente in questa classe?
+ if (hasUser) {
+ const panel = form.closest('.bookings-panel');
+ const already = panel.find('.booking-item[data-iduser="' + parseInt(userid) + '"]').length > 0;
+ if (already) {
+ Swal.fire(
+ 'Già prenotato',
+ 'Questa persona è già presente in questa classe.',
+ 'info'
+ );
+ return;
+ }
+ }
+
+ if (hasUser) {
+ $.getJSON('get_user_orders.php', {
+ userid: userid
+ }).done(function(data) {
+ showBookingModal(form, data.orders || []);
+ }).fail(function() {
+ showBookingModal(form, []);
+ });
+ } else {
+ showBookingModal(form, []);
+ }
+ });
+
+ function showBookingModal(form, orders) {
+ const usableOrders = orders.filter(o => o.remaining > 0);
+ let orderOptions = '';
+ usableOrders.forEach(function(o) {
+ const exp = o.expireon ? ' - scade ' + o.expireon : '';
+ orderOptions += '
';
+ });
+
+ const hasUsableOrders = usableOrders.length > 0;
+
+ const d = new Date();
+ d.setMonth(d.getMonth() + 3);
+ const defExpiry = d.toISOString().slice(0, 10);
+
+ const html =
+ '
';
+
+ Swal.fire({
+ title: 'Aggiungi partecipante',
+ html: html,
+ showCancelButton: true,
+ confirmButtonText: 'Inserisci',
+ cancelButtonText: 'Annulla',
+ confirmButtonColor: '#1ebf73',
+ didOpen: () => {
+ document.querySelectorAll('input[name="bkmode"]').forEach(function(r) {
+ r.addEventListener('change', function() {
+ document.getElementById('bk-nuovo-fields').style.display =
+ (this.value === 'nuovo') ? 'block' : 'none';
+ });
+ });
+ },
+ preConfirm: () => {
+ const mode = document.querySelector('input[name="bkmode"]:checked');
+ if (!mode) {
+ Swal.showValidationMessage('Seleziona una modalità');
+ return false;
+ }
+ const result = {
+ mode: mode.value
+ };
+ if (mode.value === 'scala') {
+ const sel = document.getElementById('bk-order').value;
+ if (!sel) {
+ Swal.showValidationMessage('Seleziona un pacchetto');
+ return false;
+ }
+ result.idorder = sel;
+ }
+ if (mode.value === 'nuovo') {
+ const exp = document.getElementById('bk-expiry').value;
+ if (!exp) {
+ Swal.showValidationMessage('Inserisci la scadenza');
+ return false;
+ }
+ result.expiry = exp;
+ }
+ return result;
+ }
+ }).then((res) => {
+ if (!res.isConfirmed) return;
+ submitBooking(form, res.value);
+ });
+ }
+
+ function submitBooking(form, choice) {
+ form.find('input[name="bkmode"]').remove();
+ form.find('input[name="new_expiry"]').remove();
+
+ $('
').attr({
+ type: 'hidden',
+ name: 'bkmode',
+ value: choice.mode
+ }).appendTo(form);
+
+ if (choice.mode === 'scala') {
+ form.find('input[name="idorder"]').val(choice.idorder);
+ } else if (choice.mode === 'omaggio') {
+ form.find('input[name="idorder"]').val('');
+ } else if (choice.mode === 'nuovo') {
+ form.find('input[name="idorder"]').val('');
+ $('
').attr({
+ type: 'hidden',
+ name: 'new_expiry',
+ value: choice.expiry
+ }).appendTo(form);
+ }
+
+ form.off('submit');
+ form[0].submit();
+ }
+
let searchTimeout;
$(document).on('input', '.surname, .name', function() {
clearTimeout(searchTimeout);
diff --git a/public/api/my_lessons.php b/public/api/my_lessons.php
index c481c59e..62d6d0bc 100644
--- a/public/api/my_lessons.php
+++ b/public/api/my_lessons.php
@@ -88,6 +88,7 @@ $stmt = $db->prepare("
ON bc.idserviceschedule = ss.idserviceschedule
WHERE bc.iduser = :uid
AND bc.status != 'cancelled'
+ AND bc.is_gift = 'N'
");
$stmt->execute([
':now1' => $now,
@@ -128,6 +129,7 @@ $stmt = $db->prepare("
bc.idservice,
bc.idorder,
bc.lostlesson,
+ bc.is_gift,
ss.dateschedule,
s.servicename,
s.colorclass,
@@ -192,10 +194,17 @@ foreach ($rows as $r) {
$reprogrammed = (int) ($r['reprogrammed'] ?? 0);
$canReprogram = $reprogrammed < $maxreschedule;
- // Nella webapp i bottoni "Riprogramma" e "Cancella" compaiono entrambi
- // solo se ($canByTime && $canReprogram). Riproduciamo la stessa condizione.
- $canReschedule = $canByTime && $canReprogram;
- $canDelete = $canByTime && $canReprogram;
+ $isGift = ($r['is_gift'] ?? 'N') === 'Y';
+
+ if ($isGift) {
+ // Omaggio: mai riprogrammabile, cancellabile in base al tempo.
+ $canReschedule = false;
+ $canDelete = $canByTime;
+ } else {
+ // Lezione normale: entrambi soggetti a tempo + limite riprogrammazioni.
+ $canReschedule = $canByTime && $canReprogram;
+ $canDelete = $canByTime && $canReprogram;
+ }
$lessons[] = [
'booking_id' => (int) $r['idbookingclass'],
diff --git a/public/get_user_orders.php b/public/get_user_orders.php
new file mode 100644
index 00000000..2f4fccec
--- /dev/null
+++ b/public/get_user_orders.php
@@ -0,0 +1,101 @@
+ is_gift = 'N' esclude gli omaggi dal consumo.
+ * --------------------------------------------------------------------------
+ */
+
+$pdo = DBHandlerSelect::getInstance()->getConnection();
+
+function jsonOut(array $payload): void
+{
+ if (ob_get_length() !== false) {
+ ob_end_clean();
+ }
+ header('Content-Type: application/json; charset=utf-8');
+ echo json_encode($payload, JSON_UNESCAPED_UNICODE);
+ exit;
+}
+
+$userid = isset($_REQUEST['userid']) ? (int) $_REQUEST['userid'] : 0;
+
+if ($userid <= 0) {
+ jsonOut(['error' => 'Utente non valido', 'orders' => []]);
+}
+
+try {
+ $today = date('Y-m-d');
+
+ // Ordini dell'utente NON scaduti
+ $sql = "SELECT ob.idorderbook, ob.idservice, ob.nticket, ob.quantityclass,
+ ob.expireon, ob.maxreschedule, ob.reprogrammed,
+ s.servicename
+ FROM orderbook ob
+ LEFT JOIN service s ON ob.idservice = s.idservice
+ WHERE ob.iduser = :uid
+ AND (ob.expireon IS NULL OR ob.expireon >= :today)
+ ORDER BY ob.idorderbook DESC";
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([':uid' => $userid, ':today' => $today]);
+ $rows = $stmt->fetchAll();
+
+ // Prepared per contare le prenotazioni che consumano ticket
+ // (non cancellate e non omaggio)
+ $usedStmt = $pdo->prepare(
+ "SELECT COUNT(*) AS n
+ FROM bookingclass
+ WHERE idorder = :oid
+ AND status != 'cancelled'
+ AND is_gift = 'N'"
+ );
+
+ $orders = [];
+ foreach ($rows as $o) {
+ $orderId = (int) $o['idorderbook'];
+
+ // tickets totali: quantityclass se valorizzato, altrimenti nticket
+ $tickets = (int) (($o['quantityclass'] !== null && $o['quantityclass'] !== '')
+ ? $o['quantityclass']
+ : ($o['nticket'] ?? 0));
+
+ $usedStmt->execute([':oid' => $orderId]);
+ $used = (int) ($usedStmt->fetch()['n'] ?? 0);
+
+ $remaining = $tickets - $used;
+ if ($remaining < 0) {
+ $remaining = 0;
+ }
+
+ $orders[] = [
+ 'idorderbook' => $orderId,
+ 'service_name' => (string) ($o['servicename'] ?? ''),
+ 'tickets' => $tickets,
+ 'used' => $used,
+ 'remaining' => $remaining,
+ 'expireon' => $o['expireon'] ?? null,
+ 'maxreschedule' => (int) ($o['maxreschedule'] ?? 0),
+ 'reprogrammed' => (int) ($o['reprogrammed'] ?? 0),
+ ];
+ }
+
+ jsonOut(['orders' => $orders]);
+} catch (Throwable $ex) {
+ error_log('get_user_orders error: ' . $ex->getMessage());
+ jsonOut(['error' => 'Errore durante il recupero degli ordini.', 'orders' => []]);
+}
diff --git a/public/inserisci_record.php b/public/inserisci_record.php
index b23187c0..e26283f5 100644
--- a/public/inserisci_record.php
+++ b/public/inserisci_record.php
@@ -3,7 +3,17 @@ require_once('include/headscript.php');
/**
* Inserisce un partecipante in una classe (bookingclass).
- * Se l'utente non esiste ancora (userid mancante), lo crea in auth_users
+ *
+ * Modalità (campo bkmode dal modale):
+ * - 'scala' : aggancia a un ordine esistente (idorder valorizzato), consuma ticket
+ * - 'omaggio' : idorder NULL, is_gift='Y' (non consuma, non spostabile)
+ * - 'nuovo' : crea un nuovo orderbook (1 ticket, maxreschedule 0, scadenza da new_expiry)
+ * e vi aggancia la prenotazione
+ *
+ * Nota FK: bookingclass.idorder ha una foreign key verso orderbook.
+ * Quindi idorder deve essere NULL o un idorderbook reale, MAI 0.
+ *
+ * Se l'utente non esiste (userid mancante), lo crea in auth_users
* riusando un eventuale account con la stessa email. Tutto in transazione.
*/
$pdo = DBHandlerSelect::getInstance()->getConnection();
@@ -15,10 +25,14 @@ $email = trim($_POST['email'] ?? '');
$userid = isset($_POST['userid']) ? (int) $_POST['userid'] : 0;
$idserviceschedule = isset($_POST['idserviceschedule']) ? (int) $_POST['idserviceschedule'] : 0;
$status = trim($_POST['status'] ?? 'booked');
-$idorder = isset($_POST['idorder']) ? (int) $_POST['idorder'] : 0;
$idservice = isset($_POST['idservice']) ? (int) $_POST['idservice'] : 0;
$bookingstart = trim($_POST['bookingstart'] ?? '');
+// Nuovi campi dal modale
+$bkmode = trim($_POST['bkmode'] ?? 'scala'); // scala | omaggio | nuovo
+$idorderRaw = $_POST['idorder'] ?? '';
+$newExpiry = trim($_POST['new_expiry'] ?? '');
+
// --- Validazione minima ---
if ($idserviceschedule <= 0 || $idservice <= 0) {
header('Location: adminpanel.php?error=missingdata');
@@ -28,10 +42,10 @@ if ($idserviceschedule <= 0 || $idservice <= 0) {
try {
$pdo->beginTransaction();
- // Se non abbiamo un userid valido, dobbiamo risolvere/creare l'utente
+ // ------------------------------------------------------------------
+ // 1) Risolvi / crea utente
+ // ------------------------------------------------------------------
if ($userid <= 0) {
-
- // 1) Se è stata fornita un'email, controlliamo se l'utente esiste già
if ($email !== '') {
$check = $pdo->prepare("SELECT id FROM auth_users WHERE email = :email LIMIT 1");
$check->execute([':email' => $email]);
@@ -41,9 +55,7 @@ try {
}
}
- // 2) Se ancora non c'è, creiamo un nuovo utente
if ($userid <= 0) {
- // Password casuale hashata (questi account non usano una password condivisa)
$randomPassword = bin2hex(random_bytes(8));
$hashed = password_hash($randomPassword, PASSWORD_BCRYPT);
@@ -67,20 +79,136 @@ try {
}
}
- // 3) Inserimento della prenotazione
+ // ------------------------------------------------------------------
+ // 2) Determina idorder e is_gift in base alla modalità
+ // ------------------------------------------------------------------
+ $idorder = null; // default NULL (rispetta la FK)
+ $isGift = 'N';
+
+ if ($bkmode === 'omaggio') {
+ // Omaggio: nessun ordine, flag gift
+ $idorder = null;
+ $isGift = 'Y';
+ } elseif ($bkmode === 'nuovo') {
+ // Crea un nuovo ordine con 1 ticket, maxreschedule 0
+ if ($newExpiry === '') {
+ throw new RuntimeException('Scadenza mancante per il nuovo ordine.');
+ }
+
+ // Recupera i dati reali dell'utente (email, nome, cognome)
+ $uStmt = $pdo->prepare(
+ "SELECT first_name, last_name, email FROM auth_users WHERE id = :uid LIMIT 1"
+ );
+ $uStmt->execute([':uid' => $userid]);
+ $uRow = $uStmt->fetch() ?: [];
+ $uEmail = $uRow['email'] ?? ($email !== '' ? $email : null);
+ $uFirst = $uRow['first_name'] ?? $name;
+ $uLast = $uRow['last_name'] ?? $surname;
+
+ // Recupera il nome del servizio (per cod / product_name)
+ $sStmt = $pdo->prepare(
+ "SELECT servicename FROM service WHERE idservice = :sid LIMIT 1"
+ );
+ $sStmt->execute([':sid' => $idservice]);
+ $serviceName = (string) ($sStmt->fetchColumn() ?: '');
+
+ // first_lesson_date = data della lezione (solo parte data di bookingstart)
+ $firstLessonDate = null;
+ if ($bookingstart !== '') {
+ try {
+ $firstLessonDate = (new DateTime($bookingstart))->format('Y-m-d');
+ } catch (Throwable $e) {
+ $firstLessonDate = null;
+ }
+ }
+
+ $insOrder = $pdo->prepare(
+ "INSERT INTO orderbook
+ (iduser, idservice, nticket, quantity, quantityclass,
+ expireon, maxreschedule, reprogrammed, status,
+ order_billing_email, cod, product_name,
+ first_name, last_name, first_lesson_date, order_date_created)
+ VALUES
+ (:uid, :service, 1, 1, 1,
+ :expire, 0, 0, 'booked',
+ :bemail, :cod, :pname,
+ :first, :last, :firstlesson, :created)"
+ );
+ $insOrder->execute([
+ ':uid' => $userid,
+ ':service' => $idservice,
+ ':expire' => $newExpiry,
+ ':bemail' => $uEmail,
+ ':cod' => $serviceName,
+ ':pname' => $serviceName,
+ ':first' => $uFirst,
+ ':last' => $uLast,
+ ':firstlesson' => $firstLessonDate,
+ ':created' => date('Y-m-d'),
+ ]);
+ $idorder = (int) $pdo->lastInsertId();
+ $isGift = 'N';
+ } else {
+ // 'scala': aggancia a ordine esistente
+ $idorder = ($idorderRaw !== '' && (int) $idorderRaw > 0) ? (int) $idorderRaw : null;
+
+ // Se non è stato passato un ordine valido in modalità scala,
+ // per sicurezza NON forziamo 0 (romperebbe la FK): resta NULL.
+ if ($idorder !== null) {
+ // Verifica che l'ordine sia dell'utente e non scaduto
+ $chk = $pdo->prepare(
+ "SELECT expireon FROM orderbook
+ WHERE idorderbook = :oid AND iduser = :uid LIMIT 1"
+ );
+ $chk->execute([':oid' => $idorder, ':uid' => $userid]);
+ $row = $chk->fetch();
+ if (!$row) {
+ throw new RuntimeException('Ordine non valido per questo utente.');
+ }
+ $exp = $row['expireon'] ?? null;
+ if ($exp !== null && $exp < date('Y-m-d')) {
+ throw new RuntimeException('Il pacchetto selezionato è scaduto.');
+ }
+ }
+ $isGift = 'N';
+ }
+
+ // ------------------------------------------------------------------
+ // 2b) Controllo anti-duplicato: utente già in questa classe?
+ // ------------------------------------------------------------------
+ $dup = $pdo->prepare(
+ "SELECT COUNT(*) FROM bookingclass
+ WHERE iduser = :uid
+ AND idserviceschedule = :idschedule
+ AND status != 'cancelled'"
+ );
+ $dup->execute([
+ ':uid' => $userid,
+ ':idschedule' => $idserviceschedule,
+ ]);
+ if ((int) $dup->fetchColumn() > 0) {
+ $pdo->rollBack();
+ header('Location: adminpanel.php?error=duplicate');
+ exit;
+ }
+
+ // ------------------------------------------------------------------
+ // 3) Inserimento prenotazione
+ // ------------------------------------------------------------------
$insBooking = $pdo->prepare(
"INSERT INTO bookingclass
- (iduser, idserviceschedule, status, idorder, idservice, bookingstart)
+ (iduser, idserviceschedule, status, idorder, idservice, bookingstart, is_gift)
VALUES
- (:iduser, :idschedule, :status, :idorder, :idservice, :bookingstart)"
+ (:iduser, :idschedule, :status, :idorder, :idservice, :bookingstart, :isgift)"
);
$insBooking->execute([
':iduser' => $userid,
':idschedule' => $idserviceschedule,
':status' => $status,
- ':idorder' => $idorder,
+ ':idorder' => $idorder, // NULL o id reale, mai 0
':idservice' => $idservice,
':bookingstart' => $bookingstart !== '' ? $bookingstart : null,
+ ':isgift' => $isGift,
]);
$pdo->commit();