2026-01-27 14:53:37 +01:00

577 lines
27 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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