542 lines
24 KiB
PHP
542 lines
24 KiB
PHP
<?php require_once('include/headscript.php'); ?>
|
||
<?php
|
||
/**
|
||
* Riepilogo ordini (amministratore)
|
||
* - filtro Attivi / Scaduti (server)
|
||
* - ricerca istantanea per nome, cognome, email, n. ordine, classe (client)
|
||
* - modifica al click di: scadenza, max riprogrammazioni, riprogrammazioni effettuate
|
||
*/
|
||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||
|
||
if (!function_exists('e')) {
|
||
function e($value): string
|
||
{
|
||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
}
|
||
|
||
function reschedBadge($max, $done): string
|
||
{
|
||
$done = (int) $done;
|
||
if ($max === null || $max === '') {
|
||
return '<span class="badge bg-light text-dark">' . $done . '</span>';
|
||
}
|
||
$max = (int) $max;
|
||
$cls = $done >= $max ? 'bg-danger' : ($done > 0 ? 'bg-warning text-dark' : 'bg-success');
|
||
return '<span class="badge ' . $cls . '">' . $done . ' / ' . $max . '</span>';
|
||
}
|
||
|
||
$message = $_GET['message'] ?? 'n';
|
||
|
||
$filter = $_GET['filter'] ?? 'all';
|
||
if (!in_array($filter, ['all', 'active', 'expired'], true)) {
|
||
$filter = 'all';
|
||
}
|
||
$q = trim((string) ($_GET['q'] ?? ''));
|
||
$today = date('Y-m-d');
|
||
|
||
$where = '';
|
||
$params = [];
|
||
if ($filter === 'active') {
|
||
$where = 'WHERE o.expireon > :today';
|
||
$params[':today'] = $today;
|
||
} elseif ($filter === 'expired') {
|
||
$where = 'WHERE o.expireon <= :today';
|
||
$params[':today'] = $today;
|
||
}
|
||
|
||
$sql = "SELECT o.idorderbook, o.order_id, o.first_name, o.last_name, o.order_billing_email,
|
||
s.servicename, o.expireon, o.status, o.nticket, o.maxreschedule, o.reprogrammed
|
||
FROM orderbook o
|
||
LEFT JOIN service s ON o.idservice = s.idservice
|
||
$where
|
||
ORDER BY o.idorderbook DESC";
|
||
$stmt = $pdo->prepare($sql);
|
||
$stmt->execute($params);
|
||
$orders = $stmt->fetchAll();
|
||
|
||
$qParam = $q !== '' ? '&q=' . urlencode($q) : '';
|
||
?>
|
||
<!doctype html>
|
||
<html lang="it">
|
||
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>YogiBook - Riepilogo Ordini</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta content="YogiBook - Prenotazione facile YogaSoul" name="description" />
|
||
<meta content="Advanced Creative Solutions" name="author" />
|
||
<link rel="shortcut icon" href="assets/images/favicon.ico">
|
||
<link href="assets/css/bootstrap.min.css" id="bootstrap-style" rel="stylesheet" type="text/css" />
|
||
<link href="assets/css/icons.min.css" rel="stylesheet" type="text/css" />
|
||
<link href="assets/css/app.min.css" id="app-style" rel="stylesheet" type="text/css" />
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
|
||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
|
||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
||
|
||
<style>
|
||
.orders-toolbar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.orders-search {
|
||
margin-left: auto;
|
||
position: relative;
|
||
min-width: 300px;
|
||
}
|
||
|
||
.orders-search i {
|
||
position: absolute;
|
||
left: 12px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.orders-search input {
|
||
padding-left: 34px;
|
||
}
|
||
|
||
#searchCount {
|
||
font-size: 13px;
|
||
color: #6b7280;
|
||
min-height: 20px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.editable {
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.editable:hover {
|
||
background-color: #eef6f1 !important;
|
||
box-shadow: inset 0 0 0 1px #cfe8db;
|
||
}
|
||
|
||
.num-input {
|
||
width: 80px;
|
||
}
|
||
|
||
.expiryDateInput {
|
||
width: 130px;
|
||
}
|
||
|
||
.edit-hint {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.orders-search {
|
||
margin-left: 0;
|
||
min-width: 100%;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div id="layout-wrapper">
|
||
<header id="page-topbar" class="isvertical-topbar">
|
||
<div class="navbar-header">
|
||
<div class="d-flex">
|
||
<?php include('include/logoarea.php'); ?>
|
||
<button type="button" class="btn btn-sm px-3 font-size-24 header-item waves-effect vertical-menu-btn">
|
||
<i class="bx bx-menu align-middle"></i>
|
||
</button>
|
||
<div class="page-title-box align-self-center d-none d-md-block">
|
||
<h4 class="page-title mb-0">Riepilogo Ordini</h4>
|
||
</div>
|
||
</div>
|
||
<div class="d-flex">
|
||
<?php include('include/languageselection.php'); ?>
|
||
<?php include('include/profiletopbar.php'); ?>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
<?php include('include/sidebar.php'); ?>
|
||
<header class="ishorizontal-topbar">
|
||
<div class="navbar-header">
|
||
<div class="d-flex"></div>
|
||
</div>
|
||
<div class="topnav">
|
||
<div class="container-fluid">
|
||
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="main-content">
|
||
<div class="page-content">
|
||
<div class="container-fluid">
|
||
<div class="row">
|
||
<div class="col-xl-12">
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<h5>Riepilogo Ordini (Amministratore)</h5>
|
||
<p>Tutti gli ordini registrati</p>
|
||
|
||
<div class="orders-toolbar">
|
||
<a href="?filter=all<?php echo e($qParam); ?>" data-filter="all" class="filter-link btn btn-secondary <?php echo $filter === 'all' ? 'active' : ''; ?>">Tutti</a>
|
||
<a href="?filter=active<?php echo e($qParam); ?>" data-filter="active" class="filter-link btn btn-success <?php echo $filter === 'active' ? 'active' : ''; ?>">Attivi</a>
|
||
<a href="?filter=expired<?php echo e($qParam); ?>" data-filter="expired" class="filter-link btn btn-danger <?php echo $filter === 'expired' ? 'active' : ''; ?>">Scaduti</a>
|
||
|
||
<div class="orders-search">
|
||
<i class="fas fa-search"></i>
|
||
<input type="search" id="orderSearch" class="form-control" autocomplete="off"
|
||
placeholder="Cerca nome, cognome, email, n. ordine…" value="<?php echo e($q); ?>">
|
||
</div>
|
||
</div>
|
||
|
||
<div id="searchCount"></div>
|
||
<div class="edit-hint"><i class="fas fa-pen me-1"></i> Clicca su scadenza, max riprogr. o riprogrammate per modificarle.</div>
|
||
|
||
<div class="table-responsive">
|
||
<table class="table table-striped mb-0" id="ordersTable">
|
||
<thead>
|
||
<tr>
|
||
<th>Stato</th>
|
||
<th>N. Ordine</th>
|
||
<th>Nome</th>
|
||
<th>Cognome</th>
|
||
<th>Classe</th>
|
||
<th>Ticket</th>
|
||
<th>Scadenza</th>
|
||
<th>Max riprogr.</th>
|
||
<th>Riprogrammate</th>
|
||
<th>Status</th>
|
||
<th>Azioni</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($orders as $order) :
|
||
$id = (int) $order['idorderbook'];
|
||
$isActive = ($order['expireon'] !== null && $order['expireon'] > $today);
|
||
$max = $order['maxreschedule'];
|
||
$done = (int) $order['reprogrammed'];
|
||
$search = implode(' ', [
|
||
$order['first_name'],
|
||
$order['last_name'],
|
||
$order['order_billing_email'],
|
||
$id,
|
||
$order['order_id'],
|
||
$order['servicename'],
|
||
]);
|
||
?>
|
||
<tr class="order-row" data-search="<?php echo e($search); ?>">
|
||
<td class="state-cell">
|
||
<span class="badge <?php echo $isActive ? 'bg-success' : 'bg-danger'; ?>"><?php echo $isActive ? 'Attivo' : 'Scaduto'; ?></span>
|
||
</td>
|
||
<td><?php echo $id; ?></td>
|
||
<td><?php echo e($order['first_name']); ?></td>
|
||
<td><?php echo e($order['last_name']); ?></td>
|
||
<td><?php echo e($order['servicename']); ?></td>
|
||
<td><?php echo e($order['nticket']); ?></td>
|
||
<td class="expiry-date editable" data-order-id="<?php echo $id; ?>" title="Clicca per modificare"><?php echo e($order['expireon']); ?></td>
|
||
<td class="num-edit editable" data-order-id="<?php echo $id; ?>" data-field="maxreschedule"
|
||
data-value="<?php echo $max === null ? '' : (int) $max; ?>" title="Clicca per modificare">
|
||
<?php echo $max === null ? '<span class="text-muted">–</span>' : (int) $max; ?>
|
||
</td>
|
||
<td class="num-edit editable" data-order-id="<?php echo $id; ?>" data-field="reprogrammed"
|
||
data-value="<?php echo $done; ?>" title="Clicca per modificare">
|
||
<?php echo reschedBadge($max, $done); ?>
|
||
</td>
|
||
<td><?php echo e($order['status']); ?></td>
|
||
<td>
|
||
<button type="button" class="btn btn-sm btn-danger" onclick="confirmDeleteOrder(<?php echo $id; ?>)">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<tr id="noResults" style="display:none;">
|
||
<td colspan="11" class="text-center text-muted py-4">Nessun ordine trovato.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php include('include/footer.php'); ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||
<script src="assets/libs/metismenujs/metismenujs.min.js"></script>
|
||
<script src="assets/libs/simplebar/simplebar.min.js"></script>
|
||
<script src="assets/libs/eva-icons/eva.min.js"></script>
|
||
<script src="assets/js/app.js"></script>
|
||
|
||
<script>
|
||
function confirmDeleteOrder(id) {
|
||
Swal.fire({
|
||
title: "Sei sicuro?",
|
||
text: "Verranno cancellati l'ordine e TUTTE le prenotazioni collegate a questa persona. L'azione è irreversibile!",
|
||
icon: "warning",
|
||
showCancelButton: true,
|
||
confirmButtonColor: "#d33",
|
||
cancelButtonColor: "#3085d6",
|
||
confirmButtonText: "Sì, cancella tutto!",
|
||
cancelButtonText: "Annulla"
|
||
}).then((result) => {
|
||
if (result.isConfirmed) {
|
||
window.location.href = 'deleteorder.php?id=' + id;
|
||
}
|
||
});
|
||
}
|
||
|
||
$(function() {
|
||
var today = '<?php echo $today; ?>';
|
||
|
||
/* ===================== RICERCA ===================== */
|
||
function norm(s) {
|
||
return (s || '').toString().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||
}
|
||
|
||
$('#ordersTable tr.order-row').each(function() {
|
||
$(this).data('hay', norm($(this).attr('data-search')));
|
||
});
|
||
|
||
function applySearch() {
|
||
var raw = $('#orderSearch').val().trim();
|
||
var terms = norm(raw).split(/\s+/).filter(Boolean);
|
||
var visible = 0;
|
||
|
||
$('#ordersTable tr.order-row').each(function() {
|
||
var hay = $(this).data('hay');
|
||
var ok = terms.every(function(t) {
|
||
return hay.indexOf(t) !== -1;
|
||
});
|
||
$(this).toggle(ok);
|
||
if (ok) visible++;
|
||
});
|
||
|
||
$('#noResults').toggle(visible === 0);
|
||
$('#searchCount').text(terms.length ? visible + (visible === 1 ? ' ordine trovato' : ' ordini trovati') : '');
|
||
|
||
// i pulsanti Tutti/Attivi/Scaduti mantengono la ricerca
|
||
$('.filter-link').each(function() {
|
||
$(this).attr('href', '?filter=' + $(this).data('filter') + (raw ? '&q=' + encodeURIComponent(raw) : ''));
|
||
});
|
||
|
||
var url = new URL(window.location.href);
|
||
if (raw) url.searchParams.set('q', raw);
|
||
else url.searchParams.delete('q');
|
||
url.searchParams.delete('message');
|
||
history.replaceState(null, '', url.toString());
|
||
}
|
||
|
||
$('#orderSearch').on('input', applySearch);
|
||
applySearch();
|
||
|
||
/* ===================== SCADENZA ===================== */
|
||
$(document).on('click', 'td.expiry-date', function() {
|
||
var cell = $(this);
|
||
if (cell.find('input').length) return;
|
||
|
||
var orderId = cell.data('order-id');
|
||
var current = cell.text().trim();
|
||
cell.data('original', current).data('selected', false);
|
||
|
||
var input = $('<input type="text" class="form-control form-control-sm expiryDateInput" readonly>').val(current);
|
||
cell.empty().append(input);
|
||
|
||
input.datepicker({
|
||
dateFormat: 'yy-mm-dd',
|
||
minDate: 0,
|
||
onSelect: function(dateText) {
|
||
cell.data('selected', true);
|
||
confirmExpiry(cell, orderId, dateText);
|
||
},
|
||
onClose: function() {
|
||
setTimeout(function() {
|
||
if (!cell.data('selected')) cell.text(cell.data('original'));
|
||
}, 0);
|
||
}
|
||
});
|
||
input.datepicker('show');
|
||
});
|
||
|
||
function confirmExpiry(cell, orderId, newExpiry) {
|
||
var original = cell.data('original');
|
||
if (newExpiry === original) {
|
||
cell.text(original);
|
||
return;
|
||
}
|
||
Swal.fire({
|
||
title: "Sei sicuro?",
|
||
html: "La scadenza passerà a <strong>" + newExpiry + "</strong>.",
|
||
icon: "warning",
|
||
showCancelButton: true,
|
||
confirmButtonColor: "#3085d6",
|
||
cancelButtonColor: "#d33",
|
||
confirmButtonText: "Sì, modifica!",
|
||
cancelButtonText: "Annulla"
|
||
}).then(function(result) {
|
||
if (!result.isConfirmed) {
|
||
cell.text(original);
|
||
return;
|
||
}
|
||
$.ajax({
|
||
url: 'update_expiry.php',
|
||
type: 'POST',
|
||
dataType: 'json',
|
||
data: {
|
||
order_id: orderId,
|
||
new_expiry: newExpiry,
|
||
update_expiry: 1
|
||
}
|
||
}).done(function(res) {
|
||
if (res.status === 'success') {
|
||
cell.text(newExpiry);
|
||
var active = newExpiry > today;
|
||
cell.closest('tr').find('.state-cell').html(
|
||
'<span class="badge ' + (active ? 'bg-success' : 'bg-danger') + '">' + (active ? 'Attivo' : 'Scaduto') + '</span>'
|
||
);
|
||
Swal.fire('Successo!', res.message, 'success');
|
||
} else {
|
||
cell.text(original);
|
||
Swal.fire('Errore!', res.message, 'error');
|
||
}
|
||
}).fail(function(xhr, status, error) {
|
||
console.error('Errore AJAX:', xhr.responseText);
|
||
cell.text(original);
|
||
Swal.fire('Errore!', 'Errore durante la richiesta al server: ' + error, 'error');
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ============ MAX RIPROGRAMMAZIONI / RIPROGRAMMATE ============ */
|
||
function badgeHtml(max, done) {
|
||
done = parseInt(done, 10) || 0;
|
||
if (max === null || max === '' || typeof max === 'undefined') {
|
||
return '<span class="badge bg-light text-dark">' + done + '</span>';
|
||
}
|
||
max = parseInt(max, 10);
|
||
var cls = done >= max ? 'bg-danger' : (done > 0 ? 'bg-warning text-dark' : 'bg-success');
|
||
return '<span class="badge ' + cls + '">' + done + ' / ' + max + '</span>';
|
||
}
|
||
|
||
function renderResched(row, max, done) {
|
||
var maxCell = row.find('td[data-field="maxreschedule"]');
|
||
var doneCell = row.find('td[data-field="reprogrammed"]');
|
||
var hasMax = !(max === null || max === '' || typeof max === 'undefined');
|
||
maxCell.attr('data-value', hasMax ? max : '').html(hasMax ? max : '<span class="text-muted">–</span>');
|
||
doneCell.attr('data-value', done).html(badgeHtml(max, done));
|
||
}
|
||
|
||
$(document).on('click', 'td.num-edit', function() {
|
||
var cell = $(this);
|
||
if (cell.find('input').length) return;
|
||
|
||
var field = cell.data('field');
|
||
var orderId = cell.data('order-id');
|
||
var current = String(cell.attr('data-value'));
|
||
var input = $('<input type="number" min="0" max="999" step="1" class="form-control form-control-sm num-input">').val(current);
|
||
|
||
cell.data('html', cell.html()).empty().append(input);
|
||
input.trigger('focus').trigger('select');
|
||
|
||
var finished = false;
|
||
|
||
function finish(save) {
|
||
if (finished) return;
|
||
finished = true;
|
||
var val = String(input.val()).trim();
|
||
if (!save || val === '' || val === current) {
|
||
cell.html(cell.data('html'));
|
||
return;
|
||
}
|
||
if (!/^\d{1,3}$/.test(val)) {
|
||
cell.html(cell.data('html'));
|
||
Swal.fire('Valore non valido', 'Inserisci un numero intero da 0 a 999.', 'warning');
|
||
return;
|
||
}
|
||
confirmNumber(cell, orderId, field, parseInt(val, 10));
|
||
}
|
||
|
||
input.on('keydown', function(ev) {
|
||
if (ev.key === 'Enter') {
|
||
ev.preventDefault();
|
||
finish(true);
|
||
} else if (ev.key === 'Escape') {
|
||
finish(false);
|
||
}
|
||
});
|
||
input.on('blur', function() {
|
||
finish(true);
|
||
});
|
||
});
|
||
|
||
function confirmNumber(cell, orderId, field, value) {
|
||
var label = field === 'maxreschedule' ? 'Max riprogrammazioni' : 'Riprogrammate';
|
||
Swal.fire({
|
||
title: 'Confermi la modifica?',
|
||
html: label + ': <strong>' + value + '</strong>',
|
||
icon: 'question',
|
||
showCancelButton: true,
|
||
confirmButtonColor: '#3085d6',
|
||
cancelButtonColor: '#d33',
|
||
confirmButtonText: 'Sì, modifica!',
|
||
cancelButtonText: 'Annulla'
|
||
}).then(function(result) {
|
||
if (!result.isConfirmed) {
|
||
cell.html(cell.data('html'));
|
||
return;
|
||
}
|
||
$.ajax({
|
||
url: 'update_reschedule.php',
|
||
type: 'POST',
|
||
dataType: 'json',
|
||
data: {
|
||
order_id: orderId,
|
||
field: field,
|
||
value: value
|
||
}
|
||
}).done(function(res) {
|
||
if (res.status === 'success') {
|
||
renderResched(cell.closest('tr'), res.maxreschedule, res.reprogrammed);
|
||
Swal.fire({
|
||
icon: 'success',
|
||
title: 'Aggiornato',
|
||
timer: 1200,
|
||
showConfirmButton: false
|
||
});
|
||
} else {
|
||
cell.html(cell.data('html'));
|
||
Swal.fire('Errore!', res.message || 'Aggiornamento non riuscito.', 'error');
|
||
}
|
||
}).fail(function(xhr) {
|
||
console.error('Errore AJAX:', xhr.responseText);
|
||
cell.html(cell.data('html'));
|
||
Swal.fire('Errore!', 'Errore di comunicazione con il server.', 'error');
|
||
});
|
||
});
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<?php if ($message === 'success_delete') { ?>
|
||
<script>
|
||
Swal.fire('Fatto!', 'Ordine e prenotazioni cancellati.', 'success');
|
||
</script>
|
||
<?php } elseif ($message === 'error_delete') { ?>
|
||
<script>
|
||
Swal.fire('Errore!', 'Errore durante la cancellazione.', 'error');
|
||
</script>
|
||
<?php } ?>
|
||
</body>
|
||
|
||
</html>
|