866 lines
38 KiB
PHP
866 lines
38 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');
|
||
}
|
||
// Converte un colore #RRGGBB in rgba() con opacità, per usarlo come sfondo tenue.
|
||
// Se l'hex non è valido, torna a un lilla neutro di default.
|
||
function hexToRgba(?string $hex, float $alpha): string
|
||
{
|
||
$hex = ltrim((string)$hex, '#');
|
||
if (strlen($hex) !== 6 || !ctype_xdigit($hex)) $hex = '6366f1';
|
||
$r = hexdec(substr($hex, 0, 2));
|
||
$g = hexdec(substr($hex, 2, 2));
|
||
$b = hexdec(substr($hex, 4, 2));
|
||
return "rgba($r, $g, $b, $alpha)";
|
||
}
|
||
function statusMeta(string $status): array
|
||
{
|
||
switch ($status) {
|
||
case 'confirmed':
|
||
return ['badge' => 'bg-success', 'label' => 'Confermato'];
|
||
case 'pending':
|
||
return ['badge' => 'bg-warning text-dark', 'label' => 'In attesa'];
|
||
case 'cancelled':
|
||
return ['badge' => 'bg-danger', 'label' => 'Annullato'];
|
||
case 'no_show':
|
||
return ['badge' => 'bg-secondary', 'label' => 'No-show'];
|
||
case 'completed':
|
||
return ['badge' => 'bg-primary', 'label' => 'Completato'];
|
||
default:
|
||
return ['badge' => 'bg-dark', 'label' => ucfirst($status)];
|
||
}
|
||
}
|
||
|
||
// --- Salone dell'owner ---
|
||
$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;
|
||
|
||
// --- Staff prenotabili ---
|
||
$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);
|
||
|
||
// --- Servizi ---
|
||
$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);
|
||
|
||
// Staff selezionato (per il fallback vista singola)
|
||
$selected_staff_obj = null;
|
||
if ($selected_staff > 0) {
|
||
foreach ($staff_list as $st) {
|
||
if ((int)$st['id'] === $selected_staff) {
|
||
$selected_staff_obj = $st;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Appuntamenti del giorno ---
|
||
$params = [$shop_id, $selected_date];
|
||
$where_staff = '';
|
||
if ($selected_staff > 0) {
|
||
$where_staff = "AND a.staff_id = ?";
|
||
$params[] = $selected_staff;
|
||
}
|
||
|
||
$stmt = $pdo->prepare("
|
||
SELECT a.id, a.staff_id, a.service_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, s.duration_minutes,
|
||
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);
|
||
|
||
// Raggruppa gli appuntamenti per staff_id (per la vista a colonne)
|
||
$apptByStaff = [];
|
||
foreach ($appointments as $a) {
|
||
$apptByStaff[(int)$a['staff_id']][] = $a;
|
||
}
|
||
|
||
$flash = getFlash();
|
||
|
||
// --- Parametri griglia oraria ---
|
||
$start_hour = 8;
|
||
$end_hour = 21;
|
||
$px_per_min = 1.4; // altezza in px per minuto → blocco alto in proporzione alla durata
|
||
$hour_px = 60 * $px_per_min;
|
||
|
||
// Colonne da mostrare: se filtro un solo staff, solo quello; altrimenti tutti
|
||
$columns = $selected_staff > 0 && $selected_staff_obj ? [$selected_staff_obj] : $staff_list;
|
||
|
||
$prev_date = date('Y-m-d', strtotime($selected_date . ' -1 day'));
|
||
$next_date = date('Y-m-d', strtotime($selected_date . ' +1 day'));
|
||
|
||
// Posizione verticale (px) di un orario dentro la griglia
|
||
function topPx(string $datetime, int $start_hour, float $px_per_min): float
|
||
{
|
||
$t = strtotime($datetime);
|
||
$mins = ((int)date('H', $t) - $start_hour) * 60 + (int)date('i', $t);
|
||
return max(0, $mins * $px_per_min);
|
||
}
|
||
?>
|
||
<!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, #fff, #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;
|
||
}
|
||
|
||
.pill .dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 999px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.date-input {
|
||
width: 155px;
|
||
height: 36px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
/* ===== AGENDA A COLONNE ===== */
|
||
.agenda-scroll {
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
}
|
||
|
||
.agenda-grid {
|
||
display: grid;
|
||
min-width: 640px;
|
||
}
|
||
|
||
.agenda-head {
|
||
display: contents;
|
||
}
|
||
|
||
.agenda-col-head {
|
||
padding: 10px 8px;
|
||
border-bottom: 1px solid var(--soft-border);
|
||
border-right: 1px solid var(--soft-border);
|
||
background: #fbfcff;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 800;
|
||
font-size: .9rem;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 3;
|
||
}
|
||
|
||
.agenda-col-head .dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 999px;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.agenda-time-head {
|
||
background: #fbfcff;
|
||
border-bottom: 1px solid var(--soft-border);
|
||
border-right: 1px solid var(--soft-border);
|
||
position: sticky;
|
||
top: 0;
|
||
left: 0;
|
||
z-index: 4;
|
||
}
|
||
|
||
.agenda-body {
|
||
display: contents;
|
||
}
|
||
|
||
.time-gutter {
|
||
border-right: 1px solid var(--soft-border);
|
||
background: #fbfcff;
|
||
position: sticky;
|
||
left: 0;
|
||
z-index: 2;
|
||
}
|
||
|
||
.time-label {
|
||
font-size: 11px;
|
||
color: var(--muted);
|
||
font-weight: 800;
|
||
text-align: right;
|
||
padding: 2px 6px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.staff-lane {
|
||
position: relative;
|
||
border-right: 1px solid var(--soft-border);
|
||
}
|
||
|
||
.lane-lines {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.hour-line {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
border-top: 1px solid var(--soft-border);
|
||
}
|
||
|
||
.half-line {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
border-top: 1px dashed #eef1f6;
|
||
}
|
||
|
||
/* Slot cliccabili (celle vuote) per creare appuntamento */
|
||
.new-slot {
|
||
position: absolute;
|
||
left: 2px;
|
||
right: 2px;
|
||
cursor: pointer;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.new-slot:hover {
|
||
background: rgba(37, 99, 235, .06);
|
||
outline: 1px dashed #bcd0f5;
|
||
}
|
||
|
||
.appt-block {
|
||
position: absolute;
|
||
left: 3px;
|
||
right: 3px;
|
||
border-radius: 10px;
|
||
padding: 6px 8px;
|
||
box-sizing: border-box;
|
||
border-left: 6px solid #6366f1;
|
||
border: 1px solid var(--soft-border);
|
||
box-shadow: 0 6px 18px rgba(17, 24, 39, .06);
|
||
cursor: pointer;
|
||
overflow: hidden;
|
||
transition: transform .12s ease, box-shadow .12s ease;
|
||
}
|
||
|
||
.appt-block:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 10px 24px rgba(17, 24, 39, .12);
|
||
z-index: 5;
|
||
}
|
||
|
||
.appt-block .ttl {
|
||
font-weight: 800;
|
||
font-size: .82rem;
|
||
line-height: 1.1;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.appt-block .sub {
|
||
color: var(--muted);
|
||
font-size: .75rem;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.appt-block.status-cancelled {
|
||
opacity: .5;
|
||
}
|
||
|
||
.appt-block.status-cancelled .ttl {
|
||
text-decoration: line-through;
|
||
}
|
||
|
||
.appt-block.status-no_show {
|
||
opacity: .75;
|
||
}
|
||
|
||
.appt-block.status-completed {
|
||
filter: grayscale(.25);
|
||
}
|
||
|
||
/* Nastrino diagonale d'angolo per lo stato, sempre visibile */
|
||
.appt-block {
|
||
position: absolute;
|
||
}
|
||
|
||
.status-dot {
|
||
display: inline-block;
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 999px;
|
||
margin-right: 4px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.st-pending {
|
||
background: #f59e0b;
|
||
}
|
||
|
||
.st-confirmed {
|
||
background: #16a34a;
|
||
}
|
||
|
||
.st-completed {
|
||
background: #2563eb;
|
||
}
|
||
|
||
.st-no_show {
|
||
background: #6b7280;
|
||
}
|
||
|
||
.st-cancelled {
|
||
background: #dc2626;
|
||
}
|
||
|
||
.badge-mini {
|
||
display: inline-block;
|
||
font-size: .66rem;
|
||
padding: .05rem .4rem;
|
||
border-radius: 999px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
@media (max-width:576px) {
|
||
.date-input {
|
||
width: 135px;
|
||
}
|
||
}
|
||
|
||
.slot-action-legacy button {
|
||
background: #2563eb;
|
||
color: #fff;
|
||
}
|
||
</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>
|
||
|
||
<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="d-flex flex-wrap gap-2 align-items-center mb-3">
|
||
<span class="text-muted small me-2"><i class="bx bx-info-circle me-1"></i>Clicca uno spazio libero per creare, un appuntamento per gestirlo</span>
|
||
<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>
|
||
|
||
<?php if (empty($columns)): ?>
|
||
<div class="text-center py-5 text-muted">Nessun parrucchiere disponibile. Aggiungine uno per gestire l'agenda.</div>
|
||
<?php else: ?>
|
||
<?php
|
||
$nCols = count($columns);
|
||
$totalMins = ($end_hour - $start_hour) * 60;
|
||
$laneHeight = $totalMins * $px_per_min;
|
||
?>
|
||
<div class="agenda-scroll">
|
||
<div class="agenda-grid" style="grid-template-columns:56px repeat(<?= $nCols ?>, minmax(180px, 1fr));">
|
||
|
||
<!-- Intestazioni -->
|
||
<div class="agenda-time-head"></div>
|
||
<?php foreach ($columns as $col): ?>
|
||
<div class="agenda-col-head">
|
||
<span class="dot" style="background: <?= e($col['color_hex'] ?: '#6366f1') ?>;"></span>
|
||
<?= e($col['first_name'] . ' ' . $col['last_name']) ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
|
||
<!-- Colonna orari -->
|
||
<div class="time-gutter" style="height: <?= $laneHeight ?>px; position:relative;">
|
||
<?php for ($h = $start_hour; $h <= $end_hour; $h++): ?>
|
||
<div class="time-label" style="position:absolute; top: <?= ($h - $start_hour) * $hour_px ?>px;">
|
||
<?= sprintf('%02d:00', $h) ?>
|
||
</div>
|
||
<?php endfor; ?>
|
||
</div>
|
||
|
||
<!-- Una lane per parrucchiere -->
|
||
<?php foreach ($columns as $col): ?>
|
||
<?php $cid = (int)$col['id'];
|
||
$laneAppts = $apptByStaff[$cid] ?? []; ?>
|
||
<div class="staff-lane" style="height: <?= $laneHeight ?>px;">
|
||
|
||
<!-- Linee guida orarie -->
|
||
<div class="lane-lines">
|
||
<?php for ($h = $start_hour; $h <= $end_hour; $h++): ?>
|
||
<div class="hour-line" style="top: <?= ($h - $start_hour) * $hour_px ?>px;"></div>
|
||
<?php if ($h < $end_hour): ?>
|
||
<div class="half-line" style="top: <?= ($h - $start_hour) * $hour_px + $hour_px / 2 ?>px;"></div>
|
||
<?php endif; ?>
|
||
<?php endfor; ?>
|
||
</div>
|
||
|
||
<!-- Slot vuoti cliccabili (ogni 30 min) -->
|
||
<?php
|
||
for ($h = $start_hour; $h < $end_hour; $h++):
|
||
foreach ([0, 30] as $mm):
|
||
$slotTop = (($h - $start_hour) * 60 + $mm) * $px_per_min;
|
||
$slotTime = sprintf('%02d:%02d', $h, $mm);
|
||
?>
|
||
<div class="new-slot open-new-appt"
|
||
style="top: <?= $slotTop ?>px; height: <?= 30 * $px_per_min ?>px;"
|
||
data-date="<?= e($selected_date) ?>"
|
||
data-time="<?= e($slotTime) ?>"
|
||
data-staff-id="<?= $cid ?>"></div>
|
||
<?php endforeach;
|
||
endfor; ?>
|
||
|
||
<!-- Blocchi appuntamento -->
|
||
<?php foreach ($laneAppts as $a): ?>
|
||
<?php
|
||
$top = topPx($a['start_at'], $start_hour, $px_per_min);
|
||
$durMin = max(15, (strtotime($a['end_at']) - strtotime($a['start_at'])) / 60);
|
||
$blockH = $durMin * $px_per_min;
|
||
$meta = statusMeta((string)$a['status']);
|
||
$color = $a['staff_color'] ?: '#6366f1';
|
||
$startHi = date('H:i', strtotime($a['start_at']));
|
||
$endHi = date('H:i', strtotime($a['end_at']));
|
||
|
||
// Payload per la modale dettaglio (data-* JSON-safe)
|
||
$payload = e(json_encode([
|
||
'id' => (int)$a['id'],
|
||
'service_id' => (int)$a['service_id'],
|
||
'staff_id' => (int)$a['staff_id'],
|
||
'service' => $a['service_name'] ?: 'Servizio',
|
||
'customer' => $a['customer_name'] ?: 'Cliente',
|
||
'phone' => $a['customer_phone'] ?: '',
|
||
'status' => $a['status'],
|
||
'date' => date('Y-m-d', strtotime($a['start_at'])),
|
||
'time' => $startHi,
|
||
'range' => $startHi . '–' . $endHi,
|
||
'notes' => $a['notes'] ?: '',
|
||
], JSON_UNESCAPED_UNICODE));
|
||
?>
|
||
<div class="appt-block status-<?= e($a['status']) ?> open-appt-detail"
|
||
style="top: <?= $top ?>px; height: <?= $blockH ?>px; border-left-color: <?= e($color) ?>; background: <?= hexToRgba($color, 0.14) ?>; border-color: <?= hexToRgba($color, 0.35) ?>;"
|
||
data-appt='<?= $payload ?>'>
|
||
<div class="ttl">
|
||
<span class="status-dot st-<?= e($a['status']) ?>"></span><?= e($a['service_name'] ?: 'Servizio') ?>
|
||
</div>
|
||
<div class="sub"><?= e($startHi) ?>–<?= e($endHi) ?> · <?= e($a['customer_name'] ?: 'Cliente') ?></div>
|
||
<span class="badge <?= e($meta['badge']) ?> badge-mini"><?= e($meta['label']) ?></span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<?php include('include/footer.php'); ?>
|
||
</div>
|
||
|
||
<?php include('jsinclude.php'); ?>
|
||
|
||
<!-- ===== MODALE NUOVO APPUNTAMENTO (invariata, POST a create_appointments.php) ===== -->
|
||
<div class="modal fade" id="newApptModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">Nuovo Appuntamento</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<form method="POST" action="create_appointments.php">
|
||
<div class="modal-body">
|
||
<div class="mb-3">
|
||
<label class="form-label">Data *</label>
|
||
<input type="date" class="form-control" name="date" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label 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 class="form-label">Parrucchiere *</label>
|
||
<select class="form-select" 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 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 class="form-label">Nome cliente *</label>
|
||
<input type="text" class="form-control" name="customer_name" placeholder="Mario Rossi" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Telefono</label>
|
||
<input type="tel" class="form-control" name="customer_phone" placeholder="+39 333 1234567">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Note</label>
|
||
<textarea class="form-control" name="notes" rows="2"></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>
|
||
|
||
<!-- ===== MODALE DETTAGLIO / GESTIONE APPUNTAMENTO ===== -->
|
||
<div class="modal fade" id="apptDetailModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="adTitle">Appuntamento</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<!-- Riepilogo -->
|
||
<div class="mb-3">
|
||
<div class="d-flex justify-content-between"><span class="text-muted">Cliente</span><span id="adCustomer" class="fw-bold"></span></div>
|
||
<div class="d-flex justify-content-between"><span class="text-muted">Telefono</span><span id="adPhone"></span></div>
|
||
<div class="d-flex justify-content-between"><span class="text-muted">Orario</span><span id="adRange"></span></div>
|
||
<div class="d-flex justify-content-between align-items-center"><span class="text-muted">Stato</span><span id="adStatus"></span></div>
|
||
<div id="adNotesWrap" class="mt-2 d-none"><span class="text-muted">Note:</span> <span id="adNotes"></span></div>
|
||
</div>
|
||
|
||
<!-- Cambio stato -->
|
||
<label class="form-label fw-bold small text-uppercase text-muted">Cambia stato</label>
|
||
<form method="POST" action="appointment_action.php" class="d-flex flex-wrap gap-2 mb-3">
|
||
<input type="hidden" name="action" value="set_status">
|
||
<input type="hidden" name="appointment_id" id="adId_status">
|
||
<input type="hidden" name="date" id="adDate_status">
|
||
<input type="hidden" name="redirect_staff" value="<?= (int)$selected_staff ?>">
|
||
<button name="status" value="confirmed" class="btn btn-sm btn-success">Conferma</button>
|
||
<button name="status" value="completed" class="btn btn-sm btn-primary">Completato</button>
|
||
<button name="status" value="no_show" class="btn btn-sm btn-secondary">No-show</button>
|
||
<button name="status" value="cancelled" class="btn btn-sm btn-danger">Annulla</button>
|
||
</form>
|
||
|
||
<!-- Modifica -->
|
||
<details class="mb-2">
|
||
<summary class="form-label fw-bold small text-uppercase text-muted" style="cursor:pointer;">Modifica orario / servizio / staff</summary>
|
||
<form method="POST" action="appointment_action.php" class="mt-2">
|
||
<input type="hidden" name="action" value="edit">
|
||
<input type="hidden" name="appointment_id" id="adId_edit">
|
||
<input type="hidden" name="date" id="adDate_edit">
|
||
<input type="hidden" name="redirect_staff" value="<?= (int)$selected_staff ?>">
|
||
<div class="row g-2">
|
||
<div class="col-6"><input type="date" class="form-control form-control-sm" name="edit_date" id="adEditDate" required></div>
|
||
<div class="col-6"><input type="time" class="form-control form-control-sm" name="edit_time" id="adEditTime" required></div>
|
||
</div>
|
||
<div class="mt-2">
|
||
<select class="form-select form-select-sm" name="edit_staff_id" id="adEditStaff" required>
|
||
<?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="mt-2">
|
||
<select class="form-select form-select-sm" name="edit_service_id" id="adEditService" required>
|
||
<?php foreach ($services_list as $srv): ?>
|
||
<option value="<?= (int)$srv['id'] ?>"><?= e($srv['name']) ?> (<?= (int)$srv['duration_minutes'] ?> min)</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<button class="btn btn-sm btn-outline-primary mt-2 w-100">Salva modifiche</button>
|
||
</form>
|
||
</details>
|
||
</div>
|
||
<div class="modal-footer justify-content-between">
|
||
<form method="POST" action="appointment_action.php" onsubmit="return confirm('Eliminare definitivamente questo appuntamento?');">
|
||
<input type="hidden" name="action" value="delete">
|
||
<input type="hidden" name="appointment_id" id="adId_delete">
|
||
<input type="hidden" name="date" id="adDate_delete">
|
||
<input type="hidden" name="redirect_staff" value="<?= (int)$selected_staff ?>">
|
||
<button class="btn btn-sm btn-outline-danger"><i class="bx bx-trash"></i> Elimina</button>
|
||
</form>
|
||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Chiudi</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// --- Nuovo appuntamento: click su slot vuoto ---
|
||
document.addEventListener('click', function(ev) {
|
||
var btn = ev.target.closest('.open-new-appt');
|
||
if (!btn) return;
|
||
var modalEl = document.getElementById('newApptModal');
|
||
if (!modalEl) return;
|
||
modalEl.querySelector('[name="date"]').value = btn.dataset.date || '';
|
||
modalEl.querySelector('[name="start_time"]').value = btn.dataset.time || '';
|
||
var staffSel = modalEl.querySelector('[name="staff_id"]');
|
||
if (staffSel && btn.dataset.staffId && parseInt(btn.dataset.staffId, 10) > 0) {
|
||
staffSel.value = btn.dataset.staffId;
|
||
}
|
||
if (window.bootstrap && bootstrap.Modal) bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
||
});
|
||
|
||
// --- Dettaglio appuntamento: click su blocco ---
|
||
document.addEventListener('click', function(ev) {
|
||
var block = ev.target.closest('.open-appt-detail');
|
||
if (!block) return;
|
||
var d;
|
||
try {
|
||
d = JSON.parse(block.dataset.appt);
|
||
} catch (e) {
|
||
return;
|
||
}
|
||
|
||
document.getElementById('adCustomer').textContent = d.customer;
|
||
document.getElementById('adPhone').textContent = d.phone || '—';
|
||
document.getElementById('adRange').textContent = d.range;
|
||
document.getElementById('adTitle').textContent = d.service;
|
||
|
||
var statusLabels = {
|
||
pending: 'In attesa',
|
||
confirmed: 'Confermato',
|
||
cancelled: 'Annullato',
|
||
no_show: 'No-show',
|
||
completed: 'Completato'
|
||
};
|
||
document.getElementById('adStatus').textContent = statusLabels[d.status] || d.status;
|
||
|
||
var nw = document.getElementById('adNotesWrap');
|
||
if (d.notes) {
|
||
document.getElementById('adNotes').textContent = d.notes;
|
||
nw.classList.remove('d-none');
|
||
} else {
|
||
nw.classList.add('d-none');
|
||
}
|
||
|
||
// Popola gli hidden dei form
|
||
['adId_status', 'adId_edit', 'adId_delete'].forEach(function(id) {
|
||
document.getElementById(id).value = d.id;
|
||
});
|
||
['adDate_status', 'adDate_edit', 'adDate_delete'].forEach(function(id) {
|
||
document.getElementById(id).value = d.date;
|
||
});
|
||
|
||
// Precompila la sezione modifica
|
||
document.getElementById('adEditDate').value = d.date;
|
||
document.getElementById('adEditTime').value = d.time;
|
||
document.getElementById('adEditStaff').value = d.staff_id;
|
||
document.getElementById('adEditService').value = d.service_id;
|
||
|
||
var modalEl = document.getElementById('apptDetailModal');
|
||
if (window.bootstrap && bootstrap.Modal) bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
||
});
|
||
|
||
// --- Auto-calcolo (invariato) ---
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
var serviceSelect = document.getElementById('service_id');
|
||
var startTimeInput = document.getElementById('start_time');
|
||
if (serviceSelect && startTimeInput) {
|
||
serviceSelect.addEventListener('change', function() {
|
||
// hook lasciato per eventuale campo end_time
|
||
});
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|