193 lines
7.1 KiB
PHP
193 lines
7.1 KiB
PHP
<?php
|
|
// book_slots.php — Calcola gli slot disponibili per shop+staff+service+data (AJAX, JSON)
|
|
// Chiamato da book.php quando l'utente sceglie staff e data.
|
|
// Risposta: { ok:true, slots:["09:00","09:30",...] } oppure { ok:false, error:"..." }
|
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
header('Content-Type: application/json');
|
|
|
|
require_once(__DIR__ . '/userarea/class/db-functions.php');
|
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
|
|
|
// --- Input ---
|
|
$shop_id = (int)($_GET['shop'] ?? 0);
|
|
$service_id = (int)($_GET['service'] ?? 0);
|
|
$staff_id = (int)($_GET['staff'] ?? 0);
|
|
$date = trim($_GET['date'] ?? ''); // formato Y-m-d
|
|
|
|
if ($shop_id <= 0 || $service_id <= 0 || $staff_id <= 0 || $date === '') {
|
|
echo json_encode(['ok' => false, 'error' => 'missing_params']);
|
|
exit;
|
|
}
|
|
|
|
// Valida la data
|
|
$d = DateTime::createFromFormat('Y-m-d', $date);
|
|
if (!$d || $d->format('Y-m-d') !== $date) {
|
|
echo json_encode(['ok' => false, 'error' => 'invalid_date']);
|
|
exit;
|
|
}
|
|
|
|
try {
|
|
// --- 1. Servizio: durata (indispensabile per capire se lo slot "ci sta") ---
|
|
$svc = $pdo->prepare("SELECT duration_minutes FROM services WHERE id = ? AND shop_id = ? AND is_active = 1");
|
|
$svc->execute([$service_id, $shop_id]);
|
|
$service = $svc->fetch(PDO::FETCH_ASSOC);
|
|
if (!$service) {
|
|
echo json_encode(['ok' => false, 'error' => 'service_not_found']);
|
|
exit;
|
|
}
|
|
$duration = (int)$service['duration_minutes'];
|
|
if ($duration <= 0) $duration = 30; // fallback prudente
|
|
|
|
// --- 2. Impostazioni salone (con fallback ai default se manca la riga) ---
|
|
$st = $pdo->prepare("SELECT appointment_slot_interval, min_booking_notice_hours,
|
|
max_booking_days_ahead, allowed_start_minutes
|
|
FROM shop_settings WHERE shop_id = ?");
|
|
$st->execute([$shop_id]);
|
|
$set = $st->fetch(PDO::FETCH_ASSOC) ?: [];
|
|
|
|
$slotInterval = (int)($set['appointment_slot_interval'] ?? 30);
|
|
if ($slotInterval <= 0) $slotInterval = 30;
|
|
$minNoticeHours = (int)($set['min_booking_notice_hours'] ?? 2);
|
|
$maxDaysAhead = (int)($set['max_booking_days_ahead'] ?? 90);
|
|
|
|
// Minuti d'inizio consentiti (es. "00,30"). Se vuoto → nessun filtro extra.
|
|
$allowedMinutes = [];
|
|
if (!empty($set['allowed_start_minutes'])) {
|
|
foreach (explode(',', $set['allowed_start_minutes']) as $m) {
|
|
$m = trim($m);
|
|
if ($m !== '' && is_numeric($m)) $allowedMinutes[] = (int)$m;
|
|
}
|
|
}
|
|
|
|
// --- 3. Finestra temporale: la data richiesta è dentro i limiti? ---
|
|
$today = new DateTime('today');
|
|
$reqDay = new DateTime($date . ' 00:00:00');
|
|
$maxDay = (clone $today)->modify("+{$maxDaysAhead} days");
|
|
|
|
if ($reqDay < $today) {
|
|
echo json_encode(['ok' => true, 'slots' => []]); // data passata → nessuno slot
|
|
exit;
|
|
}
|
|
if ($reqDay > $maxDay) {
|
|
echo json_encode(['ok' => true, 'slots' => []]); // troppo in là nel tempo
|
|
exit;
|
|
}
|
|
|
|
// --- 4. Il salone è chiuso quel giorno? (chiusure straordinarie/ricorrenti) ---
|
|
// is_recurring=1 → confronto solo mese-giorno (ricorre ogni anno).
|
|
$off = $pdo->prepare("
|
|
SELECT is_recurring, `date` FROM shop_day_off
|
|
WHERE shop_id = ?
|
|
AND ( `date` = ? OR ( is_recurring = 1 AND DATE_FORMAT(`date`, '%m-%d') = ? ) )
|
|
LIMIT 1
|
|
");
|
|
$off->execute([$shop_id, $date, $d->format('m-d')]);
|
|
if ($off->fetch()) {
|
|
echo json_encode(['ok' => true, 'slots' => [], 'closed' => true]);
|
|
exit;
|
|
}
|
|
|
|
// --- 5. Orari di apertura per quel giorno della settimana ---
|
|
// day_of_week: usiamo 1=lunedì ... 7=domenica (ISO). Adegua se il salone usa 0=domenica.
|
|
$dow = (int)$d->format('N'); // 1..7
|
|
$hrs = $pdo->prepare("SELECT is_open, open_time, close_time, open_time_2, close_time_2
|
|
FROM shop_hours WHERE shop_id = ? AND day_of_week = ?");
|
|
$hrs->execute([$shop_id, $dow]);
|
|
$hours = $hrs->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$hours || (int)$hours['is_open'] !== 1) {
|
|
echo json_encode(['ok' => true, 'slots' => [], 'closed' => true]);
|
|
exit;
|
|
}
|
|
|
|
// Raccogliamo le fasce di apertura (mattina + eventuale pomeriggio)
|
|
$ranges = [];
|
|
if (!empty($hours['open_time']) && !empty($hours['close_time'])) {
|
|
$ranges[] = [$hours['open_time'], $hours['close_time']];
|
|
}
|
|
if (!empty($hours['open_time_2']) && !empty($hours['close_time_2'])) {
|
|
$ranges[] = [$hours['open_time_2'], $hours['close_time_2']];
|
|
}
|
|
if (empty($ranges)) {
|
|
echo json_encode(['ok' => true, 'slots' => [], 'closed' => true]);
|
|
exit;
|
|
}
|
|
|
|
// --- 6. Appuntamenti già presi per quello staff in quel giorno ---
|
|
// Solo pending/confirmed/completed occupano; cancelled/no_show liberano.
|
|
$ap = $pdo->prepare("
|
|
SELECT start_at, end_at FROM appointments
|
|
WHERE staff_id = ?
|
|
AND DATE(start_at) = ?
|
|
AND status IN ('pending','confirmed','completed')
|
|
");
|
|
$ap->execute([$staff_id, $date]);
|
|
$busy = [];
|
|
foreach ($ap->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$busy[] = [strtotime($row['start_at']), strtotime($row['end_at'])];
|
|
}
|
|
|
|
// --- 7. Soglia minima: non prima di adesso + preavviso minimo ---
|
|
$earliest = (new DateTime())->modify("+{$minNoticeHours} hours");
|
|
|
|
// --- 8. Generazione slot ---
|
|
$slots = [];
|
|
foreach ($ranges as [$openStr, $closeStr]) {
|
|
$open = new DateTime($date . ' ' . $openStr);
|
|
$close = new DateTime($date . ' ' . $closeStr);
|
|
|
|
$cursor = clone $open;
|
|
while (true) {
|
|
// Fine servizio se iniziasse a $cursor
|
|
$end = (clone $cursor)->modify("+{$duration} minutes");
|
|
|
|
// Lo slot deve stare INTERO dentro la fascia
|
|
if ($end > $close) break;
|
|
|
|
$startTs = $cursor->getTimestamp();
|
|
$endTs = $end->getTimestamp();
|
|
|
|
$valid = true;
|
|
|
|
// Filtro minuti d'inizio consentiti (es. solo :00 e :30)
|
|
if (!empty($allowedMinutes) && !in_array((int)$cursor->format('i'), $allowedMinutes, true)) {
|
|
$valid = false;
|
|
}
|
|
|
|
// Non nel passato / rispetta preavviso minimo
|
|
if ($valid && $cursor < $earliest) {
|
|
$valid = false;
|
|
}
|
|
|
|
// Non deve sovrapporsi a un appuntamento esistente
|
|
if ($valid) {
|
|
foreach ($busy as [$bStart, $bEnd]) {
|
|
if ($startTs < $bEnd && $endTs > $bStart) { // overlap classico
|
|
$valid = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($valid) {
|
|
$slots[] = $cursor->format('H:i');
|
|
}
|
|
|
|
$cursor->modify("+{$slotInterval} minutes");
|
|
}
|
|
}
|
|
|
|
// Ordina e deduplica (se le fasce si toccano)
|
|
$slots = array_values(array_unique($slots));
|
|
sort($slots);
|
|
|
|
echo json_encode(['ok' => true, 'slots' => $slots]);
|
|
} catch (Throwable $e) {
|
|
error_log('book_slots: ' . $e->getMessage());
|
|
echo json_encode(['ok' => false, 'error' => 'server_error']);
|
|
exit;
|
|
}
|