Files

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;
}