fixed shop
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Percorso "Primi passi" della scuola:
|
||||
* setup_state JSON con i passi segnati a mano / saltati e l'ultimo avanzamento
|
||||
* setup_completed_at quando l'owner ha chiuso il percorso (nasconde il promemoria)
|
||||
* Le scuole già in uso (es. Seregno) vengono segnate come completate.
|
||||
*/
|
||||
final class AddSetupStateToOption extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->addColumn('setup_state', 'text', ['null' => true, 'comment' => 'JSON percorso Primi passi'])
|
||||
->addColumn('setup_completed_at', 'datetime', ['null' => true])
|
||||
->update();
|
||||
|
||||
// Scuole che hanno già ordini o prenotazioni: percorso considerato concluso
|
||||
$this->execute(
|
||||
"UPDATE `option` o SET o.setup_completed_at = NOW()
|
||||
WHERE EXISTS (SELECT 1 FROM orderbook ob WHERE ob.idschool = o.idschool)
|
||||
OR EXISTS (SELECT 1 FROM bookingclass b WHERE b.idschool = o.idschool)"
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->removeColumn('setup_state')
|
||||
->removeColumn('setup_completed_at')
|
||||
->update();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@ class SchoolScope
|
||||
'orderbook' => 'idorderbook',
|
||||
'employee' => 'idemployee',
|
||||
'dayoff' => 'iddayoff',
|
||||
'expiryparameter' => 'idexpiryparameter',
|
||||
'certificateuserprofile' => 'idcertificateuserprofile',
|
||||
'events' => 'id',
|
||||
'event_bookings' => 'id',
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SchoolSetup
|
||||
* Percorso "Primi passi" per l'owner di una nuova scuola.
|
||||
* Ogni passo si considera fatto guardando i dati reali della scuola;
|
||||
* pochi passi (da verificare a occhio) si segnano a mano. I passi facoltativi si possono saltare.
|
||||
* Lo stato manuale e l'ultimo avanzamento stanno in option.setup_state (JSON).
|
||||
*/
|
||||
|
||||
if (!class_exists('DBHandlerSelect')) {
|
||||
require_once __DIR__ . '/db-functions.php';
|
||||
}
|
||||
|
||||
class SchoolSetup
|
||||
{
|
||||
const PHASES = [
|
||||
'school' => ['La tua scuola', 'Come ti presenti ad allieve e insegnanti'],
|
||||
'classes' => ['Classi e calendario', 'Cosa si può prenotare e quando'],
|
||||
'sell' => ['Vendita', 'Cosa vendi e come ti pagano'],
|
||||
'test' => ['Prova finale', 'Fai un giro completo come farà un\'allieva'],
|
||||
];
|
||||
|
||||
/** Stato manuale salvato: ['done' => [key => data], 'skip' => [key => data], 'progress' => 'x/y'] */
|
||||
public static function state(int $idschool): array
|
||||
{
|
||||
$raw = (string) SchoolSettings::get('setup_state', '', $idschool);
|
||||
$s = json_decode($raw, true);
|
||||
return is_array($s) ? $s + ['done' => [], 'skip' => []] : ['done' => [], 'skip' => []];
|
||||
}
|
||||
|
||||
public static function saveState(int $idschool, array $state): void
|
||||
{
|
||||
SchoolSettings::save(['setup_state' => json_encode($state, JSON_UNESCAPED_UNICODE)], $idschool);
|
||||
}
|
||||
|
||||
public static function completed(int $idschool): bool
|
||||
{
|
||||
return (string) SchoolSettings::get('setup_completed_at', '', $idschool) !== '';
|
||||
}
|
||||
|
||||
/** Elenco dei passi con stato calcolato */
|
||||
public static function steps(int $idschool, int $userId): array
|
||||
{
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$one = function (string $sql, array $args) use ($pdo) {
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute($args);
|
||||
return $st->fetchColumn();
|
||||
};
|
||||
$state = self::state($idschool);
|
||||
|
||||
$st = $pdo->prepare("SELECT * FROM schools WHERE idschool = ?");
|
||||
$st->execute([$idschool]);
|
||||
$sc = $st->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
$filled = fn($k) => trim((string) ($sc[$k] ?? '')) !== '';
|
||||
|
||||
$teachers = (int) $one("SELECT COUNT(*) FROM employee WHERE idschool = ? AND active = 'Y'", [$idschool]);
|
||||
$classes = (int) $one("SELECT COUNT(*) FROM service WHERE idschool = ?", [$idschool]);
|
||||
$future = (int) $one("SELECT COUNT(*) FROM serviceschedule WHERE idschool = ? AND dateschedule > NOW()", [$idschool]);
|
||||
$dayoffs = (int) $one("SELECT COUNT(*) FROM dayoff WHERE idschool = ? AND dayoffdate >= CURDATE()", [$idschool]);
|
||||
$rules = (int) $one("SELECT COUNT(*) FROM expiryparameter WHERE idschool = ?", [$idschool]);
|
||||
$products = (int) $one(
|
||||
"SELECT COUNT(*) FROM products p WHERE p.idschool = ? AND p.active = 'Y'
|
||||
AND EXISTS (SELECT 1 FROM product_variants v WHERE v.idproduct = p.idproduct AND v.active = 'Y')",
|
||||
[$idschool]
|
||||
);
|
||||
$bankOk = SchoolSettings::get('bank_enabled', 'N', $idschool) === 'Y' && trim((string) SchoolSettings::get('bank_iban', '', $idschool)) !== '';
|
||||
$cardOk = SchoolSettings::get('stripe_charges_enabled', 'N', $idschool) === 'Y';
|
||||
$shopOn = SchoolSettings::get('shop_enabled', 'N', $idschool) === 'Y';
|
||||
$testOrder = (int) $one("SELECT COUNT(*) FROM orderbook WHERE idschool = ? AND iduser = ? AND idproduct IS NOT NULL", [$idschool, $userId]);
|
||||
$testDone = (int) $one("SELECT COUNT(*) FROM orderbook WHERE idschool = ? AND iduser = ? AND idproduct IS NOT NULL AND payment_status IN ('paid', 'cancelled')", [$idschool, $userId]);
|
||||
$students = (int) $one("SELECT COUNT(*) FROM school_user WHERE idschool = ? AND school_role = 'student' AND active = 'Y' AND user_id <> ?", [$idschool, $userId]);
|
||||
|
||||
$sub = null;
|
||||
if (is_file(__DIR__ . '/PlatformBilling.php')) {
|
||||
require_once __DIR__ . '/PlatformBilling.php';
|
||||
try {
|
||||
$row = PlatformBilling::school($idschool);
|
||||
$sub = $row ? PlatformBilling::state($row) : null;
|
||||
} catch (Throwable $ex) {
|
||||
$sub = null;
|
||||
}
|
||||
}
|
||||
|
||||
$steps = [
|
||||
// ---------------- La scuola
|
||||
[
|
||||
'key' => 'profile',
|
||||
'phase' => 'school',
|
||||
'required' => true,
|
||||
'title' => 'Dati della scuola',
|
||||
'text' => 'Nome, email, indirizzo e città: compaiono nelle email alle allieve, nella pagina pubblica e nelle ricevute.',
|
||||
'url' => 'optionsschool.php?tab=profile',
|
||||
'cta' => 'Compila i dati',
|
||||
'done' => $filled('name') && $filled('email') && $filled('address') && $filled('city'),
|
||||
'detail' => $filled('email') ? '' : 'Manca l\'email della scuola: serve come mittente delle email.'
|
||||
],
|
||||
[
|
||||
'key' => 'public',
|
||||
'phase' => 'school',
|
||||
'required' => false,
|
||||
'title' => 'Logo e presentazione',
|
||||
'text' => 'Il logo appare in alto nell\'app; slogan e descrizione nella pagina della scuola.',
|
||||
'url' => 'optionsschool.php?tab=public',
|
||||
'cta' => 'Carica il logo',
|
||||
'done' => $filled('logo') && ($filled('short_description') || $filled('description'))
|
||||
],
|
||||
[
|
||||
'key' => 'look',
|
||||
'phase' => 'school',
|
||||
'required' => false,
|
||||
'manual' => true,
|
||||
'title' => 'Colori dell\'app',
|
||||
'text' => 'Scegli i colori della tua scuola. Se non li tocchi resta il tema YogiBook.',
|
||||
'url' => 'optionsschool.php?tab=theme',
|
||||
'cta' => 'Scegli i colori',
|
||||
'done' => false
|
||||
],
|
||||
[
|
||||
'key' => 'teachers',
|
||||
'phase' => 'school',
|
||||
'required' => true,
|
||||
'title' => 'Insegnanti',
|
||||
'text' => 'Aggiungi chi insegna: servono per assegnare le classi. Puoi inserire anche te stessa.',
|
||||
'url' => 'insegnanti.php',
|
||||
'cta' => 'Aggiungi insegnanti',
|
||||
'done' => $teachers > 0,
|
||||
'detail' => $teachers ? $teachers . ' attivi' : ''
|
||||
],
|
||||
|
||||
// ---------------- Classi e calendario
|
||||
[
|
||||
'key' => 'classes',
|
||||
'phase' => 'classes',
|
||||
'required' => true,
|
||||
'title' => 'Classi',
|
||||
'text' => 'Crea le categorie (es. Hatha, Aerial) e le classi settimanali con giorno, orario, durata e posti.',
|
||||
'url' => 'admin-services.php',
|
||||
'cta' => 'Crea le classi',
|
||||
'done' => $classes > 0,
|
||||
'detail' => $classes ? $classes . ' classi' : ''
|
||||
],
|
||||
[
|
||||
'key' => 'calendar',
|
||||
'phase' => 'classes',
|
||||
'required' => true,
|
||||
'title' => 'Calendario delle lezioni',
|
||||
'text' => 'Genera le lezioni in calendario per le prossime settimane: sono quelle che le allieve prenotano.',
|
||||
'url' => 'admin-services.php',
|
||||
'cta' => 'Programma le lezioni',
|
||||
'done' => $future > 0,
|
||||
'detail' => $future ? $future . ' lezioni in calendario' : ''
|
||||
],
|
||||
[
|
||||
'key' => 'dayoff',
|
||||
'phase' => 'classes',
|
||||
'required' => false,
|
||||
'title' => 'Giorni di chiusura',
|
||||
'text' => 'Festività e chiusure: in quei giorni non si prenota e i pacchetti non scalano lezioni.',
|
||||
'url' => 'dayoff.php',
|
||||
'cta' => 'Aggiungi chiusure',
|
||||
'done' => $dayoffs > 0
|
||||
],
|
||||
[
|
||||
'key' => 'booking',
|
||||
'phase' => 'classes',
|
||||
'required' => true,
|
||||
'manual' => true,
|
||||
'title' => 'Regole di prenotazione',
|
||||
'text' => 'Lista d\'attesa, insegnante visibile, ore minime per annullare, avviso per le allieve. Controllale e segna il passo come fatto.',
|
||||
'url' => 'optionsschool.php?tab=booking',
|
||||
'cta' => 'Controlla le regole',
|
||||
'done' => false
|
||||
],
|
||||
[
|
||||
'key' => 'packages',
|
||||
'phase' => 'classes',
|
||||
'required' => true,
|
||||
'title' => 'Scadenza e recuperi dei pacchetti',
|
||||
'text' => 'Per ogni numero di lezioni: dopo quante lezioni scade il pacchetto e quanti recuperi sono ammessi.',
|
||||
'url' => 'package_rules.php',
|
||||
'cta' => 'Imposta le regole',
|
||||
'done' => $rules > 0,
|
||||
'detail' => $rules ? $rules . ' regole' : ''
|
||||
],
|
||||
|
||||
// ---------------- Vendita
|
||||
[
|
||||
'key' => 'products',
|
||||
'phase' => 'sell',
|
||||
'required' => true,
|
||||
'title' => 'Prodotti',
|
||||
'text' => 'Cosa vendi: pacchetti a classe fissa, carnet, lezione di prova, tessera, buoni regalo. Con foto, prezzi e opzioni.',
|
||||
'url' => 'products.php',
|
||||
'cta' => 'Crea i prodotti',
|
||||
'done' => $products > 0,
|
||||
'detail' => $products ? $products . ' in vendita' : ''
|
||||
],
|
||||
[
|
||||
'key' => 'payments',
|
||||
'phase' => 'sell',
|
||||
'required' => true,
|
||||
'title' => 'Pagamenti',
|
||||
'text' => 'Attiva il bonifico (intestatario e IBAN) e, quando disponibile, la carta. Imposta anche le condizioni di acquisto.',
|
||||
'url' => 'optionsschool.php?tab=shop',
|
||||
'cta' => 'Configura i pagamenti',
|
||||
'done' => $bankOk || $cardOk,
|
||||
'detail' => trim(($bankOk ? 'Bonifico' : '') . ($bankOk && $cardOk ? ' · ' : '') . ($cardOk ? 'Carta' : ''))
|
||||
],
|
||||
[
|
||||
'key' => 'shop',
|
||||
'phase' => 'sell',
|
||||
'required' => true,
|
||||
'title' => 'Apri il negozio',
|
||||
'text' => 'Accendi "Negozio attivo": da quel momento le allieve possono acquistare dall\'app.',
|
||||
'url' => 'optionsschool.php?tab=shop',
|
||||
'cta' => 'Attiva il negozio',
|
||||
'done' => $shopOn
|
||||
],
|
||||
[
|
||||
'key' => 'subscription',
|
||||
'phase' => 'sell',
|
||||
'required' => false,
|
||||
'title' => 'Abbonamento YogiBook',
|
||||
'text' => 'Controlla il tuo piano e la scadenza della prova.',
|
||||
'url' => 'subscription.php',
|
||||
'cta' => 'Vedi l\'abbonamento',
|
||||
'done' => $sub !== null && in_array($sub['code'], ['active', 'expiring', 'exempt'], true),
|
||||
'detail' => $sub ? $sub['label'] . ($sub['ends_on'] ? ' fino al ' . date('d/m/Y', strtotime($sub['ends_on'])) : '') : ''
|
||||
],
|
||||
|
||||
// ---------------- Prova finale
|
||||
[
|
||||
'key' => 'test_buy',
|
||||
'phase' => 'test',
|
||||
'required' => true,
|
||||
'title' => 'Acquista come un\'allieva',
|
||||
'text' => 'Dal selettore Vista in alto nel menu passa a "Allievo", apri il Negozio e compra un prodotto con bonifico. Controlla riepilogo, causale e pagina di conferma.',
|
||||
'url' => 'shop.php',
|
||||
'cta' => 'Apri il negozio',
|
||||
'done' => $testOrder > 0
|
||||
],
|
||||
[
|
||||
'key' => 'test_confirm',
|
||||
'phase' => 'test',
|
||||
'required' => true,
|
||||
'title' => 'Gestisci l\'ordine di prova',
|
||||
'text' => 'Torna alla vista "Scuola", apri Ordini: segna il pagamento come ricevuto (e propagalo se è a classe fissa) oppure annullalo.',
|
||||
'url' => 'school_orders.php',
|
||||
'cta' => 'Apri gli ordini',
|
||||
'done' => $testDone > 0
|
||||
],
|
||||
[
|
||||
'key' => 'test_book',
|
||||
'phase' => 'test',
|
||||
'required' => false,
|
||||
'manual' => true,
|
||||
'title' => 'Prenota e annulla una lezione',
|
||||
'text' => 'Sempre come allieva, prenota una lezione dal pannello Prenotazioni e poi annullala: verifichi posti e regole di cancellazione.',
|
||||
'url' => 'userpanel.php',
|
||||
'cta' => 'Apri le prenotazioni',
|
||||
'done' => false
|
||||
],
|
||||
[
|
||||
'key' => 'invite',
|
||||
'phase' => 'test',
|
||||
'required' => false,
|
||||
'manual' => true,
|
||||
'title' => 'Invita le tue allieve',
|
||||
'text' => 'Comunica alle allieve l\'indirizzo di YogiBook per registrarsi e scegli tu quando aprire il negozio a tutte.',
|
||||
'url' => '',
|
||||
'cta' => '',
|
||||
'done' => $students > 0,
|
||||
'detail' => $students ? $students . ' allieve iscritte' : ''
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($steps as &$s) {
|
||||
$s += ['manual' => false, 'detail' => ''];
|
||||
$s['auto'] = $s['done'];
|
||||
$s['done'] = $s['done'] || isset($state['done'][$s['key']]);
|
||||
$s['skipped'] = !$s['done'] && !$s['required'] && isset($state['skip'][$s['key']]);
|
||||
}
|
||||
unset($s);
|
||||
return $steps;
|
||||
}
|
||||
|
||||
/** [fatti (o saltati), totale, obbligatori mancanti] */
|
||||
public static function progress(array $steps): array
|
||||
{
|
||||
$done = 0;
|
||||
$missing = 0;
|
||||
foreach ($steps as $s) {
|
||||
if ($s['done'] || $s['skipped']) {
|
||||
$done++;
|
||||
} elseif ($s['required']) {
|
||||
$missing++;
|
||||
}
|
||||
}
|
||||
return [$done, count($steps), $missing];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,976 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ShopService
|
||||
* Logica del negozio della scuola in un unico posto: catalogo, classi valide,
|
||||
* posti liberi, situazione dell'allieva. Usata da:
|
||||
* - shop.php (negozio dell'allieva nella webapp)
|
||||
* - in seguito: checkout, API pubblica per WordPress (apiwp), app mobile
|
||||
*
|
||||
* Tutti i metodi ricevono $idschool esplicito: funziona anche senza sessione.
|
||||
* Posti occupati = prenotazioni 'booked' + 'pending' della lezione (come BookingRules).
|
||||
*/
|
||||
|
||||
if (!class_exists('DBHandlerSelect')) {
|
||||
require_once __DIR__ . '/db-functions.php';
|
||||
}
|
||||
|
||||
class ShopService
|
||||
{
|
||||
/** Giorni di calendario considerati per i posti delle classi fisse */
|
||||
const LOOKAHEAD_DAYS = 120;
|
||||
/** Lezioni singole/prova acquistabili nei prossimi N giorni */
|
||||
const SINGLE_DAYS = 21;
|
||||
/** Massimo di lezioni future lette per ogni classe */
|
||||
const MAX_OCCURRENCES = 30;
|
||||
|
||||
const GIORNI = ['domenica', 'lunedì', 'martedì', 'mercoledì', 'giovedì', 'venerdì', 'sabato'];
|
||||
const MESI = [1 => 'gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'];
|
||||
|
||||
/**
|
||||
* Catalogo completo, pronto per json_encode (lo stesso formato servirà all'API WordPress).
|
||||
*
|
||||
* @param int $idschool
|
||||
* @param int|null $userId allieva loggata (per prova già usata, tessera, pacchetti attivi)
|
||||
* @param bool $onlineOnly true = solo prodotti "acquistabili online"
|
||||
*/
|
||||
public static function catalog(int $idschool, ?int $userId = null, bool $onlineOnly = true): array
|
||||
{
|
||||
$pdo = self::pdo();
|
||||
|
||||
// ---- Prodotti e opzioni
|
||||
$st = $pdo->prepare(
|
||||
"SELECT * FROM products WHERE idschool = ? AND active = 'Y'" . ($onlineOnly ? " AND visible_online = 'Y'" : "") . "
|
||||
ORDER BY FIELD(kind, 'lessons', 'membership', 'voucher'), sort_order, title"
|
||||
);
|
||||
$st->execute([$idschool]);
|
||||
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$variantsBy = [];
|
||||
$st = $pdo->prepare("SELECT * FROM product_variants WHERE idschool = ? AND active = 'Y' ORDER BY sort_order, idvariant");
|
||||
$st->execute([$idschool]);
|
||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $v) {
|
||||
$variantsBy[(int) $v['idproduct']][] = $v;
|
||||
}
|
||||
|
||||
$linksBy = [];
|
||||
$st = $pdo->prepare("SELECT idproduct, idservice, idservicecategory FROM product_services WHERE idschool = ?");
|
||||
$st->execute([$idschool]);
|
||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $l) {
|
||||
$linksBy[(int) $l['idproduct']][] = $l;
|
||||
}
|
||||
|
||||
$galleryBy = [];
|
||||
$st = $pdo->prepare("SELECT idproduct, filename FROM product_images WHERE idschool = ? ORDER BY sort_order, idimage");
|
||||
$st->execute([$idschool]);
|
||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $g) {
|
||||
$galleryBy[(int) $g['idproduct']][] = $g['filename'];
|
||||
}
|
||||
|
||||
$variantLabel = [];
|
||||
$st = $pdo->prepare("SELECT v.idvariant, v.label, p.title FROM product_variants v JOIN products p ON p.idproduct = v.idproduct WHERE v.idschool = ?");
|
||||
$st->execute([$idschool]);
|
||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$variantLabel[(int) $r['idvariant']] = $r['title'] . ' · ' . $r['label'];
|
||||
}
|
||||
|
||||
// ---- Classi della scuola
|
||||
$showTeacher = SchoolSettings::get('show_teacher', 'N', $idschool) === 'Y';
|
||||
$st = $pdo->prepare(
|
||||
"SELECT s.idservice, s.servicename, s.day, s.time, s.category, s.maxcapacity, s.colorclass, s.classduration,
|
||||
TRIM(CONCAT(COALESCE(em.nameemployee, ''), ' ', COALESCE(em.surnameemployee, ''))) AS teacher
|
||||
FROM service s
|
||||
LEFT JOIN employee em ON em.idemployee = s.idteacher AND em.idschool = s.idschool
|
||||
WHERE s.idschool = ?"
|
||||
);
|
||||
$st->execute([$idschool]);
|
||||
$servicesAll = [];
|
||||
$byCategory = [];
|
||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $s) {
|
||||
$servicesAll[(int) $s['idservice']] = $s;
|
||||
$byCategory[(int) $s['category']][] = (int) $s['idservice'];
|
||||
}
|
||||
|
||||
$st = $pdo->prepare("SELECT idservicecategory, namecategory FROM servicecategory WHERE idschool = ? ORDER BY namecategory");
|
||||
$st->execute([$idschool]);
|
||||
$catNames = [];
|
||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $c) {
|
||||
$catNames[(int) $c['idservicecategory']] = $c['namecategory'];
|
||||
}
|
||||
|
||||
// ---- Situazione dell'allieva
|
||||
$user = self::userContext($idschool, $userId);
|
||||
|
||||
// ---- Costruzione prodotti
|
||||
$products = [];
|
||||
$usedSvcs = [];
|
||||
$usedCats = [];
|
||||
foreach ($rows as $p) {
|
||||
$pid = (int) $p['idproduct'];
|
||||
if (empty($variantsBy[$pid])) {
|
||||
continue; // nessuna opzione attiva = non acquistabile
|
||||
}
|
||||
$svc = [];
|
||||
foreach ($linksBy[$pid] ?? [] as $l) {
|
||||
if (!empty($l['idservice']) && isset($servicesAll[(int) $l['idservice']])) {
|
||||
$svc[] = (int) $l['idservice'];
|
||||
}
|
||||
if (!empty($l['idservicecategory'])) {
|
||||
$svc = array_merge($svc, $byCategory[(int) $l['idservicecategory']] ?? []);
|
||||
}
|
||||
}
|
||||
$svc = array_values(array_unique($svc));
|
||||
if ($p['kind'] === 'lessons' && !$svc) {
|
||||
continue; // pacchetto senza classi: non vendibile
|
||||
}
|
||||
|
||||
$cats = [];
|
||||
foreach ($svc as $sid) {
|
||||
$cid = (int) $servicesAll[$sid]['category'];
|
||||
if (isset($catNames[$cid])) {
|
||||
$cats[$cid] = true;
|
||||
}
|
||||
}
|
||||
$cats = array_keys($cats);
|
||||
if ($p['kind'] === 'lessons') {
|
||||
$usedSvcs = array_merge($usedSvcs, $svc);
|
||||
$usedCats = array_merge($usedCats, $cats);
|
||||
}
|
||||
|
||||
$variants = [];
|
||||
foreach ($variantsBy[$pid] as $v) {
|
||||
$variants[] = [
|
||||
'id' => (int) $v['idvariant'],
|
||||
'label' => $v['label'],
|
||||
'lessons' => $v['lessons'] !== null ? (int) $v['lessons'] : null,
|
||||
'price' => (float) $v['price'],
|
||||
'compare' => $v['compare_price'] !== null ? (float) $v['compare_price'] : null,
|
||||
'note' => $v['price_note'],
|
||||
'vmode' => $v['validity_mode'],
|
||||
'vvalue' => $v['validity_value'] !== null ? (int) $v['validity_value'] : null,
|
||||
'vunit' => $v['validity_unit'],
|
||||
'vstart' => $v['validity_start'],
|
||||
'target' => $v['target_idvariant'] !== null ? (int) $v['target_idvariant'] : null,
|
||||
'targetLabel' => $v['target_idvariant'] !== null ? ($variantLabel[(int) $v['target_idvariant']] ?? '') : '',
|
||||
];
|
||||
}
|
||||
|
||||
$mode = $p['kind'] === 'lessons' ? $p['booking_mode'] : $p['kind'];
|
||||
$products[] = [
|
||||
'id' => $pid,
|
||||
'slug' => $p['slug'],
|
||||
'kind' => $p['kind'],
|
||||
'mode' => $mode, // fixed | free | single | membership | voucher
|
||||
'group' => self::group($p), // per i filtri del catalogo
|
||||
'slots' => (int) $p['fixed_slots'],
|
||||
'weekly' => $p['weekly_limit'] !== null ? (int) $p['weekly_limit'] : null,
|
||||
'maxres' => $p['max_reschedule'] !== null ? (int) $p['max_reschedule'] : null,
|
||||
'once' => $p['once_per_student'] === 'Y',
|
||||
'title' => $p['title'],
|
||||
'subtitle' => (string) $p['subtitle'],
|
||||
'badge' => (string) $p['badge'],
|
||||
'cover' => (string) $p['cover_image'],
|
||||
'gallery' => $galleryBy[$pid] ?? [],
|
||||
'description' => (string) $p['description'], // HTML già ripulito al salvataggio
|
||||
'suitable' => (string) $p['suitable_for'],
|
||||
'requirements' => (string) $p['requirements'],
|
||||
'pros' => self::lines($p['pros']),
|
||||
'cons' => self::lines($p['cons']),
|
||||
'services' => $svc,
|
||||
'cats' => $cats,
|
||||
'variants' => $variants,
|
||||
'blocked' => ($p['once_per_student'] === 'Y' && in_array($pid, $user['usedProducts'], true)) ? 'used' : null,
|
||||
];
|
||||
}
|
||||
|
||||
// ---- Calendario e posti delle classi usate dai pacchetti
|
||||
$services = self::servicesWithOccurrences($idschool, array_values(array_unique($usedSvcs)), $servicesAll, $showTeacher, $userId);
|
||||
|
||||
// ---- Discipline (categorie) presenti nel catalogo
|
||||
$categories = [];
|
||||
foreach (array_unique($usedCats) as $cid) {
|
||||
$categories[] = ['id' => $cid, 'name' => $catNames[$cid]];
|
||||
}
|
||||
usort($categories, fn($a, $b) => strcasecmp($a['name'], $b['name']));
|
||||
|
||||
// ---- Date utili (tessere)
|
||||
$seasonStart = max(1, min(12, (int) SchoolSettings::get('membership_season_start', 9, $idschool)));
|
||||
$today = new DateTimeImmutable('today');
|
||||
$y = (int) $today->format('Y');
|
||||
$endYear = ((int) $today->format('n') >= $seasonStart) ? $y + 1 : $y;
|
||||
$seasonEnd = (new DateTimeImmutable(sprintf('%04d-%02d-01', $endYear, $seasonStart)))->modify('-1 day');
|
||||
|
||||
// Prodotto tessera da proporre se serve
|
||||
$user['membershipProduct'] = null;
|
||||
foreach ($products as $p) {
|
||||
if ($p['kind'] === 'membership') {
|
||||
$user['membershipProduct'] = $p['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($user['usedProducts']);
|
||||
|
||||
return [
|
||||
'today' => $today->format('Y-m-d'),
|
||||
'yearEnd' => $y . '-12-31',
|
||||
'seasonEnd' => $seasonEnd->format('Y-m-d'),
|
||||
'singleDays' => self::SINGLE_DAYS,
|
||||
'products' => $products,
|
||||
'services' => (object) $services,
|
||||
'categories' => $categories,
|
||||
'user' => $user,
|
||||
];
|
||||
}
|
||||
|
||||
/** Gruppo del catalogo: single | fixed | free | membership | voucher */
|
||||
public static function group(array $p): string
|
||||
{
|
||||
return $p['kind'] === 'lessons' ? (string) $p['booking_mode'] : (string) $p['kind'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Situazione dell'allieva nella scuola: prodotti "una volta" già acquistati,
|
||||
* tessera valida, pacchetti attivi.
|
||||
*/
|
||||
public static function userContext(int $idschool, ?int $userId): array
|
||||
{
|
||||
$required = SchoolSettings::get('membership_required', 'N', $idschool) === 'Y';
|
||||
$ctx = [
|
||||
'logged' => (bool) $userId,
|
||||
'membershipRequired' => $required,
|
||||
'hasMembership' => false,
|
||||
'membershipUntil' => null,
|
||||
'packages' => [],
|
||||
'usedProducts' => [],
|
||||
];
|
||||
if (!$userId) {
|
||||
return $ctx;
|
||||
}
|
||||
$pdo = self::pdo();
|
||||
|
||||
$st = $pdo->prepare(
|
||||
"SELECT DISTINCT idproduct FROM orderbook
|
||||
WHERE iduser = ? AND idschool = ? AND idproduct IS NOT NULL
|
||||
AND (payment_status IS NULL OR payment_status NOT IN ('cancelled', 'refunded'))"
|
||||
);
|
||||
$st->execute([$userId, $idschool]);
|
||||
$ctx['usedProducts'] = array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
|
||||
|
||||
$st = $pdo->prepare(
|
||||
"SELECT MAX(valid_to) FROM memberships
|
||||
WHERE iduser = ? AND idschool = ? AND status = 'active' AND valid_from <= CURDATE() AND valid_to >= CURDATE()"
|
||||
);
|
||||
$st->execute([$userId, $idschool]);
|
||||
$until = $st->fetchColumn();
|
||||
$ctx['hasMembership'] = (bool) $until;
|
||||
$ctx['membershipUntil'] = $until ?: null;
|
||||
|
||||
// Pacchetti attivi: non scaduti e con lezioni rimaste
|
||||
$st = $pdo->prepare(
|
||||
"SELECT ob.idorderbook, ob.product_name, ob.nticket, ob.quantityclass, ob.expireon, ob.payment_status,
|
||||
s.servicename,
|
||||
(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
|
||||
FROM orderbook ob
|
||||
LEFT JOIN service s ON s.idservice = ob.idservice
|
||||
WHERE ob.iduser = ? AND ob.idschool = ?
|
||||
AND (ob.expireon IS NULL OR ob.expireon >= CURDATE())
|
||||
AND (ob.payment_status IS NULL OR ob.payment_status NOT IN ('cancelled', 'refunded'))
|
||||
ORDER BY ob.expireon IS NULL, ob.expireon"
|
||||
);
|
||||
$st->execute([$userId, $idschool]);
|
||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $o) {
|
||||
$tot = (int) $o['nticket'] > 0 ? (int) $o['nticket'] : (int) $o['quantityclass'];
|
||||
$left = $tot > 0 ? max(0, $tot - (int) $o['used']) : null;
|
||||
if ($left === 0) {
|
||||
continue;
|
||||
}
|
||||
$ctx['packages'][] = [
|
||||
'title' => (string) ($o['product_name'] ?: $o['servicename'] ?: 'Pacchetto'),
|
||||
'left' => $left,
|
||||
'expire' => $o['expireon'],
|
||||
'pending' => $o['payment_status'] === 'pending',
|
||||
];
|
||||
}
|
||||
return $ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lezioni future delle classi indicate, con posti liberi.
|
||||
* @return array idservice => [name, day, teacher, cap, cat, occ => [[id, date, time, label, free, mine]]]
|
||||
*/
|
||||
public static function servicesWithOccurrences(int $idschool, array $ids, array $servicesAll, bool $showTeacher, ?int $userId = null): array
|
||||
{
|
||||
$out = [];
|
||||
foreach ($ids as $sid) {
|
||||
if (!isset($servicesAll[$sid])) {
|
||||
continue;
|
||||
}
|
||||
$s = $servicesAll[$sid];
|
||||
$out[$sid] = [
|
||||
'name' => $s['servicename'],
|
||||
'day' => trim($s['day'] . ' ' . $s['time']),
|
||||
'teacher' => $showTeacher ? trim((string) $s['teacher']) : '',
|
||||
'cap' => ($s['maxcapacity'] !== null && (int) $s['maxcapacity'] > 0) ? (int) $s['maxcapacity'] : null,
|
||||
'cat' => (int) $s['category'],
|
||||
'color' => preg_match('/^#[0-9a-fA-F]{6}$/', (string) ($s['colorclass'] ?? '')) ? strtolower($s['colorclass']) : null,
|
||||
'minutes' => (int) round((float) ($s['classduration'] ?? 0) * ((float) ($s['classduration'] ?? 0) <= 5 ? 60 : 1)),
|
||||
'occ' => [],
|
||||
];
|
||||
}
|
||||
if (!$out) {
|
||||
return $out;
|
||||
}
|
||||
|
||||
$ph = implode(',', array_fill(0, count($out), '?'));
|
||||
$st = self::pdo()->prepare(
|
||||
"SELECT ss.idserviceschedule, ss.idservice, ss.dateschedule,
|
||||
(SELECT COUNT(*) FROM bookingclass b
|
||||
WHERE b.idserviceschedule = ss.idserviceschedule AND b.idschool = ss.idschool
|
||||
AND b.status IN ('booked', 'pending')) AS taken
|
||||
" . ($userId ? ", (SELECT COUNT(*) FROM bookingclass b2
|
||||
WHERE b2.idserviceschedule = ss.idserviceschedule AND b2.idschool = ss.idschool
|
||||
AND b2.iduser = ? AND b2.status <> 'cancelled') AS mine" : ", 0 AS mine") . "
|
||||
FROM serviceschedule ss
|
||||
WHERE ss.idschool = ?
|
||||
AND ss.idservice IN ($ph)
|
||||
AND ss.dateschedule > NOW()
|
||||
AND ss.dateschedule < CURDATE() + INTERVAL " . (int) self::LOOKAHEAD_DAYS . " DAY
|
||||
AND NOT EXISTS (SELECT 1 FROM dayoff d WHERE d.idschool = ss.idschool AND d.dayoffdate = DATE(ss.dateschedule))
|
||||
ORDER BY ss.dateschedule"
|
||||
);
|
||||
$args = $userId ? [$userId, $idschool] : [$idschool];
|
||||
$st->execute(array_merge($args, array_keys($out)));
|
||||
|
||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $o) {
|
||||
$sid = (int) $o['idservice'];
|
||||
if (count($out[$sid]['occ']) >= self::MAX_OCCURRENCES) {
|
||||
continue;
|
||||
}
|
||||
$ts = strtotime($o['dateschedule']);
|
||||
$cap = $out[$sid]['cap'];
|
||||
$out[$sid]['occ'][] = [
|
||||
'id' => (int) $o['idserviceschedule'],
|
||||
'date' => date('Y-m-d', $ts),
|
||||
'time' => date('H:i', $ts),
|
||||
'label' => self::dateLabel($ts),
|
||||
'free' => $cap === null ? null : max(0, $cap - (int) $o['taken']),
|
||||
'mine' => (int) $o['mine'] > 0,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** "Martedì 13 ottobre · 18:00" */
|
||||
public static function dateLabel(int $ts): string
|
||||
{
|
||||
return ucfirst(self::GIORNI[(int) date('w', $ts)]) . ' ' . date('j', $ts) . ' ' . self::MESI[(int) date('n', $ts)] . ' · ' . date('H:i', $ts);
|
||||
}
|
||||
|
||||
public static function lines(?string $t): array
|
||||
{
|
||||
return array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', (string) $t))));
|
||||
}
|
||||
|
||||
/* ==================================================================
|
||||
* CHECKOUT
|
||||
* ================================================================ */
|
||||
|
||||
/** Metodi di pagamento attivi per la scuola */
|
||||
public static function paymentMethods(int $idschool): array
|
||||
{
|
||||
$bank = SchoolSettings::get('bank_enabled', 'N', $idschool) === 'Y'
|
||||
&& trim((string) SchoolSettings::get('bank_holder', '', $idschool)) !== ''
|
||||
&& trim((string) SchoolSettings::get('bank_iban', '', $idschool)) !== '';
|
||||
// La carta arriverà con il collegamento Stripe Connect della scuola
|
||||
$card = SchoolSettings::get('stripe_charges_enabled', 'N', $idschool) === 'Y'
|
||||
&& trim((string) SchoolSettings::get('stripe_account_id', '', $idschool)) !== '';
|
||||
return ['bank' => $bank, 'card' => $card];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ricontrolla lato server la scelta arrivata dal negozio.
|
||||
* $in: idvariant, services (array di idservice), idschedule, recipient_name, recipient_email, recipient_message
|
||||
* @return array product, variant, services, schedule, recipient
|
||||
* @throws RuntimeException con messaggio per l'allieva
|
||||
*/
|
||||
public static function selection(int $idschool, int $userId, array $in): array
|
||||
{
|
||||
$pdo = self::pdo();
|
||||
$st = $pdo->prepare(
|
||||
"SELECT v.*, p.kind, p.title, p.booking_mode, p.fixed_slots, p.weekly_limit, p.max_reschedule,
|
||||
p.once_per_student, p.active AS p_active, p.visible_online, p.slug
|
||||
FROM product_variants v
|
||||
JOIN products p ON p.idproduct = v.idproduct AND p.idschool = v.idschool
|
||||
WHERE v.idvariant = ? AND v.idschool = ?"
|
||||
);
|
||||
$st->execute([(int) ($in['idvariant'] ?? 0), $idschool]);
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row || $row['active'] !== 'Y' || $row['p_active'] !== 'Y' || $row['visible_online'] !== 'Y') {
|
||||
throw new RuntimeException('Questo prodotto non è più disponibile.');
|
||||
}
|
||||
$pid = (int) $row['idproduct'];
|
||||
$mode = $row['kind'] === 'lessons' ? $row['booking_mode'] : $row['kind'];
|
||||
|
||||
if ($row['once_per_student'] === 'Y') {
|
||||
$st = $pdo->prepare(
|
||||
"SELECT 1 FROM orderbook WHERE iduser = ? AND idschool = ? AND idproduct = ?
|
||||
AND (payment_status IS NULL OR payment_status NOT IN ('cancelled', 'refunded')) LIMIT 1"
|
||||
);
|
||||
$st->execute([$userId, $idschool, $pid]);
|
||||
if ($st->fetchColumn()) {
|
||||
throw new RuntimeException('Hai già utilizzato questo prodotto: si può acquistare una sola volta.');
|
||||
}
|
||||
}
|
||||
|
||||
$allowed = self::productServiceIds($idschool, $pid);
|
||||
$sel = [
|
||||
'product' => $row,
|
||||
'mode' => $mode,
|
||||
'variant' => $row,
|
||||
'services' => [],
|
||||
'schedule' => null,
|
||||
'recipient' => null,
|
||||
];
|
||||
|
||||
if ($mode === 'fixed') {
|
||||
$slots = (int) $row['fixed_slots'] === 2 ? 2 : 1;
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', (array) ($in['services'] ?? [])))));
|
||||
if (count($ids) !== $slots) {
|
||||
throw new RuntimeException($slots === 2 ? 'Scegli le tue due classi.' : 'Scegli la tua classe.');
|
||||
}
|
||||
$weeks = $row['lessons'] ? (int) ceil((int) $row['lessons'] / $slots) : 8;
|
||||
foreach ($ids as $sid) {
|
||||
if (!in_array($sid, $allowed, true)) {
|
||||
throw new RuntimeException('Una delle classi scelte non è inclusa in questo prodotto.');
|
||||
}
|
||||
$free = self::fixedFree($idschool, $sid, $weeks);
|
||||
if ($free === -1) {
|
||||
throw new RuntimeException('Una delle classi scelte non ha lezioni in calendario.');
|
||||
}
|
||||
if ($free === 0) {
|
||||
throw new RuntimeException('Una delle classi scelte è al completo. Torna indietro e scegline un\'altra.');
|
||||
}
|
||||
}
|
||||
$sel['services'] = $ids;
|
||||
}
|
||||
|
||||
if ($mode === 'single') {
|
||||
$st = $pdo->prepare(
|
||||
"SELECT ss.*, s.servicename, s.maxcapacity FROM serviceschedule ss
|
||||
JOIN service s ON s.idservice = ss.idservice
|
||||
WHERE ss.idserviceschedule = ? AND ss.idschool = ? AND ss.dateschedule > NOW()"
|
||||
);
|
||||
$st->execute([(int) ($in['idschedule'] ?? 0), $idschool]);
|
||||
$sch = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$sch || !in_array((int) $sch['idservice'], $allowed, true)) {
|
||||
throw new RuntimeException('La lezione scelta non è più disponibile.');
|
||||
}
|
||||
$st = $pdo->prepare("SELECT 1 FROM bookingclass WHERE idserviceschedule = ? AND iduser = ? AND idschool = ? AND status <> 'cancelled' LIMIT 1");
|
||||
$st->execute([(int) $sch['idserviceschedule'], $userId, $idschool]);
|
||||
if ($st->fetchColumn()) {
|
||||
throw new RuntimeException('Sei già iscritta a questa lezione.');
|
||||
}
|
||||
if ((int) $sch['maxcapacity'] > 0 && self::occupied($idschool, (int) $sch['idserviceschedule']) >= (int) $sch['maxcapacity']) {
|
||||
throw new RuntimeException('La lezione scelta è al completo. Torna indietro e scegli un\'altra data.');
|
||||
}
|
||||
$sel['schedule'] = $sch;
|
||||
}
|
||||
|
||||
if ($mode === 'voucher') {
|
||||
$name = trim((string) ($in['recipient_name'] ?? ''));
|
||||
$email = trim((string) ($in['recipient_email'] ?? ''));
|
||||
if ($name === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new RuntimeException('Indica nome ed email di chi riceve il buono.');
|
||||
}
|
||||
if (empty($row['target_idvariant'])) {
|
||||
throw new RuntimeException('Questo buono non è configurato correttamente. Contatta la scuola.');
|
||||
}
|
||||
$sel['recipient'] = [
|
||||
'name' => mb_substr($name, 0, 191),
|
||||
'email' => mb_substr($email, 0, 191),
|
||||
'message' => mb_substr(trim((string) ($in['recipient_message'] ?? '')), 0, 500),
|
||||
];
|
||||
}
|
||||
return $sel;
|
||||
}
|
||||
|
||||
/** idservice validi per un prodotto (classi singole + categorie intere) */
|
||||
public static function productServiceIds(int $idschool, int $idproduct): array
|
||||
{
|
||||
$st = self::pdo()->prepare(
|
||||
"SELECT DISTINCT s.idservice
|
||||
FROM product_services ps
|
||||
JOIN service s ON s.idschool = ps.idschool
|
||||
AND (s.idservice = ps.idservice OR (ps.idservicecategory IS NOT NULL AND s.category = ps.idservicecategory))
|
||||
WHERE ps.idproduct = ? AND ps.idschool = ?"
|
||||
);
|
||||
$st->execute([$idproduct, $idschool]);
|
||||
return array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
|
||||
}
|
||||
|
||||
public static function occupied(int $idschool, int $idschedule): int
|
||||
{
|
||||
$st = self::pdo()->prepare(
|
||||
"SELECT COUNT(*) FROM bookingclass WHERE idserviceschedule = ? AND idschool = ? AND status IN ('booked', 'pending')"
|
||||
);
|
||||
$st->execute([$idschedule, $idschool]);
|
||||
return (int) $st->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Posti liberi minimi sulle prossime $weeks lezioni di una classe.
|
||||
* null = senza limite, -1 = nessuna lezione in calendario
|
||||
*/
|
||||
public static function fixedFree(int $idschool, int $idservice, int $weeks): ?int
|
||||
{
|
||||
$pdo = self::pdo();
|
||||
$st = $pdo->prepare("SELECT maxcapacity FROM service WHERE idservice = ? AND idschool = ?");
|
||||
$st->execute([$idservice, $idschool]);
|
||||
$cap = (int) $st->fetchColumn();
|
||||
|
||||
$st = $pdo->prepare(
|
||||
"SELECT (SELECT COUNT(*) FROM bookingclass b WHERE b.idserviceschedule = ss.idserviceschedule
|
||||
AND b.idschool = ss.idschool AND b.status IN ('booked', 'pending')) AS taken
|
||||
FROM serviceschedule ss
|
||||
WHERE ss.idservice = ? AND ss.idschool = ? AND ss.dateschedule > NOW()
|
||||
AND NOT EXISTS (SELECT 1 FROM dayoff d WHERE d.idschool = ss.idschool AND d.dayoffdate = DATE(ss.dateschedule))
|
||||
ORDER BY ss.dateschedule
|
||||
LIMIT " . max(1, $weeks)
|
||||
);
|
||||
$st->execute([$idservice, $idschool]);
|
||||
$taken = array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
|
||||
if (!$taken) {
|
||||
return -1;
|
||||
}
|
||||
return $cap > 0 ? max(0, $cap - max($taken)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica un codice sconto per l'opzione scelta (l'utente è sempre quello della sessione).
|
||||
* @return array idcoupon, code, discount
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function checkCoupon(int $idschool, int $userId, string $code, array $variant): array
|
||||
{
|
||||
$code = strtoupper(trim($code));
|
||||
$price = (float) $variant['price'];
|
||||
$pdo = self::pdo();
|
||||
|
||||
$st = $pdo->prepare("SELECT * FROM coupons WHERE idschool = ? AND code = ? AND active = 'Y' LIMIT 1");
|
||||
$st->execute([$idschool, $code]);
|
||||
$c = $st->fetch(PDO::FETCH_ASSOC);
|
||||
$today = date('Y-m-d');
|
||||
if (!$c || ($c['valid_from'] && $today < $c['valid_from']) || ($c['valid_to'] && $today > $c['valid_to'])) {
|
||||
throw new RuntimeException('Codice non valido o scaduto.');
|
||||
}
|
||||
|
||||
$st = $pdo->prepare("SELECT idproduct, idvariant FROM coupon_products WHERE idcoupon = ? AND idschool = ?");
|
||||
$st->execute([(int) $c['idcoupon'], $idschool]);
|
||||
$limits = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($limits) {
|
||||
$ok = false;
|
||||
foreach ($limits as $l) {
|
||||
if (
|
||||
(int) $l['idvariant'] === (int) $variant['idvariant']
|
||||
|| ((int) $l['idproduct'] === (int) $variant['idproduct'] && empty($l['idvariant']))
|
||||
) {
|
||||
$ok = true;
|
||||
}
|
||||
}
|
||||
if (!$ok) {
|
||||
throw new RuntimeException('Questo codice non vale su questo prodotto.');
|
||||
}
|
||||
}
|
||||
if ($c['min_amount'] !== null && $price < (float) $c['min_amount']) {
|
||||
throw new RuntimeException('Codice valido per acquisti da ' . number_format((float) $c['min_amount'], 2, ',', '.') . ' €.');
|
||||
}
|
||||
if ($c['max_uses'] !== null) {
|
||||
$st = $pdo->prepare("SELECT COUNT(*) FROM coupon_redemptions WHERE idcoupon = ? AND idschool = ?");
|
||||
$st->execute([(int) $c['idcoupon'], $idschool]);
|
||||
if ((int) $st->fetchColumn() >= (int) $c['max_uses']) {
|
||||
throw new RuntimeException('Codice esaurito.');
|
||||
}
|
||||
}
|
||||
if ($c['max_uses_per_user'] !== null) {
|
||||
$st = $pdo->prepare("SELECT COUNT(*) FROM coupon_redemptions WHERE idcoupon = ? AND idschool = ? AND iduser = ?");
|
||||
$st->execute([(int) $c['idcoupon'], $idschool, $userId]);
|
||||
if ((int) $st->fetchColumn() >= (int) $c['max_uses_per_user']) {
|
||||
throw new RuntimeException('Hai già usato questo codice.');
|
||||
}
|
||||
}
|
||||
if ($c['new_students_only'] === 'Y') {
|
||||
$st = $pdo->prepare("SELECT COUNT(*) FROM orderbook WHERE iduser = ? AND idschool = ?");
|
||||
$st->execute([$userId, $idschool]);
|
||||
if ((int) $st->fetchColumn() > 0) {
|
||||
throw new RuntimeException('Codice riservato alle nuove allieve.');
|
||||
}
|
||||
}
|
||||
$value = (float) $c['discount_value'];
|
||||
$discount = $c['discount_type'] === 'fixed' ? min($value, $price) : round($price * min($value, 100) / 100, 2);
|
||||
return ['idcoupon' => (int) $c['idcoupon'], 'code' => $c['code'], 'discount' => $discount];
|
||||
}
|
||||
|
||||
/** Scadenza della tessera per un'opzione (Y-m-d o null) */
|
||||
public static function membershipEnd(int $idschool, array $variant, ?string $from = null): ?string
|
||||
{
|
||||
$from = $from ?? date('Y-m-d');
|
||||
if ($variant['validity_mode'] === 'calendar_year') {
|
||||
return substr($from, 0, 4) . '-12-31';
|
||||
}
|
||||
if ($variant['validity_mode'] === 'season') {
|
||||
$start = max(1, min(12, (int) SchoolSettings::get('membership_season_start', 9, $idschool)));
|
||||
$y = (int) substr($from, 0, 4);
|
||||
$endYear = ((int) substr($from, 5, 2) >= $start) ? $y + 1 : $y;
|
||||
return (new DateTimeImmutable(sprintf('%04d-%02d-01', $endYear, $start)))->modify('-1 day')->format('Y-m-d');
|
||||
}
|
||||
if (!empty($variant['validity_value'])) {
|
||||
return self::addPeriod($from, (int) $variant['validity_value'], (string) $variant['validity_unit'])->modify('-1 day')->format('Y-m-d');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function addPeriod(string $from, int $n, string $unit): DateTimeImmutable
|
||||
{
|
||||
$d = new DateTimeImmutable($from);
|
||||
if ($unit === 'days') {
|
||||
return $d->modify('+' . $n . ' days');
|
||||
}
|
||||
if ($unit === 'weeks') {
|
||||
return $d->modify('+' . ($n * 7) . ' days');
|
||||
}
|
||||
// mesi senza sforare (31 gennaio + 1 mese = fine febbraio)
|
||||
$y = (int) $d->format('Y');
|
||||
$m = (int) $d->format('n') + $n;
|
||||
$y += intdiv($m - 1, 12);
|
||||
$m = (($m - 1) % 12) + 1;
|
||||
$last = (int) (new DateTimeImmutable(sprintf('%04d-%02d-01', $y, $m)))->format('t');
|
||||
return $d->setDate($y, $m, min((int) $d->format('j'), $last));
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea l'ordine (orderbook) e tutto ciò che ne deriva, in un'unica transazione.
|
||||
* - classe fissa: orderbook_services (le prenotazioni le crea la scuola alla propagazione)
|
||||
* - lezione singola: prenotazione subito (bonifico = 'pending', carta = 'booked')
|
||||
* - buono regalo: gift_vouchers in attesa del pagamento
|
||||
* - tessera: la riga in memberships nasce alla conferma del pagamento
|
||||
* @param string $method 'bank' | 'stripe'
|
||||
* @return int idorderbook
|
||||
*/
|
||||
public static function createOrder(int $idschool, int $userId, array $sel, string $method, ?array $coupon): int
|
||||
{
|
||||
$pdo = self::pdo();
|
||||
$p = $sel['product'];
|
||||
$v = $sel['variant'];
|
||||
$mode = $sel['mode'];
|
||||
$paid = $method === 'stripe';
|
||||
|
||||
$st = $pdo->prepare("SELECT first_name, last_name, email FROM auth_users WHERE id = ?");
|
||||
$st->execute([$userId]);
|
||||
$u = $st->fetch(PDO::FETCH_ASSOC) ?: ['first_name' => '', 'last_name' => '', 'email' => ''];
|
||||
|
||||
$price = round((float) $v['price'], 2);
|
||||
$discount = $coupon ? min(round((float) $coupon['discount'], 2), $price) : 0.0;
|
||||
$total = round($price - $discount, 2);
|
||||
$lessons = $v['lessons'] !== null ? (int) $v['lessons'] : null;
|
||||
$today = date('Y-m-d');
|
||||
|
||||
// Scadenza
|
||||
$expire = null;
|
||||
if ($mode === 'free' && $v['validity_mode'] === 'duration' && $v['validity_value'] && $v['validity_start'] === 'purchase') {
|
||||
$expire = self::addPeriod($today, (int) $v['validity_value'], (string) $v['validity_unit'])->format('Y-m-d');
|
||||
} elseif ($mode === 'single' && $sel['schedule']) {
|
||||
$expire = substr($sel['schedule']['dateschedule'], 0, 10);
|
||||
} elseif ($mode === 'membership') {
|
||||
$expire = self::membershipEnd($idschool, $v, $today);
|
||||
}
|
||||
|
||||
// Recuperi: dal prodotto, altrimenti dai parametri della scuola per quel numero di lezioni
|
||||
$maxres = $p['max_reschedule'] !== null ? (int) $p['max_reschedule'] : null;
|
||||
if ($maxres === null && $lessons && in_array($mode, ['fixed', 'free'], true)) {
|
||||
$st = $pdo->prepare("SELECT reschedulenumber FROM expiryparameter WHERE quantityclass = ? AND idschool = ? LIMIT 1");
|
||||
$st->execute([$lessons, $idschool]);
|
||||
$r = $st->fetchColumn();
|
||||
$maxres = $r !== false ? (int) $r : null;
|
||||
}
|
||||
if ($mode === 'single') {
|
||||
$maxres = 0;
|
||||
}
|
||||
|
||||
$idservice = null;
|
||||
if ($mode === 'fixed') {
|
||||
$idservice = $sel['services'][0];
|
||||
} elseif ($mode === 'single') {
|
||||
$idservice = (int) $sel['schedule']['idservice'];
|
||||
}
|
||||
|
||||
$snapshot = [
|
||||
'mode' => $mode,
|
||||
'fixed_slots' => (int) $p['fixed_slots'],
|
||||
'weekly_limit' => $p['weekly_limit'] !== null ? (int) $p['weekly_limit'] : null,
|
||||
'max_reschedule' => $maxres,
|
||||
'lessons' => $lessons,
|
||||
'validity_mode' => $v['validity_mode'],
|
||||
'validity_value' => $v['validity_value'] !== null ? (int) $v['validity_value'] : null,
|
||||
'validity_unit' => $v['validity_unit'],
|
||||
'validity_start' => $v['validity_start'],
|
||||
'services' => $mode === 'free' || $mode === 'single' ? self::productServiceIds($idschool, (int) $p['idproduct']) : $sel['services'],
|
||||
'price' => $price,
|
||||
];
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
// Lezione singola: blocca la riga della lezione e ricontrolla il posto
|
||||
if ($mode === 'single') {
|
||||
$st = $pdo->prepare("SELECT ss.idserviceschedule, s.maxcapacity FROM serviceschedule ss JOIN service s ON s.idservice = ss.idservice WHERE ss.idserviceschedule = ? AND ss.idschool = ? FOR UPDATE");
|
||||
$st->execute([(int) $sel['schedule']['idserviceschedule'], $idschool]);
|
||||
$lock = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$lock || ((int) $lock['maxcapacity'] > 0 && self::occupied($idschool, (int) $lock['idserviceschedule']) >= (int) $lock['maxcapacity'])) {
|
||||
throw new RuntimeException('Nel frattempo la lezione si è riempita. Scegli un\'altra data.');
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->prepare(
|
||||
"INSERT INTO orderbook
|
||||
(order_date_created, order_billing_email, order_total, product_name, line_total, quantity,
|
||||
idservice, is_openflexy, iduser, nticket, expireon, status, quantityclass,
|
||||
first_name, last_name, maxreschedule, reprogrammed, idschool,
|
||||
idproduct, idvariant, booking_mode, weekly_limit, rules_snapshot, source, payment_status,
|
||||
amount, discount_amount, idcoupon)
|
||||
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
)->execute([
|
||||
$today,
|
||||
$u['email'],
|
||||
number_format($total, 2, '.', ''),
|
||||
mb_substr($p['title'] . ' · ' . $v['label'], 0, 200),
|
||||
number_format($total, 2, '.', ''),
|
||||
$idservice,
|
||||
$mode === 'free' ? 'Y' : 'N',
|
||||
$userId,
|
||||
$lessons,
|
||||
$expire,
|
||||
$paid ? ($mode === 'fixed' ? 'pending' : 'completed') : 'on-hold',
|
||||
$lessons,
|
||||
$u['first_name'],
|
||||
$u['last_name'],
|
||||
$maxres,
|
||||
$idschool,
|
||||
(int) $p['idproduct'],
|
||||
(int) $v['idvariant'],
|
||||
in_array($mode, ['fixed', 'free', 'single'], true) ? $mode : null,
|
||||
$p['weekly_limit'] !== null ? (int) $p['weekly_limit'] : null,
|
||||
json_encode($snapshot, JSON_UNESCAPED_UNICODE),
|
||||
$method,
|
||||
$paid ? 'paid' : 'pending',
|
||||
$total,
|
||||
$discount > 0 ? $discount : null,
|
||||
$coupon ? (int) $coupon['idcoupon'] : null,
|
||||
]);
|
||||
$idorder = (int) $pdo->lastInsertId();
|
||||
|
||||
if ($mode === 'fixed') {
|
||||
$ins = $pdo->prepare("INSERT INTO orderbook_services (idorderbook, idschool, idservice, slot) VALUES (?, ?, ?, ?)");
|
||||
foreach ($sel['services'] as $i => $sid) {
|
||||
$ins->execute([$idorder, $idschool, $sid, $i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($mode === 'single') {
|
||||
$pdo->prepare(
|
||||
"INSERT INTO bookingclass (idserviceschedule, bookingstart, status, idservice, iduser, idorder, cancellation_token, idschool)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
)->execute([
|
||||
(int) $sel['schedule']['idserviceschedule'],
|
||||
$sel['schedule']['dateschedule'],
|
||||
$paid ? 'booked' : 'pending',
|
||||
(int) $sel['schedule']['idservice'],
|
||||
$userId,
|
||||
$idorder,
|
||||
bin2hex(random_bytes(16)),
|
||||
$idschool,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($mode === 'voucher') {
|
||||
$codeSt = $pdo->prepare("SELECT 1 FROM gift_vouchers WHERE idschool = ? AND code = ?");
|
||||
do {
|
||||
$code = 'YB-' . self::randomCode(4) . '-' . self::randomCode(4);
|
||||
$codeSt->execute([$idschool, $code]);
|
||||
} while ($codeSt->fetchColumn());
|
||||
$vExpire = $v['validity_value'] ? self::addPeriod($today, (int) $v['validity_value'], (string) $v['validity_unit'])->format('Y-m-d') : null;
|
||||
$pdo->prepare(
|
||||
"INSERT INTO gift_vouchers (idschool, code, idvariant, buyer_iduser, buyer_name, buyer_email, recipient_name, recipient_email,
|
||||
message, source, amount, expires_at, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
)->execute([
|
||||
$idschool,
|
||||
$code,
|
||||
(int) $v['target_idvariant'],
|
||||
$userId,
|
||||
trim($u['first_name'] . ' ' . $u['last_name']),
|
||||
$u['email'],
|
||||
$sel['recipient']['name'],
|
||||
$sel['recipient']['email'],
|
||||
$sel['recipient']['message'] ?: null,
|
||||
$method,
|
||||
$total,
|
||||
$vExpire,
|
||||
$paid ? 'active' : 'pending',
|
||||
]);
|
||||
// collega il buono all'ordine (per attivarlo alla conferma del pagamento)
|
||||
$pdo->prepare("UPDATE orderbook SET cod = ? WHERE idorderbook = ?")->execute([$code, $idorder]);
|
||||
}
|
||||
|
||||
if ($mode === 'membership' && $paid) {
|
||||
$pdo->prepare("INSERT INTO memberships (idschool, iduser, idorderbook, valid_from, valid_to, status) VALUES (?, ?, ?, ?, ?, 'active')")
|
||||
->execute([$idschool, $userId, $idorder, $today, $expire ?? '2099-12-31']);
|
||||
}
|
||||
|
||||
if ($coupon) {
|
||||
$pdo->prepare("INSERT INTO coupon_redemptions (idcoupon, idschool, iduser, idorderbook, discount_amount) VALUES (?, ?, ?, ?, ?)")
|
||||
->execute([(int) $coupon['idcoupon'], $idschool, $userId, $idorder, $discount]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
return $idorder;
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/** Dati del bonifico per un ordine: intestatario, IBAN, banca, causale, scadenza */
|
||||
public static function bankInfo(int $idschool, array $order): array
|
||||
{
|
||||
$tpl = trim((string) SchoolSettings::get('bank_reference', '', $idschool)) ?: 'Ordine {ordine} - {nome}';
|
||||
$iban = strtoupper(preg_replace('/\s+/', '', (string) SchoolSettings::get('bank_iban', '', $idschool)));
|
||||
$days = max(1, (int) SchoolSettings::get('bank_due_days', 5, $idschool));
|
||||
$created = $order['order_date_created'] ?: date('Y-m-d');
|
||||
return [
|
||||
'holder' => (string) SchoolSettings::get('bank_holder', '', $idschool),
|
||||
'iban' => trim(chunk_split($iban, 4, ' ')),
|
||||
'bank' => (string) SchoolSettings::get('bank_name', '', $idschool),
|
||||
'reference' => mb_substr(strtr($tpl, ['{ordine}' => (string) $order['idorderbook'], '{nome}' => trim($order['first_name'] . ' ' . $order['last_name'])]), 0, 140),
|
||||
'due' => date('Y-m-d', strtotime($created . ' +' . $days . ' days')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* La scuola conferma il pagamento (bonifico ricevuto) di un ordine in attesa.
|
||||
* - ordine pagato
|
||||
* - lezione singola: la prenotazione passa da 'pending' a 'booked'
|
||||
* - tessera: nasce la riga in memberships
|
||||
* - buono regalo: diventa attivo
|
||||
* Idempotente: false se l'ordine non è (più) in attesa.
|
||||
*/
|
||||
public static function markPaid(int $idschool, int $idorder): bool
|
||||
{
|
||||
$pdo = self::pdo();
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$st = $pdo->prepare(
|
||||
"SELECT ob.*, p.kind FROM orderbook ob LEFT JOIN products p ON p.idproduct = ob.idproduct
|
||||
WHERE ob.idorderbook = ? AND ob.idschool = ? FOR UPDATE"
|
||||
);
|
||||
$st->execute([$idorder, $idschool]);
|
||||
$o = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$o || $o['payment_status'] !== 'pending') {
|
||||
$pdo->rollBack();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Classe fissa: 'pending' = da propagare (stesso significato degli ordini storici)
|
||||
$pdo->prepare("UPDATE orderbook SET payment_status = 'paid', status = ? WHERE idorderbook = ? AND idschool = ?")
|
||||
->execute([$o['booking_mode'] === 'fixed' ? 'pending' : 'completed', $idorder, $idschool]);
|
||||
|
||||
if ($o['booking_mode'] === 'single') {
|
||||
$pdo->prepare("UPDATE bookingclass SET status = 'booked' WHERE idorder = ? AND idschool = ? AND status = 'pending'")
|
||||
->execute([$idorder, $idschool]);
|
||||
}
|
||||
if ($o['kind'] === 'membership') {
|
||||
$pdo->prepare("INSERT INTO memberships (idschool, iduser, idorderbook, valid_from, valid_to, status) VALUES (?, ?, ?, CURDATE(), ?, 'active')")
|
||||
->execute([$idschool, (int) $o['iduser'], $idorder, $o['expireon'] ?: '2099-12-31']);
|
||||
}
|
||||
if (!empty($o['cod'])) {
|
||||
$pdo->prepare("UPDATE gift_vouchers SET status = 'active' WHERE idschool = ? AND code = ? AND status = 'pending'")
|
||||
->execute([$idschool, $o['cod']]);
|
||||
}
|
||||
$pdo->commit();
|
||||
return true;
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Annulla un ordine in attesa di pagamento (dall'allieva o dalla scuola):
|
||||
* libera il posto della lezione singola, annulla il buono e l'uso del coupon.
|
||||
* $userId: se indicato, solo se l'ordine è suo.
|
||||
*/
|
||||
public static function cancelPending(int $idschool, int $idorder, ?int $userId = null): bool
|
||||
{
|
||||
$pdo = self::pdo();
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$sql = "SELECT * FROM orderbook WHERE idorderbook = ? AND idschool = ?" . ($userId ? " AND iduser = ?" : "") . " FOR UPDATE";
|
||||
$args = $userId ? [$idorder, $idschool, $userId] : [$idorder, $idschool];
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute($args);
|
||||
$o = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$o || $o['payment_status'] !== 'pending') {
|
||||
$pdo->rollBack();
|
||||
return false;
|
||||
}
|
||||
$pdo->prepare("UPDATE orderbook SET payment_status = 'cancelled', status = 'cancelled' WHERE idorderbook = ? AND idschool = ?")
|
||||
->execute([$idorder, $idschool]);
|
||||
$pdo->prepare("UPDATE bookingclass SET status = 'cancelled' WHERE idorder = ? AND idschool = ? AND status <> 'cancelled'")
|
||||
->execute([$idorder, $idschool]);
|
||||
$pdo->prepare("DELETE FROM coupon_redemptions WHERE idorderbook = ? AND idschool = ?")->execute([$idorder, $idschool]);
|
||||
if (!empty($o['cod'])) {
|
||||
$pdo->prepare("UPDATE gift_vouchers SET status = 'cancelled' WHERE idschool = ? AND code = ? AND status = 'pending'")
|
||||
->execute([$idschool, $o['cod']]);
|
||||
}
|
||||
$pdo->commit();
|
||||
return true;
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/** Ordini su cui la scuola deve agire: da pagare + da propagare (contatore in sidebar) */
|
||||
public static function pendingCount(int $idschool): int
|
||||
{
|
||||
$st = self::pdo()->prepare(
|
||||
"SELECT COUNT(*) FROM orderbook
|
||||
WHERE idschool = ?
|
||||
AND (payment_status = 'pending'
|
||||
OR (status = 'pending' AND (payment_status IS NULL OR payment_status = 'paid')))"
|
||||
);
|
||||
$st->execute([$idschool]);
|
||||
return (int) $st->fetchColumn();
|
||||
}
|
||||
|
||||
private static function randomCode(int $len): string
|
||||
{
|
||||
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
$out = '';
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$out .= $chars[random_int(0, strlen($chars) - 1)];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private static function pdo(): PDO
|
||||
{
|
||||
return DBHandlerSelect::getInstance()->getConnection();
|
||||
}
|
||||
}
|
||||
+54
-10
@@ -57,19 +57,45 @@ if ($__mode === 'school' && $__isAdmin && !empty($__sc['idschool'])) {
|
||||
}
|
||||
}
|
||||
|
||||
// Percorso "Primi passi" non ancora concluso: promemoria con l'avanzamento salvato
|
||||
$__setup = null;
|
||||
if ($__mode === 'school' && $__isAdmin && !empty($__sc['idschool'])) {
|
||||
try {
|
||||
if ((string) SchoolSettings::get('setup_completed_at', '') === '') {
|
||||
$__ss = json_decode((string) SchoolSettings::get('setup_state', ''), true);
|
||||
$__setup = is_array($__ss) && !empty($__ss['progress']) ? (string) $__ss['progress'] : '';
|
||||
}
|
||||
} catch (Throwable $__ex) {
|
||||
error_log('[sidebar] primi passi: ' . $__ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Ordini su cui agire: da pagare + da propagare (contatore nel menu della scuola)
|
||||
$__toPay = 0;
|
||||
if ($__mode === 'school' && $__isAdmin && !empty($__sc['idschool'])) {
|
||||
try {
|
||||
require_once dirname(__DIR__) . '/class/ShopService.php';
|
||||
$__toPay = ShopService::pendingCount((int) $__sc['idschool']);
|
||||
} catch (Throwable $__ex) {
|
||||
error_log('[sidebar] ordini: ' . $__ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('sb_item')) {
|
||||
/** Voce singola. $also = altre pagine che la accendono (es. il form di modifica). */
|
||||
function sb_item(string $href, string $icon, string $label, string $current, array $also = []): void
|
||||
function sb_item(string $href, string $icon, string $label, string $current, array $also = [], int $badge = 0): void
|
||||
{
|
||||
$on = ($href === $current || in_array($current, $also, true));
|
||||
echo '<li class="' . ($on ? 'mm-active' : '') . '"><a href="' . $href . '" class="' . ($on ? 'active' : '') . '">'
|
||||
. '<i class="' . $icon . ' icon nav-icon"></i>'
|
||||
. '<span class="menu-item">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span></a></li>';
|
||||
. '<span class="menu-item">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span>'
|
||||
. ($badge > 0 ? '<span class="badge rounded-pill bg-warning text-dark ms-1 sb-badge" title="Da pagare o da propagare">' . $badge . '</span>' : '')
|
||||
. '</a></li>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppo richiudibile (metisMenu). Si apre da solo se contiene la pagina corrente.
|
||||
* $items = [ [href, etichetta, [pagine collegate]], ... ] (le voci null vengono saltate)
|
||||
* $items = [ [href, etichetta, [pagine collegate], contatore], ... ] (le voci null vengono saltate)
|
||||
*/
|
||||
function sb_group(string $icon, string $label, array $items, string $current): void
|
||||
{
|
||||
@@ -78,7 +104,9 @@ if (!function_exists('sb_item')) {
|
||||
return;
|
||||
}
|
||||
$open = false;
|
||||
$badge = 0;
|
||||
foreach ($items as $it) {
|
||||
$badge += (int) ($it[3] ?? 0);
|
||||
if ($it[0] === $current || in_array($current, $it[2] ?? [], true)) {
|
||||
$open = true;
|
||||
}
|
||||
@@ -86,12 +114,16 @@ if (!function_exists('sb_item')) {
|
||||
echo '<li class="' . ($open ? 'mm-active' : '') . '">'
|
||||
. '<a href="javascript: void(0);" class="has-arrow" aria-expanded="' . ($open ? 'true' : 'false') . '">'
|
||||
. '<i class="' . $icon . ' icon nav-icon"></i>'
|
||||
. '<span class="menu-item">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span></a>'
|
||||
. '<span class="menu-item">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span>'
|
||||
. ($badge > 0 ? '<span class="badge rounded-pill bg-warning text-dark ms-1 sb-badge">' . $badge . '</span>' : '')
|
||||
. '</a>'
|
||||
. '<ul class="sub-menu" aria-expanded="' . ($open ? 'true' : 'false') . '">';
|
||||
foreach ($items as $it) {
|
||||
$on = ($it[0] === $current || in_array($current, $it[2] ?? [], true));
|
||||
echo '<li class="' . ($on ? 'mm-active' : '') . '"><a href="' . $it[0] . '" class="' . ($on ? 'active' : '') . '">'
|
||||
. htmlspecialchars($it[1], ENT_QUOTES, 'UTF-8') . '</a></li>';
|
||||
. htmlspecialchars($it[1], ENT_QUOTES, 'UTF-8')
|
||||
. (!empty($it[3]) ? ' <span class="badge rounded-pill bg-warning text-dark ms-1">' . (int) $it[3] . '</span>' : '')
|
||||
. '</a></li>';
|
||||
}
|
||||
echo '</ul></li>';
|
||||
}
|
||||
@@ -210,6 +242,10 @@ if (!function_exists('sb_item')) {
|
||||
border-color: #cfe5e6;
|
||||
}
|
||||
|
||||
body[data-sidebar-size="sm"] .sb-badge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sub-alert:hover {
|
||||
filter: brightness(.97);
|
||||
}
|
||||
@@ -270,6 +306,14 @@ if (!function_exists('sb_item')) {
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($__setup !== null && $__page !== 'school_setup.php') : ?>
|
||||
<!-- Promemoria Primi passi -->
|
||||
<a href="school_setup.php" class="sub-alert info">
|
||||
<i class="bx bx-rocket"></i>
|
||||
<span><b>Primi passi<?php echo $__setup !== '' ? ' · ' . htmlspecialchars($__setup, ENT_QUOTES, 'UTF-8') : ''; ?></b>Configura e prova la tua scuola</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($__sub && $__page !== 'subscription.php') : ?>
|
||||
<!-- Avviso abbonamento -->
|
||||
<a href="subscription.php" class="sub-alert <?php echo $__sub[0]; ?>">
|
||||
@@ -312,10 +356,7 @@ if (!function_exists('sb_item')) {
|
||||
['statistiche.php', 'Statistiche'],
|
||||
], $__page); ?>
|
||||
|
||||
<?php sb_group('bx bx-receipt', 'Ordini', [
|
||||
['orderbooklist.php', 'Da propagare'],
|
||||
['ordersummary.php', 'Sommario ordini'],
|
||||
], $__page); ?>
|
||||
<?php sb_item('school_orders.php', 'bx bx-receipt', 'Ordini', $__page, ['orderbooklist.php', 'ordersummary.php'], $__toPay); ?>
|
||||
|
||||
<?php sb_group('bx bx-store-alt', 'Negozio', [
|
||||
['products.php', 'Prodotti', ['product_form.php']],
|
||||
@@ -330,7 +371,9 @@ if (!function_exists('sb_item')) {
|
||||
], $__page); ?>
|
||||
|
||||
<?php sb_group('bx bx-cog', 'Impostazioni', [
|
||||
['school_setup.php', 'Primi passi'],
|
||||
['optionsschool.php', 'Opzioni scuola'],
|
||||
['package_rules.php', 'Regole pacchetti'],
|
||||
['insegnanti.php', 'Insegnanti'],
|
||||
['subscription.php', 'Abbonamento YogiBook'],
|
||||
], $__page); ?>
|
||||
@@ -347,7 +390,8 @@ if (!function_exists('sb_item')) {
|
||||
<li class="menu-title">Le mie lezioni</li>
|
||||
<?php sb_item('studentdashboard.php', 'bx bx-home-heart', 'Home', $__page); ?>
|
||||
<?php sb_item('userpanel.php', 'bx bx-calendar-event', 'Prenotazioni', $__page); ?>
|
||||
<?php sb_item('orders.php', 'bx bx-detail', 'I miei pacchetti', $__page); ?>
|
||||
<?php sb_item('orders.php', 'bx bx-detail', 'I miei pacchetti', $__page, ['shop_order.php']); ?>
|
||||
<?php sb_item('shop.php', 'bx bx-store-alt', 'Negozio', $__page, ['shop_checkout.php']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Account personale: sempre presente -->
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
SchoolContext::requireSchoolAdmin();
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
if (!function_exists('e')) {
|
||||
|
||||
if (!function_exists('e')) {
|
||||
function e($value): string
|
||||
{
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/csrf.php');
|
||||
SchoolContext::requireSchoolAdmin();
|
||||
|
||||
/**
|
||||
* SCUOLA - Regole dei pacchetti a numero di lezioni (tabella expiryparameter).
|
||||
* Per ogni numero di lezioni: dopo quante lezioni in calendario scade il pacchetto
|
||||
* e quanti recuperi sono consentiti. Usate da calcolo scadenza / propagazione,
|
||||
* ordini manuali e WooCommerce, e dai prodotti con "Recuperi" lasciato vuoto.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function back(string $msg, string $type = 'msg'): void
|
||||
{
|
||||
header('Location: package_rules.php?' . $type . '=' . urlencode($msg));
|
||||
exit();
|
||||
}
|
||||
|
||||
/** Valori consigliati per una scuola che parte da zero */
|
||||
const PR_DEFAULTS = [[4, 5, 1], [6, 8, 2], [8, 10, 2], [10, 12, 2], [12, 15, 3]];
|
||||
|
||||
/* ---------------------------------------------------------------- AZIONI */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
yb_csrf_check();
|
||||
$action = (string) ($_POST['action'] ?? '');
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
|
||||
$int = function (string $k, int $min, int $max): ?int {
|
||||
$v = trim((string) ($_POST[$k] ?? ''));
|
||||
return (ctype_digit($v) && (int) $v >= $min && (int) $v <= $max) ? (int) $v : null;
|
||||
};
|
||||
|
||||
try {
|
||||
if ($action === 'save') {
|
||||
$qty = $int('quantityclass', 1, 200);
|
||||
$exp = $int('expirylessons', 1, 400);
|
||||
$res = $int('reschedulenumber', 0, 99);
|
||||
if ($qty === null || $exp === null || $res === null) {
|
||||
back('Controlla i valori: numeri interi, lezioni almeno 1.', 'err');
|
||||
}
|
||||
if ($exp < $qty) {
|
||||
back('La scadenza (' . $exp . ' lezioni) non può essere minore delle lezioni del pacchetto (' . $qty . ').', 'err');
|
||||
}
|
||||
$st = $pdo->prepare("SELECT 1 FROM expiryparameter WHERE idschool = ? AND quantityclass = ? AND idexpiryparameter <> ? LIMIT 1");
|
||||
$st->execute([(int) $idschool, $qty, $id]);
|
||||
if ($st->fetchColumn()) {
|
||||
back('Esiste già una regola per i pacchetti da ' . $qty . ' lezioni.', 'err');
|
||||
}
|
||||
if ($id > 0) {
|
||||
$st = $pdo->prepare("UPDATE expiryparameter SET quantityclass = ?, expirylessons = ?, reschedulenumber = ? WHERE idexpiryparameter = ? AND idschool = ?");
|
||||
$st->execute([$qty, $exp, $res, $id, (int) $idschool]);
|
||||
back('Regola per ' . $qty . ' lezioni aggiornata.');
|
||||
}
|
||||
$pdo->prepare("INSERT INTO expiryparameter (quantityclass, expirylessons, reschedulenumber, idschool) VALUES (?, ?, ?, ?)")
|
||||
->execute([$qty, $exp, $res, (int) $idschool]);
|
||||
back('Regola per ' . $qty . ' lezioni aggiunta.');
|
||||
}
|
||||
|
||||
if ($action === 'delete' && $id > 0) {
|
||||
$pdo->prepare("DELETE FROM expiryparameter WHERE idexpiryparameter = ? AND idschool = ?")->execute([$id, (int) $idschool]);
|
||||
back('Regola eliminata.');
|
||||
}
|
||||
|
||||
if ($action === 'defaults') {
|
||||
$st = $pdo->prepare("SELECT COUNT(*) FROM expiryparameter WHERE idschool = ?");
|
||||
$st->execute([(int) $idschool]);
|
||||
if ((int) $st->fetchColumn() > 0) {
|
||||
back('Ci sono già delle regole: i valori consigliati si caricano solo su una tabella vuota.', 'err');
|
||||
}
|
||||
$ins = $pdo->prepare("INSERT INTO expiryparameter (quantityclass, expirylessons, reschedulenumber, idschool) VALUES (?, ?, ?, ?)");
|
||||
foreach (PR_DEFAULTS as [$q, $x, $r]) {
|
||||
$ins->execute([$q, $x, $r, (int) $idschool]);
|
||||
}
|
||||
back('Valori consigliati caricati: modificali come preferisci.');
|
||||
}
|
||||
} catch (PDOException $ex) {
|
||||
error_log('[package_rules] ' . $ex->getMessage());
|
||||
back('Errore database, operazione non eseguita.', 'err');
|
||||
}
|
||||
back('Azione non valida.', 'err');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ DATI */
|
||||
$st = $pdo->prepare("SELECT * FROM expiryparameter WHERE idschool = ? ORDER BY quantityclass, idexpiryparameter");
|
||||
$st->execute([(int) $idschool]);
|
||||
$rules = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Quanti ordini usano ogni numero di lezioni (per sapere cosa si tocca)
|
||||
$st = $pdo->prepare("SELECT quantityclass, COUNT(*) AS n FROM orderbook WHERE idschool = ? AND quantityclass IS NOT NULL GROUP BY quantityclass");
|
||||
$st->execute([(int) $idschool]);
|
||||
$usage = [];
|
||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $u) {
|
||||
$usage[(int) $u['quantityclass']] = (int) $u['n'];
|
||||
}
|
||||
$missing = array_diff(array_keys($usage), array_map(fn($r) => (int) $r['quantityclass'], $rules));
|
||||
sort($missing);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Regole pacchetti</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>
|
||||
.pr-row input {
|
||||
max-width: 110px;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
.pr-row .warn-cell {
|
||||
color: #b03a2e;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pr-new {
|
||||
background: #F4FAFA;
|
||||
}
|
||||
|
||||
.pr-example {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
}
|
||||
</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">Regole pacchetti</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">
|
||||
<div class="row">
|
||||
<div class="col-xl-9">
|
||||
|
||||
<?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="section-title"><span class="disc"></span>Scadenza e recuperi dei pacchetti</div>
|
||||
<p class="section-lead">
|
||||
Per ogni pacchetto a numero di lezioni: dopo quante lezioni in calendario della sua classe scade, e quanti recuperi sono consentiti.
|
||||
Valgono per la propagazione degli ordini, per gli ordini inseriti a mano e per i prodotti del negozio con "Recuperi" lasciato vuoto.
|
||||
</p>
|
||||
|
||||
<div class="info-box info">
|
||||
<i class="bx bx-bulb"></i>
|
||||
<div class="pr-example">
|
||||
<strong>Esempio:</strong> pacchetto da <strong>8</strong> lezioni, scade dopo <strong>10</strong> lezioni, <strong>2</strong> recuperi.
|
||||
Se la classe è il martedì, il pacchetto vale per le prossime 10 lezioni del martedì (2 settimane di margine per le assenze) e l'allieva può spostare fino a 2 lezioni.
|
||||
I giorni di chiusura non contano.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($missing) : ?>
|
||||
<div class="info-box warn">
|
||||
<i class="bx bx-error"></i>
|
||||
<div>Ci sono ordini con un numero di lezioni senza regola: <strong><?php echo e(implode(', ', $missing)); ?></strong>. Per questi la scadenza non si calcola da sola: aggiungi una regola o inseriscila a mano alla propagazione.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table yb-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Lezioni del pacchetto</th>
|
||||
<th>Scade dopo (lezioni)</th>
|
||||
<th>Recuperi consentiti</th>
|
||||
<th>Margine</th>
|
||||
<th>Ordini</th>
|
||||
<th class="text-end">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!$rules) : ?>
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<div class="empty-state">
|
||||
<i class="bx bx-list-ol"></i>
|
||||
Nessuna regola ancora.
|
||||
<form method="post" action="package_rules.php" class="mt-3">
|
||||
<?php echo yb_csrf_field(); ?>
|
||||
<input type="hidden" name="action" value="defaults">
|
||||
<button type="submit" class="btn btn-yb"><i class="bx bx-magic-wand"></i> Carica i valori consigliati</button>
|
||||
</form>
|
||||
<div class="hint">4 → 5 · 6 → 8 · 8 → 10 · 10 → 12 · 12 → 15, poi li modifichi.</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($rules as $r) :
|
||||
$rid = (int) $r['idexpiryparameter'];
|
||||
$q = (int) $r['quantityclass'];
|
||||
$x = (int) $r['expirylessons'];
|
||||
$bad = $x < $q; ?>
|
||||
<tr class="pr-row">
|
||||
<td>
|
||||
<form method="post" action="package_rules.php" id="f-<?php echo $rid; ?>">
|
||||
<?php echo yb_csrf_field(); ?>
|
||||
<input type="hidden" name="action" value="save">
|
||||
<input type="hidden" name="id" value="<?php echo $rid; ?>">
|
||||
</form>
|
||||
<input form="f-<?php echo $rid; ?>" type="number" min="1" max="200" name="quantityclass" class="form-control" value="<?php echo $q; ?>" aria-label="Lezioni del pacchetto">
|
||||
</td>
|
||||
<td><input form="f-<?php echo $rid; ?>" type="number" min="1" max="400" name="expirylessons" class="form-control" value="<?php echo $x; ?>" aria-label="Scade dopo"></td>
|
||||
<td><input form="f-<?php echo $rid; ?>" type="number" min="0" max="99" name="reschedulenumber" class="form-control" value="<?php echo (int) $r['reschedulenumber']; ?>" aria-label="Recuperi consentiti"></td>
|
||||
<td>
|
||||
<?php if ($bad) : ?>
|
||||
<span class="warn-cell"><i class="bx bx-error"></i> scade prima di finire le lezioni</span>
|
||||
<?php else : ?>
|
||||
<?php echo $x - $q; ?> <?php echo $x - $q === 1 ? 'lezione' : 'lezioni'; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo $usage[$q] ?? 0; ?></td>
|
||||
<td class="text-end">
|
||||
<button type="submit" form="f-<?php echo $rid; ?>" class="btn-act ok"><i class="bx bx-save"></i> Salva</button>
|
||||
<form method="post" action="package_rules.php" class="d-inline confirm-form" data-msg="Eliminare la regola per <?php echo $q; ?> lezioni?" data-text="Gli ordini già propagati non cambiano. I nuovi ordini da <?php echo $q; ?> lezioni non avranno più scadenza automatica." data-ok="Elimina" data-color="#e74c3c">
|
||||
<?php echo yb_csrf_field(); ?>
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?php echo $rid; ?>">
|
||||
<button type="submit" class="btn-act danger" aria-label="Elimina regola"><i class="bx bx-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<!-- Nuova regola -->
|
||||
<tr class="pr-row pr-new">
|
||||
<td>
|
||||
<form method="post" action="package_rules.php" id="f-new">
|
||||
<?php echo yb_csrf_field(); ?>
|
||||
<input type="hidden" name="action" value="save">
|
||||
<input type="hidden" name="id" value="0">
|
||||
</form>
|
||||
<input form="f-new" type="number" min="1" max="200" name="quantityclass" class="form-control" placeholder="es. 16" required aria-label="Lezioni del nuovo pacchetto">
|
||||
</td>
|
||||
<td><input form="f-new" type="number" min="1" max="400" name="expirylessons" class="form-control" placeholder="es. 20" required aria-label="Scade dopo"></td>
|
||||
<td><input form="f-new" type="number" min="0" max="99" name="reschedulenumber" class="form-control" placeholder="es. 3" required aria-label="Recuperi consentiti"></td>
|
||||
<td colspan="2" class="hint">Nuova regola</td>
|
||||
<td class="text-end"><button type="submit" form="f-new" class="btn-act ok"><i class="bx bx-plus"></i> Aggiungi</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="hint">Le modifiche valgono per i prossimi calcoli: gli ordini già propagati mantengono scadenza e recuperi che hanno (li cambi da Ordini).</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 src="assets/js/yb-confirm.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,510 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/csrf.php');
|
||||
require_once(__DIR__ . '/class/SchoolSetup.php');
|
||||
SchoolContext::requireSchoolAdmin();
|
||||
|
||||
/**
|
||||
* SCUOLA - Primi passi: percorso guidato per configurare e provare la scuola.
|
||||
* I passi si completano da soli quando i dati ci sono; alcuni si segnano a mano,
|
||||
* i facoltativi si possono saltare. Alla fine l'owner chiude il percorso.
|
||||
*/
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$sid = (int) $idschool;
|
||||
|
||||
/* ---------------------------------------------------------------- AZIONI */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
yb_csrf_check();
|
||||
$action = (string) ($_POST['action'] ?? '');
|
||||
$key = preg_replace('/[^a-z_]/', '', (string) ($_POST['key'] ?? ''));
|
||||
$state = SchoolSetup::state($sid);
|
||||
$keys = array_column(SchoolSetup::steps($sid, (int) $iduserlogin), null, 'key');
|
||||
|
||||
if (isset($keys[$key])) {
|
||||
if ($action === 'mark') {
|
||||
$state['done'][$key] = date('Y-m-d');
|
||||
unset($state['skip'][$key]);
|
||||
} elseif ($action === 'unmark') {
|
||||
unset($state['done'][$key]);
|
||||
} elseif ($action === 'skip' && !$keys[$key]['required']) {
|
||||
$state['skip'][$key] = date('Y-m-d');
|
||||
} elseif ($action === 'unskip') {
|
||||
unset($state['skip'][$key]);
|
||||
}
|
||||
SchoolSetup::saveState($sid, $state);
|
||||
}
|
||||
if ($action === 'complete') {
|
||||
[,, $missing] = SchoolSetup::progress(SchoolSetup::steps($sid, (int) $iduserlogin));
|
||||
if ($missing === 0) {
|
||||
SchoolSettings::save(['setup_completed_at' => date('Y-m-d H:i:s')], $sid);
|
||||
header('Location: school_setup.php?done=1');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
if ($action === 'reopen') {
|
||||
SchoolSettings::save(['setup_completed_at' => null], $sid);
|
||||
}
|
||||
header('Location: school_setup.php' . ($key !== '' ? '#step-' . $key : ''));
|
||||
exit();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ DATI */
|
||||
$steps = SchoolSetup::steps($sid, (int) $iduserlogin);
|
||||
[$done, $total, $missing] = SchoolSetup::progress($steps);
|
||||
$pct = $total ? (int) round($done / $total * 100) : 0;
|
||||
$completedAt = (string) SchoolSettings::get('setup_completed_at', '');
|
||||
|
||||
// Avanzamento in cache per il promemoria in sidebar (scritto solo se cambia)
|
||||
$state = SchoolSetup::state($sid);
|
||||
if (($state['progress'] ?? '') !== $done . '/' . $total) {
|
||||
$state['progress'] = $done . '/' . $total;
|
||||
SchoolSetup::saveState($sid, $state);
|
||||
}
|
||||
|
||||
$next = null;
|
||||
foreach ($steps as $s) {
|
||||
if (!$s['done'] && !$s['skipped']) {
|
||||
$next = $s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$byPhase = [];
|
||||
foreach ($steps as $i => $s) {
|
||||
$s['n'] = $i + 1;
|
||||
$byPhase[$s['phase']][] = $s;
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Primi passi</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" />
|
||||
<style>
|
||||
.su-wrap {
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
.su-hero {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.su-ring {
|
||||
--p: 0;
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: conic-gradient(#2F7F84 calc(var(--p) * 1%), #DBF6F9 0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.su-ring::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 11px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.su-ring b {
|
||||
position: relative;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: #1E4A4F;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.su-ring small {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #6b7280;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.su-hero h1 {
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: #1E4A4F;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.su-hero p {
|
||||
margin: 0;
|
||||
color: #4b5563;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.su-next {
|
||||
border: 2px solid #6FBABE;
|
||||
background: #F4FAFA;
|
||||
border-radius: 16px;
|
||||
padding: 18px 20px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.su-next .k {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
color: #2F7F84;
|
||||
}
|
||||
|
||||
.su-next .t {
|
||||
font-size: 19px;
|
||||
font-weight: 800;
|
||||
color: #1E4A4F;
|
||||
margin: 2px 0 4px;
|
||||
}
|
||||
|
||||
.su-next .d {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
max-width: 560px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.su-phase {
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.su-phase-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.su-phase-head h2 {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .04em;
|
||||
text-transform: uppercase;
|
||||
color: #1E4A4F;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.su-phase-head span {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.su-step {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
background: #fff;
|
||||
border: 1px solid #e6ecec;
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
scroll-margin-top: 90px;
|
||||
}
|
||||
|
||||
.su-step.is-next {
|
||||
border-color: #6FBABE;
|
||||
box-shadow: 0 0 0 3px rgba(111, 186, 190, .18);
|
||||
}
|
||||
|
||||
.su-dot {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
background: #eef3f3;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.su-step.done .su-dot {
|
||||
background: #2F7F84;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
box-shadow: 5px 0 0 #DBF6F9;
|
||||
}
|
||||
|
||||
.su-step.skipped .su-dot {
|
||||
background: #f3f4f6;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.su-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.su-title {
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.su-step.done .su-title {
|
||||
color: #1E4A4F;
|
||||
}
|
||||
|
||||
.su-step.skipped .su-title {
|
||||
color: #9ca3af;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.su-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
background: #eef0f3;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.su-text {
|
||||
font-size: 13.5px;
|
||||
color: #4b5563;
|
||||
line-height: 1.45;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.su-step.done .su-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.su-detail {
|
||||
font-size: 12.5px;
|
||||
color: #2F7F84;
|
||||
font-weight: 600;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.su-detail.warn {
|
||||
color: #92560b;
|
||||
}
|
||||
|
||||
.su-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.su-actions form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.su-link {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
text-decoration: underline;
|
||||
padding: 6px 4px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.su-link:hover {
|
||||
color: #1E4A4F;
|
||||
}
|
||||
|
||||
.su-finish {
|
||||
text-align: center;
|
||||
padding: 26px 16px;
|
||||
}
|
||||
|
||||
.su-finish i {
|
||||
font-size: 46px;
|
||||
color: #2F7F84;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.su-step {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.su-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
padding-left: 48px;
|
||||
}
|
||||
}
|
||||
</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">Primi passi</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">
|
||||
<div class="su-wrap">
|
||||
|
||||
<?php if (isset($_GET['done'])) : ?>
|
||||
<div class="alert alert-success">Configurazione completata. Il promemoria in alto nel menu non comparirà più: puoi tornare qui da Impostazioni → Primi passi.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="su-hero">
|
||||
<div class="su-ring" style="--p: <?php echo $pct; ?>;" role="img" aria-label="<?php echo $done; ?> passi su <?php echo $total; ?>">
|
||||
<b><?php echo $done; ?>/<?php echo $total; ?><small>passi</small></b>
|
||||
</div>
|
||||
<div style="flex:1;min-width:240px;">
|
||||
<h1><?php echo $completedAt ? 'La tua scuola è pronta' : 'Prepariamo la tua scuola'; ?></h1>
|
||||
<p>
|
||||
<?php if ($completedAt) : ?>
|
||||
Percorso chiuso il <?php echo e(date('d/m/Y', strtotime($completedAt))); ?>. Puoi sempre ricontrollare i passi qui sotto.
|
||||
<?php else : ?>
|
||||
Segui i passi in ordine: ogni volta che inserisci i dati il passo si spunta da solo.
|
||||
Alla fine fai una prova come allieva, così sai esattamente cosa vedranno.
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($next && !$completedAt) : ?>
|
||||
<div class="su-next mt-4">
|
||||
<div>
|
||||
<div class="k">Prossimo passo · <?php echo e(SchoolSetup::PHASES[$next['phase']][0]); ?></div>
|
||||
<div class="t"><?php echo e($next['title']); ?></div>
|
||||
<div class="d"><?php echo e($next['text']); ?></div>
|
||||
</div>
|
||||
<?php if ($next['url'] !== '') : ?>
|
||||
<a href="<?php echo e($next['url']); ?>" class="btn btn-yb" style="min-height:46px;display:inline-flex;align-items:center;gap:6px;"><?php echo e($next['cta']); ?> <i class="bx bx-right-arrow-alt"></i></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php foreach (SchoolSetup::PHASES as $pk => [$pTitle, $pLead]) :
|
||||
if (empty($byPhase[$pk])) continue; ?>
|
||||
<section class="su-phase" aria-labelledby="ph-<?php echo $pk; ?>">
|
||||
<div class="su-phase-head">
|
||||
<h2 id="ph-<?php echo $pk; ?>"><?php echo e($pTitle); ?></h2>
|
||||
<span><?php echo e($pLead); ?></span>
|
||||
</div>
|
||||
<?php foreach ($byPhase[$pk] as $s) :
|
||||
$isNext = $next && $next['key'] === $s['key'] && !$completedAt;
|
||||
$cls = $s['done'] ? 'done' : ($s['skipped'] ? 'skipped' : ''); ?>
|
||||
<div class="su-step <?php echo $cls; ?> <?php echo $isNext ? 'is-next' : ''; ?>" id="step-<?php echo e($s['key']); ?>">
|
||||
<div class="su-dot" aria-hidden="true"><?php echo $s['done'] ? '<i class="bx bx-check"></i>' : ($s['skipped'] ? '–' : $s['n']); ?></div>
|
||||
<div class="su-body">
|
||||
<div class="su-title">
|
||||
<?php echo e($s['title']); ?>
|
||||
<?php if (!$s['required']) : ?><span class="su-tag">facoltativo</span><?php endif; ?>
|
||||
<?php if ($s['done'] && $s['manual'] && !$s['auto']) : ?><span class="su-tag">segnato a mano</span><?php endif; ?>
|
||||
</div>
|
||||
<div class="su-text"><?php echo e($s['text']); ?></div>
|
||||
<?php if ($s['detail'] !== '') : ?>
|
||||
<div class="su-detail <?php echo $s['done'] ? '' : 'warn'; ?>"><?php echo e($s['detail']); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="su-actions">
|
||||
<?php if ($s['url'] !== '') : ?>
|
||||
<a href="<?php echo e($s['url']); ?>" class="btn-act <?php echo !$s['done'] && !$s['skipped'] ? 'ok' : ''; ?>"><?php echo $s['done'] ? 'Rivedi' : e($s['cta']); ?></a>
|
||||
<?php endif; ?>
|
||||
<?php if (!$s['done'] && !$s['skipped'] && $s['manual']) : ?>
|
||||
<form method="post" action="school_setup.php"><?php echo yb_csrf_field(); ?><input type="hidden" name="action" value="mark"><input type="hidden" name="key" value="<?php echo e($s['key']); ?>"><button type="submit" class="btn-act"><i class="bx bx-check"></i> Fatto</button></form>
|
||||
<?php endif; ?>
|
||||
<?php if ($s['done'] && !$s['auto']) : ?>
|
||||
<form method="post" action="school_setup.php"><?php echo yb_csrf_field(); ?><input type="hidden" name="action" value="unmark"><input type="hidden" name="key" value="<?php echo e($s['key']); ?>"><button type="submit" class="su-link">Non ancora</button></form>
|
||||
<?php endif; ?>
|
||||
<?php if (!$s['done'] && !$s['required'] && !$s['skipped']) : ?>
|
||||
<form method="post" action="school_setup.php"><?php echo yb_csrf_field(); ?><input type="hidden" name="action" value="skip"><input type="hidden" name="key" value="<?php echo e($s['key']); ?>"><button type="submit" class="su-link">Salta</button></form>
|
||||
<?php endif; ?>
|
||||
<?php if ($s['skipped']) : ?>
|
||||
<form method="post" action="school_setup.php"><?php echo yb_csrf_field(); ?><input type="hidden" name="action" value="unskip"><input type="hidden" name="key" value="<?php echo e($s['key']); ?>"><button type="submit" class="su-link">Ripristina</button></form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="card admin-card mt-4">
|
||||
<div class="card-body su-finish">
|
||||
<?php if ($completedAt) : ?>
|
||||
<i class="bx bx-party"></i>
|
||||
<h2 class="mt-2" style="font-size:20px;font-weight:800;color:#1E4A4F;">Configurazione completata</h2>
|
||||
<form method="post" action="school_setup.php" class="mt-2"><?php echo yb_csrf_field(); ?><input type="hidden" name="action" value="reopen"><button type="submit" class="su-link">Riapri il percorso e mostra di nuovo il promemoria</button></form>
|
||||
<?php elseif ($missing === 0) : ?>
|
||||
<i class="bx bx-rocket"></i>
|
||||
<h2 class="mt-2" style="font-size:20px;font-weight:800;color:#1E4A4F;">Tutto pronto!</h2>
|
||||
<p class="text-muted">Hai completato tutti i passi necessari. Chiudi il percorso: il promemoria nel menu sparirà.</p>
|
||||
<form method="post" action="school_setup.php"><?php echo yb_csrf_field(); ?><input type="hidden" name="action" value="complete"><button type="submit" class="btn btn-yb" style="min-height:48px;padding:0 26px;">Ho finito</button></form>
|
||||
<?php else : ?>
|
||||
<p class="mb-0 text-muted">Mancano <strong><?php echo $missing; ?></strong> <?php echo $missing === 1 ? 'passo necessario' : 'passi necessari'; ?>. I passi facoltativi puoi farli dopo o saltarli.</p>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/csrf.php');
|
||||
require_once(__DIR__ . '/class/ShopService.php');
|
||||
|
||||
/**
|
||||
* NEGOZIO della scuola (allieva loggata): catalogo, scheda prodotto, scelte.
|
||||
* Il pulsante finale invia le scelte a shop_checkout.php (riepilogo, coupon, pagamento).
|
||||
*
|
||||
* Indirizzi: shop.php catalogo
|
||||
* shop.php#/slug scheda prodotto (condivisibile)
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$shopOn = SchoolSettings::get('shop_enabled', 'N') === 'Y';
|
||||
$shopUrl = (string) SchoolSettings::get('shop_url', '');
|
||||
|
||||
// Negozio spento: l'allieva vede il link esterno (se c'è); lo staff vede comunque il catalogo
|
||||
$showCatalog = $shopOn || $isstaff;
|
||||
$data = $showCatalog ? ShopService::catalog((int) $idschool, (int) $iduserlogin, true) : null;
|
||||
if ($data) {
|
||||
$data['schoolName'] = (string) $schoolname;
|
||||
}
|
||||
$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>Negozio - <?php echo e($schoolname); ?></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-shop.css" rel="stylesheet" type="text/css" />
|
||||
</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">Negozio</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 (!$showCatalog) : ?>
|
||||
<div class="ys-wrap">
|
||||
<div class="ys-empty">
|
||||
<i class="bx bx-store-alt"></i>
|
||||
<h2>Il negozio non è ancora aperto</h2>
|
||||
<?php if ($shopUrl !== '') : ?>
|
||||
<p>Per acquistare pacchetti e abbonamenti usa il negozio online della scuola.</p>
|
||||
<a class="ys-btn" href="<?php echo e($shopUrl); ?>" target="_blank" rel="noopener">Vai al negozio <i class="bx bx-link-external"></i></a>
|
||||
<?php else : ?>
|
||||
<p>Per acquistare chiedi in segreteria.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<?php if (!$shopOn) : ?>
|
||||
<div class="ys-wrap">
|
||||
<div class="ys-staffnote"><i class="bx bx-show"></i> Il negozio non è ancora attivo per le allieve: lo vedi perché fai parte dello staff. Si attiva da Opzioni Scuola → Negozio.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="ys-wrap" id="shop" aria-live="polite"></div>
|
||||
<form id="checkout-form" method="post" action="shop_checkout.php" hidden>
|
||||
<?php echo yb_csrf_field(); ?>
|
||||
<input type="hidden" name="idvariant" id="f-variant">
|
||||
<input type="hidden" name="services" id="f-services">
|
||||
<input type="hidden" name="idschedule" id="f-schedule">
|
||||
<input type="hidden" name="recipient_name" id="f-rname">
|
||||
<input type="hidden" name="recipient_email" id="f-remail">
|
||||
<input type="hidden" name="recipient_message" id="f-rmsg">
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
</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>
|
||||
<?php if ($showCatalog) : ?>
|
||||
<script>
|
||||
window.YB_SHOP = <?php echo json_encode($data, $jsonFlags); ?>;
|
||||
</script>
|
||||
<script src="assets/js/yb-shop.js"></script>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/csrf.php');
|
||||
require_once(__DIR__ . '/class/ShopService.php');
|
||||
|
||||
/**
|
||||
* NEGOZIO - riepilogo e conferma dell'acquisto.
|
||||
* Riceve le scelte da shop.php (POST), le ricontrolla sul server, applica il coupon,
|
||||
* fa scegliere il pagamento e crea l'ordine (ShopService::createOrder).
|
||||
* Ogni passo è un POST con CSRF; la conferma usa un token monouso contro il doppio invio.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function euro($v): string
|
||||
{
|
||||
return number_format((float) $v, 2, ',', '.') . ' €';
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: shop.php');
|
||||
exit();
|
||||
}
|
||||
yb_csrf_check();
|
||||
|
||||
$shopOn = SchoolSettings::get('shop_enabled', 'N') === 'Y';
|
||||
if (!$shopOn && !$isstaff) {
|
||||
header('Location: shop.php');
|
||||
exit();
|
||||
}
|
||||
if (!SchoolScope::isMember((int) $iduserlogin)) {
|
||||
header('Location: shop.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$in = [
|
||||
'idvariant' => (int) ($_POST['idvariant'] ?? 0),
|
||||
'services' => array_values(array_filter(array_map('intval', explode(',', (string) ($_POST['services'] ?? ''))))),
|
||||
'idschedule' => (int) ($_POST['idschedule'] ?? 0),
|
||||
'recipient_name' => (string) ($_POST['recipient_name'] ?? ''),
|
||||
'recipient_email' => (string) ($_POST['recipient_email'] ?? ''),
|
||||
'recipient_message' => (string) ($_POST['recipient_message'] ?? ''),
|
||||
];
|
||||
$action = (string) ($_POST['action'] ?? 'review');
|
||||
$couponCode = strtoupper(trim((string) ($_POST['coupon_code'] ?? '')));
|
||||
$method = (string) ($_POST['method'] ?? '');
|
||||
$error = '';
|
||||
$couponMsg = '';
|
||||
$sel = null;
|
||||
$coupon = null;
|
||||
|
||||
try {
|
||||
$sel = ShopService::selection((int) $idschool, (int) $iduserlogin, $in);
|
||||
} catch (RuntimeException $ex) {
|
||||
$error = $ex->getMessage();
|
||||
} catch (PDOException $ex) {
|
||||
error_log('[shop_checkout] selection: ' . $ex->getMessage());
|
||||
$error = 'Si è verificato un errore. Riprova tra poco.';
|
||||
}
|
||||
|
||||
if ($sel) {
|
||||
if ($action === 'remove_coupon') {
|
||||
$couponCode = '';
|
||||
}
|
||||
if ($couponCode !== '') {
|
||||
try {
|
||||
$coupon = ShopService::checkCoupon((int) $idschool, (int) $iduserlogin, $couponCode, $sel['variant']);
|
||||
} catch (RuntimeException $ex) {
|
||||
$couponMsg = $ex->getMessage();
|
||||
$couponCode = '';
|
||||
}
|
||||
}
|
||||
|
||||
$methods = ShopService::paymentMethods((int) $idschool);
|
||||
|
||||
if ($action === 'confirm') {
|
||||
$token = (string) ($_POST['checkout_token'] ?? '');
|
||||
if ($token === '' || !hash_equals((string) ($_SESSION['yb_checkout_token'] ?? ''), $token)) {
|
||||
$error = 'Questo ordine è già stato inviato. Controlla in "I miei pacchetti".';
|
||||
$sel = null;
|
||||
} elseif (($_POST['terms'] ?? '') !== 'Y') {
|
||||
$couponMsg = $couponMsg ?: '';
|
||||
$formError = 'Per continuare accetta le condizioni di acquisto.';
|
||||
} elseif (!(($method === 'bank' && $methods['bank']) || ($method === 'stripe' && $methods['card']))) {
|
||||
$formError = 'Scegli come vuoi pagare.';
|
||||
} elseif ($method === 'stripe') {
|
||||
$formError = 'Il pagamento con carta sarà disponibile a breve. Scegli il bonifico.';
|
||||
} else {
|
||||
unset($_SESSION['yb_checkout_token']);
|
||||
try {
|
||||
$idorder = ShopService::createOrder((int) $idschool, (int) $iduserlogin, $sel, $method, $coupon);
|
||||
header('Location: shop_order.php?id=' . $idorder . '&new=1');
|
||||
exit();
|
||||
} catch (RuntimeException $ex) {
|
||||
$formError = $ex->getMessage();
|
||||
} catch (PDOException $ex) {
|
||||
error_log('[shop_checkout] createOrder: ' . $ex->getMessage());
|
||||
$formError = 'Non è stato possibile registrare l\'ordine. Riprova.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Token monouso per la conferma
|
||||
$_SESSION['yb_checkout_token'] = bin2hex(random_bytes(16));
|
||||
|
||||
/* ----------------------------------------------- dati per il riepilogo */
|
||||
$lines = [];
|
||||
$termsUrl = (string) SchoolSettings::get('shop_terms_url', '');
|
||||
$termsText = (string) SchoolSettings::get('shop_terms_text', '');
|
||||
$membershipWarn = false;
|
||||
if ($sel) {
|
||||
$v = $sel['variant'];
|
||||
$mode = $sel['mode'];
|
||||
$price = (float) $v['price'];
|
||||
$discount = $coupon ? min((float) $coupon['discount'], $price) : 0.0;
|
||||
$total = round($price - $discount, 2);
|
||||
$lessons = $v['lessons'] !== null ? (int) $v['lessons'] : null;
|
||||
$unit = ['days' => ['giorno', 'giorni'], 'weeks' => ['settimana', 'settimane'], 'months' => ['mese', 'mesi']][$v['validity_unit']] ?? ['', ''];
|
||||
$validity = $v['validity_value'] ? $v['validity_value'] . ' ' . ((int) $v['validity_value'] === 1 ? $unit[0] : $unit[1]) : '';
|
||||
|
||||
if ($mode === 'fixed') {
|
||||
$st = $pdo->prepare("SELECT idservice, servicename, day, time FROM service WHERE idschool = ? AND idservice IN (" . implode(',', array_fill(0, count($sel['services']), '?')) . ")");
|
||||
$st->execute(array_merge([(int) $idschool], $sel['services']));
|
||||
$names = [];
|
||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $s) {
|
||||
$names[(int) $s['idservice']] = trim($s['servicename'] . ' · ' . $s['day'] . ' ' . $s['time']);
|
||||
}
|
||||
$lines[] = [count($sel['services']) > 1 ? 'Le tue classi' : 'La tua classe', implode(' + ', array_map(fn($sid) => $names[$sid] ?? '', $sel['services']))];
|
||||
$lines[] = ['Lezioni', $lessons ?: 'Illimitate'];
|
||||
if ($validity) {
|
||||
$lines[] = ['Validità', $validity . ($v['validity_start'] === 'purchase' ? ' dall\'acquisto' : ' dalla prima lezione')];
|
||||
}
|
||||
$lines[] = ['Prima lezione', 'Te la conferma la scuola'];
|
||||
} elseif ($mode === 'free') {
|
||||
$lines[] = ['Lezioni', $lessons ?: 'Illimitate'];
|
||||
if ($validity) {
|
||||
$lines[] = ['Validità', $validity . ($v['validity_start'] === 'purchase' ? ' dall\'acquisto' : ' dalla prima lezione')];
|
||||
}
|
||||
if ($sel['product']['weekly_limit']) {
|
||||
$lines[] = ['Limite', 'Max ' . (int) $sel['product']['weekly_limit'] . ' lezioni a settimana'];
|
||||
}
|
||||
$lines[] = ['Prenotazioni', 'Le fai tu dal calendario'];
|
||||
} elseif ($mode === 'single') {
|
||||
$lines[] = ['Lezione', $sel['schedule']['servicename']];
|
||||
$lines[] = ['Quando', ShopService::dateLabel(strtotime($sel['schedule']['dateschedule']))];
|
||||
} elseif ($mode === 'membership') {
|
||||
$end = ShopService::membershipEnd((int) $idschool, $v);
|
||||
$lines[] = ['Validità', $end ? 'Fino al ' . date('d/m/Y', strtotime($end)) : 'Senza scadenza'];
|
||||
} elseif ($mode === 'voucher') {
|
||||
$lines[] = ['Per', $sel['recipient']['name']];
|
||||
$lines[] = ['Invio a', $sel['recipient']['email']];
|
||||
if ($validity) {
|
||||
$lines[] = ['Scadenza del buono', $validity];
|
||||
}
|
||||
}
|
||||
|
||||
// Tessera obbligatoria mancante (avviso, non blocca)
|
||||
if ($sel['product']['kind'] === 'lessons' && SchoolSettings::get('membership_required', 'N') === 'Y') {
|
||||
$st = $pdo->prepare("SELECT 1 FROM memberships WHERE iduser = ? AND idschool = ? AND status = 'active' AND valid_to >= CURDATE() LIMIT 1");
|
||||
$st->execute([(int) $iduserlogin, (int) $idschool]);
|
||||
$membershipWarn = !$st->fetchColumn();
|
||||
}
|
||||
if ($method === '' || !(($method === 'bank' && $methods['bank']) || ($method === 'stripe' && $methods['card']))) {
|
||||
$method = $methods['card'] ? 'stripe' : ($methods['bank'] ? 'bank' : '');
|
||||
}
|
||||
}
|
||||
|
||||
/** Campi nascosti che riportano la scelta a ogni invio */
|
||||
function keepFields(array $in, string $coupon): string
|
||||
{
|
||||
$h = '';
|
||||
$h .= '<input type="hidden" name="idvariant" value="' . (int) $in['idvariant'] . '">';
|
||||
$h .= '<input type="hidden" name="services" value="' . e(implode(',', $in['services'])) . '">';
|
||||
$h .= '<input type="hidden" name="idschedule" value="' . (int) $in['idschedule'] . '">';
|
||||
foreach (['recipient_name', 'recipient_email', 'recipient_message'] as $k) {
|
||||
$h .= '<input type="hidden" name="' . $k . '" value="' . e($in[$k]) . '">';
|
||||
}
|
||||
$h .= '<input type="hidden" name="coupon_code" value="' . e($coupon) . '">';
|
||||
return $h;
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Riepilogo ordine - <?php echo e($schoolname); ?></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-shop.css" rel="stylesheet" type="text/css" />
|
||||
</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 ordine</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">
|
||||
<div class="ys-wrap ys-checkout">
|
||||
|
||||
<?php if (!$sel) : ?>
|
||||
<div class="ys-empty">
|
||||
<i class="bx bx-error-circle" style="color:#d9a066;"></i>
|
||||
<h2>Non possiamo completare l'ordine</h2>
|
||||
<p><?php echo e($error); ?></p>
|
||||
<a class="ys-btn" href="shop.php"><i class="bx bx-chevron-left"></i> Torna al negozio</a>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<a class="ys-back" href="shop.php#/<?php echo e(rawurlencode($sel['product']['slug'])); ?>"><i class="bx bx-chevron-left"></i> Modifica la scelta</a>
|
||||
<h1 class="ys-h1" style="margin-bottom:18px;">Riepilogo</h1>
|
||||
|
||||
<?php if (!empty($formError)) : ?>
|
||||
<div class="ys-alert" style="background:#FDECEA;border-color:#f2c9c4;color:#8f2f24;"><i class="bx bx-error"></i>
|
||||
<div><?php echo e($formError); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($membershipWarn) : ?>
|
||||
<div class="ys-alert"><i class="bx bx-id-card"></i>
|
||||
<div>Per frequentare le lezioni serve anche la <strong>tessera associativa</strong> valida. Puoi acquistarla dal <a href="shop.php">negozio</a> dopo questo ordine.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="ys-co-grid">
|
||||
<div class="ys-co-main">
|
||||
<!-- Prodotto -->
|
||||
<div class="ys-panel">
|
||||
<div class="ys-co-prod">
|
||||
<div>
|
||||
<div class="ys-opt-title"><?php echo e($sel['product']['title']); ?></div>
|
||||
<div class="ys-muted"><?php echo e($v['label']); ?></div>
|
||||
</div>
|
||||
<div class="ys-opt-title"><?php echo e(euro($price)); ?></div>
|
||||
</div>
|
||||
<dl class="ys-kv">
|
||||
<?php foreach ($lines as [$k, $val]) : ?>
|
||||
<dt><?php echo e($k); ?></dt>
|
||||
<dd><?php echo e($val); ?></dd>
|
||||
<?php endforeach; ?>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Coupon -->
|
||||
<div class="ys-panel">
|
||||
<form method="post" action="shop_checkout.php" class="ys-coupon">
|
||||
<?php echo yb_csrf_field(); ?>
|
||||
<?php echo keepFields($in, ''); ?>
|
||||
<input type="hidden" name="method" value="<?php echo e($method); ?>">
|
||||
<label class="ys-label" for="coupon" style="margin:0;">Hai un codice sconto?</label>
|
||||
<?php if ($coupon) : ?>
|
||||
<div class="ys-co-ok">
|
||||
<span><i class="bx bx-check-circle"></i> <?php echo e($coupon['code']); ?> · −<?php echo e(euro($discount)); ?></span>
|
||||
<input type="hidden" name="coupon_code" value="">
|
||||
<button type="submit" name="action" value="remove_coupon" class="ys-linkbtn">Rimuovi</button>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="d-flex gap-2">
|
||||
<input type="text" id="coupon" name="coupon_code" class="ys-input" style="text-transform:uppercase;" maxlength="40" autocomplete="off" placeholder="Codice">
|
||||
<button type="submit" name="action" value="coupon" class="ys-btn ys-btn-light">Applica</button>
|
||||
</div>
|
||||
<?php if ($couponMsg) : ?><div class="ys-co-err"><?php echo e($couponMsg); ?></div><?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagamento -->
|
||||
<form method="post" action="shop_checkout.php" class="ys-co-side" id="confirm-form">
|
||||
<?php echo yb_csrf_field(); ?>
|
||||
<?php echo keepFields($in, $coupon ? $coupon['code'] : ''); ?>
|
||||
<input type="hidden" name="checkout_token" value="<?php echo e($_SESSION['yb_checkout_token']); ?>">
|
||||
<div class="ys-panel">
|
||||
<span class="ys-label" style="margin:0;">Come vuoi pagare?</span>
|
||||
<?php if (!$methods['bank'] && !$methods['card']) : ?>
|
||||
<div class="ys-muted">Il pagamento online non è ancora attivo. Contatta la segreteria per completare l'acquisto.</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($methods['card']) : ?>
|
||||
<label class="ys-opt <?php echo $method === 'stripe' ? 'sel' : ''; ?>"><input type="radio" name="method" value="stripe" class="ys-radio-input" <?php echo $method === 'stripe' ? 'checked' : ''; ?>><span class="ys-radio"></span><span class="ys-opt-main"><span class="ys-opt-title" style="font-size:15px;">Carta di credito</span><span class="ys-muted">Pagamento immediato e sicuro.</span></span></label>
|
||||
<?php endif; ?>
|
||||
<?php if ($methods['bank']) : ?>
|
||||
<label class="ys-opt <?php echo $method === 'bank' ? 'sel' : ''; ?>"><input type="radio" name="method" value="bank" class="ys-radio-input" <?php echo $method === 'bank' ? 'checked' : ''; ?>><span class="ys-radio"></span><span class="ys-opt-main"><span class="ys-opt-title" style="font-size:15px;">Bonifico bancario</span><span class="ys-muted"><?php echo $sel['mode'] === 'single' ? 'Il posto resta prenotato in attesa del pagamento.' : 'Il pacchetto si attiva quando la scuola riceve il pagamento.'; ?></span></span></label>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="ys-co-totals">
|
||||
<div><span>Subtotale</span><span><?php echo e(euro($price)); ?></span></div>
|
||||
<?php if ($discount > 0) : ?><div class="ok"><span>Sconto <?php echo e($coupon['code']); ?></span><span>− <?php echo e(euro($discount)); ?></span></div><?php endif; ?>
|
||||
<div class="tot"><span>Totale</span><span><?php echo e(euro($total)); ?></span></div>
|
||||
</div>
|
||||
|
||||
<label class="ys-terms" for="terms">
|
||||
<input type="checkbox" id="terms" name="terms" value="Y">
|
||||
<span>Ho letto e accetto le
|
||||
<?php if ($termsUrl !== '') : ?><a href="<?php echo e($termsUrl); ?>" target="_blank" rel="noopener">condizioni di acquisto</a>
|
||||
<?php elseif ($termsText !== '') : ?><a href="#" id="terms-open">condizioni di acquisto</a>
|
||||
<?php else : ?>condizioni di acquisto
|
||||
<?php endif; ?> della scuola.</span>
|
||||
</label>
|
||||
<?php if ($termsUrl === '' && $termsText !== '') : ?>
|
||||
<div class="ys-terms-text" id="terms-text" hidden><?php echo nl2br(e($termsText)); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<button type="submit" name="action" value="confirm" class="ys-btn" id="confirm-btn" style="width:100%;justify-content:center;" <?php echo (!$methods['bank'] && !$methods['card']) ? 'disabled' : ''; ?>>
|
||||
<?php echo $method === 'stripe' ? 'Paga ' . e(euro($total)) : 'Conferma ordine · ' . e(euro($total)); ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</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() {
|
||||
var form = document.getElementById('confirm-form');
|
||||
if (!form) return;
|
||||
var total = <?php echo json_encode(isset($total) ? euro($total) : ''); ?>;
|
||||
var btn = document.getElementById('confirm-btn');
|
||||
form.addEventListener('change', function(ev) {
|
||||
if (ev.target.name !== 'method') return;
|
||||
form.querySelectorAll('label.ys-opt').forEach(function(l) {
|
||||
l.classList.toggle('sel', l.querySelector('input').checked);
|
||||
});
|
||||
btn.textContent = ev.target.value === 'stripe' ? 'Paga ' + total : 'Conferma ordine · ' + total;
|
||||
});
|
||||
form.addEventListener('submit', function(ev) {
|
||||
if (!document.getElementById('terms').checked) {
|
||||
ev.preventDefault();
|
||||
document.querySelector('.ys-terms').classList.add('need');
|
||||
return;
|
||||
}
|
||||
setTimeout(function() {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Attendi…';
|
||||
}, 0);
|
||||
});
|
||||
var open = document.getElementById('terms-open');
|
||||
if (open) open.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
var t = document.getElementById('terms-text');
|
||||
t.hidden = !t.hidden;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/csrf.php');
|
||||
require_once(__DIR__ . '/class/ShopService.php');
|
||||
|
||||
/**
|
||||
* NEGOZIO - dettaglio di un ordine dell'allieva (anche pagina di conferma dopo l'acquisto).
|
||||
* shop_order.php?id=123 dettaglio
|
||||
* shop_order.php?id=123&new=1 conferma appena creato
|
||||
* Solo ordini dell'utente della sessione nella scuola corrente.
|
||||
* L'allieva può annullare un ordine con bonifico finché è in attesa di pagamento.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function euro($v): string
|
||||
{
|
||||
return number_format((float) $v, 2, ',', '.') . ' €';
|
||||
}
|
||||
|
||||
$id = (int) ($_GET['id'] ?? $_POST['id'] ?? 0);
|
||||
$st = $pdo->prepare("SELECT ob.*, p.title AS ptitle, p.kind FROM orderbook ob LEFT JOIN products p ON p.idproduct = ob.idproduct WHERE ob.idorderbook = ? AND ob.iduser = ? AND ob.idschool = ?");
|
||||
$st->execute([$id, (int) $iduserlogin, (int) $idschool]);
|
||||
$o = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$o) {
|
||||
header('Location: shop.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
/* ------------------------------------------- annullo (solo bonifico in attesa) */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'cancel') {
|
||||
yb_csrf_check();
|
||||
try {
|
||||
ShopService::cancelPending((int) $idschool, $id, (int) $iduserlogin);
|
||||
} catch (PDOException $ex) {
|
||||
error_log('[shop_order] cancel: ' . $ex->getMessage());
|
||||
header('Location: shop_order.php?id=' . $id . '&err=1');
|
||||
exit();
|
||||
}
|
||||
header('Location: shop_order.php?id=' . $id . '&cancelled=1');
|
||||
exit();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ DATI */
|
||||
$rules = json_decode((string) $o['rules_snapshot'], true) ?: [];
|
||||
$mode = $o['booking_mode'] ?: ($o['kind'] ?? '');
|
||||
|
||||
$classes = [];
|
||||
if ($mode === 'fixed') {
|
||||
$st = $pdo->prepare("SELECT s.servicename, s.day, s.time FROM orderbook_services os JOIN service s ON s.idservice = os.idservice WHERE os.idorderbook = ? AND os.idschool = ? ORDER BY os.slot");
|
||||
$st->execute([$id, (int) $idschool]);
|
||||
$classes = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
$lesson = null;
|
||||
if ($mode === 'single') {
|
||||
$st = $pdo->prepare("SELECT b.bookingstart, b.status, s.servicename FROM bookingclass b JOIN service s ON s.idservice = b.idservice WHERE b.idorder = ? AND b.idschool = ? ORDER BY b.idbookingclass LIMIT 1");
|
||||
$st->execute([$id, (int) $idschool]);
|
||||
$lesson = $st->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
}
|
||||
$voucher = null;
|
||||
if (!empty($o['cod'])) {
|
||||
$st = $pdo->prepare("SELECT * FROM gift_vouchers WHERE idschool = ? AND code = ?");
|
||||
$st->execute([(int) $idschool, $o['cod']]);
|
||||
$voucher = $st->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
}
|
||||
|
||||
$pending = $o['payment_status'] === 'pending';
|
||||
$cancelled = in_array($o['payment_status'], ['cancelled', 'refunded'], true);
|
||||
$bank = ($o['source'] === 'bank' && $pending) ? ShopService::bankInfo((int) $idschool, $o) : null;
|
||||
$isNew = isset($_GET['new']);
|
||||
|
||||
$statusLabel = $cancelled ? ['Annullato', 'muted'] : ($pending ? ['In attesa di pagamento', 'warn'] : ['Pagato', 'ok']);
|
||||
|
||||
$next = '';
|
||||
if (!$cancelled) {
|
||||
if ($mode === 'fixed') {
|
||||
$next = $pending
|
||||
? 'Quando la scuola riceve il bonifico ti conferma la data della prima lezione; da lì le lezioni vengono prenotate in automatico ogni settimana.'
|
||||
: 'La scuola ti conferma la data della prima lezione; da lì le lezioni vengono prenotate in automatico ogni settimana.';
|
||||
} elseif ($mode === 'free') {
|
||||
$next = $pending
|
||||
? 'Appena la scuola riceve il bonifico il pacchetto si attiva e puoi prenotare le lezioni dal calendario.'
|
||||
: 'Il pacchetto è attivo: prenota le lezioni dal calendario quando vuoi.';
|
||||
} elseif ($mode === 'single' && $lesson) {
|
||||
$next = 'Il tuo posto è ' . ($pending ? 'riservato in attesa del pagamento' : 'prenotato') . ': ' . $lesson['servicename'] . ', ' . mb_strtolower(ShopService::dateLabel(strtotime($lesson['bookingstart']))) . '.';
|
||||
} elseif ($o['kind'] === 'membership') {
|
||||
$next = $pending ? 'La tessera si attiva quando la scuola riceve il pagamento.' : 'La tessera è attiva' . ($o['expireon'] ? ' fino al ' . date('d/m/Y', strtotime($o['expireon'])) : '') . '.';
|
||||
} elseif ($o['kind'] === 'voucher' && $voucher) {
|
||||
$next = $pending
|
||||
? 'Il buono per ' . $voucher['recipient_name'] . ' si attiva quando la scuola riceve il pagamento.'
|
||||
: 'Il buono per ' . $voucher['recipient_name'] . ' è attivo. Codice: ' . $voucher['code'] . '.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Ordine n. <?php echo (int) $id; ?> - <?php echo e($schoolname); ?></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-shop.css" rel="stylesheet" type="text/css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
</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">Ordine n. <?php echo (int) $id; ?></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">
|
||||
<div class="ys-wrap ys-checkout" style="max-width:760px;">
|
||||
|
||||
<?php if (isset($_GET['cancelled'])) : ?>
|
||||
<div class="ys-alert"><i class="bx bx-info-circle"></i>
|
||||
<div>Ordine annullato.</div>
|
||||
</div>
|
||||
<?php elseif (isset($_GET['err'])) : ?>
|
||||
<div class="ys-alert" style="background:#FDECEA;border-color:#f2c9c4;color:#8f2f24;"><i class="bx bx-error"></i>
|
||||
<div>Non è stato possibile annullare l'ordine. Riprova o contatta la segreteria.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isNew && !$cancelled) : ?>
|
||||
<div class="ys-done">
|
||||
<span class="ys-done-ico"><i class="bx bx-check"></i></span>
|
||||
<h1 class="ys-h1"><?php echo $pending ? 'Ordine registrato' : 'Acquisto completato'; ?></h1>
|
||||
<p class="ys-lead"><?php echo $bank ? 'Ultimo passo: fai il bonifico con i dati qui sotto.' : 'Grazie! Trovi tutto anche in "I miei pacchetti".'; ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($bank) : ?>
|
||||
<div class="ys-panel ys-bank">
|
||||
<div class="ys-label" style="margin:0;"><i class="bx bx-building-house"></i> Dati per il bonifico</div>
|
||||
<dl class="ys-kv">
|
||||
<dt>Intestatario</dt>
|
||||
<dd><?php echo e($bank['holder']); ?></dd>
|
||||
<dt>IBAN</dt>
|
||||
<dd><span class="ys-mono" id="iban"><?php echo e($bank['iban']); ?></span> <button type="button" class="ys-linkbtn" data-copy="iban">Copia</button></dd>
|
||||
<?php if ($bank['bank'] !== '') : ?><dt>Banca</dt>
|
||||
<dd><?php echo e($bank['bank']); ?></dd><?php endif; ?>
|
||||
<dt>Importo</dt>
|
||||
<dd><strong><?php echo e(euro($o['amount'])); ?></strong></dd>
|
||||
<dt>Causale</dt>
|
||||
<dd><strong id="ref"><?php echo e($bank['reference']); ?></strong> <button type="button" class="ys-linkbtn" data-copy="ref">Copia</button></dd>
|
||||
<dt>Entro il</dt>
|
||||
<dd><?php echo e(date('d/m/Y', strtotime($bank['due']))); ?></dd>
|
||||
</dl>
|
||||
<div class="ys-muted">Usa esattamente questa causale: così la scuola riconosce subito il tuo pagamento.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="ys-panel">
|
||||
<div class="ys-co-prod">
|
||||
<div>
|
||||
<div class="ys-opt-title"><?php echo e($o['product_name']); ?></div>
|
||||
<div class="ys-muted">Ordine n. <?php echo (int) $id; ?> del <?php echo e(date('d/m/Y', strtotime($o['order_date_created']))); ?></div>
|
||||
</div>
|
||||
<span class="ys-status <?php echo $statusLabel[1]; ?>"><?php echo e($statusLabel[0]); ?></span>
|
||||
</div>
|
||||
<dl class="ys-kv">
|
||||
<?php foreach ($classes as $c) : ?>
|
||||
<dt>Classe</dt>
|
||||
<dd><?php echo e(trim($c['servicename'] . ' · ' . $c['day'] . ' ' . $c['time'])); ?></dd>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($lesson) : ?>
|
||||
<dt>Lezione</dt>
|
||||
<dd><?php echo e($lesson['servicename'] . ' · ' . ShopService::dateLabel(strtotime($lesson['bookingstart']))); ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($o['nticket']) : ?><dt>Lezioni</dt>
|
||||
<dd><?php echo (int) $o['nticket']; ?></dd><?php endif; ?>
|
||||
<?php if ($o['expireon'] && $mode !== 'single') : ?><dt>Valido fino al</dt>
|
||||
<dd><?php echo e(date('d/m/Y', strtotime($o['expireon']))); ?></dd><?php endif; ?>
|
||||
<?php if ($voucher) : ?><dt>Buono per</dt>
|
||||
<dd><?php echo e($voucher['recipient_name'] . ' · ' . $voucher['recipient_email']); ?></dd><?php endif; ?>
|
||||
<?php if ((float) $o['discount_amount'] > 0) : ?><dt>Sconto</dt>
|
||||
<dd>− <?php echo e(euro($o['discount_amount'])); ?></dd><?php endif; ?>
|
||||
<dt>Totale</dt>
|
||||
<dd><strong><?php echo e(euro($o['amount'])); ?></strong> · <?php echo $o['source'] === 'bank' ? 'bonifico' : ($o['source'] === 'stripe' ? 'carta' : e($o['source'])); ?></dd>
|
||||
</dl>
|
||||
<?php if ($next !== '') : ?>
|
||||
<div class="ys-note"><i class="bx bx-info-circle" style="color:#2F7F84;font-size:18px;"></i><span><?php echo e($next); ?></span></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 flex-wrap justify-content-between align-items-center mt-3">
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<a class="ys-btn" href="orders.php">I miei pacchetti</a>
|
||||
<a class="ys-btn ys-btn-light" href="shop.php">Torna al negozio</a>
|
||||
</div>
|
||||
<?php if ($bank) : ?>
|
||||
<form method="post" action="shop_order.php?id=<?php echo (int) $id; ?>" id="cancel-form">
|
||||
<?php echo yb_csrf_field(); ?>
|
||||
<input type="hidden" name="action" value="cancel">
|
||||
<button type="submit" class="ys-linkbtn" style="color:#b03a2e;">Annulla ordine</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
document.querySelectorAll('[data-copy]').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var text = document.getElementById(btn.getAttribute('data-copy')).textContent.trim();
|
||||
var done = function() {
|
||||
btn.textContent = 'Copiato';
|
||||
setTimeout(function() {
|
||||
btn.textContent = 'Copia';
|
||||
}, 1500);
|
||||
};
|
||||
if (navigator.clipboard) navigator.clipboard.writeText(text).then(done, function() {
|
||||
window.prompt('Copia:', text);
|
||||
});
|
||||
else window.prompt('Copia:', text);
|
||||
});
|
||||
});
|
||||
var cf = document.getElementById('cancel-form');
|
||||
if (cf) cf.addEventListener('submit', function(ev) {
|
||||
if (cf.dataset.ok) return;
|
||||
ev.preventDefault();
|
||||
Swal.fire({
|
||||
title: 'Annullare l\'ordine?',
|
||||
text: 'Fallo solo se non hai ancora fatto il bonifico.<?php echo $mode === 'single' ? ' Il posto nella lezione verrà liberato.' : ''; ?>',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
reverseButtons: true,
|
||||
confirmButtonText: 'Annulla ordine',
|
||||
cancelButtonText: 'Chiudi',
|
||||
confirmButtonColor: '#c0392b'
|
||||
}).then(function(r) {
|
||||
if (r.isConfirmed) {
|
||||
cf.dataset.ok = '1';
|
||||
cf.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
Reference in New Issue
Block a user