diff --git a/public/userarea/appointments.php b/public/userarea/appointments.php new file mode 100644 index 0000000..937f4f1 --- /dev/null +++ b/public/userarea/appointments.php @@ -0,0 +1,805 @@ +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')); +?> + + + + + + + + + + Appuntamenti - <?= e($shop_name) ?> + + + + + +
+ + + +
+
+ +
+
+
+ +
+
+ Appuntamenti • +
+ + + + + + + + + + Tutti i parrucchieri + + +
+ + +
+
+ + + + +
+ + +
+ + + + +
+ + + Oggi + + + +
+ +
+
+ +
+ + + + +
+ +
+
+ + Slot da min • :00–:00 +
+ +
+ Confermato + In attesa + Annullato + No-show + Completato +
+
+ +
+ 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]; + } + ?> +
+
+ +
+ + +
+ +
+ + +
+ Slot libero + +
+ + +
+
+
+
+
+
+ + + + + +
+ +
+ + + ID +
+ + +
+ +
120 ? mb_substr($notes, 0, 120) . '…' : $notes) ?>
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/userarea/create_appointments.php b/public/userarea/create_appointments.php new file mode 100644 index 0000000..44badd6 --- /dev/null +++ b/public/userarea/create_appointments.php @@ -0,0 +1,155 @@ +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; +} diff --git a/public/userarea/include/navbar.php b/public/userarea/include/navbar.php index 7b85382..d7bc5e7 100644 --- a/public/userarea/include/navbar.php +++ b/public/userarea/include/navbar.php @@ -68,17 +68,12 @@
  • - -
    - -
    -
  • -
  • - -
    - +
    +
    +
  • +
  • @@ -138,6 +133,12 @@
  • +
  • + +
    + +
    +
  • diff --git a/public/userarea/profile.php b/public/userarea/profile.php new file mode 100644 index 0000000..b9b8735 --- /dev/null +++ b/public/userarea/profile.php @@ -0,0 +1,386 @@ +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(); +?> + + + + + + + + + + + Il Mio Profilo + + + + +
    + + + +
    +
    +
    + + +
    + + + + +
    + +
    +
    + + +
    + Avatar + + JPG, PNG, HEIC/HEIF (max 5MB) +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + +
    +
    +
    + + +
    +
    +
    +
    Modifica Password
    +
    +
    +
    + + +
    + + +
    + +
    + + + Minimo 8 caratteri +
    + +
    + + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    + + + + + + + \ No newline at end of file diff --git a/public/userarea/salon_dashboard.php b/public/userarea/salon_dashboard.php index fc07c24..606fda6 100644 --- a/public/userarea/salon_dashboard.php +++ b/public/userarea/salon_dashboard.php @@ -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(); Appuntamenti Staff Incassi - Nuovo Appuntamento diff --git a/public/userarea/salon_settings.php b/public/userarea/salon_settings.php index 58b7f32..a58fe58 100644 --- a/public/userarea/salon_settings.php +++ b/public/userarea/salon_settings.php @@ -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; + } @@ -253,43 +313,45 @@ $flash = getFlash();
    -
    -
    - -
    - > -
    +
    + +
    + >
    -
    - I clienti vedranno i prezzi durante la prenotazione online. +
    + + +
    + +
    + >
    -
    +
    - Anteprima slot (es. dalle 9:00): + Anteprima slot orari (es. dalle 9:00):
    -
    +
    +
    + + +
    Es: 30 = slot ogni mezz'ora
    +
    +
    Es: 90 = massimo 90 giorni da oggi.
    + +
    + +
    + > +
    +
    Utile se più parrucchieri possono lavorare contemporaneamente sullo stesso slot.
    +
    + +
    Gestione No-Show (assenze)
    +
    +
    + + +
    0 = disattivato. Es: 3 = dopo 3 no-show invia alert al titolare.
    +
    + +
    + + +
    0 = mai bloccare. Es: 5 = dopo 5 no-show impedisce nuove prenotazioni.
    +
    +
    + +
    diff --git a/public/userarea/shop_hours.php b/public/userarea/shop_hours.php new file mode 100644 index 0000000..bec5c8a --- /dev/null +++ b/public/userarea/shop_hours.php @@ -0,0 +1,620 @@ +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(); +?> + + + + + + + + + + + Orari di Apertura - <?= htmlspecialchars($shop_name) ?> + + + + +
    + + + +
    +
    +
    +
    +
    Orari di Apertura -
    + + Dashboard + +
    + +
    + + + + +
    + $name): + $h = $hours[$d]; + ?> +
    +
    + +
    + > + +
    + +
    + + - + +
    + +
    + + - + +
    + + + + +
    + + +
    +
    + + +
    + +
    +
    + + +
    +
    Anteprima Settimanale
    + $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'] : '')) + : '—'; + ?> +
    + + + + +
    + +
    +
    +
    +
    +
    + + +
    + + + + + + + \ No newline at end of file diff --git a/public/userarea/shop_hours_save.php b/public/userarea/shop_hours_save.php new file mode 100644 index 0000000..9a97606 --- /dev/null +++ b/public/userarea/shop_hours_save.php @@ -0,0 +1,85 @@ +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()]); +} diff --git a/public/userarea/user_dashboard.php b/public/userarea/user_dashboard.php new file mode 100644 index 0000000..e3d222d --- /dev/null +++ b/public/userarea/user_dashboard.php @@ -0,0 +1,262 @@ +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(); +?> + + + + + + + + + + + La Mia Dashboard + + + +
    + + + +
    +
    + + +
    +
    +
    +
    + Avatar +
    +
    +

    Ciao, !

    +

    +
    + +

    +
    + +
    +
    +
    + + + + + +
    +
    +
    Prossimi Appuntamenti
    + + Vedi tutti + +
    +
    + +
    + + Non hai appuntamenti futuri.
    + Prenota il tuo prossimo taglio! +
    + +
    + 'bg-success', + 'pending' => 'bg-warning', + 'cancelled' => 'bg-danger', + 'no_show' => 'bg-secondary', + default => 'bg-info' + }; + ?> +
    +
    +
    +
    +
    + +
    + + + +
    +
    + - +
    +
    + + + + presso +
    + + + +
    +
    +
    + +
    + +
    +
    + + +
    +
    +
    Ultimi Appuntamenti
    + + Vedi tutti + +
    +
    + +
    + Non hai ancora appuntamenti passati. +
    + +
    + 'bg-success', + 'cancelled' => 'bg-danger', + 'no_show' => 'bg-secondary', + default => 'bg-info' + }; + ?> + +
    +
    +
    + + + +
    + + + +
    +
    + +
    + +
    +
    + +
    +
    + + +
    + + + + + \ No newline at end of file