added modal inser lesson maually by teacher

This commit is contained in:
2026-08-22 09:19:31 +02:00
parent 7124b08c9b
commit d7937f1d61
5 changed files with 457 additions and 16 deletions
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
/**
* Aggiunge la colonna is_gift a bookingclass.
*
* is_gift = 'Y' indica una lezione OMAGGIO inserita dall'insegnante:
* - non consuma ticket dell'ordine (esclusa dai conteggi residui)
* - non è riprogrammabile (legata alla data della classe)
* - tipicamente ha idorder = NULL
*
* Default 'N' = prenotazione normale.
*/
final class AddIsGiftToBookingclass extends AbstractMigration
{
public function up(): void
{
$table = $this->table('bookingclass');
$table->addColumn('is_gift', 'char', [
'limit' => 1,
'null' => false,
'default' => 'N',
'after' => 'is_reprogrammed',
])->update();
}
public function down(): void
{
$table = $this->table('bookingclass');
$table->removeColumn('is_gift')->update();
}
}
+169 -1
View File
@@ -479,6 +479,17 @@ $italianMonths = [
<?php endif;
endif; ?>
<?php if (isset($_GET['error'])) :
$errMap = [
'duplicate' => 'Questo utente è già prenotato in questa classe.',
'insert' => 'Errore durante l\'inserimento. Riprova.',
'missingdata' => 'Dati mancanti per l\'inserimento.',
];
$em = $errMap[$_GET['error']] ?? 'Si è verificato un errore.';
?>
<div class="alert alert-danger" role="alert"><?php echo e($em); ?></div>
<?php endif; ?>
<div class="month-navigation">
<a href="?prev_month=<?php echo e(date('Y-m', strtotime('-1 month', strtotime($currentMonthStart)))); ?>"><i class="fas fa-chevron-left fa-2x"></i></a>
<h2><?php echo e($italianMonths[date("F", strtotime($currentMonthStart))] . ' ' . date("Y", strtotime($currentMonthStart))); ?></h2>
@@ -542,7 +553,7 @@ $italianMonths = [
$ini = initials($b['first_name'] ?? '', $b['last_name'] ?? '');
$avColor = avatarColor($fullName);
?>
<div class="booking-item <?php echo $isLost ? 'is-lost' : ''; ?>">
<div class="booking-item <?php echo $isLost ? 'is-lost' : ''; ?>" data-iduser="<?php echo (int) $b['iduser']; ?>">
<div class="avatar" style="background: <?php echo $isLost ? '#7f8c8d' : $avColor; ?>;"><?php echo e($ini); ?></div>
<div class="booking-info">
<span class="booking-name"><?php echo e($fullName); ?></span>
@@ -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 += '<option value="' + o.idorderbook + '">' +
o.service_name + ' (' + o.remaining + '/' + o.tickets + ' residui)' + exp +
'</option>';
});
const hasUsableOrders = usableOrders.length > 0;
const d = new Date();
d.setMonth(d.getMonth() + 3);
const defExpiry = d.toISOString().slice(0, 10);
const html =
'<div style="text-align:left;">' +
'<label style="display:block;margin:8px 0;">' +
'<input type="radio" name="bkmode" value="scala" ' + (hasUsableOrders ? 'checked' : 'disabled') + '> ' +
'<strong>Scala da un pacchetto</strong>' + (hasUsableOrders ? '' : ' (nessun pacchetto con residui)') +
'</label>' +
'<select id="bk-order" class="form-control" style="margin:6px 0 14px;" ' + (hasUsableOrders ? '' : 'disabled') + '>' +
orderOptions +
'</select>' +
'<label style="display:block;margin:8px 0;">' +
'<input type="radio" name="bkmode" value="omaggio" ' + (hasUsableOrders ? '' : 'checked') + '> ' +
'<strong>Aggiungi come omaggio</strong> <span style="color:#6b7280;font-size:12px;">(non consuma pacchetti, non spostabile)</span>' +
'</label>' +
'<label style="display:block;margin:8px 0;">' +
'<input type="radio" name="bkmode" value="nuovo"> ' +
'<strong>Crea nuovo ordine</strong> <span style="color:#6b7280;font-size:12px;">(1 lezione)</span>' +
'</label>' +
'<div id="bk-nuovo-fields" style="display:none;margin:6px 0 0;padding-left:22px;">' +
'<label style="font-size:13px;">Scadenza ordine:</label>' +
'<input type="date" id="bk-expiry" class="form-control" value="' + defExpiry + '">' +
'</div>' +
'</div>';
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();
$('<input>').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('');
$('<input>').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);
+11 -2
View File
@@ -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.
$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'],
+101
View File
@@ -0,0 +1,101 @@
<?php
// Bufferizziamo da subito così la risposta resta JSON puro
// (headscript.php può emettere HTML).
ob_start();
require_once('include/headscript.php');
/**
* get_user_orders.php
* --------------------------------------------------------------------------
* Restituisce gli ordini ATTIVI (non scaduti) di un utente, con il numero
* di ticket residui, per popolare il modale "Aggiungi partecipante"
* nell'adminpanel.
*
* Pattern: webapp (sessione + PDO), come searchemail.php.
* Metodo: GET o POST
* Param: userid (int)
*
* Residui = nticket - (prenotazioni non cancellate e NON omaggio)
* -> 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' => []]);
}
+139 -11
View File
@@ -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();