added appointments
This commit is contained in:
parent
75d63261bb
commit
b647fe4934
805
public/userarea/appointments.php
Normal file
805
public/userarea/appointments.php
Normal file
@ -0,0 +1,805 @@
|
|||||||
|
<?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>
|
||||||
155
public/userarea/create_appointments.php
Normal file
155
public/userarea/create_appointments.php
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_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];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che sia una richiesta POST
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
setFlash('danger', 'Richiesta non valida.');
|
||||||
|
header("Location: appointments.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recupera i dati dal form
|
||||||
|
$date = trim($_POST['date'] ?? '');
|
||||||
|
$start_time = trim($_POST['start_time'] ?? '');
|
||||||
|
$staff_id = (int)($_POST['staff_id'] ?? 0);
|
||||||
|
$customer_name = trim($_POST['customer_name'] ?? '');
|
||||||
|
$customer_phone = trim($_POST['customer_phone'] ?? '');
|
||||||
|
$service_id = (int)($_POST['service_id'] ?? 0);
|
||||||
|
$notes = trim($_POST['notes'] ?? '');
|
||||||
|
|
||||||
|
// Validazione base
|
||||||
|
if (!$date || !$start_time || !$staff_id || !$service_id || !$customer_name) {
|
||||||
|
setFlash('danger', 'Compila tutti i campi obbligatori.');
|
||||||
|
header("Location: appointments.php?date=" . urlencode($date));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Verifica che lo staff appartenga al salone dell'utente
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT s.shop_id, srv.duration_minutes
|
||||||
|
FROM staff s
|
||||||
|
JOIN shops sh ON s.shop_id = sh.id
|
||||||
|
LEFT JOIN services srv ON srv.id = ? AND srv.shop_id = sh.id
|
||||||
|
WHERE s.id = ? AND sh.owner_id = ?
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute([$service_id, $staff_id, $iduserlogin]);
|
||||||
|
$check = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$check) {
|
||||||
|
setFlash('danger', 'Staff o servizio non valido.');
|
||||||
|
header("Location: appointments.php?date=" . urlencode($date));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shop_id = (int)$check['shop_id'];
|
||||||
|
$duration = (int)$check['duration_minutes'];
|
||||||
|
|
||||||
|
// 2. Calcola start_at e end_at
|
||||||
|
$start_at = $date . ' ' . $start_time . ':00';
|
||||||
|
$end_timestamp = strtotime($start_at) + ($duration * 60);
|
||||||
|
$end_at = date('Y-m-d H:i:s', $end_timestamp);
|
||||||
|
|
||||||
|
// 3. Verifica disponibilità (evita sovrapposizioni)
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT COUNT(*) FROM appointments
|
||||||
|
WHERE staff_id = ?
|
||||||
|
AND status NOT IN ('cancelled', 'no_show')
|
||||||
|
AND (
|
||||||
|
(start_at < ? AND end_at > ?) OR
|
||||||
|
(start_at < ? AND end_at > ?) OR
|
||||||
|
(start_at >= ? AND end_at <= ?)
|
||||||
|
)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$staff_id,
|
||||||
|
$end_at,
|
||||||
|
$start_at, // nuovo finisce dopo inizio esistente
|
||||||
|
$end_at,
|
||||||
|
$start_at, // nuovo inizia prima fine esistente
|
||||||
|
$start_at,
|
||||||
|
$end_at // nuovo è contenuto
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ((int)$stmt->fetchColumn() > 0) {
|
||||||
|
setFlash('warning', 'Slot già occupato. Scegli un altro orario.');
|
||||||
|
header("Location: appointments.php?date=" . urlencode($date) . "&staff_id=" . $staff_id);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Cerca o crea il cliente
|
||||||
|
$customer_id = null;
|
||||||
|
|
||||||
|
if ($customer_phone) {
|
||||||
|
// Cerca cliente esistente per telefono
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT id FROM customers
|
||||||
|
WHERE shop_id = ? AND phone = ?
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute([$shop_id, $customer_phone]);
|
||||||
|
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$customer_id = (int)$existing['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se non trovato, crea nuovo cliente
|
||||||
|
if (!$customer_id) {
|
||||||
|
$names = explode(' ', $customer_name, 2);
|
||||||
|
$first_name = $names[0];
|
||||||
|
$last_name = $names[1] ?? '';
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO customers (shop_id, first_name, last_name, phone, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([$shop_id, $first_name, $last_name, $customer_phone]);
|
||||||
|
$customer_id = (int)$pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Inserisci l'appuntamento
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO appointments (
|
||||||
|
shop_id, customer_id, staff_id, service_id,
|
||||||
|
start_at, end_at, status, notes, created_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, 'confirmed', ?, NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$shop_id,
|
||||||
|
$customer_id,
|
||||||
|
$staff_id,
|
||||||
|
$service_id,
|
||||||
|
$start_at,
|
||||||
|
$end_at,
|
||||||
|
$notes
|
||||||
|
]);
|
||||||
|
|
||||||
|
setFlash('success', 'Appuntamento creato con successo!');
|
||||||
|
header("Location: appointments.php?date=" . urlencode($date) . "&staff_id=" . $staff_id);
|
||||||
|
exit;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Errore creazione appuntamento: " . $e->getMessage());
|
||||||
|
setFlash('danger', 'Errore durante la creazione: ' . $e->getMessage());
|
||||||
|
header("Location: appointments.php?date=" . urlencode($date));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@ -68,17 +68,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="shop.php">
|
<a href="my_favourites.php">
|
||||||
<div class="parent-icon"><i class="bx bx-store"></i></div>
|
<div class="parent-icon"><i class="bx bx-calendar-check"></i></div>
|
||||||
<div class="menu-title">Prodotti & Servizi</div>
|
<div class="menu-title">I miei saloni preferiti</div>
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="cart.php">
|
|
||||||
<div class="parent-icon"><i class="bx bx-cart"></i></div>
|
|
||||||
<div class="menu-title">Carrello</div>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="profile.php">
|
<a href="profile.php">
|
||||||
<div class="parent-icon"><i class="bx bx-user"></i></div>
|
<div class="parent-icon"><i class="bx bx-user"></i></div>
|
||||||
@ -138,6 +133,12 @@
|
|||||||
<div class="menu-title">Prodotti</div>
|
<div class="menu-title">Prodotti</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="shop_hours.php">
|
||||||
|
<div class="parent-icon"><i class="bx bx-calendar-x"></i></div>
|
||||||
|
<div class="menu-title">Orari di Apertura</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="day_off.php">
|
<a href="day_off.php">
|
||||||
<div class="parent-icon"><i class="bx bx-calendar-x"></i></div>
|
<div class="parent-icon"><i class="bx bx-calendar-x"></i></div>
|
||||||
|
|||||||
386
public/userarea/profile.php
Normal file
386
public/userarea/profile.php
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
<?php
|
||||||
|
// Forza la visualizzazione degli errori (solo dev)
|
||||||
|
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');
|
||||||
|
|
||||||
|
// Connessione DB
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
// Verifica utente loggato
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
header("Location: ../login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers flash
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch dati utente
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT first_name, last_name, phone, email, avatar, address, birthday
|
||||||
|
FROM auth_users
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$iduserlogin]);
|
||||||
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
die("Errore: utente non trovato.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST - Aggiorna profilo (escluso password)
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_profile') {
|
||||||
|
try {
|
||||||
|
$first_name = trim($_POST['first_name'] ?? '');
|
||||||
|
$last_name = trim($_POST['last_name'] ?? '');
|
||||||
|
$phone = trim($_POST['phone'] ?? '');
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$address = trim($_POST['address'] ?? '');
|
||||||
|
$birthday = !empty($_POST['birthday']) ? $_POST['birthday'] : null;
|
||||||
|
|
||||||
|
// Validazioni
|
||||||
|
if (empty($first_name) || empty($last_name)) {
|
||||||
|
throw new Exception("Nome e Cognome sono obbligatori.");
|
||||||
|
}
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
throw new Exception("Email non valida.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload avatar
|
||||||
|
$avatar = $user['avatar'];
|
||||||
|
$upload_dir = '../upload/users/';
|
||||||
|
if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true);
|
||||||
|
|
||||||
|
if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$file = $_FILES['avatar'];
|
||||||
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
// Formati accettati (inclusi HEIC/HEIF da iPhone)
|
||||||
|
$allowed = ['jpg', 'jpeg', 'png', 'heic', 'heif'];
|
||||||
|
if (!in_array($ext, $allowed)) {
|
||||||
|
throw new Exception("Formato non supportato. Usa JPG, PNG o HEIC/HEIF.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nome file sicuro
|
||||||
|
$original_name = preg_replace('/[^A-Za-z0-9\._-]/', '', pathinfo($file['name'], PATHINFO_FILENAME));
|
||||||
|
$timestamp = time();
|
||||||
|
$new_filename = "{$iduserlogin}-{$timestamp}-{$original_name}.{$ext}";
|
||||||
|
$dest_path = $upload_dir . $new_filename;
|
||||||
|
|
||||||
|
// Sposta file temporaneo
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $dest_path)) {
|
||||||
|
// Ridimensiona (max 400x400)
|
||||||
|
list($width, $height) = getimagesize($dest_path);
|
||||||
|
$max_size = 400;
|
||||||
|
if ($width > $max_size || $height > $max_size) {
|
||||||
|
$ratio = $max_size / max($width, $height);
|
||||||
|
$new_width = (int)($width * $ratio);
|
||||||
|
$new_height = (int)($height * $ratio);
|
||||||
|
|
||||||
|
$thumb = imagecreatetruecolor($new_width, $new_height);
|
||||||
|
if ($ext === 'png') {
|
||||||
|
imagealphablending($thumb, false);
|
||||||
|
imagesavealpha($thumb, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = match ($ext) {
|
||||||
|
'jpg', 'jpeg' => imagecreatefromjpeg($dest_path),
|
||||||
|
'png' => imagecreatefrompng($dest_path),
|
||||||
|
'heic', 'heif' => imagecreatefromstring(file_get_contents($dest_path)), // HEIC richiede GD recente o Imagick
|
||||||
|
default => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($source) {
|
||||||
|
imagecopyresampled($thumb, $source, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
|
||||||
|
imagejpeg($thumb, $dest_path, 85); // salva come jpg per compatibilità
|
||||||
|
imagedestroy($source);
|
||||||
|
imagedestroy($thumb);
|
||||||
|
$new_filename = "{$iduserlogin}-{$timestamp}-{$original_name}.jpg"; // aggiorna estensione
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancella vecchio avatar se esiste
|
||||||
|
if ($avatar && file_exists('../' . $avatar)) {
|
||||||
|
@unlink('../' . $avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
$avatar = "upload/users/" . $new_filename;
|
||||||
|
} else {
|
||||||
|
throw new Exception("Errore durante il caricamento dell'immagine.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update DB
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE auth_users SET
|
||||||
|
first_name = ?, last_name = ?, phone = ?, email = ?,
|
||||||
|
address = ?, birthday = ?, avatar = ?, updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$ok = $stmt->execute([
|
||||||
|
$first_name,
|
||||||
|
$last_name,
|
||||||
|
$phone ?: null,
|
||||||
|
$email,
|
||||||
|
$address ?: null,
|
||||||
|
$birthday,
|
||||||
|
$avatar,
|
||||||
|
$iduserlogin
|
||||||
|
]);
|
||||||
|
|
||||||
|
setFlash($ok ? 'success' : 'danger', $ok ? "Profilo aggiornato con successo!" : "Errore durante il salvataggio.");
|
||||||
|
header("Location: profile.php");
|
||||||
|
exit;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
setFlash('danger', $e->getMessage());
|
||||||
|
header("Location: profile.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST - Cambio password
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'change_password') {
|
||||||
|
try {
|
||||||
|
$old_password = $_POST['old_password'] ?? '';
|
||||||
|
$new_password = $_POST['new_password'] ?? '';
|
||||||
|
$confirm_password = $_POST['confirm_password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($old_password) || empty($new_password) || empty($confirm_password)) {
|
||||||
|
throw new Exception("Tutti i campi sono obbligatori.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($new_password !== $confirm_password) {
|
||||||
|
throw new Exception("Le nuove password non coincidono.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($new_password) < 8) {
|
||||||
|
throw new Exception("La nuova password deve avere almeno 8 caratteri.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica vecchia password (Laravel Hash)
|
||||||
|
$stmt = $pdo->prepare("SELECT password FROM auth_users WHERE id = ?");
|
||||||
|
$stmt->execute([$iduserlogin]);
|
||||||
|
$hashed = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if (!password_verify($old_password, $hashed)) {
|
||||||
|
throw new Exception("La vecchia password non è corretta.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nuova password
|
||||||
|
$new_hashed = password_hash($new_password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE auth_users SET password = ?, updated_at = NOW() WHERE id = ?");
|
||||||
|
$ok = $stmt->execute([$new_hashed, $iduserlogin]);
|
||||||
|
|
||||||
|
setFlash($ok ? 'success' : 'danger', $ok ? "Password cambiata con successo!" : "Errore durante il cambio password.");
|
||||||
|
header("Location: profile.php");
|
||||||
|
exit;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
setFlash('danger', $e->getMessage());
|
||||||
|
header("Location: profile.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$flash = getFlash();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!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>Il Mio Profilo</title>
|
||||||
|
<style>
|
||||||
|
.avatar-preview {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 4px solid #e5e7eb;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 2rem 0 1.5rem;
|
||||||
|
color: #1f2937;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
left: 0;
|
||||||
|
width: 60px;
|
||||||
|
height: 3px;
|
||||||
|
background: #6366f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save {
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
</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 radius-10">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">Il Mio Profilo</h6>
|
||||||
|
<a href="dashboard.php" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="bx bx-arrow-back me-1"></i> Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> alert-dismissible fade show mb-4" role="alert">
|
||||||
|
<?= htmlspecialchars($flash['text']) ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="row g-5">
|
||||||
|
<!-- Colonna sinistra: dati profilo -->
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<form action="" method="POST" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="action" value="update_profile">
|
||||||
|
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img src="../upload/users/<?= htmlspecialchars($user['avatar'] ? '' . $user['avatar'] : '../assets/images/default-user.png') ?>"
|
||||||
|
alt="Avatar" class="avatar-preview" id="avatarPreview">
|
||||||
|
<input type="file" class="form-control form-control-sm mt-2" name="avatar" id="avatarInput" accept="image/jpeg,image/png,image/heic,image/heif">
|
||||||
|
<small class="form-text text-muted">JPG, PNG, HEIC/HEIF (max 5MB)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Nome</label>
|
||||||
|
<input type="text" class="form-control" name="first_name" value="<?= htmlspecialchars($user['first_name'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Cognome</label>
|
||||||
|
<input type="text" class="form-control" name="last_name" value="<?= htmlspecialchars($user['last_name'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Telefono</label>
|
||||||
|
<input type="tel" class="form-control" name="phone" value="<?= htmlspecialchars($user['phone'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Email</label>
|
||||||
|
<input type="email" class="form-control" name="email" value="<?= htmlspecialchars($user['email']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Data di nascita</label>
|
||||||
|
<input type="date" class="form-control" name="birthday" value="<?= htmlspecialchars($user['birthday'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Indirizzo</label>
|
||||||
|
<input type="text" class="form-control" name="address" value="<?= htmlspecialchars($user['address'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Salva Modifiche Profilo</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna destra: cambio password -->
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="card border shadow-sm">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="mb-0">Modifica Password</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="" method="POST">
|
||||||
|
<input type="hidden" name="action" value="change_password">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Vecchia password</label>
|
||||||
|
<input type="password" class="form-control" name="old_password" required autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Nuova password</label>
|
||||||
|
<input type="password" class="form-control" name="new_password" required minlength="8" autocomplete="new-password">
|
||||||
|
<small class="form-text text-muted">Minimo 8 caratteri</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Conferma nuova password</label>
|
||||||
|
<input type="password" class="form-control" name="confirm_password" required autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-warning">Cambia Password</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Anteprima avatar
|
||||||
|
document.getElementById('avatarInput').addEventListener('change', function(e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(ev) {
|
||||||
|
document.getElementById('avatarPreview').src = ev.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -85,7 +85,6 @@ $stmt = $pdo->prepare("
|
|||||||
a.end_at,
|
a.end_at,
|
||||||
a.status,
|
a.status,
|
||||||
a.notes,
|
a.notes,
|
||||||
a.price_paid,
|
|
||||||
c.first_name AS customer_first,
|
c.first_name AS customer_first,
|
||||||
c.last_name AS customer_last,
|
c.last_name AS customer_last,
|
||||||
c.phone AS customer_phone,
|
c.phone AS customer_phone,
|
||||||
@ -167,7 +166,6 @@ $appointments = $stmt->fetchAll();
|
|||||||
<a href="appointments.php" class="btn btn-success px-4 py-3"><i class="bx bx-calendar-check me-2"></i> Appuntamenti</a>
|
<a href="appointments.php" class="btn btn-success px-4 py-3"><i class="bx bx-calendar-check me-2"></i> Appuntamenti</a>
|
||||||
<a href="staff.php" class="btn btn-info px-4 py-3"><i class="bx bx-group me-2"></i> Staff</a>
|
<a href="staff.php" class="btn btn-info px-4 py-3"><i class="bx bx-group me-2"></i> Staff</a>
|
||||||
<a href="finances.php" class="btn btn-warning px-4 py-3"><i class="bx bx-euro me-2"></i> Incassi</a>
|
<a href="finances.php" class="btn btn-warning px-4 py-3"><i class="bx bx-euro me-2"></i> Incassi</a>
|
||||||
<a href="new_appointment.php" class="btn btn-dark px-4 py-3"><i class="bx bx-plus-medical me-2"></i> Nuovo Appuntamento</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -63,17 +63,27 @@ function getFlash(): ?array
|
|||||||
// POST - Salva impostazioni
|
// POST - Salva impostazioni
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
try {
|
try {
|
||||||
$show_prices_online = isset($_POST['show_prices_online']) ? 1 : 0;
|
$show_prices_online = isset($_POST['show_prices_online']) ? 1 : 0;
|
||||||
$restrict_start_minutes = trim($_POST['restrict_start_minutes'] ?? '00,30');
|
$appointment_slot_interval = (int)($_POST['appointment_slot_interval'] ?? 30);
|
||||||
$min_booking_notice_hours = (int)($_POST['min_booking_notice_hours'] ?? 2);
|
$allow_same_time_multiple = isset($_POST['allow_same_time_multiple']) ? 1 : 0;
|
||||||
$max_booking_days_ahead = (int)($_POST['max_booking_days_ahead'] ?? 90);
|
$min_booking_notice_hours = (int)($_POST['min_booking_notice_hours'] ?? 2);
|
||||||
|
$max_booking_days_ahead = (int)($_POST['max_booking_days_ahead'] ?? 90);
|
||||||
|
$require_appointment_confirmation = isset($_POST['require_appointment_confirmation']) ? 1 : 0;
|
||||||
|
$no_show_warning_after = (int)($_POST['no_show_warning_after'] ?? 3);
|
||||||
|
$no_show_block_after = (int)($_POST['no_show_block_after'] ?? 5);
|
||||||
|
|
||||||
// Validazione restrict_start_minutes
|
// Validazione restrict_start_minutes
|
||||||
|
$restrict_start_minutes = trim($_POST['restrict_start_minutes'] ?? '00,30');
|
||||||
$valid_options = ['any', '00', '00,30', '00,15,30,45'];
|
$valid_options = ['any', '00', '00,30', '00,15,30,45'];
|
||||||
if (!in_array($restrict_start_minutes, $valid_options)) {
|
if (!in_array($restrict_start_minutes, $valid_options)) {
|
||||||
$restrict_start_minutes = '00,30';
|
$restrict_start_minutes = '00,30';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validazione intervallo slot
|
||||||
|
if ($appointment_slot_interval < 5 || $appointment_slot_interval > 120) {
|
||||||
|
$appointment_slot_interval = 30;
|
||||||
|
}
|
||||||
|
|
||||||
// Controlla esistenza
|
// Controlla esistenza
|
||||||
$stmt = $pdo->prepare("SELECT id FROM shop_settings WHERE shop_id = ?");
|
$stmt = $pdo->prepare("SELECT id FROM shop_settings WHERE shop_id = ?");
|
||||||
$stmt->execute([$shop_id]);
|
$stmt->execute([$shop_id]);
|
||||||
@ -83,32 +93,49 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE shop_settings SET
|
UPDATE shop_settings SET
|
||||||
show_prices_online = ?,
|
show_prices_online = ?,
|
||||||
allowed_start_minutes = ?,
|
appointment_slot_interval = ?,
|
||||||
|
allow_same_time_multiple = ?,
|
||||||
min_booking_notice_hours = ?,
|
min_booking_notice_hours = ?,
|
||||||
max_booking_days_ahead = ?,
|
max_booking_days_ahead = ?,
|
||||||
|
allowed_start_minutes = ?,
|
||||||
|
require_appointment_confirmation = ?,
|
||||||
|
no_show_warning_after = ?,
|
||||||
|
no_show_block_after = ?,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE shop_id = ?
|
WHERE shop_id = ?
|
||||||
");
|
");
|
||||||
$ok = $stmt->execute([
|
$ok = $stmt->execute([
|
||||||
$show_prices_online,
|
$show_prices_online,
|
||||||
$restrict_start_minutes,
|
$appointment_slot_interval,
|
||||||
|
$allow_same_time_multiple,
|
||||||
$min_booking_notice_hours,
|
$min_booking_notice_hours,
|
||||||
$max_booking_days_ahead,
|
$max_booking_days_ahead,
|
||||||
|
$restrict_start_minutes,
|
||||||
|
$require_appointment_confirmation,
|
||||||
|
$no_show_warning_after,
|
||||||
|
$no_show_block_after,
|
||||||
$shop_id
|
$shop_id
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
INSERT INTO shop_settings (
|
INSERT INTO shop_settings (
|
||||||
shop_id, show_prices_online, allowed_start_minutes,
|
shop_id, show_prices_online, appointment_slot_interval,
|
||||||
min_booking_notice_hours, max_booking_days_ahead
|
allow_same_time_multiple, min_booking_notice_hours, max_booking_days_ahead,
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
allowed_start_minutes, require_appointment_confirmation,
|
||||||
|
no_show_warning_after, no_show_block_after
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
");
|
");
|
||||||
$ok = $stmt->execute([
|
$ok = $stmt->execute([
|
||||||
$shop_id,
|
$shop_id,
|
||||||
$show_prices_online,
|
$show_prices_online,
|
||||||
$restrict_start_minutes,
|
$appointment_slot_interval,
|
||||||
|
$allow_same_time_multiple,
|
||||||
$min_booking_notice_hours,
|
$min_booking_notice_hours,
|
||||||
$max_booking_days_ahead
|
$max_booking_days_ahead,
|
||||||
|
$restrict_start_minutes,
|
||||||
|
$require_appointment_confirmation,
|
||||||
|
$no_show_warning_after,
|
||||||
|
$no_show_block_after
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,10 +153,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$stmt = $pdo->prepare("SELECT * FROM shop_settings WHERE shop_id = ?");
|
$stmt = $pdo->prepare("SELECT * FROM shop_settings WHERE shop_id = ?");
|
||||||
$stmt->execute([$shop_id]);
|
$stmt->execute([$shop_id]);
|
||||||
$settings = $stmt->fetch(PDO::FETCH_ASSOC) ?: [
|
$settings = $stmt->fetch(PDO::FETCH_ASSOC) ?: [
|
||||||
'show_prices_online' => 1,
|
'show_prices_online' => 1,
|
||||||
'allowed_start_minutes' => '00,30',
|
'appointment_slot_interval' => 30,
|
||||||
'min_booking_notice_hours' => 2,
|
'allow_same_time_multiple' => 0,
|
||||||
'max_booking_days_ahead' => 90
|
'min_booking_notice_hours' => 2,
|
||||||
|
'max_booking_days_ahead' => 90,
|
||||||
|
'allowed_start_minutes' => '00,30',
|
||||||
|
'require_appointment_confirmation' => 1,
|
||||||
|
'no_show_warning_after' => 3,
|
||||||
|
'no_show_block_after' => 5
|
||||||
];
|
];
|
||||||
|
|
||||||
$flash = getFlash();
|
$flash = getFlash();
|
||||||
@ -225,6 +257,34 @@ $flash = getFlash();
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.3);
|
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggle-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1.25rem 0;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-group:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input-lg {
|
||||||
|
width: 3.2rem;
|
||||||
|
height: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input-lg:checked {
|
||||||
|
background-color: #6366f1;
|
||||||
|
border-color: #6366f1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -253,43 +313,45 @@ $flash = getFlash();
|
|||||||
|
|
||||||
<form action="" method="POST">
|
<form action="" method="POST">
|
||||||
<!-- Visibilità prezzi -->
|
<!-- Visibilità prezzi -->
|
||||||
<div class="form-group">
|
<div class="toggle-group">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<label class="toggle-label" for="showPrices">Mostra i prezzi online ai clienti</label>
|
||||||
<label class="form-label mb-0" for="showPrices">Mostra i prezzi online ai clienti</label>
|
<div class="form-check form-switch form-switch-lg">
|
||||||
<div class="form-check form-switch form-switch-lg">
|
<input class="form-check-input form-check-input-lg" type="checkbox" name="show_prices_online" id="showPrices" <?= $settings['show_prices_online'] ? 'checked' : '' ?>>
|
||||||
<input class="form-check-input" type="checkbox" name="show_prices_online" id="showPrices" <?= $settings['show_prices_online'] ? 'checked' : '' ?>>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text">
|
</div>
|
||||||
I clienti vedranno i prezzi durante la prenotazione online.
|
|
||||||
|
<!-- Richiesta conferma manuale -->
|
||||||
|
<div class="toggle-group">
|
||||||
|
<label class="toggle-label" for="requireConfirmation">Richiedi conferma manuale per ogni nuovo appuntamento</label>
|
||||||
|
<div class="form-check form-switch form-switch-lg">
|
||||||
|
<input class="form-check-input form-check-input-lg" type="checkbox" name="require_appointment_confirmation" id="requireConfirmation" <?= $settings['require_appointment_confirmation'] ? 'checked' : '' ?>>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Orari inizio -->
|
<!-- Orari inizio -->
|
||||||
<div class="form-group">
|
<div class="form-group mt-4">
|
||||||
<label class="form-label" for="restrictSelect">Orari di inizio appuntamenti permessi</label>
|
<label class="form-label" for="restrictSelect">Orari di inizio appuntamenti permessi</label>
|
||||||
<select class="form-select form-select-lg" name="restrict_start_minutes" id="restrictSelect">
|
<select class="form-select form-select-lg" name="restrict_start_minutes" id="restrictSelect">
|
||||||
<option value="any" <?= $settings['allowed_start_minutes'] === 'any' ? 'selected' : '' ?>>
|
<option value="any" <?= $settings['allowed_start_minutes'] === 'any' ? 'selected' : '' ?>>Qualsiasi minuto (libero)</option>
|
||||||
Qualsiasi minuto (libero)
|
<option value="00" <?= $settings['allowed_start_minutes'] === '00' ? 'selected' : '' ?>>Solo ore piene (:00)</option>
|
||||||
</option>
|
<option value="00,30" <?= $settings['allowed_start_minutes'] === '00,30' ? 'selected' : '' ?>>Solo :00 e :30 (ogni mezz'ora piena)</option>
|
||||||
<option value="00" <?= $settings['allowed_start_minutes'] === '00' ? 'selected' : '' ?>>
|
<option value="00,15,30,45" <?= $settings['allowed_start_minutes'] === '00,15,30,45' ? 'selected' : '' ?>>Ogni 15 minuti (:00, :15, :30, :45)</option>
|
||||||
Solo ore piene (:00)
|
|
||||||
</option>
|
|
||||||
<option value="00,30" <?= $settings['allowed_start_minutes'] === '00,30' ? 'selected' : '' ?>>
|
|
||||||
Solo :00 e :30 (ogni mezz'ora piena)
|
|
||||||
</option>
|
|
||||||
<option value="00,15,30,45" <?= $settings['allowed_start_minutes'] === '00,15,30,45' ? 'selected' : '' ?>>
|
|
||||||
Ogni 15 minuti (:00, :15, :30, :45)
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
<div class="preview-box">
|
<div class="preview-box">
|
||||||
<strong>Anteprima slot (es. dalle 9:00):</strong>
|
<strong>Anteprima slot orari (es. dalle 9:00):</strong>
|
||||||
<div class="preview-slots" id="previewSlots"></div>
|
<div class="preview-slots" id="previewSlots"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Limiti temporali -->
|
<!-- Limiti temporali -->
|
||||||
<div class="row g-4">
|
<div class="row g-4 mt-4">
|
||||||
|
<div class="col-md-6 form-group">
|
||||||
|
<label class="form-label" for="slotInterval">Intervallo slot appuntamenti (minuti)</label>
|
||||||
|
<input type="number" class="form-control form-control-lg" name="appointment_slot_interval" id="slotInterval" min="5" max="120"
|
||||||
|
value="<?= (int)$settings['appointment_slot_interval'] ?>">
|
||||||
|
<div class="form-text">Es: 30 = slot ogni mezz'ora</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 form-group">
|
<div class="col-md-6 form-group">
|
||||||
<label class="form-label" for="minNotice">Preavviso minimo prenotazione (ore)</label>
|
<label class="form-label" for="minNotice">Preavviso minimo prenotazione (ore)</label>
|
||||||
<input type="number" class="form-control form-control-lg" name="min_booking_notice_hours" id="minNotice" min="0" max="72"
|
<input type="number" class="form-control form-control-lg" name="min_booking_notice_hours" id="minNotice" min="0" max="72"
|
||||||
@ -303,11 +365,38 @@ $flash = getFlash();
|
|||||||
value="<?= (int)$settings['max_booking_days_ahead'] ?>">
|
value="<?= (int)$settings['max_booking_days_ahead'] ?>">
|
||||||
<div class="form-text">Es: 90 = massimo 90 giorni da oggi.</div>
|
<div class="form-text">Es: 90 = massimo 90 giorni da oggi.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 form-group">
|
||||||
|
<label class="form-label" for="allowMultiple">Permetti più appuntamenti allo stesso orario</label>
|
||||||
|
<div class="form-check form-switch form-switch-lg mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" name="allow_same_time_multiple" id="allowMultiple" <?= $settings['allow_same_time_multiple'] ? 'checked' : '' ?>>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Utile se più parrucchieri possono lavorare contemporaneamente sullo stesso slot.</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- No-show -->
|
||||||
|
<div class="section-title mt-5">Gestione No-Show (assenze)</div>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6 form-group">
|
||||||
|
<label class="form-label" for="noShowWarn">Segnala utente dopo quante assenze consecutive</label>
|
||||||
|
<input type="number" class="form-control form-control-lg" name="no_show_warning_after" id="noShowWarn" min="0" max="20"
|
||||||
|
value="<?= (int)$settings['no_show_warning_after'] ?>">
|
||||||
|
<div class="form-text">0 = disattivato. Es: 3 = dopo 3 no-show invia alert al titolare.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 form-group">
|
||||||
|
<label class="form-label" for="noShowBlock">Blocca utente dopo quante assenze consecutive</label>
|
||||||
|
<input type="number" class="form-control form-control-lg" name="no_show_block_after" id="noShowBlock" min="0" max="50"
|
||||||
|
value="<?= (int)$settings['no_show_block_after'] ?>">
|
||||||
|
<div class="form-text">0 = mai bloccare. Es: 5 = dopo 5 no-show impedisce nuove prenotazioni.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pulsante salva -->
|
||||||
<div class="d-grid mt-5">
|
<div class="d-grid mt-5">
|
||||||
<button type="submit" class="btn btn-save">
|
<button type="submit" class="btn btn-save">
|
||||||
<i class="bx bx-save me-2"></i> Salva Impostazioni
|
<i class="bx bx-save me-2"></i> Salva Impostazioni Salone
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
620
public/userarea/shop_hours.php
Normal file
620
public/userarea/shop_hours.php
Normal file
@ -0,0 +1,620 @@
|
|||||||
|
<?php
|
||||||
|
// Forza la visualizzazione degli errori (solo dev)
|
||||||
|
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');
|
||||||
|
|
||||||
|
// Connessione DB
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
// Verifica utente loggato
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controlla se esiste almeno un salone
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM shops WHERE owner_id = ?");
|
||||||
|
$stmt->execute([$iduserlogin]);
|
||||||
|
if ((int)$stmt->fetchColumn() === 0) {
|
||||||
|
header("Location: onboarding_salon.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prendi il primo salone
|
||||||
|
$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'];
|
||||||
|
|
||||||
|
// Helpers flash
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Fetch orari esistenti
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM shop_hours WHERE shop_id = ? ORDER BY day_of_week");
|
||||||
|
$stmt->execute([$shop_id]);
|
||||||
|
$hours_raw = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Organizza per giorno
|
||||||
|
$hours = [];
|
||||||
|
for ($d = 0; $d <= 6; $d++) {
|
||||||
|
$hours[$d] = [
|
||||||
|
'is_open' => 0,
|
||||||
|
'open_time' => '09:00',
|
||||||
|
'close_time' => '19:00',
|
||||||
|
'open_time_2' => null,
|
||||||
|
'close_time_2' => null,
|
||||||
|
'notes' => ''
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($hours_raw as $h) {
|
||||||
|
$d = (int)$h['day_of_week'];
|
||||||
|
$hours[$d] = $h;
|
||||||
|
}
|
||||||
|
|
||||||
|
$flash = getFlash();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!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>Orari di Apertura - <?= htmlspecialchars($shop_name) ?></title>
|
||||||
|
<style>
|
||||||
|
.hours-card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hours-header {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 1.8rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-row {
|
||||||
|
padding: 1.25rem 2rem;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-input {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input-lg {
|
||||||
|
width: 2.8rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input-lg:checked {
|
||||||
|
background-color: #8b5cf6;
|
||||||
|
border-color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-week {
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-day {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-bottom: 1px dashed #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-day:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-day.active {
|
||||||
|
animation: pulse 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paste-day:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</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 hours-card">
|
||||||
|
<div class="hours-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Orari di Apertura - <?= htmlspecialchars($shop_name) ?></h5>
|
||||||
|
<a href="salon_dashboard.php" class="btn btn-light btn-sm px-4">
|
||||||
|
<i class="bx bx-arrow-back me-2"></i> Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section p-4">
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-<?= $flash['type'] ?> alert-dismissible fade show mb-4" role="alert">
|
||||||
|
<?= htmlspecialchars($flash['text']) ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form action="" method="POST">
|
||||||
|
<?php
|
||||||
|
$days_names = ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'];
|
||||||
|
foreach ($days_names as $d => $name):
|
||||||
|
$h = $hours[$d];
|
||||||
|
?>
|
||||||
|
<div class="day-row d-flex align-items-center gap-4 flex-wrap" data-day="<?= $d ?>">
|
||||||
|
<div class="day-name"><?= $name ?></div>
|
||||||
|
|
||||||
|
<div class="form-check form-switch form-switch-lg">
|
||||||
|
<input class="form-check-input form-check-input-lg" type="checkbox" name="is_open_<?= $d ?>" id="open_<?= $d ?>" <?= $h['is_open'] ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label fw-bold" for="open_<?= $d ?>">
|
||||||
|
Aperto
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-3 align-items-center">
|
||||||
|
<input type="time" class="form-control time-input" name="open_time_<?= $d ?>" value="<?= htmlspecialchars($h['open_time'] ?? '09:00') ?>">
|
||||||
|
<span>-</span>
|
||||||
|
<input type="time" class="form-control time-input" name="close_time_<?= $d ?>" value="<?= htmlspecialchars($h['close_time'] ?? '19:00') ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-3 align-items-center">
|
||||||
|
<input type="time" class="form-control time-input" name="open_time_2_<?= $d ?>" value="<?= htmlspecialchars($h['open_time_2'] ?? '') ?>" placeholder="Opzionale">
|
||||||
|
<span>-</span>
|
||||||
|
<input type="time" class="form-control time-input" name="close_time_2_<?= $d ?>" value="<?= htmlspecialchars($h['close_time_2'] ?? '') ?>" placeholder="Opzionale">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="text" class="form-control flex-grow-1" name="notes_<?= $d ?>" value="<?= htmlspecialchars($h['notes'] ?? '') ?>" placeholder="Note (es. chiuso per pranzo dalle 13:00 alle 14:30)">
|
||||||
|
|
||||||
|
<!-- PULSANTI COPIA/INCOLLA -->
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary copy-day" data-day="<?= $d ?>" title="Copia questi orari">
|
||||||
|
<i class="bx bx-copy"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-success paste-day" data-day="<?= $d ?>" title="Incolla orari copiati">
|
||||||
|
<i class="bx bx-paste"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<div class="d-grid mt-5">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">
|
||||||
|
<i class="bx bx-save me-2"></i> Salva Orari di Apertura
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Anteprima settimanale -->
|
||||||
|
<div class="preview-week">
|
||||||
|
<h6 class="mb-3">Anteprima Settimanale</h6>
|
||||||
|
<?php foreach ($days_names as $d => $name):
|
||||||
|
$h = $hours[$d];
|
||||||
|
$status = $h['is_open'] ? 'Aperto' : 'Chiuso';
|
||||||
|
$times = $h['is_open']
|
||||||
|
? ($h['open_time'] . ' - ' . $h['close_time'] . ($h['open_time_2'] ? ' / ' . $h['open_time_2'] . ' - ' . $h['close_time_2'] : ''))
|
||||||
|
: '—';
|
||||||
|
?>
|
||||||
|
<div class="preview-day d-flex justify-content-between align-items-center">
|
||||||
|
<strong><?= $name ?></strong>
|
||||||
|
<span class="badge <?= $h['is_open'] ? 'bg-success' : 'bg-secondary' ?> px-3 py-2">
|
||||||
|
<?= $status ?> <?= $times ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const form = document.querySelector('form[method="POST"]');
|
||||||
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
// NASCONDI il pulsante salva
|
||||||
|
submitBtn.style.display = 'none';
|
||||||
|
|
||||||
|
// Clipboard per gli orari
|
||||||
|
let copiedData = null;
|
||||||
|
|
||||||
|
// Timeout per debounce
|
||||||
|
let saveTimeout = null;
|
||||||
|
|
||||||
|
// ========== AUTO-SAVE ==========
|
||||||
|
function autoSave() {
|
||||||
|
clearTimeout(saveTimeout);
|
||||||
|
showSavingIndicator();
|
||||||
|
|
||||||
|
saveTimeout = setTimeout(() => {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
fetch('shop_hours_save.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
showSaveResult(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Errore:', error);
|
||||||
|
showSaveResult({
|
||||||
|
type: 'danger',
|
||||||
|
message: 'Errore durante il salvataggio'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSavingIndicator() {
|
||||||
|
removeAlerts();
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = 'alert alert-info mb-4 saving-indicator';
|
||||||
|
alertDiv.innerHTML = '<i class="bx bx-loader-alt bx-spin me-2"></i> Salvando...';
|
||||||
|
const formSection = document.querySelector('.form-section');
|
||||||
|
formSection.insertBefore(alertDiv, formSection.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSaveResult(data) {
|
||||||
|
removeAlerts();
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = `alert alert-${data.type} alert-dismissible fade show mb-4`;
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
${data.message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
const formSection = document.querySelector('.form-section');
|
||||||
|
formSection.insertBefore(alertDiv, formSection.firstChild);
|
||||||
|
|
||||||
|
if (data.type === 'success') {
|
||||||
|
setTimeout(() => alertDiv.remove(), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAlerts() {
|
||||||
|
document.querySelectorAll('.alert').forEach(alert => alert.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== COPIA/INCOLLA ==========
|
||||||
|
|
||||||
|
// Funzione per ottenere i dati di un giorno
|
||||||
|
function getDayData(day) {
|
||||||
|
const row = document.querySelector(`.day-row[data-day="${day}"]`);
|
||||||
|
return {
|
||||||
|
is_open: row.querySelector(`input[name="is_open_${day}"]`).checked,
|
||||||
|
open_time: row.querySelector(`input[name="open_time_${day}"]`).value,
|
||||||
|
close_time: row.querySelector(`input[name="close_time_${day}"]`).value,
|
||||||
|
open_time_2: row.querySelector(`input[name="open_time_2_${day}"]`).value,
|
||||||
|
close_time_2: row.querySelector(`input[name="close_time_2_${day}"]`).value,
|
||||||
|
notes: row.querySelector(`input[name="notes_${day}"]`).value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funzione per impostare i dati di un giorno
|
||||||
|
function setDayData(day, data) {
|
||||||
|
const row = document.querySelector(`.day-row[data-day="${day}"]`);
|
||||||
|
row.querySelector(`input[name="is_open_${day}"]`).checked = data.is_open;
|
||||||
|
row.querySelector(`input[name="open_time_${day}"]`).value = data.open_time;
|
||||||
|
row.querySelector(`input[name="close_time_${day}"]`).value = data.close_time;
|
||||||
|
row.querySelector(`input[name="open_time_2_${day}"]`).value = data.open_time_2;
|
||||||
|
row.querySelector(`input[name="close_time_2_${day}"]`).value = data.close_time_2;
|
||||||
|
row.querySelector(`input[name="notes_${day}"]`).value = data.notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestione COPIA
|
||||||
|
document.querySelectorAll('.copy-day').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const day = this.dataset.day;
|
||||||
|
copiedData = getDayData(day);
|
||||||
|
|
||||||
|
// Feedback visivo
|
||||||
|
document.querySelectorAll('.copy-day').forEach(b => {
|
||||||
|
b.classList.remove('active', 'btn-primary');
|
||||||
|
b.classList.add('btn-outline-primary');
|
||||||
|
});
|
||||||
|
this.classList.remove('btn-outline-primary');
|
||||||
|
this.classList.add('btn-primary', 'active');
|
||||||
|
|
||||||
|
// Alert temporaneo
|
||||||
|
const tempAlert = document.createElement('div');
|
||||||
|
tempAlert.className = 'alert alert-success position-fixed top-0 start-50 translate-middle-x mt-3';
|
||||||
|
tempAlert.style.zIndex = '9999';
|
||||||
|
tempAlert.innerHTML = '<i class="bx bx-check me-2"></i> Orari copiati!';
|
||||||
|
document.body.appendChild(tempAlert);
|
||||||
|
setTimeout(() => tempAlert.remove(), 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gestione INCOLLA
|
||||||
|
document.querySelectorAll('.paste-day').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
if (!copiedData) {
|
||||||
|
alert('Nessun orario copiato! Prima clicca su "Copia" su un giorno.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const day = this.dataset.day;
|
||||||
|
setDayData(day, copiedData);
|
||||||
|
|
||||||
|
// Trigger auto-save
|
||||||
|
autoSave();
|
||||||
|
|
||||||
|
// Feedback visivo
|
||||||
|
this.classList.add('btn-success');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.classList.remove('btn-success');
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== LISTENER AUTO-SAVE ==========
|
||||||
|
const allInputs = form.querySelectorAll('input, select, textarea');
|
||||||
|
allInputs.forEach(input => {
|
||||||
|
if (input.type === 'checkbox') {
|
||||||
|
input.addEventListener('change', autoSave);
|
||||||
|
} else {
|
||||||
|
input.addEventListener('input', autoSave);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const form = document.querySelector('form[method="POST"]');
|
||||||
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
// NASCONDI il pulsante salva
|
||||||
|
submitBtn.style.display = 'none';
|
||||||
|
|
||||||
|
// Clipboard per gli orari
|
||||||
|
let copiedData = null;
|
||||||
|
|
||||||
|
// Timeout per debounce
|
||||||
|
let saveTimeout = null;
|
||||||
|
|
||||||
|
// Nomi dei giorni
|
||||||
|
const daysNames = ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'];
|
||||||
|
|
||||||
|
// ========== AGGIORNA ANTEPRIMA ==========
|
||||||
|
function updatePreview() {
|
||||||
|
for (let d = 0; d <= 6; d++) {
|
||||||
|
const isOpen = document.querySelector(`input[name="is_open_${d}"]`).checked;
|
||||||
|
const openTime = document.querySelector(`input[name="open_time_${d}"]`).value;
|
||||||
|
const closeTime = document.querySelector(`input[name="close_time_${d}"]`).value;
|
||||||
|
const openTime2 = document.querySelector(`input[name="open_time_2_${d}"]`).value;
|
||||||
|
const closeTime2 = document.querySelector(`input[name="close_time_2_${d}"]`).value;
|
||||||
|
|
||||||
|
// Trova il badge nella preview
|
||||||
|
const previewDays = document.querySelectorAll('.preview-day');
|
||||||
|
const badge = previewDays[d].querySelector('.badge');
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
let times = `${openTime} - ${closeTime}`;
|
||||||
|
if (openTime2 && closeTime2) {
|
||||||
|
times += ` / ${openTime2} - ${closeTime2}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
badge.className = 'badge bg-success px-3 py-2';
|
||||||
|
badge.textContent = `Aperto ${times}`;
|
||||||
|
} else {
|
||||||
|
badge.className = 'badge bg-secondary px-3 py-2';
|
||||||
|
badge.textContent = 'Chiuso —';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== AUTO-SAVE ==========
|
||||||
|
function autoSave() {
|
||||||
|
clearTimeout(saveTimeout);
|
||||||
|
showSavingIndicator();
|
||||||
|
|
||||||
|
// AGGIORNA ANTEPRIMA IMMEDIATAMENTE
|
||||||
|
updatePreview();
|
||||||
|
|
||||||
|
saveTimeout = setTimeout(() => {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
fetch('shop_hours_save.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
showSaveResult(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Errore:', error);
|
||||||
|
showSaveResult({
|
||||||
|
type: 'danger',
|
||||||
|
message: 'Errore durante il salvataggio'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSavingIndicator() {
|
||||||
|
removeAlerts();
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = 'alert alert-info mb-4 saving-indicator';
|
||||||
|
alertDiv.innerHTML = '<i class="bx bx-loader-alt bx-spin me-2"></i> Salvando...';
|
||||||
|
const formSection = document.querySelector('.form-section');
|
||||||
|
formSection.insertBefore(alertDiv, formSection.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSaveResult(data) {
|
||||||
|
removeAlerts();
|
||||||
|
const alertDiv = document.createElement('div');
|
||||||
|
alertDiv.className = `alert alert-${data.type} alert-dismissible fade show mb-4`;
|
||||||
|
alertDiv.innerHTML = `
|
||||||
|
${data.message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
const formSection = document.querySelector('.form-section');
|
||||||
|
formSection.insertBefore(alertDiv, formSection.firstChild);
|
||||||
|
|
||||||
|
if (data.type === 'success') {
|
||||||
|
setTimeout(() => alertDiv.remove(), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAlerts() {
|
||||||
|
document.querySelectorAll('.alert').forEach(alert => alert.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== COPIA/INCOLLA ==========
|
||||||
|
|
||||||
|
function getDayData(day) {
|
||||||
|
const row = document.querySelector(`.day-row[data-day="${day}"]`);
|
||||||
|
return {
|
||||||
|
is_open: row.querySelector(`input[name="is_open_${day}"]`).checked,
|
||||||
|
open_time: row.querySelector(`input[name="open_time_${day}"]`).value,
|
||||||
|
close_time: row.querySelector(`input[name="close_time_${day}"]`).value,
|
||||||
|
open_time_2: row.querySelector(`input[name="open_time_2_${day}"]`).value,
|
||||||
|
close_time_2: row.querySelector(`input[name="close_time_2_${day}"]`).value,
|
||||||
|
notes: row.querySelector(`input[name="notes_${day}"]`).value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDayData(day, data) {
|
||||||
|
const row = document.querySelector(`.day-row[data-day="${day}"]`);
|
||||||
|
row.querySelector(`input[name="is_open_${day}"]`).checked = data.is_open;
|
||||||
|
row.querySelector(`input[name="open_time_${day}"]`).value = data.open_time;
|
||||||
|
row.querySelector(`input[name="close_time_${day}"]`).value = data.close_time;
|
||||||
|
row.querySelector(`input[name="open_time_2_${day}"]`).value = data.open_time_2;
|
||||||
|
row.querySelector(`input[name="close_time_2_${day}"]`).value = data.close_time_2;
|
||||||
|
row.querySelector(`input[name="notes_${day}"]`).value = data.notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.copy-day').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const day = this.dataset.day;
|
||||||
|
copiedData = getDayData(day);
|
||||||
|
|
||||||
|
document.querySelectorAll('.copy-day').forEach(b => {
|
||||||
|
b.classList.remove('active', 'btn-primary');
|
||||||
|
b.classList.add('btn-outline-primary');
|
||||||
|
});
|
||||||
|
this.classList.remove('btn-outline-primary');
|
||||||
|
this.classList.add('btn-primary', 'active');
|
||||||
|
|
||||||
|
const tempAlert = document.createElement('div');
|
||||||
|
tempAlert.className = 'alert alert-success position-fixed top-0 start-50 translate-middle-x mt-3';
|
||||||
|
tempAlert.style.zIndex = '9999';
|
||||||
|
tempAlert.innerHTML = '<i class="bx bx-check me-2"></i> Orari copiati!';
|
||||||
|
document.body.appendChild(tempAlert);
|
||||||
|
setTimeout(() => tempAlert.remove(), 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.paste-day').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
if (!copiedData) {
|
||||||
|
alert('Nessun orario copiato! Prima clicca su "Copia" su un giorno.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const day = this.dataset.day;
|
||||||
|
setDayData(day, copiedData);
|
||||||
|
|
||||||
|
// AGGIORNA ANTEPRIMA IMMEDIATAMENTE
|
||||||
|
updatePreview();
|
||||||
|
|
||||||
|
// Trigger auto-save
|
||||||
|
autoSave();
|
||||||
|
|
||||||
|
this.classList.add('btn-success');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.classList.remove('btn-success');
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== LISTENER AUTO-SAVE + PREVIEW ==========
|
||||||
|
const allInputs = form.querySelectorAll('input, select, textarea');
|
||||||
|
allInputs.forEach(input => {
|
||||||
|
if (input.type === 'checkbox') {
|
||||||
|
input.addEventListener('change', autoSave);
|
||||||
|
} else {
|
||||||
|
input.addEventListener('input', autoSave);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
85
public/userarea/shop_hours_save.php
Normal file
85
public/userarea/shop_hours_save.php
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
error_reporting(0);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
// Verifica utente loggato
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
echo json_encode(['type' => 'danger', 'message' => 'Sessione scaduta']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prendi shop_id
|
||||||
|
$stmt = $pdo->prepare("SELECT id FROM shops WHERE owner_id = ? ORDER BY created_at ASC LIMIT 1");
|
||||||
|
$stmt->execute([$iduserlogin]);
|
||||||
|
$shop = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$shop) {
|
||||||
|
echo json_encode(['type' => 'danger', 'message' => 'Salone non trovato']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shop_id = (int)$shop['id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$days = ['0', '1', '2', '3', '4', '5', '6'];
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Cancella vecchi orari
|
||||||
|
$pdo->prepare("DELETE FROM shop_hours WHERE shop_id = ?")->execute([$shop_id]);
|
||||||
|
|
||||||
|
foreach ($days as $day) {
|
||||||
|
$is_open = isset($_POST["is_open_$day"]) ? 1 : 0;
|
||||||
|
$open_time = !empty($_POST["open_time_$day"]) ? $_POST["open_time_$day"] : null;
|
||||||
|
$close_time = !empty($_POST["close_time_$day"]) ? $_POST["close_time_$day"] : null;
|
||||||
|
$open_time_2 = !empty($_POST["open_time_2_$day"]) ? $_POST["open_time_2_$day"] : null;
|
||||||
|
$close_time_2 = !empty($_POST["close_time_2_$day"]) ? $_POST["close_time_2_$day"] : null;
|
||||||
|
$notes = trim($_POST["notes_$day"] ?? '');
|
||||||
|
|
||||||
|
// Validazione
|
||||||
|
if ($is_open) {
|
||||||
|
if (!$open_time || !$close_time) {
|
||||||
|
throw new Exception("Per i giorni aperti è obbligatorio impostare orario apertura e chiusura.");
|
||||||
|
}
|
||||||
|
if (strtotime($close_time) <= strtotime($open_time)) {
|
||||||
|
throw new Exception("L'orario di chiusura deve essere dopo l'apertura.");
|
||||||
|
}
|
||||||
|
if ($open_time_2 && $close_time_2 && strtotime($close_time_2) <= strtotime($open_time_2)) {
|
||||||
|
throw new Exception("Il secondo orario di chiusura deve essere dopo l'apertura.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO shop_hours
|
||||||
|
(shop_id, day_of_week, is_open, open_time, close_time, open_time_2, close_time_2, notes)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$shop_id,
|
||||||
|
$day,
|
||||||
|
$is_open,
|
||||||
|
$open_time,
|
||||||
|
$close_time,
|
||||||
|
$open_time_2,
|
||||||
|
$close_time_2,
|
||||||
|
$notes ?: null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['type' => 'success', 'message' => 'Orari di apertura salvati con successo!']);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
echo json_encode(['type' => 'danger', 'message' => 'Errore: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
262
public/userarea/user_dashboard.php
Normal file
262
public/userarea/user_dashboard.php
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
<?php
|
||||||
|
// Forza la visualizzazione degli errori (solo dev)
|
||||||
|
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');
|
||||||
|
|
||||||
|
// Connessione DB
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
// Verifica utente loggato
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dati utente
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT first_name, last_name, phone, email, avatar
|
||||||
|
FROM auth_users
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$iduserlogin]);
|
||||||
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
die("Errore: utente non trovato.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_name = htmlspecialchars(trim($user['first_name'] . ' ' . $user['last_name']) ?: 'Cliente');
|
||||||
|
|
||||||
|
// Prossimi appuntamenti (da oggi in poi, max 5)
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT a.id, a.start_at, a.end_at, a.status,
|
||||||
|
s.name AS service_name, s.color_hex AS service_color,
|
||||||
|
st.first_name AS staff_first, st.last_name AS staff_last, st.color_hex AS staff_color,
|
||||||
|
sh.name AS shop_name
|
||||||
|
FROM appointments a
|
||||||
|
LEFT JOIN services s ON a.service_id = s.id
|
||||||
|
LEFT JOIN staff st ON a.staff_id = st.id
|
||||||
|
LEFT JOIN shops sh ON a.shop_id = sh.id
|
||||||
|
WHERE a.customer_id = (SELECT id FROM customers WHERE user_id = ? LIMIT 1)
|
||||||
|
AND DATE(a.start_at) >= ?
|
||||||
|
ORDER BY a.start_at ASC
|
||||||
|
LIMIT 5
|
||||||
|
");
|
||||||
|
$stmt->execute([$iduserlogin, $today]);
|
||||||
|
$future_appts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Ultimi appuntamenti passati (max 5)
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT a.id, a.start_at, a.end_at, a.status,
|
||||||
|
s.name AS service_name, s.color_hex AS service_color,
|
||||||
|
st.first_name AS staff_first, st.last_name AS staff_last, st.color_hex AS staff_color,
|
||||||
|
sh.name AS shop_name
|
||||||
|
FROM appointments a
|
||||||
|
LEFT JOIN services s ON a.service_id = s.id
|
||||||
|
LEFT JOIN staff st ON a.staff_id = st.id
|
||||||
|
LEFT JOIN shops sh ON a.shop_id = sh.id
|
||||||
|
WHERE a.customer_id = (SELECT id FROM customers WHERE user_id = ? LIMIT 1)
|
||||||
|
AND DATE(a.start_at) < ?
|
||||||
|
ORDER BY a.start_at DESC
|
||||||
|
LIMIT 5
|
||||||
|
");
|
||||||
|
$stmt->execute([$iduserlogin, $today]);
|
||||||
|
$past_appts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Helpers flash (copia-incolla esattamente qui)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
// Flash
|
||||||
|
$flash = getFlash();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!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>La Mia Dashboard</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<?php include('include/navbar.php'); ?>
|
||||||
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="page-content">
|
||||||
|
|
||||||
|
<!-- Benvenuto utente -->
|
||||||
|
<div class="card radius-10 mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center flex-wrap gap-4">
|
||||||
|
<div>
|
||||||
|
<img src="../upload/users/<?= htmlspecialchars($user['avatar'] ?: 'assets/images/default-user.png') ?>"
|
||||||
|
alt="Avatar" class="rounded-circle" style="width:90px;height:90px;object-fit:cover;border:3px solid #e5e7eb;">
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h4 class="mb-1">Ciao, <?= $user_name ?>!</h4>
|
||||||
|
<p class="mb-1 text-muted">
|
||||||
|
<i class="bx bx-phone me-1"></i> <?= htmlspecialchars($user['phone'] ?: '—') ?><br>
|
||||||
|
<i class="bx bx-envelope me-1"></i> <?= htmlspecialchars($user['email']) ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<a href="profile.php" class="btn btn-warning">
|
||||||
|
<i class="bx bx-edit me-1"></i> Modifica Profilo
|
||||||
|
</a>
|
||||||
|
<a href="user_settings.php" class="btn btn-info">
|
||||||
|
<i class="bx bx-bell me-1"></i> Notifiche
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pulsanti rapidi -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex flex-wrap justify-content-center gap-3">
|
||||||
|
<a href="book_appointment.php" class="btn btn-success px-5 py-3 shadow-sm">
|
||||||
|
<i class="bx bx-calendar-plus bx-lg me-2"></i> Prenota Appuntamento
|
||||||
|
</a>
|
||||||
|
<a href="my_appointments.php" class="btn btn-primary px-5 py-3 shadow-sm">
|
||||||
|
<i class="bx bx-calendar-check bx-lg me-2"></i> I Miei Appuntamenti
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Prossimi appuntamenti -->
|
||||||
|
<div class="card radius-10 mb-4">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">Prossimi Appuntamenti</h6>
|
||||||
|
<a href="my_appointments.php?filter=future" class="btn btn-outline-primary btn-sm">
|
||||||
|
Vedi tutti <i class="bx bx-right-arrow-alt ms-1"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (empty($future_appts)): ?>
|
||||||
|
<div class="alert alert-info text-center py-4 mb-0">
|
||||||
|
<i class="bx bx-calendar-x bx-lg mb-3 d-block"></i>
|
||||||
|
Non hai appuntamenti futuri.<br>
|
||||||
|
<a href="book_appointment.php" class="alert-link">Prenota il tuo prossimo taglio!</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="row g-3">
|
||||||
|
<?php foreach ($future_appts as $a):
|
||||||
|
$status_class = match ($a['status']) {
|
||||||
|
'confirmed' => 'bg-success',
|
||||||
|
'pending' => 'bg-warning',
|
||||||
|
'cancelled' => 'bg-danger',
|
||||||
|
'no_show' => 'bg-secondary',
|
||||||
|
default => 'bg-info'
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<h6 class="card-title mb-0 fw-bold">
|
||||||
|
<?= htmlspecialchars($a['service_name']) ?>
|
||||||
|
</h6>
|
||||||
|
<span class="badge <?= $status_class ?>">
|
||||||
|
<?= ucfirst($a['status']) ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted mb-2">
|
||||||
|
<?= date('d/m/Y H:i', strtotime($a['start_at'])) ?> - <?= date('H:i', strtotime($a['end_at'])) ?>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<span class="badge rounded-pill" style="background: <?= htmlspecialchars($a['staff_color'] ?? '#6c757d') ?>">
|
||||||
|
<?= htmlspecialchars($a['staff_first'] . ' ' . substr($a['staff_last'], 0, 1) . '.') ?>
|
||||||
|
</span>
|
||||||
|
<small class="text-muted">presso <?= htmlspecialchars($a['shop_name']) ?></small>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bx bx-phone me-1"></i> <?= htmlspecialchars($a['customer_phone'] ?? '—') ?>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ultimi appuntamenti passati -->
|
||||||
|
<div class="card radius-10">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">Ultimi Appuntamenti</h6>
|
||||||
|
<a href="my_appointments.php?filter=past" class="btn btn-outline-secondary btn-sm">
|
||||||
|
Vedi tutti <i class="bx bx-right-arrow-alt ms-1"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (empty($past_appts)): ?>
|
||||||
|
<div class="text-center text-muted py-4">
|
||||||
|
Non hai ancora appuntamenti passati.
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<?php foreach ($past_appts as $a):
|
||||||
|
$status_class = match ($a['status']) {
|
||||||
|
'completed' => 'bg-success',
|
||||||
|
'cancelled' => 'bg-danger',
|
||||||
|
'no_show' => 'bg-secondary',
|
||||||
|
default => 'bg-info'
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<a href="appointment_detail.php?id=<?= $a['id'] ?>" class="list-group-item list-group-item-action">
|
||||||
|
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1 fw-bold"><?= htmlspecialchars($a['service_name']) ?></h6>
|
||||||
|
<small class="text-muted">
|
||||||
|
<?= htmlspecialchars($a['staff_first'] . ' ' . $a['staff_last']) ?> • <?= date('d/m/Y', strtotime($a['start_at'])) ?>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<span class="badge <?= $status_class ?>">
|
||||||
|
<?= ucfirst($a['status']) ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user