comelifacciamo/public/userarea/appointments.php
2026-02-01 20:37:49 +01:00

805 lines
32 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
if (session_status() === PHP_SESSION_NONE) session_start();
include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
if (!isset($iduserlogin)) {
header("Location: login.php");
exit;
}
function setFlash(string $type, string $text): void
{
$_SESSION['flash'] = ['type' => $type, 'text' => $text];
}
function getFlash(): ?array
{
if (!isset($_SESSION['flash'])) return null;
$f = $_SESSION['flash'];
unset($_SESSION['flash']);
return $f;
}
function e(?string $v): string
{
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
}
function statusMeta(string $status): array
{
switch ($status) {
case 'confirmed':
return ['class' => 'status-confirmed', 'badge' => 'bg-success', 'label' => 'Confermato'];
case 'pending':
return ['class' => 'status-pending', 'badge' => 'bg-warning text-dark', 'label' => 'In attesa'];
case 'cancelled':
return ['class' => 'status-cancelled', 'badge' => 'bg-danger', 'label' => 'Annullato'];
case 'no_show':
return ['class' => 'status-no_show', 'badge' => 'bg-secondary', 'label' => 'No-show'];
case 'completed':
return ['class' => 'status-completed', 'badge' => 'bg-primary', 'label' => 'Completato'];
default:
return ['class' => 'status-generic', 'badge' => 'bg-dark', 'label' => ucfirst($status)];
}
}
$stmt = $pdo->prepare("SELECT COUNT(*) FROM shops WHERE owner_id = ?");
$stmt->execute([$iduserlogin]);
if ((int)$stmt->fetchColumn() === 0) {
header("Location: onboarding_salon.php");
exit;
}
$stmt = $pdo->prepare("
SELECT id, name
FROM shops
WHERE owner_id = ?
ORDER BY created_at ASC
LIMIT 1
");
$stmt->execute([$iduserlogin]);
$shop = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$shop) die("Errore: salone non trovato.");
$shop_id = (int)$shop['id'];
$shop_name = $shop['name'];
$selected_date = $_GET['date'] ?? date('Y-m-d');
$selected_staff = isset($_GET['staff_id']) ? (int)$_GET['staff_id'] : 0;
$stmt = $pdo->prepare("
SELECT id, first_name, last_name, color_hex
FROM staff
WHERE shop_id = ? AND is_active = 1 AND can_book_online = 1
ORDER BY first_name, last_name
");
$stmt->execute([$shop_id]);
$staff_list = $stmt->fetchAll(PDO::FETCH_ASSOC);
// RECUPERA I SERVIZI DEL SALONE
$stmt = $pdo->prepare("
SELECT id, name, duration_minutes, price, category, color_hex
FROM services
WHERE shop_id = ? AND is_active = 1
ORDER BY category ASC, `order` ASC, name ASC
");
$stmt->execute([$shop_id]);
$services_list = $stmt->fetchAll(PDO::FETCH_ASSOC);
$selected_staff_obj = null;
if ($selected_staff > 0) {
foreach ($staff_list as $st) {
if ((int)$st['id'] === $selected_staff) {
$selected_staff_obj = $st;
break;
}
}
}
$where_staff = $selected_staff > 0 ? "AND a.staff_id = ?" : "";
$params = [$shop_id, $selected_date];
if ($selected_staff > 0) $params[] = $selected_staff;
$stmt = $pdo->prepare("
SELECT a.id, a.start_at, a.end_at, a.status, a.notes,
CONCAT(c.first_name, ' ', c.last_name) AS customer_name,
c.phone AS customer_phone,
s.name AS service_name,
st.first_name AS staff_first, st.last_name AS staff_last, st.color_hex AS staff_color
FROM appointments a
LEFT JOIN customers c ON a.customer_id = c.id
LEFT JOIN services s ON a.service_id = s.id
LEFT JOIN staff st ON a.staff_id = st.id
WHERE a.shop_id = ? AND DATE(a.start_at) = ?
$where_staff
ORDER BY a.start_at ASC
");
$stmt->execute($params);
$appointments = $stmt->fetchAll(PDO::FETCH_ASSOC);
$flash = getFlash();
$start_hour = 8;
$end_hour = 21;
$interval = 30;
function ts(string $dateYmd, string $timeHi): int
{
return strtotime($dateYmd . ' ' . $timeHi . ':00');
}
$prev_date = date('Y-m-d', strtotime($selected_date . ' -1 day'));
$next_date = date('Y-m-d', strtotime($selected_date . ' +1 day'));
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<?php include('siteinfo.php'); ?>
<title>Appuntamenti - <?= e($shop_name) ?></title>
<style>
:root {
--card-radius: 14px;
--soft-border: #e9edf3;
--muted: #6b7280;
--bg: #f6f7fb;
}
body {
background: var(--bg);
}
.card-shell {
border-radius: var(--card-radius);
border: 1px solid var(--soft-border);
background: #fff;
overflow: hidden;
}
.card-shell .card-header {
background: linear-gradient(180deg, #ffffff, #fbfcff);
border-bottom: 1px solid var(--soft-border);
}
.page-title {
font-weight: 900;
letter-spacing: -0.02em;
}
.pill {
display: inline-flex;
align-items: center;
gap: .5rem;
padding: .40rem .70rem;
border-radius: 999px;
border: 1px solid var(--soft-border);
background: #fff;
font-weight: 700;
font-size: .88rem;
color: #111827;
white-space: nowrap;
}
.pill .dot {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
}
/* ======= HEADER CALENDARIO COMPATTO (UNA RIGA) ======= */
.datebar {
display: flex;
align-items: center;
gap: .5rem;
flex-wrap: nowrap;
}
.icon-btn {
width: 36px;
height: 36px;
border-radius: 10px;
border: 1px solid var(--soft-border);
background: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.icon-btn:hover {
filter: brightness(0.98);
}
.date-input {
width: 155px;
height: 36px;
border-radius: 10px;
}
.toolbar {
background: #fff;
border: 1px solid var(--soft-border);
border-radius: var(--card-radius);
padding: 12px 14px;
}
.timeline {
background: #fff;
border: 1px solid var(--soft-border);
border-radius: var(--card-radius);
overflow: hidden;
}
.timeline-row {
display: grid;
grid-template-columns: 92px 1fr;
border-top: 1px solid var(--soft-border);
min-height: 56px;
}
.timeline-row:first-child {
border-top: none;
}
.time-col {
padding: 11px 12px;
background: #fbfcff;
border-right: 1px solid var(--soft-border);
color: var(--muted);
font-weight: 900;
position: sticky;
left: 0;
z-index: 1;
}
.slot-col {
padding: 8px 10px;
display: flex;
align-items: center;
gap: 10px;
}
.slot-action {
width: 34px;
height: 34px;
border-radius: 10px;
border: 1px solid var(--soft-border);
background: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.slot-action button {
width: 100%;
height: 100%;
border: 0;
border-radius: 10px;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.slot-action button:hover {
background: #f3f4f6;
}
.empty-slot {
flex: 1;
border-radius: 12px;
border: 1px dashed #d7dde7;
background: linear-gradient(180deg, #ffffff, #fbfbff);
color: #9aa3b2;
padding: 10px 12px;
font-size: .9rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.appt {
flex: 1;
border-radius: 12px;
border: 1px solid var(--soft-border);
padding: 10px 12px;
box-shadow: 0 10px 28px rgba(17, 24, 39, 0.06);
transition: transform .15s ease, box-shadow .15s ease;
border-left: 6px solid #6366f1;
background: #fff;
}
.appt:hover {
transform: translateY(-2px);
box-shadow: 0 14px 34px rgba(17, 24, 39, 0.10);
}
.appt-title {
font-weight: 900;
margin: 0;
line-height: 1.1;
}
.appt-sub {
color: var(--muted);
font-size: .88rem;
}
.appt-meta {
display: flex;
gap: .5rem;
flex-wrap: wrap;
align-items: center;
margin-top: 8px;
}
.staff-chip {
display: inline-flex;
align-items: center;
gap: .5rem;
padding: .35rem .6rem;
border-radius: 999px;
border: 1px solid var(--soft-border);
font-weight: 900;
font-size: .80rem;
background: #fff;
white-space: nowrap;
}
.staff-chip .dot {
width: 10px;
height: 10px;
border-radius: 999px;
}
.status-confirmed {
background: #ecfdf5;
}
.status-pending {
background: #fffbeb;
}
.status-cancelled {
background: #fef2f2;
opacity: .9;
}
.status-no_show {
background: #f3f4f6;
}
.status-completed {
background: #eef2ff;
}
.status-generic {
background: #f8fafc;
}
.note {
margin-top: 8px;
color: #64748b;
font-size: .86rem;
display: flex;
gap: .5rem;
align-items: flex-start;
}
@media (max-width: 576px) {
.timeline-row {
grid-template-columns: 78px 1fr;
}
.time-col {
padding: 10px 10px;
}
.date-input {
width: 135px;
}
}
.slot-action button {
width: 36px;
height: 36px;
border-radius: 10px;
border: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: #2563eb;
/* blu */
color: #fff;
box-shadow: 0 10px 22px rgba(37, 99, 235, .28);
transition: all 0.2s ease;
}
.slot-action button i {
font-size: 18px;
}
.slot-action button:hover {
background: #1d4ed8;
/* blu più scuro ma ancora visibile */
box-shadow: 0 14px 28px rgba(37, 99, 235, .35);
transform: translateY(-2px);
}
.slot-action button:active {
transform: translateY(0);
background: #1e40af;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card-shell">
<div class="card-header p-3 p-md-4">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="page-title h5 mb-0">
Appuntamenti • <?= date('d/m/Y', strtotime($selected_date)) ?>
</div>
<?php if ($selected_staff_obj): ?>
<span class="pill">
<span class="dot" style="background: <?= e($selected_staff_obj['color_hex'] ?: '#6366f1') ?>;"></span>
<?= e($selected_staff_obj['first_name'] . ' ' . $selected_staff_obj['last_name']) ?>
</span>
<?php else: ?>
<span class="pill">
<span class="dot" style="background:#94a3b8;"></span>
Tutti i parrucchieri
</span>
<?php endif; ?>
</div>
<!-- ======= CALENDARIO COMPATTO SU UNA RIGA ======= -->
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="datebar">
<a class="icon-btn" href="?date=<?= e($prev_date) ?>&staff_id=<?= (int)$selected_staff ?>" title="Precedente">
<i class="bx bx-chevron-left"></i>
</a>
<div class="position-relative">
<i class="bx bx-calendar" style="position:absolute; left:10px; top:9px; color:#6b7280;"></i>
<input type="date"
class="form-control form-control-sm date-input ps-5"
value="<?= e($selected_date) ?>"
onchange="location.href='?date=' + this.value + '&staff_id=<?= (int)$selected_staff ?>'">
</div>
<a class="icon-btn" href="?date=<?= e($next_date) ?>&staff_id=<?= (int)$selected_staff ?>" title="Successivo">
<i class="bx bx-chevron-right"></i>
</a>
</div>
<a href="?date=<?= e(date('Y-m-d')) ?>&staff_id=<?= (int)$selected_staff ?>" class="btn btn-outline-primary btn-sm">
Oggi
</a>
<select class="form-select form-select-sm"
onchange="location.href='?date=<?= e($selected_date) ?>&staff_id=' + this.value">
<option value="0" <?= $selected_staff === 0 ? 'selected' : '' ?>>Tutti i parrucchieri</option>
<?php foreach ($staff_list as $st): ?>
<option value="<?= (int)$st['id'] ?>" <?= $selected_staff === (int)$st['id'] ? 'selected' : '' ?>>
<?= e($st['first_name'] . ' ' . $st['last_name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="card-body p-0">
<?php if ($flash): ?>
<div class="alert alert-<?= e($flash['type']) ?> alert-dismissible fade show m-3" role="alert">
<?= e($flash['text']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="p-3 p-md-4">
<div class="toolbar d-flex flex-wrap justify-content-between align-items-center gap-2">
<div class="text-muted small">
<i class="bx bx-info-circle me-1"></i>
Slot da <?= (int)$interval ?> min • <?= (int)$start_hour ?>:00<?= (int)$end_hour ?>:00
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<span class="badge bg-success">Confermato</span>
<span class="badge bg-warning text-dark">In attesa</span>
<span class="badge bg-danger">Annullato</span>
<span class="badge bg-secondary">No-show</span>
<span class="badge bg-primary">Completato</span>
</div>
</div>
<div class="timeline mt-3">
<?php
$idx = 0;
$count = count($appointments);
for ($h = $start_hour; $h <= $end_hour; $h++) {
for ($m = 0; $m < 60; $m += $interval) {
if ($h === $end_hour && $m > 0) break;
$time_str = sprintf("%02d:%02d", $h, $m);
$slot_ts = ts($selected_date, $time_str);
while ($idx < $count) {
$a_end_ts = strtotime($appointments[$idx]['end_at']);
if ($a_end_ts <= $slot_ts) $idx++;
else break;
}
$appt = null;
if ($idx < $count) {
$a_start_ts = strtotime($appointments[$idx]['start_at']);
$a_end_ts = strtotime($appointments[$idx]['end_at']);
if ($a_start_ts <= $slot_ts && $a_end_ts > $slot_ts) $appt = $appointments[$idx];
}
?>
<div class="timeline-row">
<div class="time-col"><?= e($time_str) ?></div>
<div class="slot-col">
<?php if (!$appt): ?>
<!-- Bottone sinistro per creare appuntamento sullo slot -->
<div class="slot-action" title="Prendi appuntamento">
<button type="button"
class="open-new-appt"
aria-label="Nuovo appuntamento"
data-date="<?= e($selected_date) ?>"
data-time="<?= e($time_str) ?>"
data-staff-id="<?= (int)$selected_staff ?>">
<i class="bx bx-plus"></i>
</button>
</div>
<div class="empty-slot">
<span><i class="bx bx-check-circle me-1"></i>Slot libero</span>
<span class="small"><?= e($time_str) ?></span>
</div>
<?php else: ?>
<?php
$meta = statusMeta((string)$appt['status']);
$staffColor = $appt['staff_color'] ?: '#6366f1';
$staffName = trim(($appt['staff_first'] ?? '') . ' ' . ($appt['staff_last'] ?? ''));
$custName = $appt['customer_name'] ?: 'Cliente';
$phone = $appt['customer_phone'] ?: '-';
$service = $appt['service_name'] ?: 'Servizio';
$startHi = date('H:i', strtotime($appt['start_at']));
$endHi = date('H:i', strtotime($appt['end_at']));
$notes = trim((string)($appt['notes'] ?? ''));
?>
<div class="appt <?= e($meta['class']) ?>" style="border-left-color: <?= e($staffColor) ?>;">
<div class="d-flex justify-content-between align-items-start gap-2">
<div class="pe-2">
<div class="appt-title"><?= e($service) ?></div>
<div class="appt-sub"><?= e($custName) ?> • <?= e($phone) ?></div>
</div>
<span class="staff-chip">
<span class="dot" style="background: <?= e($staffColor) ?>;"></span>
<?= e($staffName ?: 'Staff') ?>
</span>
</div>
<div class="appt-meta">
<span class="badge <?= e($meta['badge']) ?>"><?= e($meta['label']) ?></span>
<span class="text-muted small"><i class="bx bx-time-five me-1"></i><?= e($startHi) ?><?= e($endHi) ?></span>
<span class="text-muted small"><i class="bx bx-hash me-1"></i>ID <?= (int)$appt['id'] ?></span>
</div>
<?php if ($notes !== ''): ?>
<div class="note">
<i class="bx bx-note"></i>
<div><?= e(mb_strlen($notes) > 120 ? mb_substr($notes, 0, 120) . '…' : $notes) ?></div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php
}
}
?>
</div>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<!-- MODALE NUOVO APPUNTAMENTO -->
<div class="modal fade" id="newApptModal" tabindex="-1" aria-labelledby="newApptModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="newApptModalLabel">Nuovo Appuntamento</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" action="create_appointments.php">
<div class="modal-body">
<div class="mb-3">
<label for="appt_date" class="form-label">Data *</label>
<input type="date" class="form-control" id="appt_date" name="date" required>
</div>
<div class="mb-3">
<label for="start_time" class="form-label">Ora inizio *</label>
<input type="time" class="form-control" id="start_time" name="start_time" required>
</div>
<div class="mb-3">
<label for="staff_id" class="form-label">Parrucchiere *</label>
<select class="form-select" id="staff_id" name="staff_id" required>
<option value="">Seleziona...</option>
<?php foreach ($staff_list as $st): ?>
<option value="<?= (int)$st['id'] ?>">
<?= e($st['first_name'] . ' ' . $st['last_name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="service_id" class="form-label">Servizio *</label>
<select class="form-select" id="service_id" name="service_id" required>
<option value="">Seleziona servizio...</option>
<?php
$current_category = null;
foreach ($services_list as $srv):
if ($srv['category'] && $srv['category'] !== $current_category):
if ($current_category !== null) echo '</optgroup>';
echo '<optgroup label="' . e($srv['category']) . '">';
$current_category = $srv['category'];
endif;
?>
<option value="<?= (int)$srv['id'] ?>"
data-duration="<?= (int)$srv['duration_minutes'] ?>">
<?= e($srv['name']) ?>
(<?= (int)$srv['duration_minutes'] ?> min - €<?= number_format($srv['price'], 2) ?>)
</option>
<?php
endforeach;
if ($current_category !== null) echo '</optgroup>';
?>
</select>
</div>
<div class="mb-3">
<label for="customer_name" class="form-label">Nome cliente *</label>
<input type="text" class="form-control" id="customer_name" name="customer_name"
placeholder="Mario Rossi" required>
</div>
<div class="mb-3">
<label for="customer_phone" class="form-label">Telefono</label>
<input type="tel" class="form-control" id="customer_phone" name="customer_phone"
placeholder="+39 333 1234567">
</div>
<div class="mb-3">
<label for="notes" class="form-label">Note</label>
<textarea class="form-control" id="notes" name="notes" rows="2"
placeholder="Richieste speciali..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary">Crea Appuntamento</button>
</div>
</form>
</div>
</div>
</div>
<script>
function fillNewApptModal(dateStr, timeStr, staffId) {
const modal = document.getElementById('newApptModal');
if (!modal) return;
// Adatta ai tuoi campi reali:
const dateInput = modal.querySelector('input[name="date"], #date, input[name="appt_date"]');
const timeInput = modal.querySelector('input[name="start_time"], #start_time, input[name="time"]');
const staffSel = modal.querySelector('select[name="staff_id"], #staff_id');
const startAt = modal.querySelector('input[name="start_at"], #start_at');
if (dateInput) dateInput.value = dateStr;
if (timeInput) timeInput.value = timeStr;
if (startAt && dateStr && timeStr) startAt.value = dateStr + ' ' + timeStr + ':00';
if (staffSel && staffId && parseInt(staffId, 10) > 0) staffSel.value = staffId;
}
document.addEventListener('click', function(ev) {
const btn = ev.target.closest('.open-new-appt');
if (!btn) return;
// DEBUG: se non vedi questo log, il click non arriva
console.log('[open-new-appt] click', btn.dataset);
const d = btn.dataset.date || '';
const t = btn.dataset.time || '';
const s = btn.dataset.staffId || '0';
fillNewApptModal(d, t, s);
const modalEl = document.getElementById('newApptModal');
if (!modalEl) {
console.error('Modal #newApptModal NON trovato nella pagina.');
return;
}
// Apertura Bootstrap 5
if (window.bootstrap && bootstrap.Modal) {
bootstrap.Modal.getOrCreateInstance(modalEl).show();
} else {
console.error('Bootstrap Modal API non disponibile. Controlla bootstrap.bundle.min.js');
}
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const serviceSelect = document.getElementById('service_id');
const startTimeInput = document.getElementById('start_time');
if (serviceSelect && startTimeInput) {
serviceSelect.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
const duration = parseInt(selectedOption.dataset.duration || 0);
const startTime = startTimeInput.value;
if (duration && startTime) {
// Calcola ora di fine
const [hours, minutes] = startTime.split(':').map(Number);
const totalMinutes = hours * 60 + minutes + duration;
const endHours = Math.floor(totalMinutes / 60);
const endMinutes = totalMinutes % 60;
const endTime = String(endHours).padStart(2, '0') + ':' +
String(endMinutes).padStart(2, '0');
// Se hai un campo end_time nel form, popolalo
const endTimeInput = document.getElementById('end_time');
if (endTimeInput) {
endTimeInput.value = endTime;
}
}
});
}
});
</script>
</body>
</html>