577 lines
27 KiB
PHP
577 lines
27 KiB
PHP
<?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">Sì</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>
|