528 lines
20 KiB
PHP
528 lines
20 KiB
PHP
<?php
|
|
// book.php — Prenotazione appuntamento (frontend pubblico)
|
|
// Flusso: scegli staff → scegli data → scegli slot → conferma (login se serve)
|
|
// Arriva da: book.php?shop=X&service=Y
|
|
|
|
ini_set('display_errors', 1);
|
|
ini_set('display_startup_errors', 1);
|
|
error_reporting(E_ALL);
|
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
|
|
require_once(__DIR__ . '/userarea/class/db-functions.php');
|
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
|
|
|
// Helper flash
|
|
if (!function_exists('getFlash')) {
|
|
function getFlash()
|
|
{
|
|
if (!empty($_SESSION['flash'])) {
|
|
$f = $_SESSION['flash'];
|
|
unset($_SESSION['flash']);
|
|
return $f;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// --- Parametri ---
|
|
$shop_id = (int)($_GET['shop'] ?? 0);
|
|
$service_id = (int)($_GET['service'] ?? 0);
|
|
|
|
// Preselezioni (es. ritorno dal login o da uno slot già scelto)
|
|
$pre_staff = (int)($_GET['staff'] ?? 0);
|
|
$pre_date = trim($_GET['date'] ?? '');
|
|
$pre_time = trim($_GET['time'] ?? '');
|
|
$need_login = !empty($_GET['need_login']);
|
|
|
|
if ($shop_id <= 0 || $service_id <= 0) {
|
|
header('Location: search.php');
|
|
exit;
|
|
}
|
|
|
|
// --- Salone ---
|
|
$s = $pdo->prepare("SELECT id, name, slug, address, city, province, cover_image FROM shops WHERE id = ? AND active = 1");
|
|
$s->execute([$shop_id]);
|
|
$shop = $s->fetch(PDO::FETCH_ASSOC);
|
|
if (!$shop) {
|
|
header('Location: search.php');
|
|
exit;
|
|
}
|
|
|
|
// --- Servizio ---
|
|
$sv = $pdo->prepare("SELECT id, name, description, duration_minutes, price FROM services WHERE id = ? AND shop_id = ? AND is_active = 1");
|
|
$sv->execute([$service_id, $shop_id]);
|
|
$service = $sv->fetch(PDO::FETCH_ASSOC);
|
|
if (!$service) {
|
|
header('Location: salon.php?slug=' . urlencode($shop['slug']));
|
|
exit;
|
|
}
|
|
|
|
// --- Staff prenotabili online ---
|
|
$stf = $pdo->prepare("SELECT id, first_name, last_name, nickname, role, avatar, color_hex
|
|
FROM staff
|
|
WHERE shop_id = ? AND is_active = 1 AND can_book_online = 1
|
|
ORDER BY first_name ASC");
|
|
$stf->execute([$shop_id]);
|
|
$staffList = $stf->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// --- Limiti calendario ---
|
|
$setStmt = $pdo->prepare("SELECT min_booking_notice_hours, max_booking_days_ahead FROM shop_settings WHERE shop_id = ?");
|
|
$setStmt->execute([$shop_id]);
|
|
$setRow = $setStmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
|
$maxDaysAhead = (int)($setRow['max_booking_days_ahead'] ?? 90);
|
|
|
|
$minDate = (new DateTime('today'))->format('Y-m-d');
|
|
$maxDate = (new DateTime('today'))->modify("+{$maxDaysAhead} days")->format('Y-m-d');
|
|
|
|
$isLogged = !empty($_SESSION['iduserlogin']);
|
|
$flash = getFlash();
|
|
|
|
// Durata leggibile
|
|
$dur = (int)$service['duration_minutes'];
|
|
$durLabel = $dur >= 60 ? floor($dur / 60) . 'h' . ($dur % 60 ? ' ' . ($dur % 60) . 'min' : '') : $dur . ' min';
|
|
?>
|
|
<!doctype html>
|
|
<html class="no-js" lang="it">
|
|
|
|
<head>
|
|
<?php include('tophead.php'); ?>
|
|
<?php include('cssinclude.php'); ?>
|
|
<style>
|
|
.bk-wrap {
|
|
max-width: 820px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.bk-step {
|
|
background: #fff;
|
|
border: 1px solid #eef2f7;
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 4px 20px rgba(15, 23, 42, .04);
|
|
}
|
|
|
|
.bk-step h4 {
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
margin: 0 0 16px;
|
|
color: #0f172a;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.bk-step h4 .bk-num {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 50%;
|
|
background: #e11d6b;
|
|
color: #fff;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.bk-staff-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
gap: 12px;
|
|
}
|
|
|
|
.bk-staff {
|
|
border: 2px solid #eef2f7;
|
|
border-radius: 12px;
|
|
padding: 14px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all .15s ease;
|
|
background: #fff;
|
|
}
|
|
|
|
.bk-staff:hover {
|
|
border-color: #f9a8c9;
|
|
}
|
|
|
|
.bk-staff.selected {
|
|
border-color: #e11d6b;
|
|
background: #fff5f9;
|
|
}
|
|
|
|
.bk-staff-av {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
margin: 0 auto 8px;
|
|
background: #f1f5f9;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 700;
|
|
color: #64748b;
|
|
font-size: 20px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.bk-staff-av img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.bk-staff-name {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.bk-staff-role {
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.bk-slots {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.bk-slot {
|
|
border: 2px solid #eef2f7;
|
|
border-radius: 10px;
|
|
padding: 10px 0;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
color: #0f172a;
|
|
transition: all .12s ease;
|
|
background: #fff;
|
|
}
|
|
|
|
.bk-slot:hover {
|
|
border-color: #f9a8c9;
|
|
}
|
|
|
|
.bk-slot.selected {
|
|
border-color: #e11d6b;
|
|
background: #e11d6b;
|
|
color: #fff;
|
|
}
|
|
|
|
.bk-muted {
|
|
color: #94a3b8;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.bk-summary {
|
|
position: sticky;
|
|
top: 20px;
|
|
}
|
|
|
|
.bk-summary-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 6px 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.bk-summary-row span:first-child {
|
|
color: #64748b;
|
|
}
|
|
|
|
.bk-summary-row span:last-child {
|
|
font-weight: 600;
|
|
color: #0f172a;
|
|
text-align: right;
|
|
}
|
|
|
|
.bk-confirm-btn {
|
|
width: 100%;
|
|
background: #e11d6b;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 12px;
|
|
padding: 14px;
|
|
font-weight: 700;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
transition: opacity .15s;
|
|
}
|
|
|
|
.bk-confirm-btn:disabled {
|
|
opacity: .4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.bk-date-input {
|
|
width: 100%;
|
|
padding: 12px;
|
|
border: 2px solid #eef2f7;
|
|
border-radius: 10px;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.bk-hidden {
|
|
display: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<?php include('preloader.php'); ?>
|
|
<?php include('offcanvas.php'); ?>
|
|
<header class="tp-header-height">
|
|
<?php include('topbar.php'); ?>
|
|
</header>
|
|
|
|
<main>
|
|
<div class="tp-tour-area pt-100 pb-100">
|
|
<div class="container">
|
|
<div class="bk-wrap">
|
|
|
|
<h2 class="mb-10" style="font-size:26px;font-weight:700;">Prenota da <?= htmlspecialchars($shop['name']) ?></h2>
|
|
<p class="bk-muted mb-30">
|
|
<?= htmlspecialchars($service['name']) ?> · <?= $durLabel ?>
|
|
<?php if ($service['price'] !== null): ?> · <?= number_format((float)$service['price'], 2, ',', '.') ?> €<?php endif; ?>
|
|
</p>
|
|
|
|
<?php if ($flash): ?>
|
|
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> mb-20" style="padding:12px 16px;border-radius:10px;">
|
|
<?= htmlspecialchars($flash['text']) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-8">
|
|
|
|
<!-- STEP 1: Staff -->
|
|
<div class="bk-step">
|
|
<h4><span class="bk-num">1</span> Scegli l'operatore</h4>
|
|
<?php if (empty($staffList)): ?>
|
|
<p class="bk-muted">Nessun operatore disponibile per la prenotazione online. Contatta direttamente il salone.</p>
|
|
<?php else: ?>
|
|
<div class="bk-staff-grid">
|
|
<?php foreach ($staffList as $st): ?>
|
|
<?php
|
|
$stName = htmlspecialchars(trim($st['first_name'] . ' ' . $st['last_name']));
|
|
$stNick = !empty($st['nickname']) ? htmlspecialchars($st['nickname']) : $stName;
|
|
$initial = strtoupper(mb_substr($st['first_name'], 0, 1));
|
|
$sel = ($pre_staff === (int)$st['id']) ? ' selected' : '';
|
|
?>
|
|
<div class="bk-staff<?= $sel ?>" data-staff="<?= (int)$st['id'] ?>" data-name="<?= $stNick ?>">
|
|
<div class="bk-staff-av">
|
|
<?php if (!empty($st['avatar'])): ?>
|
|
<img src="<?= htmlspecialchars($st['avatar']) ?>" alt="<?= $stNick ?>">
|
|
<?php else: ?>
|
|
<?= $initial ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="bk-staff-name"><?= $stNick ?></div>
|
|
<?php if (!empty($st['role'])): ?>
|
|
<div class="bk-staff-role"><?= htmlspecialchars($st['role']) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- STEP 2: Data -->
|
|
<div class="bk-step">
|
|
<h4><span class="bk-num">2</span> Scegli il giorno</h4>
|
|
<input type="date" id="bkDate" class="bk-date-input"
|
|
min="<?= $minDate ?>" max="<?= $maxDate ?>"
|
|
value="<?= htmlspecialchars($pre_date) ?>">
|
|
</div>
|
|
|
|
<!-- STEP 3: Slot -->
|
|
<div class="bk-step">
|
|
<h4><span class="bk-num">3</span> Scegli l'orario</h4>
|
|
<div id="bkSlots">
|
|
<p class="bk-muted">Seleziona prima operatore e giorno.</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Riepilogo + conferma -->
|
|
<div class="col-lg-4">
|
|
<div class="bk-step bk-summary">
|
|
<h4 style="margin-bottom:12px;">Riepilogo</h4>
|
|
<div class="bk-summary-row"><span>Salone</span><span><?= htmlspecialchars($shop['name']) ?></span></div>
|
|
<div class="bk-summary-row"><span>Servizio</span><span><?= htmlspecialchars($service['name']) ?></span></div>
|
|
<div class="bk-summary-row"><span>Durata</span><span><?= $durLabel ?></span></div>
|
|
<div class="bk-summary-row"><span>Operatore</span><span id="sumStaff">—</span></div>
|
|
<div class="bk-summary-row"><span>Giorno</span><span id="sumDate">—</span></div>
|
|
<div class="bk-summary-row"><span>Orario</span><span id="sumTime">—</span></div>
|
|
<hr style="margin:14px 0;border-color:#eef2f7;">
|
|
|
|
<form id="bkForm" method="post" action="book_save.php">
|
|
<input type="hidden" name="shop_id" value="<?= $shop_id ?>">
|
|
<input type="hidden" name="service_id" value="<?= $service_id ?>">
|
|
<input type="hidden" name="staff_id" id="fStaff" value="<?= $pre_staff ?: '' ?>">
|
|
<input type="hidden" name="date" id="fDate" value="<?= htmlspecialchars($pre_date) ?>">
|
|
<input type="hidden" name="time" id="fTime" value="<?= htmlspecialchars($pre_time) ?>">
|
|
<textarea name="notes" placeholder="Note per il salone (opzionale)"
|
|
style="width:100%;border:2px solid #eef2f7;border-radius:10px;padding:10px;font-size:14px;margin-bottom:12px;min-height:70px;"></textarea>
|
|
<button type="submit" id="bkConfirm" class="bk-confirm-btn" disabled>
|
|
<?= $isLogged ? 'Conferma prenotazione' : 'Accedi e prenota' ?>
|
|
</button>
|
|
</form>
|
|
<p class="bk-muted mt-10" style="font-size:12px;text-align:center;">
|
|
La prenotazione sarà confermata dal salone.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<footer><?php include('footer.php'); ?></footer>
|
|
<?php include('jsinclude.php'); ?>
|
|
|
|
<script>
|
|
(function() {
|
|
var shopId = <?= $shop_id ?>;
|
|
var serviceId = <?= $service_id ?>;
|
|
var isLogged = <?= $isLogged ? 'true' : 'false' ?>;
|
|
|
|
var state = {
|
|
staff: <?= $pre_staff ?: 'null' ?>,
|
|
staffName: null,
|
|
date: <?= $pre_date ? json_encode($pre_date) : 'null' ?>,
|
|
time: <?= $pre_time ? json_encode($pre_time) : 'null' ?>
|
|
};
|
|
|
|
var slotsBox = document.getElementById('bkSlots');
|
|
var dateInp = document.getElementById('bkDate');
|
|
var confirmBtn = document.getElementById('bkConfirm');
|
|
|
|
// --- Selezione staff ---
|
|
document.querySelectorAll('.bk-staff').forEach(function(el) {
|
|
el.addEventListener('click', function() {
|
|
document.querySelectorAll('.bk-staff').forEach(function(x) {
|
|
x.classList.remove('selected');
|
|
});
|
|
el.classList.add('selected');
|
|
state.staff = parseInt(el.getAttribute('data-staff'), 10);
|
|
state.staffName = el.getAttribute('data-name');
|
|
state.time = null; // reset orario: cambia lo staff, cambiano gli slot
|
|
document.getElementById('fStaff').value = state.staff;
|
|
document.getElementById('sumStaff').textContent = state.staffName;
|
|
loadSlots();
|
|
refreshConfirm();
|
|
});
|
|
});
|
|
// Ripristina nome staff preselezionato
|
|
var preSel = document.querySelector('.bk-staff.selected');
|
|
if (preSel) {
|
|
state.staffName = preSel.getAttribute('data-name');
|
|
document.getElementById('sumStaff').textContent = state.staffName;
|
|
}
|
|
|
|
// --- Selezione data ---
|
|
dateInp.addEventListener('change', function() {
|
|
state.date = dateInp.value;
|
|
state.time = null;
|
|
document.getElementById('fDate').value = state.date;
|
|
document.getElementById('sumDate').textContent = formatDate(state.date);
|
|
loadSlots();
|
|
refreshConfirm();
|
|
});
|
|
if (state.date) {
|
|
document.getElementById('sumDate').textContent = formatDate(state.date);
|
|
}
|
|
if (state.time) {
|
|
document.getElementById('sumTime').textContent = state.time;
|
|
}
|
|
|
|
// --- Carica slot via AJAX ---
|
|
function loadSlots() {
|
|
if (!state.staff || !state.date) {
|
|
slotsBox.innerHTML = '<p class="bk-muted">Seleziona prima operatore e giorno.</p>';
|
|
return;
|
|
}
|
|
slotsBox.innerHTML = '<p class="bk-muted">Caricamento orari…</p>';
|
|
|
|
var url = 'book_slots.php?shop=' + shopId + '&service=' + serviceId +
|
|
'&staff=' + state.staff + '&date=' + encodeURIComponent(state.date);
|
|
|
|
fetch(url)
|
|
.then(function(r) {
|
|
return r.json();
|
|
})
|
|
.then(function(data) {
|
|
if (!data.ok) {
|
|
slotsBox.innerHTML = '<p class="bk-muted">Errore nel caricamento degli orari.</p>';
|
|
return;
|
|
}
|
|
if (data.closed) {
|
|
slotsBox.innerHTML = '<p class="bk-muted">Il salone è chiuso in questa data.</p>';
|
|
return;
|
|
}
|
|
if (!data.slots || data.slots.length === 0) {
|
|
slotsBox.innerHTML = '<p class="bk-muted">Nessun orario disponibile in questa data. Prova un altro giorno.</p>';
|
|
return;
|
|
}
|
|
var html = '<div class="bk-slots">';
|
|
data.slots.forEach(function(t) {
|
|
var sel = (t === state.time) ? ' selected' : '';
|
|
html += '<div class="bk-slot' + sel + '" data-time="' + t + '">' + t + '</div>';
|
|
});
|
|
html += '</div>';
|
|
slotsBox.innerHTML = html;
|
|
|
|
slotsBox.querySelectorAll('.bk-slot').forEach(function(sl) {
|
|
sl.addEventListener('click', function() {
|
|
slotsBox.querySelectorAll('.bk-slot').forEach(function(x) {
|
|
x.classList.remove('selected');
|
|
});
|
|
sl.classList.add('selected');
|
|
state.time = sl.getAttribute('data-time');
|
|
document.getElementById('fTime').value = state.time;
|
|
document.getElementById('sumTime').textContent = state.time;
|
|
refreshConfirm();
|
|
});
|
|
});
|
|
})
|
|
.catch(function() {
|
|
slotsBox.innerHTML = '<p class="bk-muted">Errore di connessione.</p>';
|
|
});
|
|
}
|
|
|
|
// --- Abilita il bottone solo quando tutto è scelto ---
|
|
function refreshConfirm() {
|
|
confirmBtn.disabled = !(state.staff && state.date && state.time);
|
|
}
|
|
|
|
function formatDate(iso) {
|
|
if (!iso) return '—';
|
|
var p = iso.split('-');
|
|
return p[2] + '/' + p[1] + '/' + p[0];
|
|
}
|
|
|
|
// Se preselezione completa (ritorno dal login), carica gli slot
|
|
if (state.staff && state.date) {
|
|
loadSlots();
|
|
}
|
|
refreshConfirm();
|
|
|
|
<?php if ($need_login && !$isLogged): ?>
|
|
// Utente tornato senza login: apri la modale di invito
|
|
if (window.openFavLoginModal) {
|
|
openFavLoginModal();
|
|
}
|
|
<?php endif; ?>
|
|
})();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|