added appointments

This commit is contained in:
Claudio 2026-02-01 20:37:49 +01:00
parent 75d63261bb
commit b647fe4934
9 changed files with 2451 additions and 50 deletions

View 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>

View 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;
}

View File

@ -68,17 +68,12 @@
</a>
</li>
<li>
<a href="shop.php">
<div class="parent-icon"><i class="bx bx-store"></i></div>
<div class="menu-title">Prodotti & Servizi</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 href="my_favourites.php">
<div class="parent-icon"><i class="bx bx-calendar-check"></i></div>
<div class="menu-title">I miei saloni preferiti</div>
</a>
</li>
<li>
<a href="profile.php">
<div class="parent-icon"><i class="bx bx-user"></i></div>
@ -138,6 +133,12 @@
<div class="menu-title">Prodotti</div>
</a>
</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>
<a href="day_off.php">
<div class="parent-icon"><i class="bx bx-calendar-x"></i></div>

386
public/userarea/profile.php Normal file
View 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>

View File

@ -85,7 +85,6 @@ $stmt = $pdo->prepare("
a.end_at,
a.status,
a.notes,
a.price_paid,
c.first_name AS customer_first,
c.last_name AS customer_last,
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="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="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>

View File

@ -63,17 +63,27 @@ function getFlash(): ?array
// POST - Salva impostazioni
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$show_prices_online = isset($_POST['show_prices_online']) ? 1 : 0;
$restrict_start_minutes = trim($_POST['restrict_start_minutes'] ?? '00,30');
$min_booking_notice_hours = (int)($_POST['min_booking_notice_hours'] ?? 2);
$max_booking_days_ahead = (int)($_POST['max_booking_days_ahead'] ?? 90);
$show_prices_online = isset($_POST['show_prices_online']) ? 1 : 0;
$appointment_slot_interval = (int)($_POST['appointment_slot_interval'] ?? 30);
$allow_same_time_multiple = isset($_POST['allow_same_time_multiple']) ? 1 : 0;
$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
$restrict_start_minutes = trim($_POST['restrict_start_minutes'] ?? '00,30');
$valid_options = ['any', '00', '00,30', '00,15,30,45'];
if (!in_array($restrict_start_minutes, $valid_options)) {
$restrict_start_minutes = '00,30';
}
// Validazione intervallo slot
if ($appointment_slot_interval < 5 || $appointment_slot_interval > 120) {
$appointment_slot_interval = 30;
}
// Controlla esistenza
$stmt = $pdo->prepare("SELECT id FROM shop_settings WHERE shop_id = ?");
$stmt->execute([$shop_id]);
@ -83,32 +93,49 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = $pdo->prepare("
UPDATE shop_settings SET
show_prices_online = ?,
allowed_start_minutes = ?,
appointment_slot_interval = ?,
allow_same_time_multiple = ?,
min_booking_notice_hours = ?,
max_booking_days_ahead = ?,
allowed_start_minutes = ?,
require_appointment_confirmation = ?,
no_show_warning_after = ?,
no_show_block_after = ?,
updated_at = NOW()
WHERE shop_id = ?
");
$ok = $stmt->execute([
$show_prices_online,
$restrict_start_minutes,
$appointment_slot_interval,
$allow_same_time_multiple,
$min_booking_notice_hours,
$max_booking_days_ahead,
$restrict_start_minutes,
$require_appointment_confirmation,
$no_show_warning_after,
$no_show_block_after,
$shop_id
]);
} else {
$stmt = $pdo->prepare("
INSERT INTO shop_settings (
shop_id, show_prices_online, allowed_start_minutes,
min_booking_notice_hours, max_booking_days_ahead
) VALUES (?, ?, ?, ?, ?)
shop_id, show_prices_online, appointment_slot_interval,
allow_same_time_multiple, min_booking_notice_hours, max_booking_days_ahead,
allowed_start_minutes, require_appointment_confirmation,
no_show_warning_after, no_show_block_after
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$ok = $stmt->execute([
$shop_id,
$show_prices_online,
$restrict_start_minutes,
$appointment_slot_interval,
$allow_same_time_multiple,
$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->execute([$shop_id]);
$settings = $stmt->fetch(PDO::FETCH_ASSOC) ?: [
'show_prices_online' => 1,
'allowed_start_minutes' => '00,30',
'min_booking_notice_hours' => 2,
'max_booking_days_ahead' => 90
'show_prices_online' => 1,
'appointment_slot_interval' => 30,
'allow_same_time_multiple' => 0,
'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();
@ -225,6 +257,34 @@ $flash = getFlash();
transform: translateY(-2px);
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>
</head>
@ -253,43 +313,45 @@ $flash = getFlash();
<form action="" method="POST">
<!-- Visibilità prezzi -->
<div class="form-group">
<div class="d-flex align-items-center justify-content-between">
<label class="form-label mb-0" for="showPrices">Mostra i prezzi online ai clienti</label>
<div class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" name="show_prices_online" id="showPrices" <?= $settings['show_prices_online'] ? 'checked' : '' ?>>
</div>
<div class="toggle-group">
<label class="toggle-label" for="showPrices">Mostra i prezzi online ai clienti</label>
<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' : '' ?>>
</div>
<div class="form-text">
I clienti vedranno i prezzi durante la prenotazione online.
</div>
<!-- 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>
<!-- Orari inizio -->
<div class="form-group">
<div class="form-group mt-4">
<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">
<option value="any" <?= $settings['allowed_start_minutes'] === 'any' ? 'selected' : '' ?>>
Qualsiasi minuto (libero)
</option>
<option value="00" <?= $settings['allowed_start_minutes'] === '00' ? 'selected' : '' ?>>
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>
<option value="any" <?= $settings['allowed_start_minutes'] === 'any' ? 'selected' : '' ?>>Qualsiasi minuto (libero)</option>
<option value="00" <?= $settings['allowed_start_minutes'] === '00' ? 'selected' : '' ?>>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>
<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>
</div>
<!-- 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">
<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"
@ -303,11 +365,38 @@ $flash = getFlash();
value="<?= (int)$settings['max_booking_days_ahead'] ?>">
<div class="form-text">Es: 90 = massimo 90 giorni da oggi.</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>
<!-- 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">
<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>
</div>
</form>

View 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>

View 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()]);
}

View 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>