805 lines
32 KiB
PHP
805 lines
32 KiB
PHP
<?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>
|