Files
yogibook_aury_new/public/school_orders.php
T
2026-10-14 10:21:55 +02:00

1123 lines
52 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
require_once('include/headscript.php');
require_once('include/csrf.php');
require_once(__DIR__ . '/class/ShopService.php');
SchoolContext::requireSchoolAdmin();
/**
* SCUOLA - Ordini: pagina unica (sostituisce "Ordini in attesa" e "Riepilogo ordini").
* - schede: Da pagare · Da propagare · Attivi · Scaduti · Annullati · Tutti
* - ricerca istantanea, colonne ordinabili
* - azioni per riga: pagamento ricevuto, annulla, propaga (con scadenza), elimina
* - modifica al clic: scadenza, max recuperi, recuperi fatti
* - nuovo ordine manuale in una finestra
* Riusa gli script esistenti: calculate_expiry, propagatesingleorder, inserisci_ordine,
* update_expiry, update_reschedule, cancelsingleorder, deleteorder, searchemail.
*/
$pdo = DBHandlerSelect::getInstance()->getConnection();
function e($value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function back(string $msg, string $type = 'msg'): void
{
$f = preg_replace('/[^a-z]/', '', (string) ($_GET['f'] ?? ''));
header('Location: school_orders.php?' . http_build_query(array_filter(['f' => $f, $type => $msg])));
exit();
}
/* ------------------------------------------------- AZIONI (pagamento, annullo) */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
yb_csrf_check();
$id = (int) ($_POST['id'] ?? 0);
if (!SchoolScope::belongs('orderbook', $id)) {
back('Ordine non trovato.', 'err');
}
try {
$action = (string) ($_POST['action'] ?? '');
if ($action === 'paid') {
ShopService::markPaid((int) $idschool, $id)
? back('Pagamento registrato per l\'ordine n. ' . $id . '.')
: back('L\'ordine n. ' . $id . ' non è più in attesa di pagamento.', 'err');
}
if ($action === 'cancel') {
ShopService::cancelPending((int) $idschool, $id)
? back('Ordine n. ' . $id . ' annullato.')
: back('Si possono annullare solo gli ordini in attesa di pagamento.', 'err');
}
} catch (PDOException $ex) {
error_log('[school_orders] ' . $ex->getMessage());
back('Errore database, operazione non eseguita.', 'err');
}
back('Azione non valida.', 'err');
}
/* ------------------------------------------------------------------ DATI */
$st = $pdo->prepare(
"SELECT ob.idorderbook, ob.order_id, ob.order_date_created, ob.first_name, ob.last_name, ob.order_billing_email,
ob.product_name, ob.cod, ob.nticket, ob.quantityclass, ob.expireon, ob.status, ob.maxreschedule, ob.reprogrammed,
ob.is_openflexy, ob.booking_mode, ob.source, ob.payment_status, ob.amount, ob.discount_amount, ob.order_total,
p.kind, s.servicename, s.day, s.time,
(SELECT COUNT(*) FROM bookingclass b WHERE b.idorder = ob.idorderbook AND b.idschool = ob.idschool
AND b.status <> 'cancelled' AND b.is_gift = 'N') AS used,
(SELECT COUNT(*) FROM bookingclass b2 WHERE b2.idorder = ob.idorderbook AND b2.idschool = ob.idschool) AS anybooking
FROM orderbook ob
LEFT JOIN products p ON p.idproduct = ob.idproduct
LEFT JOIN service s ON s.idservice = ob.idservice AND s.idschool = ob.idschool
WHERE ob.idschool = ?
ORDER BY ob.idorderbook DESC
LIMIT 3000"
);
$st->execute([(int) $idschool]);
$raw = $st->fetchAll(PDO::FETCH_ASSOC);
$ids = array_map(fn($o) => (int) $o['idorderbook'], $raw);
$classesBy = [];
$lessonBy = [];
$voucherBy = [];
if ($ids) {
$ph = implode(',', array_fill(0, count($ids), '?'));
$st = $pdo->prepare("SELECT os.idorderbook, s.servicename, s.day, s.time FROM orderbook_services os JOIN service s ON s.idservice = os.idservice WHERE os.idschool = ? AND os.idorderbook IN ($ph) ORDER BY os.slot");
$st->execute(array_merge([(int) $idschool], $ids));
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
$classesBy[(int) $r['idorderbook']][] = trim($r['servicename'] . ' ' . $r['day'] . ' ' . $r['time']);
}
$st = $pdo->prepare("SELECT b.idorder, b.bookingstart, b.status FROM bookingclass b JOIN orderbook ob ON ob.idorderbook = b.idorder WHERE b.idschool = ? AND ob.booking_mode = 'single' AND b.idorder IN ($ph)");
$st->execute(array_merge([(int) $idschool], $ids));
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
$lessonBy[(int) $r['idorder']] = $r;
}
$st = $pdo->prepare("SELECT ob.idorderbook, g.code, g.recipient_name FROM gift_vouchers g JOIN orderbook ob ON ob.cod = g.code COLLATE utf8mb4_general_ci AND ob.idschool = g.idschool WHERE g.idschool = ? AND ob.idorderbook IN ($ph)");
$st->execute(array_merge([(int) $idschool], $ids));
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
$voucherBy[(int) $r['idorderbook']] = $r;
}
}
$today = date('Y-m-d');
$dueDays = max(1, (int) SchoolSettings::get('bank_due_days', 5));
$sources = ['bank' => 'Bonifico', 'stripe' => 'Carta', 'manual' => 'Segreteria', 'voucher' => 'Buono regalo', 'woocommerce' => 'Sito'];
$rows = [];
foreach ($raw as $o) {
$oid = (int) $o['idorderbook'];
$canc = in_array($o['payment_status'], ['cancelled', 'refunded'], true) || $o['status'] === 'cancelled';
$pay = $o['payment_status'] === 'pending';
if ($canc) {
$state = 'cancelled';
} elseif ($pay) {
$state = 'topay';
} elseif ($o['status'] === 'pending') {
$state = 'prop';
} elseif ($o['expireon'] && $o['expireon'] < $today) {
$state = 'expired';
} else {
$state = 'active';
}
$details = $classesBy[$oid] ?? [];
if (!$details && $o['servicename'] && $o['booking_mode'] !== 'single') {
$details[] = trim($o['servicename'] . ' ' . $o['day'] . ' ' . $o['time']);
}
if (isset($lessonBy[$oid])) {
$details[] = ShopService::dateLabel(strtotime($lessonBy[$oid]['bookingstart'])) . ' (' . $lessonBy[$oid]['status'] . ')';
}
if (isset($voucherBy[$oid])) {
$details[] = 'Buono per ' . $voucherBy[$oid]['recipient_name'] . ' · ' . $voucherBy[$oid]['code'];
}
$due = $pay ? date('Y-m-d', strtotime(($o['order_date_created'] ?: $today) . ' +' . $dueDays . ' days')) : null;
$amount = $o['amount'] !== null ? (float) $o['amount'] : (is_numeric(str_replace(',', '.', (string) $o['order_total'])) ? (float) str_replace(',', '.', (string) $o['order_total']) : null);
$rows[] = [
'id' => $oid,
'wc' => $o['order_id'],
'date' => $o['order_date_created'],
'name' => trim($o['first_name'] . ' ' . $o['last_name']),
'email' => (string) $o['order_billing_email'],
'product' => (string) ($o['product_name'] ?: ($o['servicename'] ?: $o['cod'])),
'details' => $details,
'used' => (int) $o['used'],
'tot' => (int) $o['nticket'] > 0 ? (int) $o['nticket'] : (int) $o['quantityclass'],
'expire' => $o['expireon'],
'maxres' => $o['maxreschedule'] !== null ? (int) $o['maxreschedule'] : null,
'reprog' => (int) $o['reprogrammed'],
'amount' => $amount,
'discount' => $o['discount_amount'] !== null ? (float) $o['discount_amount'] : null,
'source' => $o['source'] ? ($sources[$o['source']] ?? $o['source']) : 'Storico',
'state' => $state,
'due' => $due,
'flex' => $o['is_openflexy'] === 'Y',
'single' => $o['booking_mode'] === 'single',
'kind' => (string) $o['kind'],
'bookings' => (int) $o['anybooking'],
];
}
// Classi per il nuovo ordine
$st = $pdo->prepare("SELECT idservice, servicename, day, time FROM service WHERE idschool = ? ORDER BY servicename, day, time");
$st->execute([(int) $idschool]);
$services = $st->fetchAll(PDO::FETCH_ASSOC);
$f = preg_replace('/[^a-z]/', '', (string) ($_GET['f'] ?? ''));
$jsonFlags = JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP;
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8" />
<title>YogiBook - Ordini</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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 href="assets/css/yb-billing.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
.ord-bar {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 14px;
}
.ord-tab {
border: 1px solid #e2e4e9;
background: #fff;
border-radius: 999px;
padding: 7px 14px;
font-weight: 600;
font-size: 14px;
color: #374151;
display: inline-flex;
gap: 6px;
align-items: center;
cursor: pointer;
}
.ord-tab:hover {
border-color: #9fcfd2;
color: #1E4A4F;
}
.ord-tab.on {
background: #1E4A4F;
border-color: #1E4A4F;
color: #fff;
}
.ord-tab .n {
background: rgba(0, 0, 0, .08);
border-radius: 999px;
padding: 0 8px;
font-size: 12px;
}
.ord-tab.on .n {
background: rgba(255, 255, 255, .2);
}
.ord-tab.todo:not(.on) .n {
background: #fff4e0;
color: #92560b;
}
.ord-search {
margin-left: auto;
min-width: 280px;
position: relative;
}
.ord-search i {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
}
.ord-search input {
padding-left: 34px;
}
#ord-count {
font-size: 13px;
color: #6b7280;
min-height: 20px;
margin-bottom: 6px;
}
.yb-table th.sort {
cursor: pointer;
user-select: none;
}
.yb-table th.sort:hover {
color: #1E4A4F;
}
.yb-table th.sort::after {
content: "\2195";
margin-left: 4px;
opacity: .35;
font-size: 11px;
}
.yb-table th.sort.asc::after {
content: "\25B2";
opacity: 1;
}
.yb-table th.sort.desc::after {
content: "\25BC";
opacity: 1;
}
.edit {
cursor: pointer;
border-radius: 6px;
padding: 2px 6px;
margin: -2px -6px;
display: inline-block;
white-space: nowrap;
}
.edit:hover {
background: #F4FAFA;
box-shadow: inset 0 0 0 1px #cfe5e6;
}
.edit input {
width: 140px;
padding: 4px 8px;
border: 1px solid #2F7F84;
border-radius: 6px;
}
.edit input[type=number] {
width: 70px;
}
.overdue {
color: #b03a2e;
font-weight: 700;
}
.bar {
height: 6px;
border-radius: 6px;
background: #eef3f3;
overflow: hidden;
margin-top: 4px;
width: 90px;
}
.bar span {
display: block;
height: 100%;
background: #6FBABE;
}
.res {
display: inline-block;
font-size: 12px;
font-weight: 700;
padding: 2px 8px;
border-radius: 10px;
background: #e7f7ee;
color: #1a7f52;
}
.res.mid {
background: #fff4e0;
color: #92560b;
}
.res.max {
background: #fdecea;
color: #b03a2e;
}
.res.none {
background: #eef0f3;
color: #4b5563;
}
.actions {
white-space: nowrap;
}
.user-results {
border: 1px solid #e2e4e9;
border-radius: 8px;
margin-top: 6px;
max-height: 220px;
overflow-y: auto;
background: #fff;
}
.user-results button {
display: block;
width: 100%;
text-align: left;
border: none;
background: #fff;
padding: 9px 12px;
border-bottom: 1px solid #f0f2f5;
font-size: 13px;
}
.user-results button:hover {
background: #F4FAFA;
}
@media (max-width: 768px) {
.ord-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">Ordini</h4>
</div>
</div>
<div class="d-flex">
<?php include('include/schoolswitcher.php'); ?><?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">
<?php if (!empty($_GET['msg'])) : ?><div class="alert alert-success"><?php echo e($_GET['msg']); ?></div><?php endif; ?>
<?php if (!empty($_GET['err'])) : ?><div class="alert alert-danger"><?php echo e($_GET['err']); ?></div><?php endif; ?>
<div class="card admin-card">
<div class="card-body">
<div class="list-head">
<div>
<div class="section-title"><span class="disc"></span>Ordini</div>
<p class="section-lead mb-0">Tutti gli ordini della scuola. Clicca su scadenza o recuperi per modificarli.</p>
</div>
<button type="button" class="btn btn-yb" data-bs-toggle="modal" data-bs-target="#newOrder"><i class="bx bx-plus"></i> Nuovo ordine</button>
</div>
<div class="ord-bar" id="ord-tabs" role="tablist"></div>
<div id="ord-count"></div>
<div class="table-responsive">
<table class="table yb-table align-middle" id="ord-table">
<thead>
<tr>
<th class="sort" data-key="id" data-type="num">N.</th>
<th class="sort" data-key="name" data-type="text">Allieva</th>
<th class="sort" data-key="product" data-type="text">Prodotto</th>
<th class="sort" data-key="left" data-type="num">Lezioni</th>
<th class="sort" data-key="expire" data-type="text">Scadenza</th>
<th class="sort" data-key="reprog" data-type="num">Recuperi</th>
<th class="sort" data-key="amount" data-type="num">Importo</th>
<th class="sort" data-key="stateOrder" data-type="num">Stato</th>
<th class="text-end">Azioni</th>
</tr>
</thead>
<tbody id="ord-body"></tbody>
</table>
</div>
</div>
</div>
<!-- Form nascosto per pagamento / annullo (POST + CSRF) -->
<form id="act-form" method="post" action="school_orders.php" hidden>
<?php echo yb_csrf_field(); ?>
<input type="hidden" name="action" id="act-name">
<input type="hidden" name="id" id="act-id">
</form>
<!-- Nuovo ordine -->
<div class="modal fade" id="newOrder" tabindex="-1" aria-labelledby="newOrderTitle" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<form class="modal-content" id="new-order-form">
<div class="modal-header">
<h5 class="modal-title" id="newOrderTitle">Nuovo ordine</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body">
<p class="hint mt-0 mb-3">Cerca l'allieva per nome o cognome, oppure scrivi i dati a mano. L'ordine finisce in "Da propagare".</p>
<div class="row g-3">
<div class="col-md-6">
<label for="no-first">Nome *</label>
<input type="text" id="no-first" class="form-control" name="formrow-firstname-input" autocomplete="off" required>
</div>
<div class="col-md-6">
<label for="no-last">Cognome *</label>
<input type="text" id="no-last" class="form-control" name="surname-input" autocomplete="off" required>
</div>
<div class="col-12" id="no-results-wrap" hidden>
<div class="user-results" id="no-results"></div>
</div>
<div class="col-md-6">
<label for="no-email">Email *</label>
<input type="email" id="no-email" class="form-control" name="formrow-email-input" required>
</div>
<div class="col-md-6">
<label for="no-date">Data ordine</label>
<input type="date" id="no-date" class="form-control" name="example-date-input" value="<?php echo e($today); ?>">
</div>
<div class="col-md-6">
<label for="no-service">Classe *</label>
<select id="no-service" class="form-select" name="formrow-inputState" required>
<option value="">Scegli…</option>
<?php foreach ($services as $svc) : ?>
<option value="<?php echo (int) $svc['idservice']; ?>"><?php echo e(trim($svc['servicename'] . ' - ' . $svc['day'] . ' - ' . $svc['time'], ' -')); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label for="no-ticket">N. lezioni *</label>
<input type="number" min="1" id="no-ticket" class="form-control" name="ticket" required>
</div>
<div class="col-md-3">
<label for="no-first-lesson">Prima lezione</label>
<input type="date" id="no-first-lesson" class="form-control" name="first-lesson-date">
</div>
</div>
<input type="hidden" name="cod" id="no-cod">
<input type="hidden" name="userid" id="no-userid">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Chiudi</button>
<button type="submit" class="btn btn-yb" id="no-submit"><i class="bx bx-save"></i> Inserisci ordine</button>
</div>
</form>
</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() {
'use strict';
var ROWS = <?php echo json_encode($rows, $jsonFlags); ?>;
var TODAY = <?php echo json_encode($today); ?>;
var TABS = [
['topay', 'Da pagare', true],
['prop', 'Da propagare', true],
['active', 'Attivi'],
['expired', 'Scaduti'],
['cancelled', 'Annullati'],
['all', 'Tutti']
];
var STATE_ORDER = {
topay: 1,
prop: 2,
active: 3,
expired: 4,
cancelled: 5
};
var counts = {};
TABS.forEach(function(t) {
counts[t[0]] = 0;
});
ROWS.forEach(function(r) {
counts[r.state]++;
counts.all++;
r.stateOrder = STATE_ORDER[r.state];
r.left = r.tot ? r.tot - r.used : -1;
});
var params = new URLSearchParams(location.search);
var S = {
tab: TABS.some(function(t) {
return t[0] === params.get('f');
}) ? params.get('f') : (counts.topay ? 'topay' : (counts.prop ? 'prop' : 'active')),
q: params.get('q') || '',
sortKey: 'id',
sortDir: -1
};
/* ---------------- utilità ---------------- */
function esc(s) {
return String(s === null || s === undefined ? '' : s).replace(/[&<>"']/g, function(c) {
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
} [c];
});
}
function norm(s) {
return String(s || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
}
function euro(n) {
return n === null ? '—' : Number(n).toLocaleString('it-IT', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}) + ' €';
}
function dIt(iso) {
if (!iso) return '—';
var p = String(iso).substr(0, 10).split('-');
return p[2] + '/' + p[1] + '/' + p[0];
}
function byId(id) {
for (var i = 0; i < ROWS.length; i++)
if (ROWS[i].id === id) return ROWS[i];
return null;
}
function resBadge(max, done) {
if (max === null) return '<span class="res none">' + done + '</span>';
return '<span class="res ' + (done >= max ? 'max' : (done > 0 ? 'mid' : '')) + '">' + done + ' / ' + max + '</span>';
}
ROWS.forEach(function(r) {
r.hay = norm([r.id, r.wc, r.name, r.email, r.product, r.details.join(' ')].join(' '));
});
/* ---------------- render ---------------- */
function renderTabs() {
var h = TABS.map(function(t) {
return '<button type="button" role="tab" aria-selected="' + (S.tab === t[0]) + '" class="ord-tab ' + (S.tab === t[0] ? 'on ' : '') + (t[2] ? 'todo' : '') + '" data-tab="' + t[0] + '">' +
esc(t[1]) + ' <span class="n">' + counts[t[0]] + '</span></button>';
}).join('');
h += '<div class="ord-search"><i class="bx bx-search"></i><input type="search" id="ord-q" class="form-control" placeholder="Cerca nome, email, prodotto, n. ordine" value="' + esc(S.q) + '" aria-label="Cerca"></div>';
document.getElementById('ord-tabs').innerHTML = h;
}
function stateCell(r) {
if (r.state === 'cancelled') return '<span class="pill muted">Annullato</span>';
if (r.state === 'topay') return '<span class="pill warn">Da pagare</span><div class="sub ' + (r.due < TODAY ? 'overdue' : '') + '">' + esc(r.source) + ' · entro il ' + dIt(r.due) + '</div>';
if (r.state === 'prop') return '<span class="pill info">Da propagare</span><div class="sub">' + esc(r.source) + '</div>';
if (r.state === 'expired') return '<span class="pill muted">Scaduto</span><div class="sub">' + esc(r.source) + '</div>';
return '<span class="pill ok">Attivo</span><div class="sub">' + esc(r.source) + (r.flex ? ' · ingressi liberi' : '') + '</div>';
}
function actionsCell(r) {
var h = '';
if (r.state === 'topay') {
h += '<button type="button" class="btn-act ok" data-act="paid" data-id="' + r.id + '"><i class="bx bx-check"></i> Pagamento ricevuto</button> ';
h += '<button type="button" class="btn-act danger" data-act="cancel" data-id="' + r.id + '" aria-label="Annulla ordine"><i class="bx bx-x"></i></button>';
} else if (r.state === 'prop') {
h += '<button type="button" class="btn-act ok" data-act="propagate" data-id="' + r.id + '"><i class="bx bx-calendar-check"></i> Propaga</button> ';
if (!r.bookings) h += '<button type="button" class="btn-act danger" data-act="discard" data-id="' + r.id + '" aria-label="Elimina ordine"><i class="bx bx-trash"></i></button>';
} else if (r.state !== 'cancelled') {
h += '<button type="button" class="btn-act danger" data-act="delete" data-id="' + r.id + '" aria-label="Elimina ordine e prenotazioni"><i class="bx bx-trash"></i></button>';
}
return h;
}
function editable(r) {
return r.state === 'active' || r.state === 'expired';
}
function renderRows() {
var terms = norm(S.q).split(/\s+/).filter(Boolean);
var list = ROWS.filter(function(r) {
if (S.tab !== 'all' && r.state !== S.tab) return false;
return terms.every(function(t) {
return r.hay.indexOf(t) !== -1;
});
});
var key = S.sortKey,
dir = S.sortDir;
list.sort(function(a, b) {
var va = a[key],
vb = b[key];
if (va === null || va === undefined || va === '') return 1;
if (vb === null || vb === undefined || vb === '') return -1;
if (typeof va === 'string') {
va = norm(va);
vb = norm(vb);
}
return va < vb ? -dir : (va > vb ? dir : 0);
});
document.getElementById('ord-count').textContent = terms.length ? list.length + (list.length === 1 ? ' ordine trovato' : ' ordini trovati') : '';
document.querySelectorAll('#ord-table th.sort').forEach(function(th) {
th.classList.toggle('asc', th.dataset.key === key && dir === 1);
th.classList.toggle('desc', th.dataset.key === key && dir === -1);
});
if (!list.length) {
document.getElementById('ord-body').innerHTML = '<tr><td colspan="9"><div class="empty-state"><i class="bx bx-receipt"></i>Nessun ordine in questo elenco.</div></td></tr>';
return;
}
document.getElementById('ord-body').innerHTML = list.map(function(r) {
var ed = editable(r);
var lessons = r.tot ? r.used + ' / ' + r.tot + '<div class="bar"><span style="width:' + Math.min(100, Math.round(r.used / r.tot * 100)) + '%;"></span></div>' : (r.flex ? 'Liberi' : '—');
var expire = ed ?
'<span class="edit" data-edit="expire" data-id="' + r.id + '" title="Clicca per modificare">' + dIt(r.expire) + '</span>' :
(r.single ? '' : dIt(r.expire));
var resched = r.single ? '—' : (ed ?
'<span class="edit" data-edit="maxreschedule" data-id="' + r.id + '" title="Max recuperi: clicca per modificare">max ' + (r.maxres === null ? '–' : r.maxres) + '</span><br>' +
'<span class="edit" data-edit="reprogrammed" data-id="' + r.id + '" title="Recuperi fatti: clicca per modificare">' + resBadge(r.maxres, r.reprog) + '</span>' :
resBadge(r.maxres, r.reprog));
return '<tr>' +
'<td><strong>' + r.id + '</strong><div class="sub">' + dIt(r.date) + '</div></td>' +
'<td>' + esc(r.name || '—') + '<div class="sub">' + esc(r.email) + '</div></td>' +
'<td>' + esc(r.product || '—') + r.details.map(function(d) {
return '<div class="sub">' + esc(d) + '</div>';
}).join('') + '</td>' +
'<td>' + lessons + '</td>' +
'<td>' + expire + '</td>' +
'<td>' + resched + '</td>' +
'<td>' + (r.amount !== null ? '<strong>' + euro(r.amount) + '</strong>' : '—') + (r.discount ? '<div class="sub">sconto ' + euro(r.discount) + '</div>' : '') + '</td>' +
'<td>' + stateCell(r) + '</td>' +
'<td class="text-end actions">' + actionsCell(r) + '</td></tr>';
}).join('');
}
function syncUrl() {
var u = new URL(location.href);
u.searchParams.set('f', S.tab);
if (S.q) u.searchParams.set('q', S.q);
else u.searchParams.delete('q');
u.searchParams.delete('msg');
u.searchParams.delete('err');
history.replaceState(null, '', u.toString());
document.getElementById('act-form').action = 'school_orders.php?f=' + encodeURIComponent(S.tab);
}
/* ---------------- eventi ---------------- */
document.getElementById('ord-tabs').addEventListener('click', function(ev) {
var b = ev.target.closest('[data-tab]');
if (!b) return;
S.tab = b.dataset.tab;
renderTabs();
bindSearch();
renderRows();
syncUrl();
});
function onSearch(ev) {
S.q = ev.target.value.trim();
renderRows();
syncUrl();
}
document.querySelector('#ord-table thead').addEventListener('click', function(ev) {
var th = ev.target.closest('th.sort');
if (!th) return;
S.sortDir = S.sortKey === th.dataset.key ? -S.sortDir : (th.dataset.type === 'text' ? 1 : -1);
S.sortKey = th.dataset.key;
renderRows();
});
// Risultato di uno script esistente che risponde con un redirect: leggo i parametri dell'indirizzo finale
function finalParams(res) {
try {
return new URL(res.url).searchParams;
} catch (e) {
return new URLSearchParams();
}
}
function reloadWith(msg, tab) {
location.href = 'school_orders.php?f=' + encodeURIComponent(tab || S.tab) + '&msg=' + encodeURIComponent(msg);
}
function fail(msg) {
Swal.fire('Errore', msg || 'Operazione non riuscita. Riprova.', 'error');
}
document.getElementById('ord-body').addEventListener('click', function(ev) {
var b = ev.target.closest('[data-act]');
if (b) {
actions[b.dataset.act](byId(+b.dataset.id));
return;
}
var ed = ev.target.closest('.edit');
if (ed && !ed.querySelector('input')) startEdit(ed, byId(+ed.dataset.id), ed.dataset.edit);
});
var actions = {
paid: function(r) {
Swal.fire({
title: 'Bonifico ricevuto?',
text: 'Ordine n. ' + r.id + ' · ' + r.name + ' · ' + euro(r.amount),
icon: 'question',
showCancelButton: true,
reverseButtons: true,
confirmButtonText: 'Sì, pagato',
cancelButtonText: 'Chiudi',
confirmButtonColor: '#2F7F84'
})
.then(function(x) {
if (x.isConfirmed) post('paid', r.id);
});
},
cancel: function(r) {
Swal.fire({
title: 'Annullare l\'ordine n. ' + r.id + '?',
text: r.single ? 'Il posto nella lezione viene liberato.' : '',
icon: 'warning',
showCancelButton: true,
reverseButtons: true,
confirmButtonText: 'Annulla ordine',
cancelButtonText: 'Chiudi',
confirmButtonColor: '#c0392b'
})
.then(function(x) {
if (x.isConfirmed) post('cancel', r.id);
});
},
propagate: function(r) {
fetch('calculate_expiry.php?idorder=' + r.id, {
credentials: 'same-origin'
})
.then(function(res) {
return res.json();
})
.then(function(d) {
if (d.error) {
fail(d.error);
return;
}
return Swal.fire({
title: 'Propaga ordine n. ' + r.id,
html: '<p style="margin:0 0 8px;">' + esc(r.name) + ' · ' + esc(r.product) + '</p><label for="sw-exp" style="font-weight:600;">Scadenza</label>' +
'<input type="date" id="sw-exp" class="swal2-input" style="margin-top:6px;" value="' + esc(d.expiry_date || '') + '">' +
'<p style="font-size:13px;color:#6b7280;margin-top:8px;">Calcolata in base alle lezioni in calendario: puoi modificarla.</p>',
showCancelButton: true,
reverseButtons: true,
confirmButtonText: 'Propaga',
cancelButtonText: 'Chiudi',
confirmButtonColor: '#2F7F84',
preConfirm: function() {
var v = document.getElementById('sw-exp').value;
if (!v) {
Swal.showValidationMessage('Indica la scadenza');
}
return v;
}
}).then(function(x) {
if (!x.isConfirmed) return;
Swal.fire({
title: 'Propagazione in corso…',
allowOutsideClick: false,
didOpen: function() {
Swal.showLoading();
}
});
fetch('propagatesingleorder.php?idorder=' + r.id + '&expiry_date=' + encodeURIComponent(x.value), {
credentials: 'same-origin'
})
.then(function(res) {
var p = finalParams(res),
m = p.get('msg');
if (m === 'propagated') {
var txt = p.get('flex') ? 'Pacchetto a ingressi liberi: l\'allieva prenota da sé.' : 'Lezioni prenotate: ' + (p.get('n') || 0) + (p.get('full') ? ' · ' + p.get('full') + ' saltate perché piene' : '') + '.';
reloadWith('Ordine n. ' + r.id + ' propagato. ' + txt, 'prop');
} else if (m) {
fail({
notfound: 'Ordine non trovato o già propagato.',
noemail: 'L\'ordine non ha un\'email valida.',
baddate: 'Data di scadenza non valida.',
error: 'Errore durante la propagazione: nessuna modifica salvata.'
} [m]);
} else {
reloadWith('Operazione eseguita.', 'prop');
}
}).catch(function() {
fail();
});
});
}).catch(function() {
fail('Impossibile calcolare la scadenza.');
});
},
discard: function(r) {
Swal.fire({
title: 'Eliminare l\'ordine n. ' + r.id + '?',
text: r.name + ' · non ha prenotazioni collegate. L\'operazione non è reversibile.',
icon: 'warning',
showCancelButton: true,
reverseButtons: true,
confirmButtonText: 'Elimina',
cancelButtonText: 'Chiudi',
confirmButtonColor: '#c0392b'
})
.then(function(x) {
if (!x.isConfirmed) return;
fetch('cancelsingleorder.php?idorder=' + r.id, {
credentials: 'same-origin'
}).then(function(res) {
var m = finalParams(res).get('msg');
if (m === 'deleted') reloadWith('Ordine n. ' + r.id + ' eliminato.');
else fail(m === 'hasbookings' ? 'L\'ordine ha già prenotazioni collegate.' : 'Eliminazione non riuscita.');
}).catch(function() {
fail();
});
});
},
'delete': function(r) {
Swal.fire({
title: 'Eliminare l\'ordine n. ' + r.id + '?',
html: 'Vengono cancellati l\'ordine e <strong>tutte le prenotazioni collegate</strong> di ' + esc(r.name) + '. L\'azione è irreversibile.',
icon: 'warning',
showCancelButton: true,
reverseButtons: true,
confirmButtonText: 'Sì, elimina tutto',
cancelButtonText: 'Chiudi',
confirmButtonColor: '#c0392b'
})
.then(function(x) {
if (!x.isConfirmed) return;
fetch('deleteorder.php?id=' + r.id, {
credentials: 'same-origin'
}).then(function(res) {
var m = finalParams(res).get('message');
if (m === 'success_delete') reloadWith('Ordine n. ' + r.id + ' e prenotazioni eliminati.');
else fail('Errore durante la cancellazione.');
}).catch(function() {
fail();
});
});
}
};
function post(action, id) {
document.getElementById('act-name').value = action;
document.getElementById('act-id').value = id;
document.getElementById('act-form').submit();
}
/* ---------------- modifica al clic ---------------- */
function startEdit(el, r, field) {
var isDate = field === 'expire';
var cur = isDate ? (r.expire || '') : String(field === 'maxreschedule' ? (r.maxres === null ? '' : r.maxres) : r.reprog);
var html = el.innerHTML;
el.innerHTML = '<input type="' + (isDate ? 'date' : 'number') + '" ' + (isDate ? '' : 'min="0" max="999" step="1" ') + 'value="' + esc(cur) + '" aria-label="Nuovo valore">';
var input = el.querySelector('input');
input.focus();
var done = false;
function restore() {
el.innerHTML = html;
}
function finish(save) {
if (done) return;
done = true;
var v = input.value.trim();
if (!save || v === '' || v === cur) {
restore();
return;
}
if (!isDate && !/^\d{1,3}$/.test(v)) {
restore();
Swal.fire('Valore non valido', 'Inserisci un numero intero da 0 a 999.', 'warning');
return;
}
var label = isDate ? 'La scadenza passa al ' + dIt(v) : (field === 'maxreschedule' ? 'Max recuperi: ' : 'Recuperi fatti: ') + v;
Swal.fire({
title: 'Confermi la modifica?',
text: 'Ordine n. ' + r.id + ' · ' + label,
icon: 'question',
showCancelButton: true,
reverseButtons: true,
confirmButtonText: 'Sì, modifica',
cancelButtonText: 'Annulla',
confirmButtonColor: '#2F7F84'
})
.then(function(x) {
if (!x.isConfirmed) {
restore();
return;
}
var fd = new FormData();
fd.append('order_id', r.id);
if (isDate) {
fd.append('new_expiry', v);
fd.append('update_expiry', '1');
} else {
fd.append('field', field);
fd.append('value', v);
}
fetch(isDate ? 'update_expiry.php' : 'update_reschedule.php', {
method: 'POST',
body: fd,
credentials: 'same-origin'
})
.then(function(res) {
return res.json();
})
.then(function(d) {
if (d.status !== 'success') {
restore();
fail(d.message);
return;
}
if (isDate) {
r.expire = v;
var ns = v < TODAY ? 'expired' : 'active';
if (ns !== r.state) {
counts[r.state]--;
counts[ns]++;
r.state = ns;
r.stateOrder = STATE_ORDER[ns];
renderTabs();
bindSearch();
}
} else {
r.maxres = d.maxreschedule === null || d.maxreschedule === '' ? null : +d.maxreschedule;
r.reprog = +d.reprogrammed;
}
renderRows();
Swal.fire({
icon: 'success',
title: 'Aggiornato',
timer: 1100,
showConfirmButton: false
});
}).catch(function() {
restore();
fail();
});
});
}
input.addEventListener('keydown', function(ev) {
if (ev.key === 'Enter') {
ev.preventDefault();
finish(true);
} else if (ev.key === 'Escape') {
finish(false);
}
});
input.addEventListener('blur', function() {
finish(true);
});
}
/* ---------------- nuovo ordine ---------------- */
(function() {
var form = document.getElementById('new-order-form');
var first = document.getElementById('no-first'),
last = document.getElementById('no-last');
var wrap = document.getElementById('no-results-wrap'),
box = document.getElementById('no-results');
var timer;
function search() {
var fn = first.value.trim(),
ln = last.value.trim();
document.getElementById('no-userid').value = '';
if (fn.length < 2 && ln.length < 2) {
wrap.hidden = true;
return;
}
clearTimeout(timer);
timer = setTimeout(function() {
var fd = new FormData();
fd.append('firstName', fn);
fd.append('lastName', ln);
fd.append('searchTerm', ln);
fetch('searchemail.php', {
method: 'POST',
body: fd,
credentials: 'same-origin'
}).then(function(r) {
return r.json();
}).then(function(d) {
var list = (d && d.results) || [];
box.innerHTML = list.length ? list.map(function(u) {
return '<button type="button" data-uid="' + esc(u.userid || u.id) + '" data-first="' + esc(u.first_name) + '" data-last="' + esc(u.last_name) + '" data-email="' + esc(u.email) + '"><strong>' + esc(u.first_name + ' ' + u.last_name) + '</strong><br><span class="sub">' + esc(u.email) + '</span></button>';
}).join('') : '<div class="sub" style="padding:9px 12px;">Nessuna allieva trovata: compila i dati a mano.</div>';
wrap.hidden = false;
}).catch(function() {
wrap.hidden = true;
});
}, 300);
}
first.addEventListener('input', search);
last.addEventListener('input', search);
box.addEventListener('click', function(ev) {
var b = ev.target.closest('button[data-uid]');
if (!b) return;
first.value = b.dataset.first;
last.value = b.dataset.last;
document.getElementById('no-email').value = b.dataset.email;
document.getElementById('no-userid').value = b.dataset.uid;
wrap.hidden = true;
});
var sel = document.getElementById('no-service');
sel.addEventListener('change', function() {
document.getElementById('no-cod').value = sel.value ? sel.options[sel.selectedIndex].text : '';
});
form.addEventListener('submit', function(ev) {
ev.preventDefault();
var btn = document.getElementById('no-submit');
btn.disabled = true;
fetch('inserisci_ordine.php', {
method: 'POST',
body: new FormData(form),
credentials: 'same-origin'
}).then(function(res) {
var m = finalParams(res).get('msg');
if (m === 'inserted') {
reloadWith('Ordine inserito: ora puoi propagarlo.', 'prop');
return;
}
btn.disabled = false;
fail(m === 'invaliddata' ? 'Dati mancanti o non validi: nome, cognome, classe e numero di lezioni sono obbligatori.' : (m === 'noemail' ? 'Email non valida.' : 'Inserimento non riuscito.'));
}).catch(function() {
btn.disabled = false;
fail();
});
});
})();
function bindSearch() {
document.getElementById('ord-q').addEventListener('input', onSearch);
}
renderTabs();
bindSearch();
renderRows();
syncUrl();
})();
</script>
</body>
</html>