This commit is contained in:
Claudio 2026-01-27 14:53:37 +01:00
parent bd9d3811f6
commit 75d63261bb
7 changed files with 2702 additions and 202 deletions

View File

@ -4,6 +4,10 @@ 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
@ -19,7 +23,7 @@ if (!isset($iduserlogin)) {
// Controlla se esiste almeno un salone
$stmt = $pdo->prepare("SELECT COUNT(*) FROM shops WHERE owner_id = ?");
$stmt->execute([$iduserlogin]);
if ($stmt->fetchColumn() === 0) {
if ((int)$stmt->fetchColumn() === 0) {
header("Location: onboarding_salon.php");
exit;
}
@ -39,66 +43,150 @@ if (!$shop) {
die("Errore: salone non trovato.");
}
$shop_id = $shop['id'];
$shop_id = (int)$shop['id'];
$shop_name = $shop['name'];
// =========================================================================
// Helpers
// =========================================================================
function isValidDateYmd(string $date): bool {
$d = DateTime::createFromFormat('Y-m-d', $date);
return $d && $d->format('Y-m-d') === $date;
}
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;
}
// =========================================================================
// Gestione POST (add / edit / delete)
// =========================================================================
$success_message = '';
$error_message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
if ($action === 'add' || $action === 'edit') {
try {
if ($action === 'add') {
$start_date = trim($_POST['start_date'] ?? '');
$end_date = trim($_POST['end_date'] ?? '');
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
$id = ($action === 'edit') ? (int)($_POST['id'] ?? 0) : 0;
// Validazioni
if (empty($start_date) || empty($end_date)) {
$error_message = "Le date di inizio e fine sono obbligatorie.";
if ($start_date === '' || $end_date === '') {
setFlash('danger', "Le date di inizio e fine sono obbligatorie.");
} elseif (!isValidDateYmd($start_date) || !isValidDateYmd($end_date)) {
setFlash('danger', "Formato data non valido.");
} elseif (strtotime($end_date) < strtotime($start_date)) {
$error_message = "La data di fine non può essere precedente alla data di inizio.";
setFlash('danger', "La data di fine non può essere precedente alla data di inizio.");
} else {
if ($action === 'add') {
$pdo->beginTransaction();
// Inserisco una riga per ogni giorno del range (B2)
$stmt = $pdo->prepare("
INSERT INTO shop_day_off (shop_id, date, title, description, is_recurring, created_at)
VALUES (?, ?, ?, ?, 0, NOW())
INSERT INTO shop_day_off (shop_id, date, title, description, is_recurring)
VALUES (?, ?, ?, ?, 0)
ON DUPLICATE KEY UPDATE
title = VALUES(title),
description = VALUES(description),
updated_at = CURRENT_TIMESTAMP
");
$ok = $stmt->execute([$shop_id, $start_date, $description ?: 'Chiusura', $description]);
$success_message = $ok ? "Giorno di chiusura aggiunto!" : "Errore durante l'aggiunta.";
} else { // edit
$start = new DateTime($start_date);
$end = new DateTime($end_date);
$end->setTime(0,0,0);
// +1 giorno per includere la fine
$period = new DatePeriod($start, new DateInterval('P1D'), (clone $end)->modify('+1 day'));
$inserted = 0;
foreach ($period as $dt) {
$day = $dt->format('Y-m-d');
$ok = $stmt->execute([
$shop_id,
$day,
$title !== '' ? $title : 'Chiusura',
$description
]);
if ($ok) $inserted++;
}
$pdo->commit();
setFlash('success', "Chiusura aggiunta: salvati/aggiornati {$inserted} giorni.");
}
header("Location: day_off.php");
exit;
}
if ($action === 'edit') {
$id = (int)($_POST['id'] ?? 0);
$date = trim($_POST['start_date'] ?? ''); // nel form edit usiamo start_date come "data singola"
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
if ($id <= 0) {
setFlash('danger', "ID non valido.");
} elseif ($date === '' || !isValidDateYmd($date)) {
setFlash('danger', "La data è obbligatoria e deve essere valida.");
} else {
$stmt = $pdo->prepare("
UPDATE shop_day_off
SET date = ?, title = ?, description = ?, updated_at = NOW()
WHERE id = ? AND shop_id = ?
");
$ok = $stmt->execute([$start_date, $description ?: 'Chiusura', $description, $id, $shop_id]);
$success_message = $ok ? "Giorno di chiusura aggiornato!" : "Errore durante l'aggiornamento.";
}
$ok = $stmt->execute([
$date,
$title !== '' ? $title : 'Chiusura',
$description,
$id,
$shop_id
]);
setFlash($ok ? 'success' : 'danger', $ok ? "Giorno di chiusura aggiornato!" : "Errore durante l'aggiornamento.");
}
header("Location: day_off.php");
exit;
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
if ($id <= 0) {
setFlash('danger', "ID non valido.");
} else {
$stmt = $pdo->prepare("DELETE FROM shop_day_off WHERE id = ? AND shop_id = ?");
$ok = $stmt->execute([$id, $shop_id]);
$success_message = $ok ? "Giorno di chiusura eliminato!" : "Errore durante l'eliminazione.";
}
setFlash($ok ? 'success' : 'danger', $ok ? "Giorno di chiusura eliminato!" : "Errore durante l'eliminazione.");
}
// Evita doppio submit
if ($success_message || $error_message) {
header("Location: day_off.php" . ($success_message ? "?msg=success" : "?msg=error"));
header("Location: day_off.php");
exit;
}
// action sconosciuta
setFlash('danger', "Azione non valida.");
header("Location: day_off.php");
exit;
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
setFlash('danger', "Errore: " . $e->getMessage());
header("Location: day_off.php");
exit;
}
}
// =========================================================================
// Recupera tutti i giorni di chiusura
// =========================================================================
$stmt = $pdo->prepare("
SELECT id, date, title, description, is_recurring
FROM shop_day_off
@ -106,12 +194,13 @@ $stmt = $pdo->prepare("
ORDER BY date ASC
");
$stmt->execute([$shop_id]);
$days_off = $stmt->fetchAll();
$days_off = $stmt->fetchAll(PDO::FETCH_ASSOC);
$flash = getFlash();
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -142,18 +231,11 @@ $days_off = $stmt->fetchAll();
</div>
<div class="card-body">
<?php if (isset($_GET['msg'])): ?>
<?php if ($_GET['msg'] === 'success'): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= htmlspecialchars($success_message) ?>
<?php if ($flash): ?>
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> alert-dismissible fade show" role="alert">
<?= htmlspecialchars($flash['text']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php elseif ($_GET['msg'] === 'error'): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= htmlspecialchars($error_message) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if (empty($days_off)): ?>
@ -176,9 +258,9 @@ $days_off = $stmt->fetchAll();
<?php foreach ($days_off as $day): ?>
<tr>
<td><?= htmlspecialchars($day['date']) ?></td>
<td><?= htmlspecialchars($day['title'] ?: $day['description'] ?: 'Chiusura') ?></td>
<td><?= htmlspecialchars(($day['title'] ?: '') . (($day['description'] ?? '') ? ' — ' . $day['description'] : '')) ?: 'Chiusura' ?></td>
<td>
<?php if ($day['is_recurring']): ?>
<?php if (!empty($day['is_recurring'])): ?>
<span class="badge bg-success"> (ogni anno)</span>
<?php else: ?>
<span class="badge bg-secondary">No</span>
@ -188,18 +270,18 @@ $days_off = $stmt->fetchAll();
<button type="button" class="btn btn-sm btn-warning me-1"
data-bs-toggle="modal" data-bs-target="#editDayOffModal"
onclick='fillEditModal(<?= json_encode([
"id" => $day['id'],
"date" => $day['date'],
"title" => htmlspecialchars($day['title'] ?? '', ENT_QUOTES),
"description" => htmlspecialchars($day['description'] ?? '', ENT_QUOTES)
]) ?>)'>
"id" => (int)$day["id"],
"date" => $day["date"],
"title" => $day["title"] ?? "",
"description" => $day["description"] ?? ""
], JSON_HEX_APOS | JSON_HEX_QUOT) ?>)'>
<i class="bx bx-edit"></i> Modifica
</button>
<form action="" method="POST" style="display:inline;"
onsubmit="return confirm('Confermi l\'eliminazione?');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= $day['id'] ?>">
<input type="hidden" name="id" value="<?= (int)$day['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">
<i class="bx bx-trash"></i> Elimina
</button>
@ -224,20 +306,37 @@ $days_off = $stmt->fetchAll();
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="addDayOffModalLabel">Aggiungi Giorno di Chiusura</h5>
<h5 class="modal-title" id="addDayOffModalLabel">Aggiungi Giorni di Chiusura</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="" method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="add">
<div class="mb-3">
<label class="form-label fw-bold">Data <span class="text-danger">*</span></label>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Dal <span class="text-danger">*</span></label>
<input type="date" class="form-control" name="start_date" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Al <span class="text-danger">*</span></label>
<input type="date" class="form-control" name="end_date" required>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Descrizione / Motivo</label>
<input type="text" class="form-control" name="description"
placeholder="Es: Ferragosto, Ferie estive, Corso formazione">
<label class="form-label fw-bold">Titolo (es. Ferie estive)</label>
<input type="text" class="form-control" name="title" placeholder="Titolo breve">
</div>
<div class="mb-3">
<label class="form-label fw-bold">Descrizione / Note</label>
<textarea class="form-control" name="description" rows="3"
placeholder="Dettagli (es. chiuso tutto il giorno per ferie)"></textarea>
</div>
<div class="alert alert-secondary mb-0">
Verrà salvata una riga per ogni giorno del range (così blocchi facilmente le prenotazioni).
</div>
</div>
<div class="modal-footer">
@ -249,7 +348,7 @@ $days_off = $stmt->fetchAll();
</div>
</div>
<!-- Modal Modifica -->
<!-- Modal Modifica (singolo giorno) -->
<div class="modal fade" id="editDayOffModal" tabindex="-1" aria-labelledby="editDayOffModalLabel">
<div class="modal-dialog">
<div class="modal-content">
@ -261,10 +360,17 @@ $days_off = $stmt->fetchAll();
<div class="modal-body">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="id" id="edit_id">
<div class="mb-3">
<label class="form-label fw-bold">Data <span class="text-danger">*</span></label>
<input type="date" class="form-control" name="start_date" id="edit_date" required>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Titolo</label>
<input type="text" class="form-control" name="title" id="edit_title" placeholder="Es: Chiusura straordinaria">
</div>
<div class="mb-3">
<label class="form-label fw-bold">Descrizione / Motivo</label>
<input type="text" class="form-control" name="description" id="edit_description"
@ -285,21 +391,17 @@ $days_off = $stmt->fetchAll();
<script>
$(document).ready(function () {
$('#daysOffTable').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
},
order: [
[0, 'asc']
]
language: { url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json' },
order: [[0, 'asc']]
});
});
function fillEditModal(data) {
document.getElementById('edit_id').value = data.id;
document.getElementById('edit_date').value = data.date;
document.getElementById('edit_title').value = data.title || '';
document.getElementById('edit_description').value = data.description || '';
}
</script>
</body>
</html>

View File

@ -85,6 +85,12 @@
<div class="menu-title">Il mio profilo</div>
</a>
</li>
<li>
<a href="user_settings.php">
<div class="parent-icon"><i class="bx bx-user"></i></div>
<div class="menu-title">Impostazioni utente</div>
</a>
</li>
<?php endif; ?>
<!-- Titolare Salone -->

View File

@ -0,0 +1,552 @@
<?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 salone
$stmt = $pdo->prepare("
SELECT *
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'];
// ===========================
// Helper: Flash messages
// ===========================
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;
}
// ===========================
// Helper: Ridimensiona immagine (come nel tuo template)
// ===========================
function resizeImage($source_path, $dest_path, $max_width = 800)
{
list($width, $height, $type) = getimagesize($source_path);
if ($width <= $max_width) {
copy($source_path, $dest_path);
return;
}
$new_width = $max_width;
$new_height = (int)(($height * $new_width) / $width);
switch ($type) {
case IMAGETYPE_JPEG:
$source = imagecreatefromjpeg($source_path);
break;
case IMAGETYPE_PNG:
$source = imagecreatefrompng($source_path);
break;
case IMAGETYPE_GIF:
$source = imagecreatefromgif($source_path);
break;
default:
throw new Exception("Formato non supportato.");
}
$dest = imagecreatetruecolor($new_width, $new_height);
if ($type == IMAGETYPE_PNG) {
imagealphablending($dest, false);
imagesavealpha($dest, true);
}
imagecopyresampled($dest, $source, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
switch ($type) {
case IMAGETYPE_JPEG:
imagejpeg($dest, $dest_path, 90);
break;
case IMAGETYPE_PNG:
imagepng($dest, $dest_path);
break;
case IMAGETYPE_GIF:
imagegif($dest, $dest_path);
break;
}
imagedestroy($source);
imagedestroy($dest);
}
// ===========================
// Helper: Geolocalizzazione con Nominatim
// ===========================
function geocodeAddress(string $address): array
{
$url = 'https://nominatim.openstreetmap.org/search';
$params = http_build_query([
'q' => $address,
'format' => 'json',
'limit' => 1,
'addressdetails' => 1,
'countrycodes' => 'it',
]);
$ch = curl_init($url . '?' . $params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'HairBook Salon Manager/1.0 (contact: info@hairbook.it)');
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$response) {
return ['success' => false, 'message' => 'Errore connessione Nominatim'];
}
$data = json_decode($response, true);
if (empty($data) || !isset($data[0])) {
return ['success' => false, 'message' => 'Indirizzo non trovato'];
}
$result = $data[0];
return [
'success' => true,
'lat' => (float)$result['lat'],
'lon' => (float)$result['lon'],
'display' => $result['display_name'] ?? ''
];
}
// ===========================
// POST - Salva profilo + geocode se necessario
// ===========================
$flash = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !isset($_POST['action'])) { // normale submit form
try {
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '');
$address = trim($_POST['address'] ?? '');
$address_extra = trim($_POST['address_extra'] ?? '');
$city = trim($_POST['city'] ?? '');
$province = strtoupper(trim($_POST['province'] ?? ''));
$zip_code = trim($_POST['zip_code'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$mobile = trim($_POST['mobile'] ?? '');
$email = trim($_POST['email'] ?? '');
$website = trim($_POST['website'] ?? '');
$instagram = trim(ltrim($_POST['instagram'] ?? '', '@'));
$facebook = trim($_POST['facebook'] ?? '');
$google_maps_url = trim($_POST['google_maps_url'] ?? '');
$latitude = !empty($_POST['latitude']) ? (float)$_POST['latitude'] : null;
$longitude = !empty($_POST['longitude']) ? (float)$_POST['longitude'] : null;
$active = isset($_POST['active']) ? 1 : 0;
// Validazioni
if ($name === '') {
throw new Exception("Nome salone obbligatorio.");
}
if ($city === '') {
throw new Exception("Città obbligatoria.");
}
// Tenta geocode automatico se lat/lon vuoti
if ($latitude === null || $longitude === null) {
$full_address = implode(', ', array_filter([
$address,
$address_extra,
$zip_code,
$city,
$province ? $province : '',
'Italia'
]));
$geocode = geocodeAddress($full_address);
if ($geocode['success']) {
$latitude = $geocode['lat'];
$longitude = $geocode['lon'];
setFlash('info', "Coordinate calcolate automaticamente!");
}
}
// Upload logo
$logo = $shop['logo'];
if (isset($_FILES['logo']) && $_FILES['logo']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['logo'];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif'])) {
$new_filename = "logos/{$shop_id}-" . time() . "." . $ext;
resizeImage($file['tmp_name'], $new_filename, 400);
if ($logo && file_exists($logo)) @unlink($logo);
$logo = $new_filename;
}
}
// Upload cover
$cover = $shop['cover_image'];
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['cover_image'];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif'])) {
$new_filename = "covers/{$shop_id}-" . time() . "." . $ext;
resizeImage($file['tmp_name'], $new_filename, 1200);
if ($cover && file_exists($cover)) @unlink($cover);
$cover = $new_filename;
}
}
// Update DB
$stmt = $pdo->prepare("
UPDATE shops SET
name = ?, description = ?, address = ?, address_extra = ?, city = ?, province = ?,
zip_code = ?, phone = ?, mobile = ?, email = ?, website = ?, instagram = ?, facebook = ?,
google_maps_url = ?, latitude = ?, longitude = ?, logo = ?, cover_image = ?, active = ?,
updated_at = NOW()
WHERE id = ? AND owner_id = ?
");
$ok = $stmt->execute([
$name,
$description ?: null,
$address,
$address_extra ?: null,
$city,
$province,
$zip_code,
$phone ?: null,
$mobile ?: null,
$email ?: null,
$website ?: null,
$instagram ?: null,
$facebook ?: null,
$google_maps_url ?: null,
$latitude,
$longitude,
$logo,
$cover,
$active,
$shop_id,
$iduserlogin
]);
setFlash($ok ? 'success' : 'danger', $ok ? "Profilo aggiornato!" : "Errore salvataggio.");
header("Location: salon_profile.php");
exit;
} catch (Exception $e) {
setFlash('danger', $e->getMessage());
header("Location: salon_profile.php");
exit;
}
}
// 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'); ?>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<title>Profilo Salone - <?= htmlspecialchars($shop['name']) ?></title>
<style>
#map {
height: 350px;
border: 1px solid #dee2e6;
border-radius: 8px;
margin-top: 10px;
}
.form-label {
font-weight: 600;
}
.input-group-text {
background: #f8f9fa;
}
</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">Modifica Profilo Salone</h6>
<a href="salon_dashboard.php" class="btn btn-outline-secondary">
<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">
<?= htmlspecialchars($flash['text']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<form action="" method="POST" enctype="multipart/form-data">
<div class="row g-4">
<!-- Colonna sinistra: dati principali -->
<div class="col-lg-8">
<div class="mb-3">
<label class="form-label">Nome Salone <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name" value="<?= htmlspecialchars($shop['name']) ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Descrizione</label>
<textarea class="form-control" name="description" rows="4"><?= htmlspecialchars($shop['description'] ?? '') ?></textarea>
</div>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">Indirizzo <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="address" id="address" value="<?= htmlspecialchars($shop['address']) ?>" required>
</div>
<div class="col-md-4">
<label class="form-label">Indirizzo extra</label>
<input type="text" class="form-control" name="address_extra" value="<?= htmlspecialchars($shop['address_extra'] ?? '') ?>">
</div>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Città <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="city" id="city" value="<?= htmlspecialchars($shop['city']) ?>" required>
</div>
<div class="col-md-3">
<label class="form-label">Provincia</label>
<input type="text" class="form-control text-uppercase" name="province" maxlength="2" value="<?= htmlspecialchars($shop['province'] ?? '') ?>">
</div>
<div class="col-md-3">
<label class="form-label">CAP</label>
<input type="text" class="form-control" name="zip_code" value="<?= htmlspecialchars($shop['zip_code']) ?>">
</div>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Telefono</label>
<input type="text" class="form-control" name="phone" value="<?= htmlspecialchars($shop['phone'] ?? '') ?>">
</div>
<div class="col-md-6">
<label class="form-label">Cellulare / WhatsApp</label>
<input type="text" class="form-control" name="mobile" value="<?= htmlspecialchars($shop['mobile'] ?? '') ?>">
</div>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="email" value="<?= htmlspecialchars($shop['email'] ?? '') ?>">
</div>
<div class="col-md-6">
<label class="form-label">Sito Web</label>
<input type="url" class="form-control" name="website" value="<?= htmlspecialchars($shop['website'] ?? '') ?>">
</div>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Instagram (solo username)</label>
<div class="input-group">
<span class="input-group-text">@</span>
<input type="text" class="form-control" name="instagram" value="<?= htmlspecialchars($shop['instagram'] ?? '') ?>">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Facebook</label>
<input type="text" class="form-control" name="facebook" value="<?= htmlspecialchars($shop['facebook'] ?? '') ?>">
</div>
</div>
<div class="mb-3">
<label class="form-label">Link Google Maps (opzionale)</label>
<input type="url" class="form-control" name="google_maps_url" value="<?= htmlspecialchars($shop['google_maps_url'] ?? '') ?>">
</div>
</div>
<!-- Colonna destra: immagini + mappa -->
<div class="col-lg-4">
<div class="mb-3">
<label class="form-label">Logo Salone (quadrato)</label>
<?php if ($shop['logo']): ?>
<img src="<?= htmlspecialchars($shop['logo']) ?>" alt="Logo" class="img-fluid rounded mb-2" style="max-height:120px;">
<?php endif; ?>
<input type="file" class="form-control" name="logo" accept="image/*">
</div>
<div class="mb-4">
<label class="form-label">Immagine di Copertina (orizzontale)</label>
<?php if ($shop['cover_image']): ?>
<img src="<?= htmlspecialchars($shop['cover_image']) ?>" alt="Cover" class="img-fluid rounded mb-2" style="max-height:120px; width:100%;">
<?php endif; ?>
<input type="file" class="form-control" name="cover_image" accept="image/*">
</div>
<!-- Geolocalizzazione -->
<div class="mb-3">
<label class="form-label">Coordinate</label>
<div class="input-group mb-2">
<input type="number" step="any" class="form-control" name="latitude" id="lat"
value="<?= htmlspecialchars($shop['latitude'] ?? '') ?>" placeholder="Latitudine">
<input type="number" step="any" class="form-control" name="longitude" id="lng"
value="<?= htmlspecialchars($shop['longitude'] ?? '') ?>" placeholder="Longitudine">
</div>
<button type="button" class="btn btn-outline-info w-100" id="geocodeBtn">
<i class="bx bx-current-location"></i> Geolocalizza indirizzo
</button>
<small class="form-text text-muted mt-1 d-block">
Clicca per calcolare lat/lon da indirizzo (usa OpenStreetMap)
</small>
</div>
<!-- Mappa -->
<div id="map"></div>
<div class="form-check mt-4">
<input class="form-check-input" type="checkbox" name="active" id="active" <?= $shop['active'] ? 'checked' : '' ?>>
<label class="form-check-label fw-bold" for="active">
Salone attivo e visibile online
</label>
</div>
</div>
</div>
<div class="d-grid mt-5">
<button type="submit" class="btn btn-primary btn-lg">Salva Modifiche</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script>
// Inizializza mappa
var map = L.map('map').setView([45.4642, 9.1900], 13); // default Milano
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
var marker = null;
<?php if ($shop['latitude'] && $shop['longitude']): ?>
var initLat = <?= (float)$shop['latitude'] ?>;
var initLng = <?= (float)$shop['longitude'] ?>;
map.setView([initLat, initLng], 15);
marker = L.marker([initLat, initLng]).addTo(map);
<?php endif; ?>
// Click su mappa → aggiorna coordinate
map.on('click', function(e) {
document.getElementById('lat').value = e.latlng.lat.toFixed(8);
document.getElementById('lng').value = e.latlng.lng.toFixed(8);
if (marker) map.removeLayer(marker);
marker = L.marker(e.latlng).addTo(map);
});
// Bottone Geolocalizza
document.getElementById('geocodeBtn').addEventListener('click', function() {
const addr = document.querySelector('[name="address"]').value.trim();
const extra = document.querySelector('[name="address_extra"]').value.trim();
const zip = document.querySelector('[name="zip_code"]').value.trim();
const city = document.querySelector('[name="city"]').value.trim();
const prov = document.querySelector('[name="province"]').value.trim().toUpperCase();
if (!addr || !city) {
alert("Compila almeno Indirizzo e Città.");
return;
}
const full = [addr, extra, zip, city, prov ? prov : '', 'Italia'].filter(Boolean).join(', ');
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'action=geocode&address=' + encodeURIComponent(full)
})
.then(r => r.json())
.then(data => {
if (data.success) {
document.getElementById('lat').value = data.lat;
document.getElementById('lng').value = data.lon;
map.setView([data.lat, data.lon], 15);
if (marker) map.removeLayer(marker);
marker = L.marker([data.lat, data.lon]).addTo(map);
alert("Trovato!\nLat: " + data.lat + "\nLon: " + data.lon);
} else {
alert("Non trovato: " + (data.message || "Indirizzo non riconosciuto"));
}
})
.catch(err => alert("Errore: " + err));
});
</script>
</body>
</html>
<?php
// Gestione AJAX geocode (in fondo alla pagina)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'geocode') {
$addr = trim($_POST['address'] ?? '');
if ($addr === '') {
echo json_encode(['success' => false, 'message' => 'Indirizzo vuoto']);
exit;
}
$result = geocodeAddress($addr);
header('Content-Type: application/json');
echo json_encode($result);
exit;
}
?>

View File

@ -0,0 +1,367 @@
<?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;
}
// 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);
// Validazione restrict_start_minutes
$valid_options = ['any', '00', '00,30', '00,15,30,45'];
if (!in_array($restrict_start_minutes, $valid_options)) {
$restrict_start_minutes = '00,30';
}
// Controlla esistenza
$stmt = $pdo->prepare("SELECT id FROM shop_settings WHERE shop_id = ?");
$stmt->execute([$shop_id]);
$exists = $stmt->fetchColumn() !== false;
if ($exists) {
$stmt = $pdo->prepare("
UPDATE shop_settings SET
show_prices_online = ?,
allowed_start_minutes = ?,
min_booking_notice_hours = ?,
max_booking_days_ahead = ?,
updated_at = NOW()
WHERE shop_id = ?
");
$ok = $stmt->execute([
$show_prices_online,
$restrict_start_minutes,
$min_booking_notice_hours,
$max_booking_days_ahead,
$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 (?, ?, ?, ?, ?)
");
$ok = $stmt->execute([
$shop_id,
$show_prices_online,
$restrict_start_minutes,
$min_booking_notice_hours,
$max_booking_days_ahead
]);
}
setFlash($ok ? 'success' : 'danger', $ok ? "Impostazioni salvate con successo!" : "Errore durante il salvataggio.");
header("Location: salon_settings.php");
exit;
} catch (Throwable $e) {
setFlash('danger', "Errore: " . $e->getMessage());
header("Location: salon_settings.php");
exit;
}
}
// Fetch impostazioni
$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
];
$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>Impostazioni Salone - <?= htmlspecialchars($shop_name) ?></title>
<style>
.settings-card {
border: none;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
}
.settings-header {
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
color: white;
padding: 1.8rem 2rem;
}
.form-section {
background: #ffffff;
padding: 2.5rem;
}
.form-group {
margin-bottom: 2rem;
}
.form-label {
font-weight: 600;
color: #1f2937;
margin-bottom: 0.75rem;
display: block;
}
.form-text {
font-size: 0.9rem;
color: #6b7280;
margin-top: 0.5rem;
}
.preview-box {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 1.25rem;
margin-top: 1rem;
}
.preview-slots {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.preview-slot {
background: #e5e7eb;
color: #374151;
padding: 6px 14px;
border-radius: 9999px;
font-size: 0.9rem;
transition: all 0.2s;
}
.preview-slot.allowed {
background: #d1fae5;
color: #065f46;
font-weight: 600;
}
.btn-save {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none;
padding: 1rem 2.5rem;
font-size: 1.15rem;
font-weight: 600;
border-radius: 12px;
transition: all 0.3s;
}
.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 settings-card">
<div class="settings-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Impostazioni Salone</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">
<?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">
<!-- 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>
<div class="form-text">
I clienti vedranno i prezzi durante la prenotazione online.
</div>
</div>
<!-- Orari inizio -->
<div class="form-group">
<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>
</select>
<div class="preview-box">
<strong>Anteprima slot (es. dalle 9:00):</strong>
<div class="preview-slots" id="previewSlots"></div>
</div>
</div>
<!-- Limiti temporali -->
<div class="row g-4">
<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"
value="<?= (int)$settings['min_booking_notice_hours'] ?>">
<div class="form-text">Es: 2 = non prenotabile per oggi se mancano meno di 2 ore.</div>
</div>
<div class="col-md-6 form-group">
<label class="form-label" for="maxDays">Giorni massimi in anticipo</label>
<input type="number" class="form-control form-control-lg" name="max_booking_days_ahead" id="maxDays" min="7" max="365"
value="<?= (int)$settings['max_booking_days_ahead'] ?>">
<div class="form-text">Es: 90 = massimo 90 giorni da oggi.</div>
</div>
</div>
<div class="d-grid mt-5">
<button type="submit" class="btn btn-save">
<i class="bx bx-save me-2"></i> Salva Impostazioni
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
const restrictSelect = document.getElementById('restrictSelect');
const previewSlots = document.getElementById('previewSlots');
function updatePreview() {
const value = restrictSelect.value;
previewSlots.innerHTML = '';
const examples = [{
val: 'any',
slots: ['9:00', '9:05', '9:10', '9:15', '9:20', '...']
},
{
val: '00',
slots: ['9:00', '10:00', '11:00', '12:00']
},
{
val: '00,30',
slots: ['9:00', '9:30', '10:00', '10:30', '11:00']
},
{
val: '00,15,30,45',
slots: ['9:00', '9:15', '9:30', '9:45', '10:00']
}
];
const selected = examples.find(e => e.val === value) || examples[2];
selected.slots.forEach(slot => {
const span = document.createElement('span');
span.className = 'preview-slot' + (value !== 'any' ? ' allowed' : '');
span.textContent = slot;
previewSlots.appendChild(span);
});
}
restrictSelect.addEventListener('change', updatePreview);
updatePreview(); // init
});
</script>
</body>
</html>

View File

@ -0,0 +1,577 @@
<?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 (o quello attivo)
$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
// ===========================
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;
}
// slug semplice e robusto (senza intl)
function makeSlug(string $str): string
{
$str = trim($str);
$str = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
$str = strtolower($str);
$str = preg_replace('/[^a-z0-9]+/', '-', $str);
$str = trim($str, '-');
return $str ?: 'servizio';
}
// assicura slug unico per shop
function uniqueSlug(PDO $pdo, int $shop_id, string $baseSlug, int $excludeId = 0): string
{
$slug = $baseSlug;
$i = 1;
while (true) {
if ($excludeId > 0) {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM services WHERE shop_id = ? AND slug = ? AND id != ?");
$stmt->execute([$shop_id, $slug, $excludeId]);
} else {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM services WHERE shop_id = ? AND slug = ?");
$stmt->execute([$shop_id, $slug]);
}
if ((int)$stmt->fetchColumn() === 0) break;
$i++;
$slug = $baseSlug . '-' . $i;
}
return $slug;
}
function clampInt($val, int $min, int $max, int $fallback): int
{
if ($val === null || $val === '') return $fallback;
if (!is_numeric($val)) return $fallback;
$n = (int)$val;
if ($n < $min) return $min;
if ($n > $max) return $max;
return $n;
}
function normalizeMoney($val): ?string
{
if ($val === null) return null;
$val = trim((string)$val);
if ($val === '') return null;
$val = str_replace(',', '.', $val);
if (!preg_match('/^\d+(\.\d{1,2})?$/', $val)) return null;
return $val;
}
// ===========================
// POST actions (add/edit/delete)
// ===========================
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
try {
if ($action === 'add' || $action === 'edit') {
$id = ($action === 'edit') ? (int)($_POST['id'] ?? 0) : 0;
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '');
$duration = clampInt($_POST['duration_minutes'] ?? null, 5, 600, 30);
$price = normalizeMoney($_POST['price'] ?? null);
$price_max = normalizeMoney($_POST['price_max'] ?? null);
$category = trim($_POST['category'] ?? '');
$color_hex = trim($_POST['color_hex'] ?? '');
$order = clampInt($_POST['order'] ?? null, 0, 9999, 0);
$is_active = isset($_POST['is_active']) ? 1 : 0;
// Validazioni base
if ($name === '') {
setFlash('danger', "Il nome del servizio è obbligatorio.");
header("Location: services.php");
exit;
}
if ($price === null) {
setFlash('danger', "Prezzo non valido (usa es. 20 o 20.00).");
header("Location: services.php");
exit;
}
if ($price_max !== null && (float)$price_max < (float)$price) {
setFlash('danger', "Il prezzo massimo non può essere inferiore al prezzo.");
header("Location: services.php");
exit;
}
if ($color_hex !== '' && !preg_match('/^#[0-9A-Fa-f]{6}$/', $color_hex)) {
setFlash('danger', "Colore non valido. Usa formato tipo #FFAA00.");
header("Location: services.php");
exit;
}
// slug
$baseSlug = makeSlug($name);
$slug = uniqueSlug($pdo, $shop_id, $baseSlug, $id);
if ($action === 'add') {
$stmt = $pdo->prepare("
INSERT INTO services
(shop_id, name, slug, description, duration_minutes, price, price_max, category, is_active, color_hex, `order`)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$ok = $stmt->execute([
$shop_id,
$name,
$slug,
$description !== '' ? $description : null,
$duration,
$price,
$price_max,
$category !== '' ? $category : null,
$is_active,
$color_hex !== '' ? $color_hex : null,
$order
]);
setFlash($ok ? 'success' : 'danger', $ok ? "Servizio aggiunto!" : "Errore durante l'aggiunta.");
header("Location: services.php");
exit;
} else {
if ($id <= 0) {
setFlash('danger', "ID non valido.");
header("Location: services.php");
exit;
}
$stmt = $pdo->prepare("
UPDATE services
SET name = ?, slug = ?, description = ?, duration_minutes = ?, price = ?, price_max = ?,
category = ?, is_active = ?, color_hex = ?, `order` = ?, updated_at = NOW()
WHERE id = ? AND shop_id = ?
");
$ok = $stmt->execute([
$name,
$slug,
$description !== '' ? $description : null,
$duration,
$price,
$price_max,
$category !== '' ? $category : null,
$is_active,
$color_hex !== '' ? $color_hex : null,
$order,
$id,
$shop_id
]);
setFlash($ok ? 'success' : 'danger', $ok ? "Servizio aggiornato!" : "Errore durante l'aggiornamento.");
header("Location: services.php");
exit;
}
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
setFlash('danger', "ID non valido.");
header("Location: services.php");
exit;
}
$stmt = $pdo->prepare("DELETE FROM services WHERE id = ? AND shop_id = ?");
$ok = $stmt->execute([$id, $shop_id]);
setFlash($ok ? 'success' : 'danger', $ok ? "Servizio eliminato!" : "Errore durante l'eliminazione.");
header("Location: services.php");
exit;
}
setFlash('danger', "Azione non valida.");
header("Location: services.php");
exit;
} catch (Throwable $e) {
setFlash('danger', "Errore: " . $e->getMessage());
header("Location: services.php");
exit;
}
}
// ===========================
// Fetch services
// ===========================
$stmt = $pdo->prepare("
SELECT id, name, slug, description, duration_minutes, price, price_max, category, is_active, color_hex, `order`
FROM services
WHERE shop_id = ?
ORDER BY `order` ASC, name ASC
");
$stmt->execute([$shop_id]);
$services = $stmt->fetchAll(PDO::FETCH_ASSOC);
$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>Servizi - <?= htmlspecialchars($shop_name) ?></title>
</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 align-items-center justify-content-between">
<h6 class="mb-0">Servizi - <?= htmlspecialchars($shop_name) ?></h6>
<div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addServiceModal">
<i class="bx bx-plus me-1"></i> Aggiungi Servizio
</button>
<a href="salon_dashboard.php" class="btn btn-outline-secondary ms-2">
<i class="bx bx-arrow-back me-1"></i> Dashboard
</a>
</div>
</div>
<div class="card-body">
<?php if ($flash): ?>
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> alert-dismissible fade show" role="alert">
<?= htmlspecialchars($flash['text']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (empty($services)): ?>
<div class="alert alert-info text-center py-4">
Non hai ancora creato servizi.<br>
Aggiungine uno per renderlo prenotabile.
</div>
<?php else: ?>
<div class="table-responsive">
<table id="servicesTable" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>Ordine</th>
<th>Nome</th>
<th>Durata</th>
<th>Prezzo</th>
<th>Categoria</th>
<th>Attivo</th>
<th>Colore</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($services as $s): ?>
<tr>
<td><?= (int)$s['order'] ?></td>
<td>
<div class="fw-bold"><?= htmlspecialchars($s['name']) ?></div>
<?php if (!empty($s['description'])): ?>
<div class="text-muted small"><?= htmlspecialchars($s['description']) ?></div>
<?php endif; ?>
</td>
<td><?= (int)$s['duration_minutes'] ?> min</td>
<td>
<?= htmlspecialchars(number_format((float)$s['price'], 2, ',', '.')) ?>
<?php if ($s['price_max'] !== null && (float)$s['price_max'] > (float)$s['price']): ?>
<span class="text-muted"> <?= htmlspecialchars(number_format((float)$s['price_max'], 2, ',', '.')) ?></span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($s['category'] ?? '-') ?></td>
<td>
<?php if ((int)$s['is_active'] === 1): ?>
<span class="badge bg-success"></span>
<?php else: ?>
<span class="badge bg-secondary">No</span>
<?php endif; ?>
</td>
<td>
<?php if (!empty($s['color_hex'])): ?>
<span class="badge" style="background: <?= htmlspecialchars($s['color_hex']) ?>;">
<?= htmlspecialchars($s['color_hex']) ?>
</span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<button type="button" class="btn btn-sm btn-warning me-1"
data-bs-toggle="modal" data-bs-target="#editServiceModal"
onclick='fillEditServiceModal(<?= json_encode([
"id" => (int)$s["id"],
"name" => $s["name"] ?? "",
"description" => $s["description"] ?? "",
"duration_minutes" => (int)$s["duration_minutes"],
"price" => (string)$s["price"],
"price_max" => $s["price_max"] !== null ? (string)$s["price_max"] : "",
"category" => $s["category"] ?? "",
"is_active" => (int)$s["is_active"],
"color_hex" => $s["color_hex"] ?? "",
"order" => (int)$s["order"],
], JSON_HEX_APOS | JSON_HEX_QUOT) ?>)'>
<i class="bx bx-edit"></i> Modifica
</button>
<form action="" method="POST" style="display:inline;"
onsubmit="return confirm('Confermi l\'eliminazione del servizio?');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= (int)$s['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">
<i class="bx bx-trash"></i> Elimina
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- Modal Aggiungi -->
<div class="modal fade" id="addServiceModal" tabindex="-1" aria-labelledby="addServiceModalLabel">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="addServiceModalLabel">Aggiungi Servizio</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="" method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="add">
<div class="row">
<div class="col-md-8 mb-3">
<label class="form-label fw-bold">Nome <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name" required placeholder="Es: Taglio Donna">
</div>
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Durata (min) <span class="text-danger">*</span></label>
<input type="number" class="form-control" name="duration_minutes" min="5" max="600" value="30" required>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Descrizione</label>
<textarea class="form-control" name="description" rows="3" placeholder="Dettagli del servizio..."></textarea>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Prezzo () <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="price" required placeholder="Es: 25.00">
</div>
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Prezzo max ()</label>
<input type="text" class="form-control" name="price_max" placeholder="Es: 35.00">
<div class="form-text">Usalo solo se il prezzo può variare.</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Categoria</label>
<input type="text" class="form-control" name="category" placeholder="Es: Donna, Uomo, Colore">
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Colore (hex)</label>
<input type="text" class="form-control" name="color_hex" placeholder="#FFAA00">
</div>
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Ordine</label>
<input type="number" class="form-control" name="order" min="0" max="9999" value="0">
</div>
<div class="col-md-4 mb-3 d-flex align-items-center">
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" name="is_active" id="add_is_active" checked>
<label class="form-check-label fw-bold" for="add_is_active">
Attivo (prenotabile)
</label>
</div>
</div>
</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">Aggiungi</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal Modifica -->
<div class="modal fade" id="editServiceModal" tabindex="-1" aria-labelledby="editServiceModalLabel">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-warning text-dark">
<h5 class="modal-title" id="editServiceModalLabel">Modifica Servizio</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="" method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="id" id="edit_id">
<div class="row">
<div class="col-md-8 mb-3">
<label class="form-label fw-bold">Nome <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name" id="edit_name" required>
</div>
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Durata (min) <span class="text-danger">*</span></label>
<input type="number" class="form-control" name="duration_minutes" id="edit_duration" min="5" max="600" required>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Descrizione</label>
<textarea class="form-control" name="description" id="edit_description" rows="3"></textarea>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Prezzo () <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="price" id="edit_price" required>
</div>
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Prezzo max ()</label>
<input type="text" class="form-control" name="price_max" id="edit_price_max">
</div>
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Categoria</label>
<input type="text" class="form-control" name="category" id="edit_category">
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Colore (hex)</label>
<input type="text" class="form-control" name="color_hex" id="edit_color_hex">
</div>
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Ordine</label>
<input type="number" class="form-control" name="order" id="edit_order" min="0" max="9999">
</div>
<div class="col-md-4 mb-3 d-flex align-items-center">
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" name="is_active" id="edit_is_active">
<label class="form-check-label fw-bold" for="edit_is_active">
Attivo (prenotabile)
</label>
</div>
</div>
</div>
<div class="alert alert-secondary mb-0">
Lo slug viene generato automaticamente dal nome e reso univoco per il salone.
</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-warning">Salva Modifiche</button>
</div>
</form>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
$('#servicesTable').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
},
order: [
[0, 'asc'],
[1, 'asc']
]
});
});
function fillEditServiceModal(data) {
document.getElementById('edit_id').value = data.id;
document.getElementById('edit_name').value = data.name || '';
document.getElementById('edit_description').value = data.description || '';
document.getElementById('edit_duration').value = data.duration_minutes || 30;
document.getElementById('edit_price').value = data.price || '';
document.getElementById('edit_price_max').value = data.price_max || '';
document.getElementById('edit_category').value = data.category || '';
document.getElementById('edit_color_hex').value = data.color_hex || '';
document.getElementById('edit_order').value = data.order || 0;
document.getElementById('edit_is_active').checked = (parseInt(data.is_active, 10) === 1);
}
</script>
</body>
</html>

547
public/userarea/staff.php Normal file
View File

@ -0,0 +1,547 @@
<?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, validazioni)
// ===========================
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 clampInt($val, int $min, int $max, int $fallback): int
{
if ($val === null || $val === '') return $fallback;
if (!is_numeric($val)) return $fallback;
$n = (int)$val;
if ($n < $min) return $min;
if ($n > $max) return $max;
return $n;
}
// ===========================
// POST actions (add/edit/delete)
// ===========================
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
try {
if ($action === 'add' || $action === 'edit') {
$id = ($action === 'edit') ? (int)($_POST['id'] ?? 0) : 0;
$first_name = trim($_POST['first_name'] ?? '');
$last_name = trim($_POST['last_name'] ?? '');
$nickname = trim($_POST['nickname'] ?? '');
$role = trim($_POST['role'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$email = trim($_POST['email'] ?? '');
$color_hex = trim($_POST['color_hex'] ?? '');
$max_app_day = clampInt($_POST['max_appointments_per_day'] ?? null, 1, 50, 10);
$can_book_online = isset($_POST['can_book_online']) ? 1 : 0;
$is_active = isset($_POST['is_active']) ? 1 : 0;
$notes = trim($_POST['notes'] ?? '');
// Validazioni base
if ($first_name === '' || $last_name === '') {
setFlash('danger', "Nome e Cognome sono obbligatori.");
header("Location: staff.php");
exit;
}
if ($color_hex !== '' && !preg_match('/^#[0-9A-Fa-f]{6}$/', $color_hex)) {
setFlash('danger', "Colore non valido. Usa formato #FFAA00.");
header("Location: staff.php");
exit;
}
if ($action === 'add') {
$stmt = $pdo->prepare("
INSERT INTO staff
(shop_id, first_name, last_name, nickname, role, phone, email, color_hex,
max_appointments_per_day, can_book_online, is_active, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$ok = $stmt->execute([
$shop_id,
$first_name,
$last_name,
$nickname ?: null,
$role ?: null,
$phone ?: null,
$email ?: null,
$color_hex ?: null,
$max_app_day,
$can_book_online,
$is_active,
$notes ?: null
]);
setFlash($ok ? 'success' : 'danger', $ok ? "Collaboratore aggiunto!" : "Errore durante l'aggiunta.");
header("Location: staff.php");
exit;
} else {
if ($id <= 0) {
setFlash('danger', "ID non valido.");
header("Location: staff.php");
exit;
}
$stmt = $pdo->prepare("
UPDATE staff
SET first_name = ?, last_name = ?, nickname = ?, role = ?, phone = ?, email = ?,
color_hex = ?, max_appointments_per_day = ?, can_book_online = ?, is_active = ?,
notes = ?, updated_at = NOW()
WHERE id = ? AND shop_id = ?
");
$ok = $stmt->execute([
$first_name,
$last_name,
$nickname ?: null,
$role ?: null,
$phone ?: null,
$email ?: null,
$color_hex ?: null,
$max_app_day,
$can_book_online,
$is_active,
$notes ?: null,
$id,
$shop_id
]);
setFlash($ok ? 'success' : 'danger', $ok ? "Collaboratore aggiornato!" : "Errore durante l'aggiornamento.");
header("Location: staff.php");
exit;
}
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
setFlash('danger', "ID non valido.");
header("Location: staff.php");
exit;
}
$stmt = $pdo->prepare("DELETE FROM staff WHERE id = ? AND shop_id = ?");
$ok = $stmt->execute([$id, $shop_id]);
setFlash($ok ? 'success' : 'danger', $ok ? "Collaboratore eliminato!" : "Errore durante l'eliminazione.");
header("Location: staff.php");
exit;
}
setFlash('danger', "Azione non valida.");
header("Location: staff.php");
exit;
} catch (Throwable $e) {
setFlash('danger', "Errore: " . $e->getMessage());
header("Location: staff.php");
exit;
}
}
// ===========================
// Fetch staff
// ===========================
$stmt = $pdo->prepare("
SELECT id, first_name, last_name, nickname, role, phone, email, color_hex,
max_appointments_per_day, can_book_online, is_active, notes
FROM staff
WHERE shop_id = ?
ORDER BY last_name ASC, first_name ASC
");
$stmt->execute([$shop_id]);
$staff_list = $stmt->fetchAll(PDO::FETCH_ASSOC);
$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>Staff / Parrucchieri - <?= htmlspecialchars($shop_name) ?></title>
</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 align-items-center justify-content-between">
<h6 class="mb-0">Staff / Parrucchieri - <?= htmlspecialchars($shop_name) ?></h6>
<div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addStaffModal">
<i class="bx bx-plus me-1"></i> Aggiungi Collaboratore
</button>
<a href="salon_dashboard.php" class="btn btn-outline-secondary ms-2">
<i class="bx bx-arrow-back me-1"></i> Dashboard
</a>
</div>
</div>
<div class="card-body">
<?php if ($flash): ?>
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> alert-dismissible fade show" role="alert">
<?= htmlspecialchars($flash['text']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (empty($staff_list)): ?>
<div class="alert alert-info text-center py-4">
Non hai ancora aggiunto collaboratori.<br>
Aggiungine uno per gestire le prenotazioni.
</div>
<?php else: ?>
<div class="table-responsive">
<table id="staffTable" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>Nome</th>
<th>Ruolo</th>
<th>Telefono / Email</th>
<th>Max app/giorno</th>
<th>Online</th>
<th>Attivo</th>
<th>Colore</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($staff_list as $s): ?>
<tr>
<td>
<div class="fw-bold">
<?= htmlspecialchars($s['first_name'] . ' ' . $s['last_name']) ?>
<?php if ($s['nickname']): ?>
<small class="text-muted">(@<?= htmlspecialchars($s['nickname']) ?>)</small>
<?php endif; ?>
</div>
</td>
<td><?= htmlspecialchars($s['role'] ?: '-') ?></td>
<td>
<?= htmlspecialchars($s['phone'] ?: '-') ?><br>
<small><?= htmlspecialchars($s['email'] ?: '-') ?></small>
</td>
<td><?= (int)$s['max_appointments_per_day'] ?></td>
<td>
<?php if ((int)$s['can_book_online'] === 1): ?>
<span class="badge bg-success"></span>
<?php else: ?>
<span class="badge bg-secondary">No</span>
<?php endif; ?>
</td>
<td>
<?php if ((int)$s['is_active'] === 1): ?>
<span class="badge bg-success"></span>
<?php else: ?>
<span class="badge bg-danger">No</span>
<?php endif; ?>
</td>
<td>
<?php if (!empty($s['color_hex'])): ?>
<span class="badge" style="background: <?= htmlspecialchars($s['color_hex']) ?>;">
<?= htmlspecialchars($s['color_hex']) ?>
</span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<button type="button" class="btn btn-sm btn-warning me-1"
data-bs-toggle="modal" data-bs-target="#editStaffModal"
onclick='fillEditStaffModal(<?= json_encode([
"id" => (int)$s["id"],
"first_name" => $s["first_name"],
"last_name" => $s["last_name"],
"nickname" => $s["nickname"] ?? "",
"role" => $s["role"] ?? "",
"phone" => $s["phone"] ?? "",
"email" => $s["email"] ?? "",
"color_hex" => $s["color_hex"] ?? "",
"max_appointments_per_day" => (int)$s["max_appointments_per_day"],
"can_book_online" => (int)$s["can_book_online"],
"is_active" => (int)$s["is_active"],
"notes" => $s["notes"] ?? ""
], JSON_HEX_APOS | JSON_HEX_QUOT) ?>)'>
<i class="bx bx-edit"></i> Modifica
</button>
<form action="" method="POST" style="display:inline;"
onsubmit="return confirm('Confermi l\'eliminazione di questo collaboratore?');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= (int)$s['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">
<i class="bx bx-trash"></i> Elimina
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- Modal Aggiungi -->
<div class="modal fade" id="addStaffModal" tabindex="-1" aria-labelledby="addStaffModalLabel">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="addStaffModalLabel">Aggiungi Collaboratore</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="" method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="add">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Nome <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="first_name" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Cognome <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="last_name" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Nickname / Nome d'arte</label>
<input type="text" class="form-control" name="nickname" placeholder="Es: Sara la Colorista">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Ruolo</label>
<input type="text" class="form-control" name="role" placeholder="Es: Parrucchiere Senior">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Telefono</label>
<input type="text" class="form-control" name="phone">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Email</label>
<input type="email" class="form-control" name="email">
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Colore calendario (hex)</label>
<input type="text" class="form-control" name="color_hex" placeholder="#3788d8">
</div>
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Max app/giorno</label>
<input type="number" class="form-control" name="max_appointments_per_day" min="1" max="50" value="10">
</div>
<div class="col-md-4 mb-3 d-flex align-items-center gap-3 mt-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="can_book_online" id="add_can_book" checked>
<label class="form-check-label" for="add_can_book">Prenotabile online</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_active" id="add_is_active" checked>
<label class="form-check-label" for="add_is_active">Attivo</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Note interne</label>
<textarea class="form-control" name="notes" rows="2" placeholder="Es: solo su appuntamento, non lavora lunedì..."></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">Aggiungi</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal Modifica -->
<div class="modal fade" id="editStaffModal" tabindex="-1" aria-labelledby="editStaffModalLabel">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-warning text-dark">
<h5 class="modal-title" id="editStaffModalLabel">Modifica Collaboratore</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="" method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="id" id="edit_id">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Nome <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="first_name" id="edit_first_name" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Cognome <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="last_name" id="edit_last_name" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Nickname / Nome d'arte</label>
<input type="text" class="form-control" name="nickname" id="edit_nickname">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Ruolo</label>
<input type="text" class="form-control" name="role" id="edit_role">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Telefono</label>
<input type="text" class="form-control" name="phone" id="edit_phone">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">Email</label>
<input type="email" class="form-control" name="email" id="edit_email">
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Colore calendario (hex)</label>
<input type="text" class="form-control" name="color_hex" id="edit_color_hex">
</div>
<div class="col-md-4 mb-3">
<label class="form-label fw-bold">Max app/giorno</label>
<input type="number" class="form-control" name="max_appointments_per_day" id="edit_max_app" min="1" max="50">
</div>
<div class="col-md-4 mb-3 d-flex align-items-center gap-3 mt-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="can_book_online" id="edit_can_book">
<label class="form-check-label" for="edit_can_book">Prenotabile online</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_active" id="edit_is_active">
<label class="form-check-label" for="edit_is_active">Attivo</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Note interne</label>
<textarea class="form-control" name="notes" id="edit_notes" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-warning">Salva Modifiche</button>
</div>
</form>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
$('#staffTable').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
},
order: [
[1, 'asc']
]
});
});
function fillEditStaffModal(data) {
document.getElementById('edit_id').value = data.id;
document.getElementById('edit_first_name').value = data.first_name || '';
document.getElementById('edit_last_name').value = data.last_name || '';
document.getElementById('edit_nickname').value = data.nickname || '';
document.getElementById('edit_role').value = data.role || '';
document.getElementById('edit_phone').value = data.phone || '';
document.getElementById('edit_email').value = data.email || '';
document.getElementById('edit_color_hex').value = data.color_hex || '';
document.getElementById('edit_max_app').value = data.max_appointments_per_day || 10;
document.getElementById('edit_can_book').checked = (parseInt(data.can_book_online, 10) === 1);
document.getElementById('edit_is_active').checked = (parseInt(data.is_active, 10) === 1);
document.getElementById('edit_notes').value = data.notes || '';
}
</script>
</body>
</html>

View File

@ -0,0 +1,349 @@
<?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;
}
// ===========================
// POST - Salva impostazioni utente
// ===========================
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$receive_newsletter = isset($_POST['receive_newsletter']) ? 1 : 0;
$receive_marketing_offers = isset($_POST['receive_marketing_offers']) ? 1 : 0;
$notify_new_appointment = isset($_POST['notify_new_appointment']) ? 1 : 0;
$notify_reminder = isset($_POST['notify_reminder']) ? 1 : 0;
$notify_cancellation = isset($_POST['notify_cancellation']) ? 1 : 0;
$notify_modification = isset($_POST['notify_modification']) ? 1 : 0;
$notify_email = isset($_POST['notify_email']) ? 1 : 0;
$notify_sms = isset($_POST['notify_sms']) ? 1 : 0;
$notify_whatsapp = isset($_POST['notify_whatsapp']) ? 1 : 0;
// Controlla esistenza riga
$stmt = $pdo->prepare("SELECT id FROM user_settings WHERE user_id = ?");
$stmt->execute([$iduserlogin]);
$exists = $stmt->fetchColumn() !== false;
if ($exists) {
$stmt = $pdo->prepare("
UPDATE user_settings SET
receive_newsletter = ?,
receive_marketing_offers = ?,
notify_new_appointment = ?,
notify_reminder = ?,
notify_cancellation = ?,
notify_modification = ?,
notify_email = ?,
notify_sms = ?,
notify_whatsapp = ?,
updated_at = NOW()
WHERE user_id = ?
");
$ok = $stmt->execute([
$receive_newsletter,
$receive_marketing_offers,
$notify_new_appointment,
$notify_reminder,
$notify_cancellation,
$notify_modification,
$notify_email,
$notify_sms,
$notify_whatsapp,
$iduserlogin
]);
} else {
$stmt = $pdo->prepare("
INSERT INTO user_settings (
user_id, receive_newsletter, receive_marketing_offers,
notify_new_appointment, notify_reminder, notify_cancellation,
notify_modification, notify_email, notify_sms, notify_whatsapp
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$ok = $stmt->execute([
$iduserlogin,
$receive_newsletter,
$receive_marketing_offers,
$notify_new_appointment,
$notify_reminder,
$notify_cancellation,
$notify_modification,
$notify_email,
$notify_sms,
$notify_whatsapp
]);
}
setFlash($ok ? 'success' : 'danger', $ok ? "Preferenze utente salvate!" : "Errore durante il salvataggio.");
header("Location: user_settings.php");
exit;
} catch (Throwable $e) {
setFlash('danger', "Errore: " . $e->getMessage());
header("Location: user_settings.php");
exit;
}
}
// Fetch impostazioni utente
$stmt = $pdo->prepare("SELECT * FROM user_settings WHERE user_id = ?");
$stmt->execute([$iduserlogin]);
$userSettings = $stmt->fetch(PDO::FETCH_ASSOC) ?: [
'receive_newsletter' => 1,
'receive_marketing_offers' => 1,
'notify_new_appointment' => 1,
'notify_reminder' => 1,
'notify_cancellation' => 1,
'notify_modification' => 1,
'notify_email' => 1,
'notify_sms' => 0,
'notify_whatsapp' => 0
];
$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>Le Mie Impostazioni - Notifiche & Newsletter</title>
<style>
.settings-card {
border: none;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
}
.settings-header {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
padding: 2rem;
}
.form-section {
background: #ffffff;
padding: 2.5rem;
}
.section-title {
font-size: 1.35rem;
font-weight: 700;
margin: 2.5rem 0 1.5rem;
color: #1f2937;
position: relative;
}
.section-title::after {
content: '';
position: absolute;
bottom: -8px;
left: 0;
width: 60px;
height: 3px;
background: #3b82f6;
border-radius: 3px;
}
.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: #3b82f6;
border-color: #3b82f6;
}
.btn-save {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none;
padding: 1.2rem 3rem;
font-size: 1.15rem;
font-weight: 600;
border-radius: 12px;
transition: all 0.3s;
}
.btn-save:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(16, 185, 129, 0.4);
}
</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="settings-card">
<div class="settings-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Le Mie Impostazioni</h5>
<a href="user_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">
<?php if ($flash): ?>
<div class="alert alert-<?= $flash['type'] ?> alert-dismissible fade show mb-5 shadow-sm" role="alert">
<i class="bx <?= $flash['type'] === 'success' ? 'bx-check-circle' : 'bx-error-circle' ?> me-2"></i>
<?= htmlspecialchars($flash['text']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<form action="" method="POST">
<!-- Sezione Newsletter & Marketing -->
<div class="section-title">Newsletter & Offerte</div>
<div class="toggle-group">
<label class="toggle-label" for="newsletter">Ricevi newsletter e promozioni</label>
<div class="form-check form-switch form-switch-lg">
<input class="form-check-input form-check-input-lg" type="checkbox" name="receive_newsletter" id="newsletter" <?= $userSettings['receive_newsletter'] ? 'checked' : '' ?>>
</div>
</div>
<div class="toggle-group">
<label class="toggle-label" for="marketing">Ricevi offerte speciali e sconti</label>
<div class="form-check form-switch form-switch-lg">
<input class="form-check-input form-check-input-lg" type="checkbox" name="receive_marketing_offers" id="marketing" <?= $userSettings['receive_marketing_offers'] ? 'checked' : '' ?>>
</div>
</div>
<!-- Sezione Notifiche Appuntamenti -->
<div class="section-title mt-5">Notifiche Appuntamenti</div>
<div class="toggle-group">
<label class="toggle-label" for="newAppt">Nuova prenotazione effettuata</label>
<div class="form-check form-switch form-switch-lg">
<input class="form-check-input form-check-input-lg" type="checkbox" name="notify_new_appointment" id="newAppt" <?= $userSettings['notify_new_appointment'] ? 'checked' : '' ?>>
</div>
</div>
<div class="toggle-group">
<label class="toggle-label" for="reminder">Promemoria appuntamento (24h prima)</label>
<div class="form-check form-switch form-switch-lg">
<input class="form-check-input form-check-input-lg" type="checkbox" name="notify_reminder" id="reminder" <?= $userSettings['notify_reminder'] ? 'checked' : '' ?>>
</div>
</div>
<div class="toggle-group">
<label class="toggle-label" for="cancel">Cancellazione appuntamento</label>
<div class="form-check form-switch form-switch-lg">
<input class="form-check-input form-check-input-lg" type="checkbox" name="notify_cancellation" id="cancel" <?= $userSettings['notify_cancellation'] ? 'checked' : '' ?>>
</div>
</div>
<div class="toggle-group">
<label class="toggle-label" for="modify">Modifica appuntamento (orario/servizio)</label>
<div class="form-check form-switch form-switch-lg">
<input class="form-check-input form-check-input-lg" type="checkbox" name="notify_modification" id="modify" <?= $userSettings['notify_modification'] ? 'checked' : '' ?>>
</div>
</div>
<!-- Sezione Canali Notifica -->
<div class="section-title mt-5">Canali di Notifica</div>
<div class="row g-4">
<div class="col-md-4">
<div class="toggle-group flex-column align-items-start">
<label class="toggle-label mb-2" for="emailNotify">Email</label>
<div class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" name="notify_email" id="emailNotify" <?= $userSettings['notify_email'] ? 'checked' : '' ?>>
</div>
</div>
</div>
<div class="col-md-4">
<div class="toggle-group flex-column align-items-start">
<label class="toggle-label mb-2" for="smsNotify">SMS</label>
<div class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" name="notify_sms" id="smsNotify" <?= $userSettings['notify_sms'] ? 'checked' : '' ?>>
</div>
</div>
</div>
<div class="col-md-4">
<div class="toggle-group flex-column align-items-start">
<label class="toggle-label mb-2" for="waNotify">WhatsApp</label>
<div class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" name="notify_whatsapp" id="waNotify" <?= $userSettings['notify_whatsapp'] ? 'checked' : '' ?>>
</div>
</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 Le Mie Preferenze
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
</body>
</html>