Merge branch 'feature/waiting-list'
This commit is contained in:
+1
-1
@@ -40,7 +40,7 @@
|
||||
"laravel/tinker": "^2.7",
|
||||
"laravel/ui": "^4.0",
|
||||
"laravelcollective/html": "^6.3",
|
||||
"phpmailer/phpmailer": "^6.8",
|
||||
"phpmailer/phpmailer": "^7.1",
|
||||
"proengsoft/laravel-jsvalidation": "^4.0.0",
|
||||
"robmorgan/phinx": "^0.16.12",
|
||||
"spatie/laravel-query-builder": "^5.0",
|
||||
|
||||
Generated
+11
-10
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9b689efb6d29cb572794705682d6edc4",
|
||||
"content-hash": "9307a6b8f5f1c5b0a5efb10e35038f7f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "akaunting/laravel-setting",
|
||||
@@ -3428,16 +3428,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.9.3",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
|
||||
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3451,13 +3451,14 @@
|
||||
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
||||
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||
"phpcompatibility/php-compatibility": "^9.3.5",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"squizlabs/php_codesniffer": "^3.7.2",
|
||||
"phpcompatibility/php-compatibility": "^10.0.0@dev",
|
||||
"squizlabs/php_codesniffer": "^3.13.5",
|
||||
"yoast/phpunit-polyfills": "^1.0.4"
|
||||
},
|
||||
"suggest": {
|
||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
|
||||
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
|
||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||
@@ -3497,7 +3498,7 @@
|
||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3"
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3505,7 +3506,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-11-24T18:04:13+00:00"
|
||||
"time": "2026-05-18T08:06:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* YogiBook - Riepilogo classi giornaliero.
|
||||
* Invia UNA mail per ogni classe del giorno:
|
||||
* - all'insegnante della classe (se notifiche attive, attivo e con email)
|
||||
* -> versione riservata: nomi "Mario R.", senza email dei partecipanti
|
||||
* - al titolare, se diverso dall'insegnante (se notifiche attive e con email)
|
||||
* -> copia separata con i dati completi
|
||||
* - se l'insegnante È il titolare: una sola mail completa
|
||||
*
|
||||
* Uso:
|
||||
* php cron/class_report.php -> classi di oggi
|
||||
* php cron/class_report.php 2026-09-25 -> classi di una data specifica
|
||||
* php cron/class_report.php --dry-run -> NON invia, salva le anteprime HTML in cron/preview/
|
||||
* php cron/class_report.php --only-at=17 -> esce senza fare nulla se in Italia non sono le 17
|
||||
*/
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception as MailException;
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(403);
|
||||
exit('Accesso consentito solo da CLI');
|
||||
}
|
||||
|
||||
date_default_timezone_set('Europe/Rome');
|
||||
|
||||
// --only-at=17 : esce senza fare nulla se in Italia non sono le 17 (server in UTC)
|
||||
foreach ($argv as $a) {
|
||||
if (preg_match('/^--only-at=(\d{1,2})$/', $a, $m) && (int) date('G') !== (int) $m[1]) {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
chdir($root);
|
||||
require_once $root . '/public/class/db-functions.php'; // include anche vendor/autoload.php
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection(); // carica anche il .env
|
||||
|
||||
/* ---------------------------------------------------------------- helper */
|
||||
|
||||
function cfg(string $key, $default = null)
|
||||
{
|
||||
$v = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key);
|
||||
return ($v === false || $v === null || $v === '') ? $default : $v;
|
||||
}
|
||||
|
||||
function h($v): string
|
||||
{
|
||||
return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function logmsg(string $m): void
|
||||
{
|
||||
echo '[' . date('Y-m-d H:i:s') . '] ' . $m . PHP_EOL;
|
||||
}
|
||||
|
||||
function chip(string $label, string $bg, string $fg): string
|
||||
{
|
||||
return '<span style="display:inline-block;background:' . $bg . ';color:' . $fg . ';font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">' . h($label) . '</span>';
|
||||
}
|
||||
|
||||
/** Nome completo (versione titolare) */
|
||||
function fullName(array $p): string
|
||||
{
|
||||
$n = trim(($p['first_name'] ?? '') . ' ' . ($p['last_name'] ?? ''));
|
||||
return $n !== '' ? $n : 'Utente #' . (int) ($p['iduser'] ?? 0);
|
||||
}
|
||||
|
||||
/** Nome + iniziale del cognome (versione insegnante): "Mario R." */
|
||||
function shortName(array $p): string
|
||||
{
|
||||
$first = trim((string) ($p['first_name'] ?? ''));
|
||||
$last = trim((string) ($p['last_name'] ?? ''));
|
||||
if ($first === '' && $last === '') {
|
||||
return 'Utente #' . (int) ($p['iduser'] ?? 0);
|
||||
}
|
||||
$ini = $last !== '' ? ' ' . mb_strtoupper(mb_substr($last, 0, 1)) . '.' : '';
|
||||
return ($first !== '' ? $first : '') . $ini;
|
||||
}
|
||||
|
||||
function personName(array $p, bool $full): string
|
||||
{
|
||||
return $full ? fullName($p) : shortName($p);
|
||||
}
|
||||
|
||||
function teacherName(?array $t): string
|
||||
{
|
||||
return $t ? trim($t['nameemployee'] . ' ' . $t['surnameemployee']) : '';
|
||||
}
|
||||
|
||||
function italianDate(DateTime $d): string
|
||||
{
|
||||
$days = ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'];
|
||||
$months = ['', 'Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'];
|
||||
return $days[(int) $d->format('w')] . ' ' . (int) $d->format('j') . ' ' . $months[(int) $d->format('n')] . ' ' . $d->format('Y');
|
||||
}
|
||||
|
||||
/** Motivo per cui non può ricevere ('' = può ricevere) */
|
||||
function whyNot(?array $t): string
|
||||
{
|
||||
if (!$t) return 'nessun insegnante assegnato';
|
||||
if (($t['active'] ?? 'N') !== 'Y') return 'insegnante non attivo';
|
||||
if (($t['notifications'] ?? 'N') !== 'Y') return 'notifiche disattivate';
|
||||
if (empty($t['email'])) return 'email mancante';
|
||||
if (!filter_var($t['email'], FILTER_VALIDATE_EMAIL)) return 'email non valida';
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Può ricevere mail? notifiche attive, attivo, email valida */
|
||||
function canReceive(?array $t): bool
|
||||
{
|
||||
return $t
|
||||
&& ($t['notifications'] ?? 'N') === 'Y'
|
||||
&& ($t['active'] ?? 'N') === 'Y'
|
||||
&& !empty($t['email'])
|
||||
&& filter_var($t['email'], FILTER_VALIDATE_EMAIL);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------- argomenti */
|
||||
|
||||
$dryRun = in_array('--dry-run', $argv, true);
|
||||
$day = date('Y-m-d');
|
||||
foreach (array_slice($argv, 1) as $a) {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $a)) {
|
||||
$day = $a;
|
||||
}
|
||||
}
|
||||
|
||||
$location = cfg('CLASS_LOCATION', 'via Valassina 62/B Seregno - Sala Contesto Yoga');
|
||||
|
||||
// Le mail agli insegnanti (non titolari) partono solo se l'opzione "Mostra insegnante" è attiva
|
||||
$optRow = $pdo->query("SELECT show_teacher FROM `option` LIMIT 1")->fetch();
|
||||
$teacherMailsEnabled = ($optRow['show_teacher'] ?? 'N') === 'Y';
|
||||
|
||||
/* ---------------------------------------------------------- insegnanti */
|
||||
|
||||
$employees = [];
|
||||
$owner = null;
|
||||
foreach ($pdo->query("SELECT idemployee, nameemployee, surnameemployee, email, is_owner, notifications, active FROM employee")->fetchAll() as $t) {
|
||||
$employees[(int) $t['idemployee']] = $t;
|
||||
if ($t['is_owner'] === 'Y' && $owner === null) {
|
||||
$owner = $t;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ classi del giorno */
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT ss.idserviceschedule, ss.dateschedule, s.servicename, s.colorclass, s.maxcapacity, s.idteacher
|
||||
FROM serviceschedule ss
|
||||
LEFT JOIN service s ON ss.idservice = s.idservice
|
||||
WHERE ss.dateschedule >= :d1 AND ss.dateschedule < :d2
|
||||
ORDER BY ss.dateschedule"
|
||||
);
|
||||
$stmt->execute([
|
||||
':d1' => $day . ' 00:00:00',
|
||||
':d2' => date('Y-m-d', strtotime($day . ' +1 day')) . ' 00:00:00',
|
||||
]);
|
||||
$classes = $stmt->fetchAll();
|
||||
|
||||
if (!$classes) {
|
||||
logmsg("Nessuna classe il $day. Nessuna mail inviata.");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$ids = array_map(fn($c) => (int) $c['idserviceschedule'], $classes);
|
||||
$ph = implode(',', array_fill(0, count($ids), '?'));
|
||||
$stmtB = $pdo->prepare(
|
||||
"SELECT bc.idbookingclass, bc.idserviceschedule, bc.lostlesson, bc.status,
|
||||
u.id AS iduser, u.first_name, u.last_name, u.email
|
||||
FROM bookingclass bc
|
||||
LEFT JOIN auth_users u ON bc.iduser = u.id
|
||||
WHERE bc.idserviceschedule IN ($ph)
|
||||
AND bc.status IN ('booked', 'pending', 'waiting')
|
||||
ORDER BY bc.created_at, u.last_name, u.first_name"
|
||||
);
|
||||
$stmtB->execute($ids);
|
||||
|
||||
$bookingsBySchedule = [];
|
||||
foreach ($stmtB->fetchAll() as $b) {
|
||||
$bookingsBySchedule[(int) $b['idserviceschedule']][] = $b;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ template */
|
||||
|
||||
function personRows(array $list, string $type, bool $full): string
|
||||
{
|
||||
if (!$list) {
|
||||
return '<tr><td style="padding:16px;color:#9ca3af;font-size:14px;text-align:center;">Nessun partecipante</td></tr>';
|
||||
}
|
||||
$numColor = ['booked' => '#1ebf73', 'pending' => '#8a6d00', 'waiting' => '#d1698a'][$type];
|
||||
$html = '';
|
||||
$i = 1;
|
||||
foreach ($list as $p) {
|
||||
$isLost = ($type === 'booked' && ($p['lostlesson'] ?? '') === 'Y');
|
||||
if ($type === 'booked') {
|
||||
$chipHtml = $isLost ? chip('Persa', '#1f2937', '#ffffff') : chip('Confermata', '#e3f2ec', '#1a7f52');
|
||||
} elseif ($type === 'pending') {
|
||||
$chipHtml = chip('In attesa', '#fff4d6', '#8a6d00');
|
||||
} else {
|
||||
$chipHtml = chip('In coda', '#fbeef3', '#d1698a');
|
||||
}
|
||||
$nameColor = $isLost ? '#9ca3af' : '#1f2937';
|
||||
$email = ($full && !empty($p['email']))
|
||||
? '<br><span style="font-size:12px;color:#6b7280;font-weight:400;">' . h($p['email']) . '</span>'
|
||||
: '';
|
||||
|
||||
$html .= '<tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;">'
|
||||
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>'
|
||||
. '<td width="34" valign="top" style="font-weight:800;color:' . $numColor . ';font-size:14px;padding-top:1px;">' . $i . ($type === 'waiting' ? '°' : '.') . '</td>'
|
||||
. '<td valign="top" style="font-size:15px;color:' . $nameColor . ';font-weight:600;">' . h(personName($p, $full)) . $email . '</td>'
|
||||
. '<td valign="top" align="right" width="100">' . $chipHtml . '</td>'
|
||||
. '</tr></table></td></tr>';
|
||||
$i++;
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
function section(string $title, string $color, string $rows): string
|
||||
{
|
||||
return '<tr><td style="padding:22px 28px 0;">'
|
||||
. '<div style="font-size:13px;font-weight:700;color:' . $color . ';text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">' . $title . '</div>'
|
||||
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;">' . $rows . '</table>'
|
||||
. '</td></tr>';
|
||||
}
|
||||
|
||||
function statBox(string $value, string $label, string $color): string
|
||||
{
|
||||
return '<td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;">'
|
||||
. '<div style="font-size:22px;font-weight:800;color:' . $color . ';line-height:1.1;">' . $value . '</div>'
|
||||
. '<div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">' . $label . '</div>'
|
||||
. '</td>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $full true = versione titolare (nomi completi + email)
|
||||
* @param string $teacherFull nome insegnante da mostrare in testata
|
||||
* @param string|null $copyNote nota "copia per il titolare" (solo se il titolare non è l'insegnante)
|
||||
*/
|
||||
function renderEmail(array $cls, array $booked, array $pending, array $waiting, string $location, bool $full, string $teacherFull, ?string $copyNote): string
|
||||
{
|
||||
$d = new DateTime($cls['dateschedule']);
|
||||
$accent = $cls['colorclass'] ?: '#1ebf73';
|
||||
$max = (int) $cls['maxcapacity'];
|
||||
$nBooked = count($booked);
|
||||
$nLost = count(array_filter($booked, fn($b) => ($b['lostlesson'] ?? '') === 'Y'));
|
||||
$free = $max > 0 ? max(0, $max - $nBooked) : '–';
|
||||
$isFull = $max > 0 && $nBooked >= $max;
|
||||
|
||||
$stats = '<table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr>'
|
||||
. statBox($nBooked . '/' . $max, 'Iscritti', $isFull ? '#c0392b' : '#1a7f52')
|
||||
. statBox((string) $free, 'Posti liberi', '#374151')
|
||||
. statBox((string) count($pending), 'In attesa', '#8a6d00')
|
||||
. statBox((string) count($waiting), 'In coda', '#d1698a')
|
||||
. '</tr></table>';
|
||||
|
||||
$lostNote = $nLost > 0
|
||||
? '<div style="font-size:13px;color:#6b7280;margin-top:6px;">di cui ' . $nLost . ' segnat' . ($nLost === 1 ? 'a' : 'e') . ' come pers' . ($nLost === 1 ? 'a' : 'e') . '</div>'
|
||||
: '';
|
||||
|
||||
$body = section('Partecipanti confermati (' . $nBooked . ')', '#374151', personRows($booked, 'booked', $full));
|
||||
if ($pending) {
|
||||
$body .= section('In attesa di conferma (' . count($pending) . ')', '#8a6d00', personRows($pending, 'pending', $full));
|
||||
}
|
||||
if ($waiting) {
|
||||
$body .= section('Lista d\'attesa (' . count($waiting) . ')', '#d1698a', personRows($waiting, 'waiting', $full));
|
||||
}
|
||||
|
||||
$copyBanner = $copyNote
|
||||
? '<tr><td style="padding:14px 28px 0;"><div style="background:#f5eee4;color:#5a4630;border-radius:10px;padding:10px 14px;font-size:13px;">' . $copyNote . '</div></td></tr>'
|
||||
: '';
|
||||
|
||||
$teacherLine = $teacherFull !== ''
|
||||
? '<div style="font-size:13px;color:#6b7280;margin-top:4px;">Insegnante: <strong style="color:#374151;">' . h($teacherFull) . '</strong></div>'
|
||||
: '';
|
||||
|
||||
return '<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>' . h($cls['servicename']) . '</title></head>'
|
||||
. '<body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;">'
|
||||
. '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center">'
|
||||
. '<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);">'
|
||||
. '<tr><td style="height:8px;background:' . h($accent) . ';font-size:0;line-height:0;"> </td></tr>'
|
||||
. $copyBanner
|
||||
. '<tr><td style="padding:26px 28px 6px;">'
|
||||
. '<div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div>'
|
||||
. '<h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">' . h($cls['servicename']) . '</h1>'
|
||||
. '<div style="font-size:15px;color:#374151;font-weight:600;">' . h(italianDate($d)) . ' · ore ' . $d->format('H:i') . '</div>'
|
||||
. '<div style="font-size:13px;color:#6b7280;margin-top:4px;">' . h($location) . '</div>'
|
||||
. $teacherLine
|
||||
. '</td></tr>'
|
||||
. '<tr><td style="padding:16px 22px 0;">' . $stats . '<div style="padding:0 6px;">' . $lostNote . '</div></td></tr>'
|
||||
. $body
|
||||
. '<tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il ' . date('d/m/Y \a\l\l\e H:i') . '</td></tr>'
|
||||
. '</table></td></tr></table></body></html>';
|
||||
}
|
||||
|
||||
function renderText(array $cls, array $booked, array $pending, array $waiting, string $location, bool $full, string $teacherFull, ?string $copyNote): string
|
||||
{
|
||||
$d = new DateTime($cls['dateschedule']);
|
||||
$max = (int) $cls['maxcapacity'];
|
||||
$t = '';
|
||||
if ($copyNote) {
|
||||
$t .= strip_tags($copyNote) . "\n\n";
|
||||
}
|
||||
$t .= $cls['servicename'] . "\n" . italianDate($d) . ' ore ' . $d->format('H:i') . "\n" . $location . "\n";
|
||||
if ($teacherFull !== '') {
|
||||
$t .= 'Insegnante: ' . $teacherFull . "\n";
|
||||
}
|
||||
$t .= "\nIscritti: " . count($booked) . '/' . $max . ' | In attesa: ' . count($pending) . ' | In coda: ' . count($waiting) . "\n\n";
|
||||
$blocks = ['CONFERMATI' => $booked, 'IN ATTESA DI CONFERMA' => $pending, "LISTA D'ATTESA" => $waiting];
|
||||
foreach ($blocks as $title => $list) {
|
||||
if ($title !== 'CONFERMATI' && !$list) {
|
||||
continue;
|
||||
}
|
||||
$t .= $title . "\n";
|
||||
if (!$list) {
|
||||
$t .= " Nessun partecipante\n";
|
||||
}
|
||||
$i = 1;
|
||||
foreach ($list as $p) {
|
||||
$lost = ($p['status'] === 'booked' && ($p['lostlesson'] ?? '') === 'Y') ? ' (PERSA)' : '';
|
||||
$t .= ' ' . $i++ . '. ' . personName($p, $full) . $lost . "\n";
|
||||
}
|
||||
$t .= "\n";
|
||||
}
|
||||
return $t;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- mailer */
|
||||
|
||||
function makeMailer(): PHPMailer
|
||||
{
|
||||
$m = new PHPMailer(true);
|
||||
$m->isSMTP();
|
||||
$m->Host = cfg('MAIL_HOST');
|
||||
$m->Port = (int) cfg('MAIL_PORT', 465);
|
||||
$m->SMTPAuth = true;
|
||||
$m->Username = cfg('MAIL_USERNAME');
|
||||
$m->Password = cfg('MAIL_PASSWORD');
|
||||
$enc = strtolower((string) cfg('MAIL_ENCRYPTION', 'ssl'));
|
||||
$m->SMTPSecure = $enc === 'ssl' ? PHPMailer::ENCRYPTION_SMTPS : ($enc === 'tls' ? PHPMailer::ENCRYPTION_STARTTLS : '');
|
||||
$m->SMTPKeepAlive = true;
|
||||
$m->CharSet = 'UTF-8';
|
||||
$m->Encoding = 'base64';
|
||||
$m->setFrom(cfg('MAIL_FROM_ADDRESS'), cfg('MAIL_FROM_NAME', 'YogiBook'));
|
||||
$m->isHTML(true);
|
||||
return $m;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- invio */
|
||||
|
||||
$mailer = $dryRun ? null : makeMailer();
|
||||
$sent = 0;
|
||||
$failed = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($classes as $cls) {
|
||||
$sid = (int) $cls['idserviceschedule'];
|
||||
$all = $bookingsBySchedule[$sid] ?? [];
|
||||
$booked = array_values(array_filter($all, fn($b) => $b['status'] === 'booked'));
|
||||
$pending = array_values(array_filter($all, fn($b) => $b['status'] === 'pending'));
|
||||
$waiting = array_values(array_filter($all, fn($b) => $b['status'] === 'waiting'));
|
||||
|
||||
$teacher = $employees[(int) $cls['idteacher']] ?? null;
|
||||
$teacherIsOwner = $teacher && $owner && (int) $teacher['idemployee'] === (int) $owner['idemployee'];
|
||||
$tName = teacherName($teacher);
|
||||
|
||||
$d = new DateTime($cls['dateschedule']);
|
||||
$baseSubject = $cls['servicename'] . ' · ' . $d->format('d/m') . ' ore ' . $d->format('H:i')
|
||||
. ' · ' . count($booked) . '/' . (int) $cls['maxcapacity'] . ' iscritti';
|
||||
if ($pending) {
|
||||
$baseSubject .= ' · ' . count($pending) . ' in attesa';
|
||||
}
|
||||
if ($waiting) {
|
||||
$baseSubject .= ' · ' . count($waiting) . ' in coda';
|
||||
}
|
||||
|
||||
// Chi riceve cosa
|
||||
$deliveries = [];
|
||||
if ($teacherIsOwner) {
|
||||
if (canReceive($owner)) {
|
||||
$deliveries[] = ['to' => $owner, 'full' => true, 'copy' => null, 'tag' => 'titolare'];
|
||||
}
|
||||
} else {
|
||||
if (!$teacherMailsEnabled) {
|
||||
logmsg("Classe #$sid ({$cls['servicename']}): mail all'insegnante non inviata (opzione insegnanti disattivata)");
|
||||
} elseif (canReceive($teacher)) {
|
||||
$deliveries[] = ['to' => $teacher, 'full' => false, 'copy' => null, 'tag' => 'insegnante'];
|
||||
} else {
|
||||
logmsg("Classe #$sid ({$cls['servicename']}): insegnante " . ($tName ?: '-') . " escluso: " . whyNot($teacher));
|
||||
}
|
||||
if (canReceive($owner)) {
|
||||
$note = $tName !== ''
|
||||
? '<strong>Copia per il titolare</strong> · classe di ' . h($tName)
|
||||
: '<strong>Copia per il titolare</strong> · classe senza insegnante assegnato';
|
||||
$deliveries[] = ['to' => $owner, 'full' => true, 'copy' => $note, 'tag' => 'titolare'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$deliveries) {
|
||||
$skipped++;
|
||||
logmsg("SALTATA classe #$sid ({$cls['servicename']}): nessun destinatario (notifiche disattivate o email mancante).");
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($deliveries as $dv) {
|
||||
$subject = ($dv['copy'] ? '[Copia] ' : '') . $baseSubject;
|
||||
$html = renderEmail($cls, $booked, $pending, $waiting, $location, $dv['full'], $tName, $dv['copy']);
|
||||
$text = renderText($cls, $booked, $pending, $waiting, $location, $dv['full'], $tName, $dv['copy']);
|
||||
$toName = teacherName($dv['to']);
|
||||
|
||||
if ($dryRun) {
|
||||
$dir = __DIR__ . '/preview';
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0775, true);
|
||||
}
|
||||
$file = $dir . '/classe_' . $sid . '_' . $dv['tag'] . '.html';
|
||||
file_put_contents($file, $html);
|
||||
logmsg("[DRY-RUN] a {$toName} <{$dv['to']['email']}> ({$dv['tag']}): $subject -> $file");
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$mailer->clearAddresses();
|
||||
$mailer->addAddress($dv['to']['email'], $toName);
|
||||
$mailer->Subject = $subject;
|
||||
$mailer->Body = $html;
|
||||
$mailer->AltBody = $text;
|
||||
$mailer->send();
|
||||
$sent++;
|
||||
logmsg("OK a {$toName} ({$dv['tag']}): $subject");
|
||||
} catch (MailException $e) {
|
||||
$failed++;
|
||||
logmsg("ERRORE classe #$sid a {$dv['to']['email']}: " . $mailer->ErrorInfo);
|
||||
}
|
||||
sleep(1); // evita limiti di invio del server SMTP
|
||||
}
|
||||
}
|
||||
|
||||
if ($mailer) {
|
||||
$mailer->smtpClose();
|
||||
}
|
||||
|
||||
logmsg("Fine: $sent inviate, $failed errori, $skipped classi senza destinatari (" . count($classes) . " classi il $day).");
|
||||
exit($failed > 0 ? 1 : 0);
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hatha Yoga</title></head><body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);"><tr><td style="height:8px;background:#fa9b9b;font-size:0;line-height:0;"> </td></tr><tr><td style="padding:26px 28px 6px;"><div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div><h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">Hatha Yoga</h1><div style="font-size:15px;color:#374151;font-weight:600;">Lunedì 5 Ottobre 2026 · ore 19:30</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">via Valassina 62/B Seregno - Sala Contesto Yoga</div></td></tr><tr><td style="padding:16px 22px 0;"><table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#c0392b;line-height:1.1;">1/1</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Iscritti</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#374151;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Posti liberi</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#8a6d00;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In attesa</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#d1698a;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In coda</div></td></tr></table><div style="padding:0 6px;"></div></td></tr><tr><td style="padding:22px 28px 0;"><div style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">Partecipanti confermati (1)</div><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;"><tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td width="34" valign="top" style="font-weight:800;color:#1ebf73;font-size:14px;padding-top:1px;">1.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Amelia Benucci<br><span style="font-size:12px;color:#6b7280;font-weight:400;">info@claudiosironi.com</span></td><td valign="top" align="right" width="100"><span style="display:inline-block;background:#e3f2ec;color:#1a7f52;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">Confermata</span></td></tr></table></td></tr></table></td></tr><tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il 01/10/2026 alle 11:15</td></tr></table></td></tr></table></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hatha Yoga</title></head><body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);"><tr><td style="height:8px;background:#fa9b9b;font-size:0;line-height:0;"> </td></tr><tr><td style="padding:26px 28px 6px;"><div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div><h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">Hatha Yoga</h1><div style="font-size:15px;color:#374151;font-weight:600;">Lunedì 5 Ottobre 2026 · ore 19:30</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">via Valassina 62/B Seregno - Sala Contesto Yoga</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">Insegnante: <strong style="color:#374151;">Francesca -</strong></div></td></tr><tr><td style="padding:16px 22px 0;"><table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#c0392b;line-height:1.1;">1/1</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Iscritti</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#374151;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Posti liberi</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#8a6d00;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In attesa</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#d1698a;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In coda</div></td></tr></table><div style="padding:0 6px;"></div></td></tr><tr><td style="padding:22px 28px 0;"><div style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">Partecipanti confermati (1)</div><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;"><tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td width="34" valign="top" style="font-weight:800;color:#1ebf73;font-size:14px;padding-top:1px;">1.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Amelia B.</td><td valign="top" align="right" width="100"><span style="display:inline-block;background:#e3f2ec;color:#1a7f52;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">Confermata</span></td></tr></table></td></tr></table></td></tr><tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il 01/10/2026 alle 11:30</td></tr></table></td></tr></table></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Hatha Yoga</title></head><body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);"><tr><td style="height:8px;background:#fa9b9b;font-size:0;line-height:0;"> </td></tr><tr><td style="padding:14px 28px 0;"><div style="background:#f5eee4;color:#5a4630;border-radius:10px;padding:10px 14px;font-size:13px;"><strong>Copia per il titolare</strong> · classe di Francesca -</div></td></tr><tr><td style="padding:26px 28px 6px;"><div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div><h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">Hatha Yoga</h1><div style="font-size:15px;color:#374151;font-weight:600;">Lunedì 5 Ottobre 2026 · ore 19:30</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">via Valassina 62/B Seregno - Sala Contesto Yoga</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">Insegnante: <strong style="color:#374151;">Francesca -</strong></div></td></tr><tr><td style="padding:16px 22px 0;"><table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#c0392b;line-height:1.1;">1/1</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Iscritti</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#374151;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Posti liberi</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#8a6d00;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In attesa</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#d1698a;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In coda</div></td></tr></table><div style="padding:0 6px;"></div></td></tr><tr><td style="padding:22px 28px 0;"><div style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">Partecipanti confermati (1)</div><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;"><tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td width="34" valign="top" style="font-weight:800;color:#1ebf73;font-size:14px;padding-top:1px;">1.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Amelia Benucci<br><span style="font-size:12px;color:#6b7280;font-weight:400;">info@claudiosironi.com</span></td><td valign="top" align="right" width="100"><span style="display:inline-block;background:#e3f2ec;color:#1a7f52;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">Confermata</span></td></tr></table></td></tr></table></td></tr><tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il 01/10/2026 alle 11:30</td></tr></table></td></tr></table></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Aerial Yoga - mix</title></head><body style="margin:0;padding:0;background:#f3f5f7;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f5f7;padding:24px 12px;"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 3px 16px rgba(0,0,0,0.06);"><tr><td style="height:8px;background:#6ED4E4;font-size:0;line-height:0;"> </td></tr><tr><td style="padding:26px 28px 6px;"><div style="font-size:12px;font-weight:700;color:#1ebf73;text-transform:uppercase;letter-spacing:0.06em;">YogiBook · Riepilogo classe</div><h1 style="margin:8px 0 6px;font-size:24px;color:#1f2937;">Aerial Yoga - mix</h1><div style="font-size:15px;color:#374151;font-weight:600;">Giovedì 24 Settembre 2026 · ore 19:00</div><div style="font-size:13px;color:#6b7280;margin-top:4px;">via Valassina 62/B Seregno - Sala Contesto Yoga</div></td></tr><tr><td style="padding:16px 22px 0;"><table role="presentation" width="100%" cellpadding="0" cellspacing="6"><tr><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#1a7f52;line-height:1.1;">2/8</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Iscritti</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#374151;line-height:1.1;">6</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">Posti liberi</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#8a6d00;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In attesa</div></td><td align="center" style="padding:12px 6px;background:#f8fafc;border-radius:10px;"><div style="font-size:22px;font-weight:800;color:#d1698a;line-height:1.1;">0</div><div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.04em;margin-top:4px;">In coda</div></td></tr></table><div style="padding:0 6px;"></div></td></tr><tr><td style="padding:22px 28px 0;"><div style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;">Partecipanti confermati (2)</div><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #eef0f3;border-radius:10px;"><tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td width="34" valign="top" style="font-weight:800;color:#1ebf73;font-size:14px;padding-top:1px;">1.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Amelia Benucci<br><span style="font-size:12px;color:#6b7280;font-weight:400;">info@claudiosironi.com</span></td><td valign="top" align="right" width="100"><span style="display:inline-block;background:#e3f2ec;color:#1a7f52;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">Confermata</span></td></tr></table></td></tr><tr><td style="padding:10px 14px;border-bottom:1px solid #eef0f3;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td width="34" valign="top" style="font-weight:800;color:#1ebf73;font-size:14px;padding-top:1px;">2.</td><td valign="top" style="font-size:15px;color:#1f2937;font-weight:600;">Aurora Masiero<br><span style="font-size:12px;color:#6b7280;font-weight:400;">info@yogasoul.it</span></td><td valign="top" align="right" width="100"><span style="display:inline-block;background:#e3f2ec;color:#1a7f52;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;white-space:nowrap;">Confermata</span></td></tr></table></td></tr></table></td></tr><tr><td style="padding:26px 28px;font-size:12px;color:#9ca3af;text-align:center;">Generato automaticamente da YogiBook il 24/09/2026 alle 10:55</td></tr></table></td></tr></table></body></html>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddWaitingListSupport extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('bookingclass');
|
||||
$table
|
||||
->addColumn('created_at', 'timestamp', [
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'null' => false,
|
||||
'after' => 'cancellation_token',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddWaitingListEnabledToOption extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->addColumn('waitinglist_enabled', 'char', [
|
||||
'limit' => 1,
|
||||
'default' => 'N',
|
||||
'null' => false,
|
||||
'after' => 'maxbeforetimecancell',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
final class CreateEventsTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('events', [
|
||||
'id' => false,
|
||||
'primary_key' => ['id'],
|
||||
'engine' => 'InnoDB',
|
||||
'encoding' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
|
||||
$table
|
||||
->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
|
||||
->addColumn('user_id', 'integer', ['signed' => false, 'null' => true, 'comment' => 'insegnante/creatore -> auth_users.id'])
|
||||
->addColumn('title', 'string', ['limit' => 191, 'null' => false])
|
||||
->addColumn('slug', 'string', ['limit' => 191, 'null' => false, 'comment' => 'URL pubblico / widget'])
|
||||
->addColumn('description', 'text', ['limit' => MysqlAdapter::TEXT_LONG, 'null' => true, 'comment' => 'HTML editor'])
|
||||
->addColumn('organizer_name', 'string', ['limit' => 191, 'null' => true, 'comment' => 'nome libero; se null usa il creatore'])
|
||||
->addColumn('starts_at', 'datetime', ['null' => false])
|
||||
->addColumn('ends_at', 'datetime', ['null' => true])
|
||||
->addColumn('address', 'string', ['limit' => 255, 'null' => true, 'comment' => 'testo per Google Maps'])
|
||||
->addColumn('lat', 'decimal', ['precision' => 10, 'scale' => 7, 'null' => true])
|
||||
->addColumn('lng', 'decimal', ['precision' => 10, 'scale' => 7, 'null' => true])
|
||||
->addColumn('cost', 'decimal', ['precision' => 8, 'scale' => 2, 'null' => false, 'default' => 0])
|
||||
->addColumn('currency', 'string', ['limit' => 3, 'null' => false, 'default' => 'EUR'])
|
||||
->addColumn('max_seats', 'integer', ['signed' => false, 'null' => false, 'default' => 0])
|
||||
->addColumn('max_per_booking', 'integer', ['signed' => false, 'null' => false, 'default' => 1])
|
||||
->addColumn('color', 'string', ['limit' => 7, 'null' => false, 'default' => '#4CAF50', 'comment' => 'hex card'])
|
||||
->addColumn('cover_image', 'string', ['limit' => 191, 'null' => true, 'comment' => 'gallery in futuro'])
|
||||
->addColumn('status', 'string', ['limit' => 20, 'null' => false, 'default' => 'draft', 'comment' => 'draft|published|cancelled'])
|
||||
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addIndex(['slug'], ['unique' => true, 'name' => 'events_slug_unique'])
|
||||
->addIndex(['status', 'starts_at'], ['name' => 'events_status_starts_at_index'])
|
||||
->addIndex(['user_id'], ['name' => 'events_user_id_index'])
|
||||
->addForeignKey('user_id', 'auth_users', 'id', [
|
||||
'delete' => 'SET_NULL',
|
||||
'update' => 'NO_ACTION',
|
||||
'constraint' => 'events_user_id_foreign',
|
||||
])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateEventBookingsTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('event_bookings', [
|
||||
'id' => false,
|
||||
'primary_key' => ['id'],
|
||||
'engine' => 'InnoDB',
|
||||
'encoding' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
|
||||
$table
|
||||
->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
|
||||
->addColumn('event_id', 'biginteger', ['signed' => false, 'null' => false])
|
||||
->addColumn('user_id', 'integer', ['signed' => false, 'null' => true, 'comment' => 'null = ospite non registrato'])
|
||||
->addColumn('name', 'string', ['limit' => 191, 'null' => false])
|
||||
->addColumn('email', 'string', ['limit' => 191, 'null' => false, 'comment' => 'raccolta contatti'])
|
||||
->addColumn('phone', 'string', ['limit' => 40, 'null' => true])
|
||||
->addColumn('seats', 'integer', ['signed' => false, 'null' => false, 'default' => 1])
|
||||
->addColumn('status', 'string', ['limit' => 20, 'null' => false, 'default' => 'pending', 'comment' => 'pending|confirmed|cancelled'])
|
||||
->addColumn('amount', 'decimal', ['precision' => 8, 'scale' => 2, 'null' => true, 'comment' => 'pronto per pagamenti'])
|
||||
->addColumn('token', 'string', ['limit' => 64, 'null' => false, 'comment' => 'gestione/annullo ospite via link email'])
|
||||
->addColumn('consent_at', 'datetime', ['null' => true, 'comment' => 'accettazione privacy/termini'])
|
||||
->addColumn('marketing_opt_in', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'comment' => 'consenso marketing Y/N'])
|
||||
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addIndex(['token'], ['unique' => true, 'name' => 'event_bookings_token_unique'])
|
||||
->addIndex(['event_id'], ['name' => 'event_bookings_event_id_index'])
|
||||
->addIndex(['email'], ['name' => 'event_bookings_email_index'])
|
||||
->addIndex(['user_id'], ['name' => 'event_bookings_user_id_index'])
|
||||
->addForeignKey('event_id', 'events', 'id', [
|
||||
'delete' => 'CASCADE',
|
||||
'update' => 'NO_ACTION',
|
||||
'constraint' => 'event_bookings_event_id_foreign',
|
||||
])
|
||||
->addForeignKey('user_id', 'auth_users', 'id', [
|
||||
'delete' => 'SET_NULL',
|
||||
'update' => 'NO_ACTION',
|
||||
'constraint' => 'event_bookings_user_id_foreign',
|
||||
])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddEventDefaultsToOptionTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->addColumn('event_default_color', 'string', ['limit' => 7, 'null' => true, 'default' => '#4CAF50', 'after' => 'colorcancelled'])
|
||||
->addColumn('event_default_max_per_booking', 'integer', ['null' => false, 'default' => 1, 'after' => 'event_default_color'])
|
||||
->addColumn('event_notify_email', 'string', ['limit' => 191, 'null' => true, 'after' => 'event_default_max_per_booking'])
|
||||
->addColumn('event_terms_text', 'text', ['null' => true, 'after' => 'event_notify_email'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateEventLocationsTable extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('event_locations', [
|
||||
'id' => false,
|
||||
'primary_key' => ['id'],
|
||||
'engine' => 'InnoDB',
|
||||
'encoding' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
|
||||
$table
|
||||
->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
|
||||
->addColumn('name', 'string', ['limit' => 191, 'null' => false, 'comment' => 'etichetta sede, es. "Studio YogaSoul"'])
|
||||
->addColumn('address', 'string', ['limit' => 255, 'null' => false])
|
||||
->addColumn('lat', 'decimal', ['precision' => 10, 'scale' => 7, 'null' => true])
|
||||
->addColumn('lng', 'decimal', ['precision' => 10, 'scale' => 7, 'null' => true])
|
||||
->addColumn('is_default', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'comment' => 'Y = preselezionata'])
|
||||
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null])
|
||||
->addIndex(['is_default'], ['name' => 'event_locations_is_default_index'])
|
||||
->create();
|
||||
|
||||
// Sede di default (MODIFICA con indirizzo e coordinate reali della scuola)
|
||||
$this->table('event_locations')->insert([
|
||||
'name' => 'Studio YogaSoul',
|
||||
'address' => 'Indirizzo da compilare',
|
||||
'lat' => null,
|
||||
'lng' => null,
|
||||
'is_default' => 'Y',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
])->saveData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddDetailsAndGalleryToEvents extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('events')
|
||||
->addColumn('instructions', 'text', ['null' => true, 'after' => 'description'])
|
||||
->addColumn('what_to_bring', 'text', ['null' => true, 'after' => 'instructions'])
|
||||
->addColumn('suitable_for', 'text', ['null' => true, 'after' => 'what_to_bring'])
|
||||
->addColumn('gallery', 'text', ['null' => true, 'comment' => 'JSON: array di path immagini', 'after' => 'suitable_for'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddWaitlistToEvents extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('events')
|
||||
->addColumn('waitlist_enabled', 'char', [
|
||||
'limit' => 1,
|
||||
'null' => false,
|
||||
'default' => 'Y',
|
||||
'comment' => "Y = lista d'attesa quando pieno",
|
||||
'after' => 'max_per_booking'
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddEventEmailOptions extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->addColumn('event_cc_booking', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'after' => 'event_terms_text'])
|
||||
->addColumn('event_cc_promotion', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'after' => 'event_cc_booking'])
|
||||
->addColumn('event_cc_user_cancel', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'after' => 'event_cc_promotion'])
|
||||
->addColumn('event_cc_event_cancel', 'char', ['limit' => 1, 'null' => false, 'default' => 'Y', 'after' => 'event_cc_user_cancel'])
|
||||
->addColumn('event_cc_reminder', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'after' => 'event_cc_event_cancel'])
|
||||
->addColumn('event_reminder_enabled', 'char', ['limit' => 1, 'null' => false, 'default' => 'N', 'after' => 'event_cc_reminder'])
|
||||
->addColumn('event_reminder_hours', 'integer', ['null' => false, 'default' => 24, 'after' => 'event_reminder_enabled'])
|
||||
->update();
|
||||
|
||||
$this->table('event_bookings')
|
||||
->addColumn('offered_at', 'datetime', ['null' => true, 'comment' => 'promozione lista attesa in attesa di riconferma', 'after' => 'consent_at'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddReminderAndOfferSettings extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
// Default generali (scuola)
|
||||
$this->table('option')
|
||||
->addColumn('event_offer_expiry_hours', 'integer', ['null' => false, 'default' => 24, 'after' => 'event_reminder_hours'])
|
||||
->update();
|
||||
|
||||
// Override per singolo evento (NULL = usa il default scuola)
|
||||
$this->table('events')
|
||||
->addColumn('reminder_enabled', 'char', ['limit' => 1, 'null' => true, 'default' => null, 'comment' => 'NULL=default scuola; Y/N override', 'after' => 'waitlist_enabled'])
|
||||
->addColumn('reminder_hours', 'integer', ['null' => true, 'default' => null, 'comment' => 'NULL=default scuola', 'after' => 'reminder_enabled'])
|
||||
->addColumn('offer_expiry_hours', 'integer', ['null' => true, 'default' => null, 'comment' => 'NULL=default scuola', 'after' => 'reminder_hours'])
|
||||
->addColumn('reminder_sent_at', 'datetime', ['null' => true, 'after' => 'offer_expiry_hours'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddPublishAtToEvents extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('events')
|
||||
->addColumn('publish_at', 'datetime', ['null' => true, 'comment' => 'apertura prenotazioni programmata', 'after' => 'status'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class ExtendEmployeeForTeachers extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('employee')
|
||||
->addColumn('email', 'string', ['limit' => 150, 'null' => true, 'after' => 'surnameemployee'])
|
||||
->addColumn('phone', 'string', ['limit' => 40, 'null' => true, 'after' => 'email'])
|
||||
->addColumn('photo', 'string', ['limit' => 191, 'null' => true, 'after' => 'phone'])
|
||||
->addColumn('is_owner', 'char', [
|
||||
'limit' => 1,
|
||||
'null' => false,
|
||||
'default' => 'N',
|
||||
'comment' => 'Y = titolare della scuola (uno solo)',
|
||||
'after' => 'photo',
|
||||
])
|
||||
->addColumn('notifications', 'char', [
|
||||
'limit' => 1,
|
||||
'null' => false,
|
||||
'default' => 'Y',
|
||||
'comment' => 'Y = riceve le email',
|
||||
'after' => 'is_owner',
|
||||
])
|
||||
->addColumn('active', 'char', ['limit' => 1, 'null' => false, 'default' => 'Y', 'after' => 'notifications'])
|
||||
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null, 'update' => 'CURRENT_TIMESTAMP'])
|
||||
->update();
|
||||
|
||||
// L'insegnante già presente (id più basso) diventa il titolare
|
||||
$this->execute("UPDATE `employee` SET `is_owner` = 'Y' ORDER BY `idemployee` LIMIT 1");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('employee')
|
||||
->removeColumn('email')
|
||||
->removeColumn('phone')
|
||||
->removeColumn('photo')
|
||||
->removeColumn('is_owner')
|
||||
->removeColumn('notifications')
|
||||
->removeColumn('active')
|
||||
->removeColumn('created_at')
|
||||
->removeColumn('updated_at')
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddShowTeacherToOption extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->addColumn('show_teacher', 'char', [
|
||||
'limit' => 1,
|
||||
'null' => false,
|
||||
'default' => 'N',
|
||||
'comment' => 'Y = mostra l\'insegnante nelle card delle classi',
|
||||
'after' => 'waitinglist_enabled',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateSchoolsTable extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('schools', [
|
||||
'id' => false,
|
||||
'primary_key' => ['idschool'],
|
||||
'engine' => 'InnoDB',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
])
|
||||
->addColumn('idschool', 'integer', ['signed' => false, 'identity' => true])
|
||||
->addColumn('name', 'string', ['limit' => 191])
|
||||
->addColumn('slug', 'string', ['limit' => 100, 'comment' => 'usato per URL/sottodominio/widget'])
|
||||
->addColumn('email', 'string', ['limit' => 191, 'null' => true])
|
||||
->addColumn('phone', 'string', ['limit' => 40, 'null' => true])
|
||||
->addColumn('address', 'string', ['limit' => 255, 'null' => true])
|
||||
->addColumn('city', 'string', ['limit' => 100, 'null' => true])
|
||||
->addColumn('postcode', 'string', ['limit' => 20, 'null' => true])
|
||||
->addColumn('country', 'string', ['limit' => 2, 'default' => 'IT'])
|
||||
->addColumn('vat_number', 'string', ['limit' => 30, 'null' => true])
|
||||
->addColumn('logo', 'string', ['limit' => 191, 'null' => true])
|
||||
->addColumn('timezone', 'string', ['limit' => 50, 'default' => 'Europe/Rome'])
|
||||
->addColumn('locale', 'string', ['limit' => 5, 'default' => 'it'])
|
||||
->addColumn('currency', 'string', ['limit' => 3, 'default' => 'EUR'])
|
||||
->addColumn('status', 'string', ['limit' => 20, 'default' => 'active', 'comment' => 'active|suspended|trial'])
|
||||
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null, 'update' => 'CURRENT_TIMESTAMP'])
|
||||
->addIndex(['slug'], ['unique' => true, 'name' => 'schools_slug_unique'])
|
||||
->addIndex(['status'], ['name' => 'schools_status_index'])
|
||||
->create();
|
||||
|
||||
// Seregno = scuola 1 (dati da completare poi dal pannello)
|
||||
$this->table('schools')->insert([
|
||||
'idschool' => 1,
|
||||
'name' => 'Scuola Yoga Seregno',
|
||||
'slug' => 'seregno',
|
||||
'city' => 'Seregno',
|
||||
'country' => 'IT',
|
||||
])->saveData();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('schools')->drop()->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateSchoolUserTable extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('school_user', [
|
||||
'id' => false,
|
||||
'primary_key' => ['idschooluser'],
|
||||
'engine' => 'InnoDB',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
])
|
||||
->addColumn('idschooluser', 'integer', ['signed' => false, 'identity' => true])
|
||||
->addColumn('user_id', 'integer', ['signed' => false, 'comment' => 'auth_users.id'])
|
||||
->addColumn('idschool', 'integer', ['signed' => false])
|
||||
->addColumn('school_role', 'string', ['limit' => 20, 'default' => 'student', 'comment' => 'student|teacher|admin|owner'])
|
||||
->addColumn('active', 'char', ['limit' => 1, 'default' => 'Y'])
|
||||
->addColumn('joined_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addIndex(['user_id', 'idschool'], ['unique' => true, 'name' => 'school_user_unique'])
|
||||
->addIndex(['idschool'], ['name' => 'school_user_idschool_index'])
|
||||
->addForeignKey('user_id', 'auth_users', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->addForeignKey('idschool', 'schools', 'idschool', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->create();
|
||||
|
||||
// Tutti gli utenti esistenti appartengono a Seregno (1)
|
||||
$this->execute("
|
||||
INSERT INTO school_user (user_id, idschool, school_role)
|
||||
SELECT id, 1, 'student' FROM auth_users
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('school_user')->drop()->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddIdschoolToDomainTables extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Tabelle applicative che diventano "per scuola".
|
||||
* Le tabelle auth_* (Vanguard) restano globali: il legame
|
||||
* utente-scuola passa da school_user.
|
||||
*/
|
||||
private const TABLES = [
|
||||
'associateclass',
|
||||
'bookingclass',
|
||||
'certificateuserprofile',
|
||||
'copyorder',
|
||||
'copyorder_items',
|
||||
'dayoff',
|
||||
'employee',
|
||||
'events',
|
||||
'event_bookings',
|
||||
'event_locations',
|
||||
'expiryparameter',
|
||||
'location',
|
||||
'option',
|
||||
'orderbook',
|
||||
'propagation',
|
||||
'service',
|
||||
'servicecategory',
|
||||
'serviceschedule',
|
||||
'userprofile',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
foreach (self::TABLES as $name) {
|
||||
$this->table($name)
|
||||
->addColumn('idschool', 'integer', [
|
||||
'signed' => false,
|
||||
'null' => false,
|
||||
'default' => 1, // TEMPORANEO: verrà rimosso quando tutte le INSERT passeranno idschool
|
||||
])
|
||||
->addIndex(['idschool'], ['name' => "idx_{$name}_idschool"])
|
||||
->update();
|
||||
}
|
||||
|
||||
// Slug eventi: unico per scuola, non più globale
|
||||
$this->table('events')
|
||||
->removeIndexByName('events_slug_unique')
|
||||
->addIndex(['idschool', 'slug'], ['unique' => true, 'name' => 'events_school_slug_unique'])
|
||||
->update();
|
||||
|
||||
// Impostazioni: una sola riga per scuola
|
||||
$this->table('option')
|
||||
->addIndex(['idschool'], ['unique' => true, 'name' => 'option_idschool_unique'])
|
||||
->update();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->removeIndexByName('option_idschool_unique')
|
||||
->update();
|
||||
|
||||
$this->table('events')
|
||||
->removeIndexByName('events_school_slug_unique')
|
||||
->addIndex(['slug'], ['unique' => true, 'name' => 'events_slug_unique'])
|
||||
->update();
|
||||
|
||||
foreach (self::TABLES as $name) {
|
||||
$this->table($name)
|
||||
->removeIndexByName("idx_{$name}_idschool")
|
||||
->removeColumn('idschool')
|
||||
->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddIdschoolToDomainTables extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Tabelle applicative che diventano "per scuola".
|
||||
* Le tabelle auth_* (Vanguard) restano globali: il legame
|
||||
* utente-scuola passa da school_user.
|
||||
*/
|
||||
private const TABLES = [
|
||||
'associateclass',
|
||||
'bookingclass',
|
||||
'certificateuserprofile',
|
||||
'copyorder',
|
||||
'copyorder_items',
|
||||
'dayoff',
|
||||
'employee',
|
||||
'events',
|
||||
'event_bookings',
|
||||
'event_locations',
|
||||
'expiryparameter',
|
||||
'location',
|
||||
'option',
|
||||
'orderbook',
|
||||
'propagation',
|
||||
'service',
|
||||
'servicecategory',
|
||||
'serviceschedule',
|
||||
'userprofile',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
foreach (self::TABLES as $name) {
|
||||
$this->table($name)
|
||||
->addColumn('idschool', 'integer', [
|
||||
'signed' => false,
|
||||
'null' => false,
|
||||
'default' => 1, // TEMPORANEO: verrà rimosso quando tutte le INSERT passeranno idschool
|
||||
])
|
||||
->addIndex(['idschool'], ['name' => "idx_{$name}_idschool"])
|
||||
->update();
|
||||
}
|
||||
|
||||
// Slug eventi: unico per scuola, non più globale
|
||||
$this->table('events')
|
||||
->removeIndexByName('events_slug_unique')
|
||||
->addIndex(['idschool', 'slug'], ['unique' => true, 'name' => 'events_school_slug_unique'])
|
||||
->update();
|
||||
|
||||
// Impostazioni: una sola riga per scuola
|
||||
$this->table('option')
|
||||
->addIndex(['idschool'], ['unique' => true, 'name' => 'option_idschool_unique'])
|
||||
->update();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('option')
|
||||
->removeIndexByName('option_idschool_unique')
|
||||
->update();
|
||||
|
||||
$this->table('events')
|
||||
->removeIndexByName('events_school_slug_unique')
|
||||
->addIndex(['slug'], ['unique' => true, 'name' => 'events_slug_unique'])
|
||||
->update();
|
||||
|
||||
foreach (self::TABLES as $name) {
|
||||
$this->table($name)
|
||||
->removeIndexByName("idx_{$name}_idschool")
|
||||
->removeColumn('idschool')
|
||||
->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateSchoolUserTable extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('school_user', [
|
||||
'id' => false,
|
||||
'primary_key' => ['idschooluser'],
|
||||
'engine' => 'InnoDB',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
])
|
||||
->addColumn('idschooluser', 'integer', ['signed' => false, 'identity' => true])
|
||||
->addColumn('user_id', 'integer', ['signed' => false, 'comment' => 'auth_users.id'])
|
||||
->addColumn('idschool', 'integer', ['signed' => false])
|
||||
->addColumn('school_role', 'string', ['limit' => 20, 'default' => 'student', 'comment' => 'student|teacher|admin|owner'])
|
||||
->addColumn('active', 'char', ['limit' => 1, 'default' => 'Y'])
|
||||
->addColumn('joined_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addIndex(['user_id', 'idschool'], ['unique' => true, 'name' => 'school_user_unique'])
|
||||
->addIndex(['idschool'], ['name' => 'school_user_idschool_index'])
|
||||
->addForeignKey('user_id', 'auth_users', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->addForeignKey('idschool', 'schools', 'idschool', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->create();
|
||||
|
||||
// Tutti gli utenti esistenti appartengono a Seregno (1)
|
||||
$this->execute("
|
||||
INSERT INTO school_user (user_id, idschool, school_role)
|
||||
SELECT id, 1, 'student' FROM auth_users
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('school_user')->drop()->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateSchoolsTable extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->table('schools', [
|
||||
'id' => false,
|
||||
'primary_key' => ['idschool'],
|
||||
'engine' => 'InnoDB',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
])
|
||||
->addColumn('idschool', 'integer', ['signed' => false, 'identity' => true])
|
||||
->addColumn('name', 'string', ['limit' => 191])
|
||||
->addColumn('slug', 'string', ['limit' => 100, 'comment' => 'usato per URL/sottodominio/widget'])
|
||||
->addColumn('email', 'string', ['limit' => 191, 'null' => true])
|
||||
->addColumn('phone', 'string', ['limit' => 40, 'null' => true])
|
||||
->addColumn('address', 'string', ['limit' => 255, 'null' => true])
|
||||
->addColumn('city', 'string', ['limit' => 100, 'null' => true])
|
||||
->addColumn('postcode', 'string', ['limit' => 20, 'null' => true])
|
||||
->addColumn('country', 'string', ['limit' => 2, 'default' => 'IT'])
|
||||
->addColumn('vat_number', 'string', ['limit' => 30, 'null' => true])
|
||||
->addColumn('logo', 'string', ['limit' => 191, 'null' => true])
|
||||
->addColumn('timezone', 'string', ['limit' => 50, 'default' => 'Europe/Rome'])
|
||||
->addColumn('locale', 'string', ['limit' => 5, 'default' => 'it'])
|
||||
->addColumn('currency', 'string', ['limit' => 3, 'default' => 'EUR'])
|
||||
->addColumn('status', 'string', ['limit' => 20, 'default' => 'active', 'comment' => 'active|suspended|trial'])
|
||||
->addColumn('created_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'default' => null, 'update' => 'CURRENT_TIMESTAMP'])
|
||||
->addIndex(['slug'], ['unique' => true, 'name' => 'schools_slug_unique'])
|
||||
->addIndex(['status'], ['name' => 'schools_status_index'])
|
||||
->create();
|
||||
|
||||
// Seregno = scuola 1 (dati da completare poi dal pannello)
|
||||
$this->table('schools')->insert([
|
||||
'idschool' => 1,
|
||||
'name' => 'Scuola Yoga Seregno',
|
||||
'slug' => 'seregno',
|
||||
'city' => 'Seregno',
|
||||
'country' => 'IT',
|
||||
])->saveData();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->table('schools')->drop()->save();
|
||||
}
|
||||
}
|
||||
+173
-6
@@ -11,9 +11,56 @@ function e($value): string
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function teacherInitials(string $first, string $last): string
|
||||
{
|
||||
$ini = mb_strtoupper(mb_substr(trim($first), 0, 1) . mb_substr(trim($last), 0, 1));
|
||||
return $ini !== '' ? $ini : '?';
|
||||
}
|
||||
|
||||
function teacherColor(string $seed): string
|
||||
{
|
||||
$palette = ['#1ebf73', '#2980b9', '#8e44ad', '#e67e22', '#16a085', '#c0392b', '#2c3e50', '#b08d57'];
|
||||
return $palette[abs(crc32($seed)) % count($palette)];
|
||||
}
|
||||
|
||||
$message = $_GET['message'] ?? 'N';
|
||||
$insertFeedback = null;
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Insegnanti (titolare per primo, poi attivi, poi alfabetico)
|
||||
* ---------------------------------------------------------------------- */
|
||||
$teachers = $pdo->query(
|
||||
"SELECT idemployee, nameemployee, surnameemployee, photo, is_owner, active
|
||||
FROM employee
|
||||
ORDER BY is_owner DESC, active DESC, surnameemployee, nameemployee"
|
||||
)->fetchAll();
|
||||
|
||||
$teachersById = [];
|
||||
$ownerId = 0;
|
||||
foreach ($teachers as $t) {
|
||||
$teachersById[(int) $t['idemployee']] = $t;
|
||||
if ($t['is_owner'] === 'Y' && !$ownerId) {
|
||||
$ownerId = (int) $t['idemployee'];
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Assegnazione insegnante a una classe esistente (POST)
|
||||
* ---------------------------------------------------------------------- */
|
||||
if (($_POST['action'] ?? '') === 'assignteacher') {
|
||||
$idservice = (int) ($_POST['idservice'] ?? 0);
|
||||
$idteacher = (int) ($_POST['idteacher'] ?? 0);
|
||||
|
||||
if ($idservice > 0 && isset($teachersById[$idteacher])) {
|
||||
$stmt = $pdo->prepare("UPDATE service SET idteacher = :t WHERE idservice = :s");
|
||||
$stmt->execute([':t' => $idteacher, ':s' => $idservice]);
|
||||
header('Location: ' . $_SERVER['PHP_SELF'] . '?message=teacherassigned');
|
||||
} else {
|
||||
header('Location: ' . $_SERVER['PHP_SELF'] . '?message=teachererror');
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Inserimento nuova classe (POST) con prepared statement
|
||||
* ---------------------------------------------------------------------- */
|
||||
@@ -26,14 +73,17 @@ if (isset($_POST['classinsert']) && $_POST['classinsert'] === 'Y') {
|
||||
$maxcapacity = $_POST['maxcapacity'] ?? '';
|
||||
$colorclass = trim($_POST['colorclass'] ?? '');
|
||||
$classduration = $_POST['classduration'] ?? '';
|
||||
$idteacher = (int) ($_POST['idteacher'] ?? 0);
|
||||
|
||||
if ($servicename === '') {
|
||||
$insertFeedback = ['type' => 'error', 'text' => 'La descrizione della classe è obbligatoria.'];
|
||||
} elseif (!isset($teachersById[$idteacher])) {
|
||||
$insertFeedback = ['type' => 'error', 'text' => 'Seleziona l\'insegnante della classe.'];
|
||||
} else {
|
||||
$sql = "INSERT INTO service
|
||||
(servicename, wpcatalognumber, day, time, category, maxcapacity, colorclass, classduration)
|
||||
(servicename, wpcatalognumber, day, time, category, maxcapacity, colorclass, classduration, idteacher)
|
||||
VALUES
|
||||
(:servicename, :wpcatalog, :day, :time, :category, :maxcapacity, :colorclass, :classduration)";
|
||||
(:servicename, :wpcatalog, :day, :time, :category, :maxcapacity, :colorclass, :classduration, :idteacher)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$ok = $stmt->execute([
|
||||
':servicename' => $servicename,
|
||||
@@ -44,6 +94,7 @@ if (isset($_POST['classinsert']) && $_POST['classinsert'] === 'Y') {
|
||||
':maxcapacity' => $maxcapacity !== '' ? (int) $maxcapacity : null,
|
||||
':colorclass' => $colorclass,
|
||||
':classduration' => $classduration !== '' ? (float) $classduration : null,
|
||||
':idteacher' => $idteacher,
|
||||
]);
|
||||
|
||||
if ($ok) {
|
||||
@@ -83,6 +134,32 @@ $propRows = $pdo->query(
|
||||
foreach ($propRows as $pr) {
|
||||
$propagationsByService[$pr['idservice']][] = $pr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opzioni del select insegnanti.
|
||||
* Gli insegnanti non attivi compaiono solo se già assegnati a quella classe.
|
||||
*/
|
||||
function teacherOptions(array $teachers, int $selectedId): string
|
||||
{
|
||||
$html = '';
|
||||
$found = false;
|
||||
foreach ($teachers as $t) {
|
||||
$tid = (int) $t['idemployee'];
|
||||
$isSel = $tid === $selectedId;
|
||||
if ($t['active'] !== 'Y' && !$isSel) {
|
||||
continue;
|
||||
}
|
||||
if ($isSel) $found = true;
|
||||
$label = trim($t['nameemployee'] . ' ' . $t['surnameemployee'])
|
||||
. ($t['is_owner'] === 'Y' ? ' (titolare)' : '')
|
||||
. ($t['active'] !== 'Y' ? ' (non attivo)' : '');
|
||||
$html .= '<option value="' . $tid . '"' . ($isSel ? ' selected' : '') . '>' . e($label) . '</option>';
|
||||
}
|
||||
if (!$found) {
|
||||
$html = '<option value="" selected disabled>— Non assegnato —</option>' . $html;
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
@@ -197,6 +274,43 @@ foreach ($propRows as $pr) {
|
||||
.action-btns .btn {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.teacher-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 210px;
|
||||
}
|
||||
|
||||
.teacher-mini {
|
||||
flex: 0 0 30px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.teacher-mini.none {
|
||||
background: #f3f4f6;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.teacher-select {
|
||||
padding: 5px 30px 5px 10px !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.teacher-select.unassigned {
|
||||
border-color: #f0b27a;
|
||||
background-color: #fff8f1;
|
||||
color: #b9770e;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -238,6 +352,12 @@ foreach ($propRows as $pr) {
|
||||
<?php if ($message === 'inserted') : ?>
|
||||
<div class="alert alert-success" role="alert">Classe inserita con successo.</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($message === 'teacherassigned') : ?>
|
||||
<div class="alert alert-success" role="alert">Insegnante assegnato con successo.</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($message === 'teachererror') : ?>
|
||||
<div class="alert alert-danger" role="alert">Impossibile assegnare l'insegnante. Riprova.</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($message === 'success') : ?>
|
||||
<div class="alert alert-success" role="alert">Propagazione avvenuta con successo.</div>
|
||||
<?php endif; ?>
|
||||
@@ -261,6 +381,11 @@ foreach ($propRows as $pr) {
|
||||
<?php echo e($insertFeedback['text']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($teachers)) : ?>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Non ci sono insegnanti: <a href="insegnanti.php">aggiungine almeno uno</a> prima di creare una classe.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Form inserimento classe -->
|
||||
<div class="row">
|
||||
@@ -343,6 +468,14 @@ foreach ($propRows as $pr) {
|
||||
<input type="text" class="form-control" id="colorclass" name="colorclass" value="#1ebf73">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="mb-3">
|
||||
<label for="idteacher" class="form-label">Insegnante</label>
|
||||
<select name="idteacher" id="idteacher" class="form-select" required>
|
||||
<?php echo teacherOptions($teachers, $ownerId); ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-save">Inserisci classe</button>
|
||||
@@ -358,7 +491,7 @@ foreach ($propRows as $pr) {
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Classi esistenti</div>
|
||||
<p class="section-lead">Propaga una classe sul calendario, associala o modificane i dettagli.</p>
|
||||
<p class="section-lead">Propaga una classe sul calendario, associala, assegna l'insegnante o modificane i dettagli.</p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table class-table align-middle mb-0">
|
||||
@@ -369,16 +502,20 @@ foreach ($propRows as $pr) {
|
||||
<th>Orario</th>
|
||||
<th>Durata</th>
|
||||
<th>Categoria</th>
|
||||
<th>Insegnante</th>
|
||||
<th class="text-end">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($services)) : ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">Nessuna classe presente.</td>
|
||||
<td colspan="7" class="text-center text-muted py-4">Nessuna classe presente.</td>
|
||||
</tr>
|
||||
<?php else : ?>
|
||||
<?php foreach ($services as $svc) : ?>
|
||||
<?php foreach ($services as $svc) :
|
||||
$svcTeacherId = (int) $svc['idteacher'];
|
||||
$svcTeacher = $teachersById[$svcTeacherId] ?? null;
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="color-dot" style="background-color: <?php echo e($svc['colorclass'] ?: '#ccc'); ?>;"></span>
|
||||
@@ -391,6 +528,25 @@ foreach ($propRows as $pr) {
|
||||
<?php echo e($svc['classduration']); ?>
|
||||
</td>
|
||||
<td><?php echo e($svc['namecategory']); ?></td>
|
||||
<td>
|
||||
<form method="post" class="teacher-cell">
|
||||
<input type="hidden" name="action" value="assignteacher">
|
||||
<input type="hidden" name="idservice" value="<?php echo (int) $svc['idservice']; ?>">
|
||||
<?php if ($svcTeacher && $svcTeacher['photo']) : ?>
|
||||
<img class="teacher-mini" src="assets/images/teachers/<?php echo e($svcTeacher['photo']); ?>" alt="">
|
||||
<?php elseif ($svcTeacher) :
|
||||
$tName = trim($svcTeacher['nameemployee'] . ' ' . $svcTeacher['surnameemployee']); ?>
|
||||
<span class="teacher-mini" style="background:<?php echo teacherColor($tName); ?>;"><?php echo e(teacherInitials($svcTeacher['nameemployee'], $svcTeacher['surnameemployee'])); ?></span>
|
||||
<?php else : ?>
|
||||
<span class="teacher-mini none"><i class="fas fa-user"></i></span>
|
||||
<?php endif; ?>
|
||||
<select name="idteacher" class="form-select teacher-select <?php echo $svcTeacher ? '' : 'unassigned'; ?>"
|
||||
data-current="<?php echo $svcTeacherId; ?>"
|
||||
data-classname="<?php echo e($svc['servicename'] . ' ' . $svc['day'] . ' ' . $svc['time']); ?>">
|
||||
<?php echo teacherOptions($teachers, $svcTeacherId); ?>
|
||||
</select>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-end action-btns">
|
||||
<button type="button" class="btn btn-primary btn-sm toggle-form">Propaga</button>
|
||||
<a href="associate-services.php?id=<?php echo (int) $svc['idservice']; ?>">
|
||||
@@ -407,7 +563,7 @@ foreach ($propRows as $pr) {
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="expanded-row">
|
||||
<td colspan="6">
|
||||
<td colspan="7">
|
||||
<div class="expanded-content">
|
||||
<form class="calendar-form" method="post" action="nextdateclass.php">
|
||||
<div class="row">
|
||||
@@ -514,6 +670,17 @@ foreach ($propRows as $pr) {
|
||||
$('#colorclass').val(col.toHexString());
|
||||
}
|
||||
});
|
||||
|
||||
// Cambio insegnante dalla tabella: conferma e salva
|
||||
$('.teacher-select').on('change', function() {
|
||||
var sel = $(this);
|
||||
var teacherName = sel.find('option:selected').text();
|
||||
if (confirm('Assegnare ' + teacherName + ' alla classe ' + sel.data('classname') + '?')) {
|
||||
sel.closest('form')[0].submit();
|
||||
} else {
|
||||
sel.val(String(sel.data('current')) === '0' ? '' : String(sel.data('current')));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Espansione riga propagazione
|
||||
|
||||
+101
-7
@@ -28,8 +28,16 @@ function avatarColor(string $seed): string
|
||||
return $palette[$idx];
|
||||
}
|
||||
|
||||
$optRow = $pdo->query("SELECT maxbeforetimecancell FROM `option` LIMIT 1")->fetch();
|
||||
$optRow = $pdo->query("SELECT maxbeforetimecancell, show_teacher FROM `option` LIMIT 1")->fetch();
|
||||
$timetocancel = $optRow ? (int) $optRow['maxbeforetimecancell'] : 24;
|
||||
$showTeacher = ($optRow['show_teacher'] ?? 'N') === 'Y';
|
||||
|
||||
$teachersById = [];
|
||||
if ($showTeacher) {
|
||||
foreach ($pdo->query("SELECT idemployee, nameemployee, surnameemployee, photo FROM employee")->fetchAll() as $t) {
|
||||
$teachersById[(int) $t['idemployee']] = $t;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['prev_month'])) {
|
||||
$currentMonthStart = preg_replace('/[^0-9\-]/', '', $_GET['prev_month']) . '-01';
|
||||
@@ -44,7 +52,7 @@ if (strtotime($currentMonthStart) === false) {
|
||||
$currentMonthEnd = date("Y-m-t", strtotime($currentMonthStart));
|
||||
|
||||
$sqlSchedule = "SELECT serviceschedule.*, service.servicename, service.colorclass,
|
||||
service.maxcapacity, service.idservice
|
||||
service.maxcapacity, service.idservice, service.idteacher
|
||||
FROM serviceschedule
|
||||
LEFT JOIN service ON serviceschedule.idservice = service.idservice
|
||||
WHERE serviceschedule.dateschedule BETWEEN :start AND DATE_ADD(:end, INTERVAL 1 DAY)
|
||||
@@ -73,9 +81,9 @@ if (!empty($scheduleIds)) {
|
||||
auth_users.first_name, auth_users.last_name, auth_users.id AS iduser
|
||||
FROM bookingclass
|
||||
LEFT JOIN auth_users ON bookingclass.iduser = auth_users.id
|
||||
WHERE bookingclass.idserviceschedule IN ($ph)
|
||||
AND bookingclass.status IN ('booked', 'pending')
|
||||
ORDER BY auth_users.last_name, auth_users.first_name";
|
||||
WHERE bookingclass.idserviceschedule IN ($ph)
|
||||
AND bookingclass.status IN ('booked', 'pending', 'waiting')
|
||||
ORDER BY bookingclass.created_at, auth_users.last_name, auth_users.first_name";
|
||||
$stmtBookings = $pdo->prepare($sqlBookings);
|
||||
$stmtBookings->execute(array_values($scheduleIds));
|
||||
foreach ($stmtBookings->fetchAll() as $b) {
|
||||
@@ -198,8 +206,8 @@ $italianMonths = [
|
||||
|
||||
.class-date {
|
||||
flex: 0 0 110px;
|
||||
background: #1ebf73;
|
||||
color: #fff;
|
||||
background: #E1D0B8;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -231,6 +239,38 @@ $italianMonths = [
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.class-teacher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.class-teacher strong {
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.teacher-avatar-sm {
|
||||
flex: 0 0 26px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background: #E1D0B8;
|
||||
color: #5a4630;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.teacher-avatar-sm.none {
|
||||
background: #fdf0e3;
|
||||
color: #e67e22;
|
||||
}
|
||||
|
||||
.class-meta {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
@@ -575,8 +615,10 @@ $italianMonths = [
|
||||
// separa confermati (booked) da in attesa (pending)
|
||||
$bookings = array_filter($allRows, fn($b) => $b['status'] === 'booked');
|
||||
$pendings = array_filter($allRows, fn($b) => $b['status'] === 'pending');
|
||||
$waitings = array_filter($allRows, fn($b) => $b['status'] === 'waiting');
|
||||
$countPersons = count($bookings); // contatore = solo confermati
|
||||
$countPending = count($pendings);
|
||||
$countWaiting = count($waitings);
|
||||
$maxcap = (int) $sch['maxcapacity'];
|
||||
$isFull = $maxcap > 0 && $countPersons >= $maxcap;
|
||||
$colorclass = $sch['colorclass'] ?: '#e6f3ff';
|
||||
@@ -599,9 +641,31 @@ $italianMonths = [
|
||||
<?php echo $countPending; ?> in attesa
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($countWaiting > 0) : ?>
|
||||
<span class="capacity-pill" style="background:#fbeef3;color:#d1698a;">
|
||||
<i class="fas fa-list-ol" style="font-size:11px;"></i>
|
||||
<?php echo $countWaiting; ?> in coda
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</h3>
|
||||
<p class="class-meta"><i class="far fa-clock me-1"></i><?php echo e($whenFmt); ?></p>
|
||||
<p class="class-meta"><i class="fas fa-location-dot me-1"></i>via Valassina 62/B Seregno - Sala Contesto Yoga</p>
|
||||
<?php if ($showTeacher) :
|
||||
$tch = $teachersById[(int) ($sch['idteacher'] ?? 0)] ?? null; ?>
|
||||
<p class="class-meta class-teacher">
|
||||
<?php if ($tch) : ?>
|
||||
<?php if (!empty($tch['photo'])) : ?>
|
||||
<img class="teacher-avatar-sm" src="assets/images/teachers/<?php echo e($tch['photo']); ?>" alt="">
|
||||
<?php else : ?>
|
||||
<span class="teacher-avatar-sm"><?php echo e(initials($tch['nameemployee'], $tch['surnameemployee'])); ?></span>
|
||||
<?php endif; ?>
|
||||
Insegnante: <strong><?php echo e(trim($tch['nameemployee'] . ' ' . $tch['surnameemployee'])); ?></strong>
|
||||
<?php else : ?>
|
||||
<span class="teacher-avatar-sm none"><i class="fas fa-user" style="font-size:10px;"></i></span>
|
||||
<span style="color:#e67e22;font-weight:600;">Insegnante non assegnato</span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="class-toolbar">
|
||||
<button class="btn-soft btn-bookings" onclick="togglePanel(this)"><i class="fas fa-list-ul me-1"></i> Vedi prenotazioni</button>
|
||||
@@ -676,6 +740,36 @@ $italianMonths = [
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($countWaiting > 0) : ?>
|
||||
<div class="bookings-header" style="margin-top:18px;">
|
||||
<span class="title" style="color:#d1698a;"><i class="fas fa-list-ol me-1"></i> Lista d'attesa (<?php echo $countWaiting; ?>)</span>
|
||||
</div>
|
||||
<div class="booking-list">
|
||||
<?php $qpos = 1;
|
||||
foreach ($waitings as $b) :
|
||||
$idbc = (int) $b['idbookingclass'];
|
||||
$fullName = trim($b['first_name'] . ' ' . $b['last_name']);
|
||||
$ini = initials($b['first_name'] ?? '', $b['last_name'] ?? '');
|
||||
$avColor = avatarColor($fullName);
|
||||
?>
|
||||
<div class="booking-item" style="background:#fdf4f8;border-color:#f0d6e2;" data-iduser="<?php echo (int) $b['iduser']; ?>">
|
||||
<div class="avatar" style="background: <?php echo $avColor; ?>;"><?php echo e($ini); ?></div>
|
||||
<div class="booking-info">
|
||||
<span class="booking-name">
|
||||
<span style="display:inline-block;min-width:22px;font-weight:800;color:#d1698a;"><?php echo $qpos; ?>°</span>
|
||||
<?php echo e($fullName); ?>
|
||||
</span>
|
||||
<span class="status-chip" style="background:#fbeef3;color:#d1698a;">In coda</span>
|
||||
</div>
|
||||
<div class="booking-actions">
|
||||
<button class="mini-btn remove" onclick="handleRemoveClick(<?php echo $idbc; ?>)"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php $qpos++;
|
||||
endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="add-box">
|
||||
<strong class="d-block mb-2" style="font-size:14px;"><i class="fas fa-user-plus me-1" style="color:#1ebf73;"></i> Aggiungi partecipante</strong>
|
||||
<form class="add-booking-form" action="inserisci_record.php" method="post">
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/include/event_mailer.php';
|
||||
|
||||
/**
|
||||
* Endpoint pubblico PRENOTAZIONE (POST).
|
||||
* Riceve i dati dal widget WordPress e crea la prenotazione.
|
||||
*
|
||||
* Campi attesi (POST form-encoded o JSON):
|
||||
* event_id, name, surname, email, seats, consent (1), marketing (1|0)
|
||||
* website -> honeypot anti-spam (deve restare vuoto)
|
||||
*
|
||||
* Logica posti (conferma immediata):
|
||||
* - seats <= posti rimanenti -> status 'confirmed'
|
||||
* - evento pieno + lista d'attesa ON -> status 'waiting'
|
||||
* - evento pieno + lista d'attesa OFF -> errore sold_out
|
||||
* - 0 < rimanenti < seats -> errore 'partial' (avvisa: solo N posti)
|
||||
*
|
||||
* Anti-oversell: transazione + lock della riga evento (SELECT ... FOR UPDATE),
|
||||
* così due prenotazioni simultanee non sforano la capienza.
|
||||
*/
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
send_json(['ok' => false, 'code' => 'method', 'error' => 'Metodo non consentito.'], 405);
|
||||
}
|
||||
|
||||
// Input: form-encoded oppure JSON
|
||||
$in = $_POST;
|
||||
if (empty($in)) {
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw) {
|
||||
$j = json_decode($raw, true);
|
||||
if (is_array($j)) {
|
||||
$in = $j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Honeypot: se compilato, è un bot
|
||||
if (!empty($in['website'])) {
|
||||
send_json(['ok' => false, 'code' => 'invalid', 'error' => 'Richiesta non valida.'], 400);
|
||||
}
|
||||
|
||||
$eventId = (int) ($in['event_id'] ?? 0);
|
||||
$name = trim((string) ($in['name'] ?? ''));
|
||||
$surname = trim((string) ($in['surname'] ?? ''));
|
||||
$email = trim((string) ($in['email'] ?? ''));
|
||||
$seats = (int) ($in['seats'] ?? 0);
|
||||
$consent = !empty($in['consent']) && $in['consent'] !== '0';
|
||||
$marketing = (!empty($in['marketing']) && $in['marketing'] !== '0') ? 'Y' : 'N';
|
||||
|
||||
// Validazioni
|
||||
if ($eventId <= 0 || $name === '' || $surname === '') {
|
||||
send_json(['ok' => false, 'code' => 'invalid', 'error' => 'Compila nome, cognome ed evento.'], 400);
|
||||
}
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
send_json(['ok' => false, 'code' => 'invalid', 'error' => 'Email non valida.'], 400);
|
||||
}
|
||||
if (!$consent) {
|
||||
send_json(['ok' => false, 'code' => 'consent', 'error' => "Devi accettare l'informativa privacy."], 400);
|
||||
}
|
||||
if ($seats < 1) {
|
||||
$seats = 1;
|
||||
}
|
||||
|
||||
$pdo = pdo_conn();
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Lock della riga evento: serializza le prenotazioni dello stesso evento
|
||||
$es = $pdo->prepare("SELECT * FROM events WHERE id = ? FOR UPDATE");
|
||||
$es->execute([$eventId]);
|
||||
$event = $es->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$event || $event['status'] !== 'published') {
|
||||
$pdo->rollBack();
|
||||
send_json(['ok' => false, 'code' => 'not_available', 'error' => 'Evento non disponibile.'], 404);
|
||||
}
|
||||
|
||||
// Prenotazioni chiuse all'orario di inizio
|
||||
if (strtotime($event['starts_at']) <= time()) {
|
||||
$pdo->rollBack();
|
||||
send_json(['ok' => false, 'code' => 'closed', 'error' => 'Le prenotazioni per questo evento sono chiuse.'], 409);
|
||||
}
|
||||
|
||||
$maxSeats = (int) $event['max_seats'];
|
||||
$maxPerBooking = max(1, (int) $event['max_per_booking']);
|
||||
$waitlistOn = ($event['waitlist_enabled'] ?? 'N') === 'Y';
|
||||
|
||||
if ($seats > $maxPerBooking) {
|
||||
$pdo->rollBack();
|
||||
send_json([
|
||||
'ok' => false,
|
||||
'code' => 'too_many',
|
||||
'max' => $maxPerBooking,
|
||||
'error' => "Puoi prenotare al massimo {$maxPerBooking} posti per prenotazione."
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Posti occupati (solo confermate/pending): protetto dal lock evento
|
||||
$os = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings
|
||||
WHERE event_id = ? AND status IN ('confirmed','pending','offered')");
|
||||
$os->execute([$eventId]);
|
||||
$occupied = (int) $os->fetchColumn();
|
||||
$remaining = max(0, $maxSeats - $occupied);
|
||||
|
||||
// Decisione stato
|
||||
if ($seats <= $remaining) {
|
||||
$status = 'confirmed';
|
||||
} elseif ($remaining <= 0 && $waitlistOn) {
|
||||
$status = 'waiting';
|
||||
} elseif ($remaining <= 0 && !$waitlistOn) {
|
||||
$pdo->rollBack();
|
||||
send_json(['ok' => false, 'code' => 'sold_out', 'error' => 'Evento esaurito.'], 409);
|
||||
} else {
|
||||
// 0 < remaining < seats: avvisa, non prenota
|
||||
$pdo->rollBack();
|
||||
send_json([
|
||||
'ok' => false,
|
||||
'code' => 'partial',
|
||||
'remaining' => $remaining,
|
||||
'error' => "Sono rimasti solo {$remaining} posti. Riduci il numero di persone; per gli altri potrai iscriverti alla lista d'attesa.",
|
||||
], 409);
|
||||
}
|
||||
|
||||
// Token univoco per gestione/disdetta da link email
|
||||
$token = '';
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$candidate = bin2hex(random_bytes(16));
|
||||
$ck = $pdo->prepare("SELECT 1 FROM event_bookings WHERE token = ? LIMIT 1");
|
||||
$ck->execute([$candidate]);
|
||||
if (!$ck->fetchColumn()) {
|
||||
$token = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($token === '') {
|
||||
throw new RuntimeException('Impossibile generare il token.');
|
||||
}
|
||||
|
||||
$amount = round($seats * (float) $event['cost'], 2);
|
||||
$fullName = trim($name . ' ' . $surname);
|
||||
|
||||
$ins = $pdo->prepare("INSERT INTO event_bookings
|
||||
(event_id, user_id, name, email, phone, seats, status, amount, token,
|
||||
consent_at, marketing_opt_in, created_at, updated_at)
|
||||
VALUES
|
||||
(:event_id, NULL, :name, :email, NULL, :seats, :status, :amount, :token,
|
||||
NOW(), :marketing, NOW(), NOW())");
|
||||
$ins->execute([
|
||||
':event_id' => $eventId,
|
||||
':name' => $fullName,
|
||||
':email' => $email,
|
||||
':seats' => $seats,
|
||||
':status' => $status,
|
||||
':amount' => $amount,
|
||||
':token' => $token,
|
||||
':marketing' => $marketing,
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// Email di conferma / lista d'attesa (best-effort: non blocca la prenotazione)
|
||||
try {
|
||||
$bookingRow = ['email' => $email, 'name' => $fullName, 'seats' => $seats, 'status' => $status, 'token' => $token];
|
||||
yse_mail_booking($bookingRow, $event);
|
||||
} catch (Throwable $mailEx) {
|
||||
error_log('[book mail] ' . $mailEx->getMessage());
|
||||
}
|
||||
|
||||
$msg = $status === 'confirmed'
|
||||
? 'Prenotazione confermata! Ti abbiamo inviato una email di riepilogo.'
|
||||
: "Sei in lista d'attesa: ti avviseremo appena si libera un posto.";
|
||||
|
||||
send_json([
|
||||
'ok' => true,
|
||||
'status' => $status,
|
||||
'seats' => $seats,
|
||||
'amount' => $amount,
|
||||
'token' => $token,
|
||||
'message' => $msg,
|
||||
]);
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('[apiwp/book] ' . $ex->getMessage());
|
||||
send_json(['ok' => false, 'code' => 'server', 'error' => 'Errore durante la prenotazione. Riprova.'], 500);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Bootstrap per gli endpoint PUBBLICI del modulo Eventi (cartella /api).
|
||||
* NON include headscript.php: niente login/redirect. Serve solo il DB.
|
||||
*
|
||||
* Posiziona la cartella /api allo stesso livello di events.php, class/, include/.
|
||||
* Requisito: class/db-functions.php (DBHandlerSelect) già presente e autonomo.
|
||||
*/
|
||||
|
||||
// Path assoluto a DBHandlerSelect (api/ -> ../class/db-functions.php)
|
||||
require_once dirname(__DIR__) . '/class/db-functions.php';
|
||||
|
||||
// URL pubblico di yogibook: usato per rendere assoluti i path delle immagini.
|
||||
if (!defined('YB_PUBLIC_BASE')) {
|
||||
define('YB_PUBLIC_BASE', 'https://yogibook.yogasoul.it/');
|
||||
}
|
||||
|
||||
// Domini WordPress autorizzati (per la prenotazione via browser).
|
||||
$YB_ALLOWED_ORIGINS = ['https://yogasoul.it', 'https://www.yogasoul.it'];
|
||||
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
if ($origin !== '' && in_array($origin, $YB_ALLOWED_ORIGINS, true)) {
|
||||
header('Access-Control-Allow-Origin: ' . $origin);
|
||||
header('Vary: Origin');
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
}
|
||||
|
||||
// Preflight CORS
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit();
|
||||
}
|
||||
|
||||
function pdo_conn(): PDO
|
||||
{
|
||||
return DBHandlerSelect::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
/** Rende assoluto un path immagine relativo (es. assets/uploads/events/x.jpg). */
|
||||
function public_url(?string $rel): ?string
|
||||
{
|
||||
if ($rel === null || $rel === '') {
|
||||
return null;
|
||||
}
|
||||
if (preg_match('#^https?://#i', $rel)) {
|
||||
return $rel;
|
||||
}
|
||||
return YB_PUBLIC_BASE . ltrim($rel, '/');
|
||||
}
|
||||
|
||||
/** Risponde in JSON e termina. */
|
||||
function send_json($data, int $code = 200): void
|
||||
{
|
||||
if (function_exists('ob_get_level') && ob_get_level()) {
|
||||
ob_clean();
|
||||
}
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit();
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/include/event_waitlist.php';
|
||||
require_once dirname(__DIR__) . '/include/event_mailer.php';
|
||||
|
||||
/**
|
||||
* Disdetta pubblica di una prenotazione tramite token (link email, senza login).
|
||||
*
|
||||
* GET apiwp/cancel.php?token=... -> mostra pagina di conferma (NON annulla:
|
||||
* evita che gli scanner email prefetchino il link e annullino da soli)
|
||||
* POST (token + confirm=1) -> annulla e, se la prenotazione occupava posti,
|
||||
* promuove automaticamente la prima in lista d'attesa.
|
||||
*/
|
||||
$pdo = pdo_conn();
|
||||
$token = trim($_POST['token'] ?? $_GET['token'] ?? '');
|
||||
$isPost = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST';
|
||||
|
||||
$booking = null;
|
||||
if ($token !== '') {
|
||||
$q = $pdo->prepare("SELECT b.*, e.title AS ev_title, e.starts_at AS ev_starts, e.id AS eid
|
||||
FROM event_bookings b JOIN events e ON e.id = b.event_id
|
||||
WHERE b.token = ?");
|
||||
$q->execute([$token]);
|
||||
$booking = $q->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$state = 'form'; // form | notfound | already | toolate | done
|
||||
$promotedCount = 0;
|
||||
$didCancel = false;
|
||||
$promoted = [];
|
||||
|
||||
if (!$booking) {
|
||||
$state = 'notfound';
|
||||
} elseif ($booking['status'] === 'cancelled') {
|
||||
$state = 'already';
|
||||
} elseif (strtotime($booking['ev_starts']) <= time()) {
|
||||
$state = 'toolate';
|
||||
} elseif ($isPost && !empty($_POST['confirm'])) {
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
// Lock evento per serializzare con eventuali prenotazioni concorrenti
|
||||
$pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([(int) $booking['eid']]);
|
||||
|
||||
// Rileggo lo stato aggiornato della prenotazione
|
||||
$bl = $pdo->prepare("SELECT * FROM event_bookings WHERE id = ?");
|
||||
$bl->execute([(int) $booking['id']]);
|
||||
$fresh = $bl->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($fresh && $fresh['status'] !== 'cancelled') {
|
||||
$wasOccupying = in_array($fresh['status'], ['confirmed', 'pending', 'offered'], true);
|
||||
$pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ?")
|
||||
->execute([(int) $fresh['id']]);
|
||||
|
||||
if ($wasOccupying) {
|
||||
$promoted = promoteFromWaitlist($pdo, (int) $booking['eid']);
|
||||
$promotedCount = count($promoted);
|
||||
}
|
||||
$didCancel = true;
|
||||
}
|
||||
$pdo->commit();
|
||||
$state = 'done';
|
||||
|
||||
// Email dopo il commit (best-effort)
|
||||
if ($didCancel) {
|
||||
try {
|
||||
$evStmt = pdo_conn()->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$evStmt->execute([(int) $booking['eid']]);
|
||||
$evRow = $evStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($evRow) {
|
||||
yse_mail_user_cancel($booking, $evRow); // B: conferma disdetta
|
||||
foreach ($promoted as $pr) { // E: promozione con riconferma
|
||||
try {
|
||||
yse_mail_promotion($pr, $evRow);
|
||||
} catch (Throwable $pe) {
|
||||
error_log('[promo mail] ' . $pe->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $mailEx) {
|
||||
error_log('[cancel mail] ' . $mailEx->getMessage());
|
||||
}
|
||||
}
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('[apiwp/cancel] ' . $ex->getMessage());
|
||||
$state = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('e')) {
|
||||
function e($v): string
|
||||
{
|
||||
return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Disdetta prenotazione — YogaSoul</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--accent: #b07aa1;
|
||||
--ink: #2b2622;
|
||||
--muted: #8c837c;
|
||||
--line: #ece7e2;
|
||||
--paper: #faf8f6;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 14px 40px rgba(43, 38, 34, .10);
|
||||
max-width: 460px;
|
||||
width: 100%;
|
||||
padding: 34px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icn {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.icn.warn {
|
||||
background: #fdecea;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.icn.ok {
|
||||
background: #e7f7ee;
|
||||
color: #1a7f52;
|
||||
}
|
||||
|
||||
.icn.info {
|
||||
background: #eef0f3;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Fraunces', serif;
|
||||
font-weight: 600;
|
||||
font-size: 23px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #4a433d;
|
||||
font-size: 15px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.ev {
|
||||
background: var(--paper);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.ev .t {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ev .d {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
border-radius: 11px;
|
||||
padding: 14px 20px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
filter: brightness(.94);
|
||||
}
|
||||
|
||||
.btn-light {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<?php if ($state === 'notfound') : ?>
|
||||
<div class="icn warn">✕</div>
|
||||
<h1>Link non valido</h1>
|
||||
<p>Questa prenotazione non esiste o il link è errato.</p>
|
||||
|
||||
<?php elseif ($state === 'already') : ?>
|
||||
<div class="icn info">✓</div>
|
||||
<h1>Già annullata</h1>
|
||||
<p>Questa prenotazione risulta già annullata.</p>
|
||||
|
||||
<?php elseif ($state === 'toolate') : ?>
|
||||
<div class="icn info">⏱</div>
|
||||
<h1>Non più annullabile</h1>
|
||||
<p>L'evento è già iniziato: non è più possibile annullare online. Contattaci per assistenza.</p>
|
||||
|
||||
<?php elseif ($state === 'error') : ?>
|
||||
<div class="icn warn">!</div>
|
||||
<h1>Si è verificato un errore</h1>
|
||||
<p>Riprova tra poco. Se il problema persiste, contattaci.</p>
|
||||
|
||||
<?php elseif ($state === 'done') : ?>
|
||||
<div class="icn ok">✓</div>
|
||||
<h1>Prenotazione annullata</h1>
|
||||
<p>La tua prenotazione per <strong><?php echo e($booking['ev_title']); ?></strong> è stata annullata.</p>
|
||||
<?php if ($promotedCount > 0) : ?>
|
||||
<p class="muted">Un posto è stato assegnato alla prima persona in lista d'attesa.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else : /* form */ ?>
|
||||
<div class="icn warn">?</div>
|
||||
<h1>Annullare la prenotazione?</h1>
|
||||
<div class="ev">
|
||||
<div class="t"><?php echo e($booking['ev_title']); ?></div>
|
||||
<div class="d"><?php echo e(date('d/m/Y H:i', strtotime($booking['ev_starts']))); ?> · <?php echo (int) $booking['seats']; ?> post<?php echo (int) $booking['seats'] === 1 ? 'o' : 'i'; ?><?php echo $booking['status'] === 'waiting' ? ' (in lista d\'attesa)' : ''; ?></div>
|
||||
</div>
|
||||
<p>Confermi di voler annullare la prenotazione a nome di <strong><?php echo e($booking['name']); ?></strong>?</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="token" value="<?php echo e($token); ?>">
|
||||
<input type="hidden" name="confirm" value="1">
|
||||
<button type="submit" class="btn btn-danger">Sì, annulla la prenotazione</button>
|
||||
</form>
|
||||
<p class="muted">Se hai cambiato idea, chiudi pure questa pagina.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/include/event_mailer.php';
|
||||
|
||||
/**
|
||||
* Riconferma di una promozione da lista d'attesa (stato 'offered' -> 'confirmed').
|
||||
* GET confirm.php?token=... -> pagina di conferma (non modifica nulla)
|
||||
* POST (token + confirm=1) -> conferma il posto e invia l'email di conferma.
|
||||
*/
|
||||
$pdo = pdo_conn();
|
||||
$token = trim($_POST['token'] ?? $_GET['token'] ?? '');
|
||||
$isPost = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST';
|
||||
|
||||
$booking = null;
|
||||
if ($token !== '') {
|
||||
$q = $pdo->prepare("SELECT b.*, e.title AS ev_title, e.starts_at AS ev_starts, e.id AS eid
|
||||
FROM event_bookings b JOIN events e ON e.id = b.event_id
|
||||
WHERE b.token = ?");
|
||||
$q->execute([$token]);
|
||||
$booking = $q->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$state = 'form'; // form | notfound | already | invalid | toolate | done | error
|
||||
|
||||
if (!$booking) {
|
||||
$state = 'notfound';
|
||||
} elseif ($booking['status'] === 'confirmed') {
|
||||
$state = 'already';
|
||||
} elseif ($booking['status'] !== 'offered') {
|
||||
// waiting / cancelled / ecc.: non c'è un'offerta da confermare
|
||||
$state = 'invalid';
|
||||
} elseif (strtotime($booking['ev_starts']) <= time()) {
|
||||
$state = 'toolate';
|
||||
} elseif ($isPost && !empty($_POST['confirm'])) {
|
||||
try {
|
||||
$upd = $pdo->prepare("UPDATE event_bookings SET status = 'confirmed', updated_at = NOW() WHERE id = ? AND status = 'offered'");
|
||||
$upd->execute([(int) $booking['id']]);
|
||||
|
||||
if ($upd->rowCount() > 0) {
|
||||
$state = 'done';
|
||||
// Email di conferma definitiva (best-effort)
|
||||
try {
|
||||
$ev = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$ev->execute([(int) $booking['eid']]);
|
||||
$evRow = $ev->fetch(PDO::FETCH_ASSOC);
|
||||
if ($evRow) {
|
||||
$booking['status'] = 'confirmed';
|
||||
yse_mail_booking($booking, $evRow);
|
||||
}
|
||||
} catch (Throwable $me) {
|
||||
error_log('[confirm mail] ' . $me->getMessage());
|
||||
}
|
||||
} else {
|
||||
$state = 'invalid';
|
||||
}
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[apiwp/confirm] ' . $ex->getMessage());
|
||||
$state = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
function e($v): string
|
||||
{
|
||||
return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Conferma posto — YogaSoul</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--accent: #1a7f52;
|
||||
--ink: #2b2622;
|
||||
--muted: #8c837c;
|
||||
--line: #ece7e2;
|
||||
--paper: #faf8f6;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 14px 40px rgba(43, 38, 34, .10);
|
||||
max-width: 460px;
|
||||
width: 100%;
|
||||
padding: 34px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icn {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 16px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.icn.ok {
|
||||
background: #e7f7ee;
|
||||
color: #1a7f52;
|
||||
}
|
||||
|
||||
.icn.info {
|
||||
background: #eef0f3;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.icn.warn {
|
||||
background: #fdecea;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Fraunces', serif;
|
||||
font-weight: 600;
|
||||
font-size: 23px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #4a433d;
|
||||
font-size: 15px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.ev {
|
||||
background: var(--paper);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.ev .t {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ev .d {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
border-radius: 11px;
|
||||
padding: 14px 20px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.btn-ok {
|
||||
background: #1a7f52;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-ok:hover {
|
||||
filter: brightness(.94);
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<?php if ($state === 'notfound') : ?>
|
||||
<div class="icn warn">✕</div>
|
||||
<h1>Link non valido</h1>
|
||||
<p>Questo link non è valido o è scaduto.</p>
|
||||
|
||||
<?php elseif ($state === 'already') : ?>
|
||||
<div class="icn ok">✓</div>
|
||||
<h1>Posto già confermato</h1>
|
||||
<p>Il tuo posto per <strong><?php echo e($booking['ev_title']); ?></strong> è già confermato. Ci vediamo lì!</p>
|
||||
|
||||
<?php elseif ($state === 'invalid') : ?>
|
||||
<div class="icn info">i</div>
|
||||
<h1>Offerta non più disponibile</h1>
|
||||
<p>Questa offerta non è più valida. Se pensi ci sia un errore, contattaci.</p>
|
||||
|
||||
<?php elseif ($state === 'toolate') : ?>
|
||||
<div class="icn info">⏱</div>
|
||||
<h1>Evento già iniziato</h1>
|
||||
<p>Non è più possibile confermare online. Contattaci per assistenza.</p>
|
||||
|
||||
<?php elseif ($state === 'error') : ?>
|
||||
<div class="icn warn">!</div>
|
||||
<h1>Si è verificato un errore</h1>
|
||||
<p>Riprova tra poco. Se il problema persiste, contattaci.</p>
|
||||
|
||||
<?php elseif ($state === 'done') : ?>
|
||||
<div class="icn ok">✓</div>
|
||||
<h1>Posto confermato!</h1>
|
||||
<p>Grazie <strong><?php echo e($booking['name']); ?></strong>, il tuo posto per <strong><?php echo e($booking['ev_title']); ?></strong> è confermato. Ti abbiamo inviato l'email con tutti i dettagli.</p>
|
||||
|
||||
<?php else : /* form */ ?>
|
||||
<div class="icn ok">🎉</div>
|
||||
<h1>Si è liberato un posto!</h1>
|
||||
<div class="ev">
|
||||
<div class="t"><?php echo e($booking['ev_title']); ?></div>
|
||||
<div class="d"><?php echo e(date('d/m/Y H:i', strtotime($booking['ev_starts']))); ?> · <?php echo (int) $booking['seats']; ?> post<?php echo (int) $booking['seats'] === 1 ? 'o' : 'i'; ?></div>
|
||||
</div>
|
||||
<p>Confermi la tua presenza, <strong><?php echo e($booking['name']); ?></strong>?</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="token" value="<?php echo e($token); ?>">
|
||||
<input type="hidden" name="confirm" value="1">
|
||||
<button type="submit" class="btn btn-ok">Sì, confermo il posto</button>
|
||||
</form>
|
||||
<p class="muted">Se non puoi partecipare, ignora pure questa email: il posto passerà a un'altra persona.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/include/event_waitlist.php';
|
||||
require_once dirname(__DIR__) . '/include/event_mailer.php';
|
||||
|
||||
/**
|
||||
* CRON unico del modulo Eventi. Da richiamare periodicamente (es. ogni 10 min).
|
||||
* Protezione: chiave segreta in .env (YSE_CRON_KEY) passata come ?key=...
|
||||
*
|
||||
* Fa tre cose a ogni giro:
|
||||
* 1) Pubblica gli eventi programmati arrivati alla data di apertura.
|
||||
* 2) Invia i promemoria automatici (ore effettive per-evento o default), 1 sola volta.
|
||||
* 3) Applica la scadenza delle offerte lista d'attesa (offered non riconfermati):
|
||||
* li annulla e offre il posto al prossimo in coda (email E).
|
||||
*/
|
||||
|
||||
$pdo = pdo_conn(); // carica anche il .env (per la chiave e le MAIL_*)
|
||||
|
||||
$expected = $_ENV['YSE_CRON_KEY'] ?? '';
|
||||
$provided = (string) ($_GET['key'] ?? '');
|
||||
if ($expected === '' || !hash_equals($expected, $provided)) {
|
||||
send_json(['ok' => false, 'error' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$report = ['published' => 0, 'reminders' => 0, 'offers_expired' => 0, 'promoted' => 0];
|
||||
|
||||
// Default scuola
|
||||
$opt = [];
|
||||
try {
|
||||
$opt = $pdo->query("SELECT * FROM `option` LIMIT 1")->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
} catch (Throwable $e) {
|
||||
$opt = [];
|
||||
}
|
||||
$defRemEnabled = ($opt['event_reminder_enabled'] ?? 'N') === 'Y';
|
||||
$defRemHours = (int) ($opt['event_reminder_hours'] ?? 24);
|
||||
$defOfferHours = (int) ($opt['event_offer_expiry_hours'] ?? 24);
|
||||
|
||||
/* 1) Pubblica gli eventi programmati arrivati a scadenza */
|
||||
try {
|
||||
$pub = $pdo->prepare("UPDATE events SET status = 'published', updated_at = NOW()
|
||||
WHERE status = 'scheduled' AND publish_at IS NOT NULL AND publish_at <= NOW()");
|
||||
$pub->execute();
|
||||
$report['published'] = $pub->rowCount();
|
||||
} catch (Throwable $e) {
|
||||
error_log('[cron publish] ' . $e->getMessage());
|
||||
}
|
||||
|
||||
/* 2) Promemoria automatici (una sola volta per evento) */
|
||||
try {
|
||||
$evs = $pdo->query("SELECT * FROM events
|
||||
WHERE status = 'published' AND reminder_sent_at IS NULL AND starts_at > NOW()")
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($evs as $ev) {
|
||||
$enabled = $ev['reminder_enabled'] !== null ? ($ev['reminder_enabled'] === 'Y') : $defRemEnabled;
|
||||
if (!$enabled) {
|
||||
continue;
|
||||
}
|
||||
$hours = $ev['reminder_hours'] !== null ? (int) $ev['reminder_hours'] : $defRemHours;
|
||||
if (strtotime($ev['starts_at']) - $hours * 3600 > time()) {
|
||||
continue; // non ancora l'ora del promemoria
|
||||
}
|
||||
$ps = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? AND status = 'confirmed'");
|
||||
$ps->execute([$ev['id']]);
|
||||
foreach ($ps->fetchAll(PDO::FETCH_ASSOC) as $p) {
|
||||
try {
|
||||
yse_mail_reminder($p, $ev);
|
||||
$report['reminders']++;
|
||||
} catch (Throwable $me) {
|
||||
error_log('[cron reminder] ' . $me->getMessage());
|
||||
}
|
||||
}
|
||||
$pdo->prepare("UPDATE events SET reminder_sent_at = NOW() WHERE id = ?")->execute([$ev['id']]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[cron reminders] ' . $e->getMessage());
|
||||
}
|
||||
|
||||
/* 3) Scadenza offerte lista d'attesa */
|
||||
try {
|
||||
$offers = $pdo->query("SELECT b.*, e.offer_expiry_hours AS ev_offer_hours
|
||||
FROM event_bookings b JOIN events e ON e.id = b.event_id
|
||||
WHERE b.status = 'offered' AND b.offered_at IS NOT NULL")
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($offers as $o) {
|
||||
$hours = $o['ev_offer_hours'] !== null ? (int) $o['ev_offer_hours'] : $defOfferHours;
|
||||
if (strtotime($o['offered_at']) + $hours * 3600 > time()) {
|
||||
continue; // offerta ancora valida
|
||||
}
|
||||
$promoted = [];
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
$pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([(int) $o['event_id']]);
|
||||
$st = $pdo->prepare("SELECT status FROM event_bookings WHERE id = ?");
|
||||
$st->execute([(int) $o['id']]);
|
||||
if ($st->fetchColumn() === 'offered') {
|
||||
$pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ?")
|
||||
->execute([(int) $o['id']]);
|
||||
$promoted = promoteFromWaitlist($pdo, (int) $o['event_id']);
|
||||
$report['offers_expired']++;
|
||||
}
|
||||
$pdo->commit();
|
||||
|
||||
if (!empty($promoted)) {
|
||||
$ev = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$ev->execute([(int) $o['event_id']]);
|
||||
$evRow = $ev->fetch(PDO::FETCH_ASSOC);
|
||||
foreach ($promoted as $pr) {
|
||||
try {
|
||||
yse_mail_promotion($pr, $evRow);
|
||||
$report['promoted']++;
|
||||
} catch (Throwable $pe) {
|
||||
error_log('[cron promo] ' . $pe->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $te) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('[cron offer] ' . $te->getMessage());
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[cron offers] ' . $e->getMessage());
|
||||
}
|
||||
|
||||
send_json(['ok' => true, 'report' => $report, 'at' => date('c')]);
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
|
||||
/**
|
||||
* Endpoint pubblico LISTA eventi (sola lettura).
|
||||
* Ritorna gli eventi PUBBLICATI e NON ancora terminati, con posti/prezzo/gallery.
|
||||
*
|
||||
* Parametri (query string):
|
||||
* ?id=12 -> un solo evento
|
||||
* ?ids=12,15,20 -> solo questi eventi (nell'ordine per data)
|
||||
* ?limit=3 -> massimo N eventi
|
||||
* Senza parametri: tutti i pubblicati futuri, per data crescente.
|
||||
*
|
||||
* Regole di visibilità:
|
||||
* status = 'published' AND COALESCE(ends_at, starts_at) >= NOW()
|
||||
* (l'evento sparisce da solo quando è terminato)
|
||||
*/
|
||||
try {
|
||||
$pdo = pdo_conn();
|
||||
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
$idsParam = trim($_GET['ids'] ?? '');
|
||||
$limit = isset($_GET['limit']) ? max(1, min(50, (int) $_GET['limit'])) : 0;
|
||||
|
||||
$where = ["e.status IN ('published','scheduled')", "COALESCE(e.ends_at, e.starts_at) >= NOW()"];
|
||||
$params = [];
|
||||
|
||||
if ($id > 0) {
|
||||
$where[] = 'e.id = ?';
|
||||
$params[] = $id;
|
||||
} elseif ($idsParam !== '') {
|
||||
$ids = array_values(array_filter(array_map('intval', explode(',', $idsParam)), function ($v) {
|
||||
return $v > 0;
|
||||
}));
|
||||
if (!empty($ids)) {
|
||||
$where[] = 'e.id IN (' . implode(',', array_fill(0, count($ids), '?')) . ')';
|
||||
foreach ($ids as $v) {
|
||||
$params[] = $v;
|
||||
}
|
||||
} else {
|
||||
send_json(['ok' => true, 'count' => 0, 'events' => []]);
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "SELECT e.*, u.first_name AS cf, u.last_name AS cl,
|
||||
COALESCE(b.occupied, 0) AS occupied,
|
||||
COALESCE(w.waiting, 0) AS waiting
|
||||
FROM events e
|
||||
LEFT JOIN auth_users u ON u.id = e.user_id
|
||||
LEFT JOIN (
|
||||
SELECT event_id, SUM(seats) AS occupied
|
||||
FROM event_bookings WHERE status IN ('confirmed','pending','offered')
|
||||
GROUP BY event_id
|
||||
) b ON b.event_id = e.id
|
||||
LEFT JOIN (
|
||||
SELECT event_id, SUM(seats) AS waiting
|
||||
FROM event_bookings WHERE status = 'waiting'
|
||||
GROUP BY event_id
|
||||
) w ON w.event_id = e.id
|
||||
WHERE " . implode(' AND ', $where) . "
|
||||
ORDER BY e.starts_at ASC";
|
||||
|
||||
if ($limit > 0) {
|
||||
$sql .= ' LIMIT ' . (int) $limit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$symbols = ['EUR' => '€', 'USD' => '$', 'GBP' => '£', 'CHF' => 'CHF'];
|
||||
$events = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$maxSeats = (int) $r['max_seats'];
|
||||
$occupied = (int) $r['occupied'];
|
||||
$remaining = max(0, $maxSeats - $occupied);
|
||||
|
||||
$gallery = [];
|
||||
if (!empty($r['gallery'])) {
|
||||
$g = json_decode($r['gallery'], true);
|
||||
if (is_array($g)) {
|
||||
foreach ($g as $path) {
|
||||
if (is_string($path) && $path !== '') {
|
||||
$gallery[] = public_url($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$organizer = trim((string) $r['organizer_name']) !== ''
|
||||
? $r['organizer_name']
|
||||
: trim(($r['cf'] ?? '') . ' ' . ($r['cl'] ?? ''));
|
||||
|
||||
$events[] = [
|
||||
'id' => (int) $r['id'],
|
||||
'title' => $r['title'],
|
||||
'slug' => $r['slug'],
|
||||
'status' => $r['status'],
|
||||
'scheduled' => $r['status'] === 'scheduled',
|
||||
'bookable' => $r['status'] === 'published',
|
||||
'publish_at' => $r['publish_at'],
|
||||
'description' => $r['description'], // HTML (sanificare al rendering in WP)
|
||||
'instructions' => $r['instructions'],
|
||||
'what_to_bring' => $r['what_to_bring'],
|
||||
'suitable_for' => $r['suitable_for'],
|
||||
'organizer' => $organizer !== '' ? $organizer : null,
|
||||
'starts_at' => $r['starts_at'],
|
||||
'ends_at' => $r['ends_at'],
|
||||
'address' => $r['address'],
|
||||
'lat' => $r['lat'] !== null ? (float) $r['lat'] : null,
|
||||
'lng' => $r['lng'] !== null ? (float) $r['lng'] : null,
|
||||
'cost' => (float) $r['cost'],
|
||||
'currency' => $r['currency'],
|
||||
'currency_symbol' => $symbols[$r['currency']] ?? $r['currency'],
|
||||
'color' => $r['color'],
|
||||
'cover_image' => public_url($r['cover_image']),
|
||||
'gallery' => $gallery,
|
||||
'max_seats' => $maxSeats,
|
||||
'max_per_booking' => (int) $r['max_per_booking'],
|
||||
'occupied' => $occupied,
|
||||
'remaining' => $remaining,
|
||||
'sold_out' => $remaining <= 0,
|
||||
'waitlist_enabled' => ($r['waitlist_enabled'] ?? 'N') === 'Y',
|
||||
'waiting' => (int) $r['waiting'],
|
||||
];
|
||||
}
|
||||
|
||||
send_json(['ok' => true, 'count' => count($events), 'events' => $events]);
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[api/events] ' . $ex->getMessage());
|
||||
send_json(['ok' => false, 'error' => 'Errore nel recupero degli eventi.'], 500);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 222 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 222 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
+17
-4
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once __DIR__ . '/class/mailer.php';
|
||||
require_once('include/waitinglist.php');
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
@@ -55,11 +56,20 @@ if ($rowUser = $resUser->fetch_assoc()) {
|
||||
}
|
||||
$stmtUser->close();
|
||||
|
||||
// Decide lo stato: pending se c'è posto, waiting se la classe è piena
|
||||
$bookingStatus = decideBookingStatus($conn, $idnewbooking);
|
||||
|
||||
if ($bookingStatus === 'full') {
|
||||
// Classe piena e lista d'attesa disattivata: non inseriamo nulla
|
||||
header("Location: userpanel.php?full=Y");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Inserisce la nuova prenotazione in bookingclass
|
||||
$insertQuery = "INSERT INTO bookingclass (idserviceschedule, iduser, prevbookingstart, idprevserviceschedule, idservice, idorder, bookingstart, status)
|
||||
VALUES (?, ?, NULL, '0', ?, ?, ?, 'pending')";
|
||||
VALUES (?, ?, NULL, '0', ?, ?, ?, ?)";
|
||||
$stmt = $conn->prepare($insertQuery);
|
||||
$stmt->bind_param("iiiis", $idnewbooking, $iduser, $idservicenew, $idorder, $newtime);
|
||||
$stmt->bind_param("iiiiss", $idnewbooking, $iduser, $idservicenew, $idorder, $newtime, $bookingStatus);
|
||||
if (!$stmt->execute()) {
|
||||
$stmt->close();
|
||||
die("Errore durante l'inserimento della nuova prenotazione: " . $conn->error);
|
||||
@@ -78,9 +88,12 @@ $stmt->close();
|
||||
|
||||
// Connessione $bkngstm gestita globalmente: non la chiudiamo qui
|
||||
|
||||
// Parametro di ritorno diverso se finito in lista d'attesa
|
||||
$redirectFlag = ($bookingStatus === 'waiting') ? 'waiting=Y' : 'reprogram=Y';
|
||||
|
||||
// Se non abbiamo un'email valida, salta l'invio ma completa comunque il flusso
|
||||
if ($emailuser === '') {
|
||||
header("Location: userpanel.php?reprogram=Y");
|
||||
header("Location: userpanel.php?$redirectFlag");
|
||||
exit();
|
||||
}
|
||||
|
||||
@@ -168,5 +181,5 @@ try {
|
||||
}
|
||||
|
||||
// Esegui il reindirizzamento
|
||||
header("Location: userpanel.php?reprogram=Y");
|
||||
header("Location: userpanel.php?$redirectFlag");
|
||||
exit();
|
||||
|
||||
@@ -507,7 +507,7 @@ $conn->close();
|
||||
|
||||
$mysqli = new mysqli($servername, $username, $password, $dbname);
|
||||
$eventId = $bookedclass->getColumnVal("idserviceschedule");
|
||||
$bookingQuery = "SELECT iduser FROM bookingclass WHERE bookingclass.idserviceschedule = ? AND bookingclass.status='booked'";
|
||||
$bookingQuery = "SELECT iduser FROM bookingclass WHERE bookingclass.idserviceschedule = ? AND bookingclass.status IN ('booked','pending')";
|
||||
$stmt = $mysqli->prepare($bookingQuery);
|
||||
$stmt->bind_param("i", $eventId);
|
||||
$stmt->execute();
|
||||
@@ -538,7 +538,7 @@ $conn->close();
|
||||
<?php } elseif ($freeplace > 0) { ?>
|
||||
<div class="custom-date-box" style="background-color:#1ebf73">
|
||||
<?php } else { ?>
|
||||
<div class="custom-date-box" style="background-color:#9C9C9C">
|
||||
<div class="custom-date-box" style="background-color:#d1698a">
|
||||
<?php } ?>
|
||||
<div class="custom-day"><?php echo $dayInItalian; ?></div>
|
||||
<div class="custom-month"><?php echo $monthInItalian; ?></div>
|
||||
@@ -548,7 +548,7 @@ $conn->close();
|
||||
<?php } elseif ($freeplace > 0) { ?>
|
||||
<div class="custom-event-details" style="border-left-color:<?php echo ($bookedclass->getColumnVal("colorclass")); ?>">
|
||||
<?php } else { ?>
|
||||
<div class="custom-event-details" style="background-color:#CDCDCD;border-left-color:#9C9C9C">
|
||||
<div class="custom-event-details" style="background-color:#fbeef3;border-left-color:#d1698a">
|
||||
<?php } ?>
|
||||
<h2 class="custom-heading"><?php echo ($bookedclass->getColumnVal("servicename")); ?> -<?php echo $countPersons; ?>/<?php echo ($bookedclass->getColumnVal("maxcapacity")); ?></h2>
|
||||
<p class="custom-paragraph">Quando: <?php echo $newDateFormat; ?></p>
|
||||
@@ -561,7 +561,11 @@ $conn->close();
|
||||
<?php } elseif ($freeplace > 0) { ?>
|
||||
<a href="rebookandgo.php?idpreviousbooking=<?php echo $idpreviousbooking; ?>&idservicenew=<?php echo $idservicenew; ?>&idnewbooking=<?php echo $idnewbooking; ?>&iduser=<?php echo $iduserlogin; ?>"><button class="booking-button"><i class="fas fa-arrow-circle-right"></i> Riprogramma Classe</button></a>
|
||||
<?php } else { ?>
|
||||
<button class="booking-button"><i class="fa fa-stop"></i> Classe Piena</button>
|
||||
<?php if ($optionquery->getColumnVal("waitinglist_enabled") === 'Y') { ?>
|
||||
<a href="rebookandgo.php?idpreviousbooking=<?php echo $idpreviousbooking; ?>&idservicenew=<?php echo $idservicenew; ?>&idnewbooking=<?php echo $idnewbooking; ?>&iduser=<?php echo $iduserlogin; ?>"><button class="booking-button" style="background-color:#d1698a;color:#fff;"><i class="fas fa-hourglass-half"></i> Mettiti in lista d'attesa</button></a>
|
||||
<?php } else { ?>
|
||||
<button class="booking-button"><i class="fa fa-stop"></i> Classe Piena</button>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -742,7 +742,7 @@ $conn->close();
|
||||
?>
|
||||
<a href="bookandgo.php?idpreviousbooking=<?php echo '0'; ?>&idorder=<?php echo $idorder; ?>&idservicenew=<?php echo $idservicenew; ?>&idnewbooking=<?php echo $idnewbooking; ?>&iduser=<?php echo $iduserlogin; ?>" ?><button class="booking-button"><i class="fas fa-arrow-circle-right"></i> Riprogramma Classe</button></a>
|
||||
<?php } else { ?>
|
||||
<button class="booking-button"><i class="fa fa-stop"></i> Classe Piena</button>
|
||||
<a href="bookandgo.php?idpreviousbooking=0&idorder=<?php echo $idorder; ?>&idservicenew=<?php echo $idservicenew; ?>&idnewbooking=<?php echo $idnewbooking; ?>&iduser=<?php echo $iduserlogin; ?>"><button class="booking-button" style="background-color:#f0ad4e;color:#fff;"><i class="fas fa-hourglass-half"></i> Mettiti in lista d'attesa</button></a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
require_once('include/headscript.php'); // per avere le credenziali DB, se sono lì
|
||||
|
||||
// Se le credenziali sono in headscript.php, sposta la connessione DOPO il require.
|
||||
// Adatta questa parte a come sono definite $servername ecc. nelle tue altre pagine.
|
||||
|
||||
if ($conn->connect_error) {
|
||||
die("Connessione fallita: " . $conn->connect_error);
|
||||
}
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header("Location: riepilogo.php?message=error_delete");
|
||||
exit();
|
||||
}
|
||||
|
||||
$idorder = (int) $_GET['id'];
|
||||
|
||||
$conn->begin_transaction();
|
||||
|
||||
try {
|
||||
// 1. Cancella tutte le prenotazioni di questo ordine
|
||||
$stmt1 = $conn->prepare("DELETE FROM bookingclass WHERE idorder = ?");
|
||||
$stmt1->bind_param("i", $idorder);
|
||||
$stmt1->execute();
|
||||
$stmt1->close();
|
||||
|
||||
// 2. Cancella l'ordine
|
||||
$stmt2 = $conn->prepare("DELETE FROM orderbook WHERE idorderbook = ?");
|
||||
$stmt2->bind_param("i", $idorder);
|
||||
$stmt2->execute();
|
||||
$stmt2->close();
|
||||
|
||||
$conn->commit();
|
||||
header("Location: riepilogo.php?message=success_delete");
|
||||
exit();
|
||||
} catch (Exception $e) {
|
||||
$conn->rollback();
|
||||
header("Location: riepilogo.php?message=error_delete");
|
||||
exit();
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/event_mailer.php'); // stub mailer (template vero: step successivo)
|
||||
|
||||
/**
|
||||
* Annullamento evento.
|
||||
* Sceglierne uno soft (consigliato): imposta status='cancelled' e mantiene le
|
||||
* prenotazioni/contatti raccolti. Se notify=1 invia la mail di annullamento a
|
||||
* tutti i partecipanti attivi (recuperati PRIMA di annullare).
|
||||
*
|
||||
* Chiamato da events.php: event_delete.php?id=123¬ify=1|0
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
$notify = (int) ($_GET['notify'] ?? 0) === 1;
|
||||
|
||||
if ($id <= 0) {
|
||||
header('Location: events.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$event = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$event) {
|
||||
header('Location: events.php?message=error');
|
||||
exit();
|
||||
}
|
||||
|
||||
// 1) Recupera i partecipanti attivi PRIMA di annullare (servono per la mail)
|
||||
$pStmt = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? AND status <> 'cancelled'");
|
||||
$pStmt->execute([$id]);
|
||||
$participants = $pStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
try {
|
||||
// 2) Annulla l'evento (soft) e le relative prenotazioni
|
||||
$pdo->prepare("UPDATE events SET status = 'cancelled', updated_at = NOW() WHERE id = ?")->execute([$id]);
|
||||
$pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE event_id = ? AND status <> 'cancelled'")->execute([$id]);
|
||||
|
||||
// 3) Notifica (opzionale). Il template vero arriva nello step successivo.
|
||||
if ($notify && !empty($participants)) {
|
||||
foreach ($participants as $p) {
|
||||
try {
|
||||
yse_mail_event_cancel($p, $event);
|
||||
} catch (Throwable $mailEx) {
|
||||
error_log('[event_delete mail] ' . $mailEx->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: events.php?message=cancelled');
|
||||
exit();
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[event_delete] ' . $ex->getMessage());
|
||||
header('Location: events.php?message=error');
|
||||
exit();
|
||||
}
|
||||
@@ -0,0 +1,964 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Modulo Eventi - form creazione/modifica (una sola pagina).
|
||||
* - event_form.php -> nuovo evento
|
||||
* - event_form.php?id=123 -> modifica evento esistente
|
||||
* Invia a event_save.php (insert/update + redirect).
|
||||
*
|
||||
* Extra rispetto alla v1:
|
||||
* - flatpickr per data/ora (fine = inizio + 1h automatica)
|
||||
* - sede da event_locations (default scuola) o indirizzo personalizzato
|
||||
* - "Calcola coordinate" da indirizzo (geocoding via event_geocode.php)
|
||||
* - copertina drag&drop -> event_upload_cover.php -> assets/uploads/events/
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
$ev = null;
|
||||
|
||||
if ($id > 0) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$ev = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$ev) {
|
||||
header('Location: events.php?message=error');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Default dal record unico di `option`
|
||||
$opt = $pdo->query("SELECT * FROM `option` LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
||||
$defaultColor = $opt['event_default_color'] ?? '#4CAF50';
|
||||
$defaultMaxPerBooking = (int) ($opt['event_default_max_per_booking'] ?? 1);
|
||||
$defReminderEnabled = ($opt['event_reminder_enabled'] ?? 'N') === 'Y';
|
||||
$defReminderHours = (int) ($opt['event_reminder_hours'] ?? 24);
|
||||
$defOfferHours = (int) ($opt['event_offer_expiry_hours'] ?? 24);
|
||||
|
||||
// Sedi salvate (se la tabella non esiste ancora, si degrada su indirizzo libero)
|
||||
$locations = [];
|
||||
try {
|
||||
$locations = $pdo->query("SELECT * FROM event_locations ORDER BY is_default DESC, name ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $e) {
|
||||
$locations = [];
|
||||
}
|
||||
|
||||
$val = [
|
||||
'title' => $ev['title'] ?? '',
|
||||
'slug' => $ev['slug'] ?? '',
|
||||
'organizer_name' => $ev['organizer_name'] ?? '',
|
||||
'starts_at' => !empty($ev['starts_at']) ? date('Y-m-d H:i', strtotime($ev['starts_at'])) : '',
|
||||
'ends_at' => !empty($ev['ends_at']) ? date('Y-m-d H:i', strtotime($ev['ends_at'])) : '',
|
||||
'address' => $ev['address'] ?? '',
|
||||
'lat' => $ev['lat'] ?? '',
|
||||
'lng' => $ev['lng'] ?? '',
|
||||
'cost' => $ev['cost'] ?? '0.00',
|
||||
'currency' => $ev['currency'] ?? 'EUR',
|
||||
'max_seats' => $ev['max_seats'] ?? '',
|
||||
'max_per_booking' => $ev['max_per_booking'] ?? $defaultMaxPerBooking,
|
||||
'color' => $ev['color'] ?? $defaultColor,
|
||||
'cover_image' => $ev['cover_image'] ?? '',
|
||||
'status' => $ev['status'] ?? 'draft',
|
||||
'description' => $ev['description'] ?? '',
|
||||
'instructions' => $ev['instructions'] ?? '',
|
||||
'what_to_bring' => $ev['what_to_bring'] ?? '',
|
||||
'suitable_for' => $ev['suitable_for'] ?? '',
|
||||
'gallery' => $ev['gallery'] ?? '',
|
||||
'waitlist_enabled' => ($ev['waitlist_enabled'] ?? 'Y') === 'Y',
|
||||
'reminder_enabled' => $ev['reminder_enabled'] ?? null,
|
||||
'reminder_hours' => $ev['reminder_hours'] ?? '',
|
||||
'offer_expiry_hours' => $ev['offer_expiry_hours'] ?? '',
|
||||
'schedule_enabled' => (($ev['status'] ?? '') === 'scheduled') || !empty($ev['publish_at']),
|
||||
'publish_at' => !empty($ev['publish_at']) ? date('Y-m-d H:i', strtotime($ev['publish_at'])) : '',
|
||||
];
|
||||
|
||||
$isEdit = $id > 0;
|
||||
$pageTitle = $isEdit ? 'Modifica evento' : 'Nuovo evento';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - <?php echo e($pageTitle); ?></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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<!-- Date/ora -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/l10n/it.min.js"></script>
|
||||
<!-- Editor testo ricco -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e4e9;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #1ebf73;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.12);
|
||||
}
|
||||
|
||||
.form-control[readonly] {
|
||||
background: #f7f8fa;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#editor {
|
||||
height: 240px;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.ql-toolbar.ql-snow,
|
||||
.ql-container.ql-snow {
|
||||
border-color: #e2e4e9;
|
||||
}
|
||||
|
||||
.ql-toolbar.ql-snow {
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
input[type="color"].form-control {
|
||||
padding: 4px;
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-geocode {
|
||||
border: 1px solid #e2e4e9;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #2f7be7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-geocode:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
border-radius: 8px;
|
||||
padding: 10px 26px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Dropzone copertina */
|
||||
.dropzone {
|
||||
border: 2px dashed #d5d9e0;
|
||||
border-radius: 12px;
|
||||
background: #fafbfc;
|
||||
padding: 22px;
|
||||
text-align: center;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropzone.dragover {
|
||||
border-color: #1ebf73;
|
||||
background: #eef6f1;
|
||||
}
|
||||
|
||||
.dz-empty i {
|
||||
font-size: 34px;
|
||||
color: #b6bcc6;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dz-empty p {
|
||||
margin: 0;
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dz-empty small {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dz-browse {
|
||||
color: #2f7be7;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dz-preview img {
|
||||
max-height: 170px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.dz-remove {
|
||||
margin-top: 12px;
|
||||
border: 1px solid #f2c9c4;
|
||||
background: #fff;
|
||||
color: #c0392b;
|
||||
border-radius: 8px;
|
||||
padding: 6px 14px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dz-remove:hover {
|
||||
background: #fdecea;
|
||||
}
|
||||
|
||||
.dz-progress {
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.coord-flash {
|
||||
border-color: #1ebf73 !important;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.18) !important;
|
||||
transition: box-shadow .2s;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.gallery-cell {
|
||||
position: relative;
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
|
||||
.gallery-cell img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gallery-remove {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gallery-remove:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
</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"><?php echo e($pageTitle); ?></h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<?php include('include/languageselection.php'); ?>
|
||||
<?php include('include/profiletopbar.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<?php include('include/sidebar.php'); ?>
|
||||
<header class="ishorizontal-topbar">
|
||||
<div class="navbar-header">
|
||||
<div class="d-flex"></div>
|
||||
</div>
|
||||
<div class="topnav">
|
||||
<div class="container-fluid">
|
||||
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title"><?php echo e($pageTitle); ?></div>
|
||||
<p class="section-lead">Compila i dati dell'evento. I campi con * sono obbligatori.</p>
|
||||
|
||||
<form action="event_save.php" method="POST" id="event-form">
|
||||
<input type="hidden" name="id" value="<?php echo (int) $id; ?>">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label class="form-label">Titolo *</label>
|
||||
<input type="text" class="form-control" name="title" required
|
||||
value="<?php echo e($val['title']); ?>" placeholder="Es. Yoga in Gravidanza & Amaca">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Slug</label>
|
||||
<input type="text" class="form-control" name="slug"
|
||||
value="<?php echo e($val['slug']); ?>" placeholder="generato dal titolo">
|
||||
<div class="hint">URL pubblico del widget. Vuoto = generato dal titolo.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Data e ora inizio *</label>
|
||||
<input type="text" class="form-control" id="starts_at" name="starts_at" required
|
||||
value="<?php echo e($val['starts_at']); ?>" placeholder="Scegli data e ora" autocomplete="off">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Data e ora fine</label>
|
||||
<input type="text" class="form-control" id="ends_at" name="ends_at"
|
||||
value="<?php echo e($val['ends_at']); ?>" placeholder="Auto: inizio + 1h" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sede / indirizzo -->
|
||||
<div class="row">
|
||||
<?php if (!empty($locations)) : ?>
|
||||
<div class="col-md-5 mb-3">
|
||||
<label class="form-label">Sede</label>
|
||||
<select id="location-select" class="form-select">
|
||||
<?php foreach ($locations as $loc) : ?>
|
||||
<option value="<?php echo (int) $loc['id']; ?>"
|
||||
data-address="<?php echo e($loc['address']); ?>"
|
||||
data-lat="<?php echo e($loc['lat']); ?>"
|
||||
data-lng="<?php echo e($loc['lng']); ?>">
|
||||
<?php echo e($loc['name']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<option value="custom">➕ Indirizzo personalizzato</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-7 mb-3">
|
||||
<label class="form-label">Indirizzo</label>
|
||||
<div class="d-flex gap-2">
|
||||
<input type="text" class="form-control" id="address-input" name="address"
|
||||
value="<?php echo e($val['address']); ?>" placeholder="Via, numero, città">
|
||||
<button type="button" class="btn-geocode" id="btn-geocode"><i class="bx bx-map-pin"></i> Calcola</button>
|
||||
</div>
|
||||
<div class="hint" id="geo-hint">Per un indirizzo personalizzato, premi "Calcola" per ottenere le coordinate mappa.</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Indirizzo</label>
|
||||
<div class="d-flex gap-2">
|
||||
<input type="text" class="form-control" id="address-input" name="address"
|
||||
value="<?php echo e($val['address']); ?>" placeholder="Via, numero, città">
|
||||
<button type="button" class="btn-geocode" id="btn-geocode"><i class="bx bx-map-pin"></i> Calcola coordinate</button>
|
||||
</div>
|
||||
<div class="hint" id="geo-hint">Premi "Calcola coordinate" per ottenere lat/lng dall'indirizzo.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="row" id="coords-row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Latitudine</label>
|
||||
<input type="text" class="form-control" id="lat-input" name="lat" value="<?php echo e($val['lat']); ?>" placeholder="45.6600">
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Longitudine</label>
|
||||
<input type="text" class="form-control" id="lng-input" name="lng" value="<?php echo e($val['lng']); ?>" placeholder="8.7900">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Organizzatore</label>
|
||||
<input type="text" class="form-control" name="organizer_name"
|
||||
value="<?php echo e($val['organizer_name']); ?>" placeholder="Lascia vuoto = tu">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Descrizione</label>
|
||||
<div id="editor"></div>
|
||||
<input type="hidden" name="description" id="description-input">
|
||||
<div class="hint">Testo formattato, elenchi, link e immagini.</div>
|
||||
</div>
|
||||
|
||||
<!-- Dettagli aggiuntivi (facoltativi) -->
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Istruzioni</label>
|
||||
<textarea class="form-control" name="instructions" rows="3" placeholder="Come arrivare, orari, note pratiche…"><?php echo e($val['instructions']); ?></textarea>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Cosa portare</label>
|
||||
<textarea class="form-control" name="what_to_bring" rows="3" placeholder="Tappetino, acqua, abbigliamento comodo…"><?php echo e($val['what_to_bring']); ?></textarea>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Per chi è adatto</label>
|
||||
<textarea class="form-control" name="suitable_for" rows="3" placeholder="Principianti, gravidanza, tutti i livelli…"><?php echo e($val['suitable_for']); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint" style="margin-top:-8px;">Questi campi sono facoltativi e verranno usati anche nella card pubblica e nelle email.</p>
|
||||
|
||||
<!-- Copertina drag&drop -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Immagine di copertina</label>
|
||||
<div class="dropzone" id="cover-dropzone">
|
||||
<div class="dz-empty" id="dz-empty" style="<?php echo $val['cover_image'] ? 'display:none;' : ''; ?>">
|
||||
<i class="bx bx-cloud-upload"></i>
|
||||
<p>Trascina qui l'immagine oppure <span class="dz-browse">sfoglia</span></p>
|
||||
<small>JPG, PNG, WEBP o GIF · max 5 MB</small>
|
||||
</div>
|
||||
<div class="dz-preview" id="dz-preview" style="<?php echo $val['cover_image'] ? '' : 'display:none;'; ?>">
|
||||
<img id="dz-img" src="<?php echo e($val['cover_image']); ?>" alt="Copertina">
|
||||
<div><button type="button" class="dz-remove" id="dz-remove"><i class="bx bx-trash"></i> Rimuovi</button></div>
|
||||
</div>
|
||||
<div class="dz-progress" id="dz-progress" style="display:none;"><i class="bx bx-loader bx-spin"></i> Caricamento…</div>
|
||||
<input type="file" id="cover-file" accept="image/*" hidden>
|
||||
</div>
|
||||
<input type="hidden" name="cover_image" id="cover_image" value="<?php echo e($val['cover_image']); ?>">
|
||||
<div class="hint">Immagine principale mostrata nella card.</div>
|
||||
</div>
|
||||
|
||||
<!-- Galleria foto -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Galleria foto</label>
|
||||
<div class="dropzone" id="gallery-dropzone">
|
||||
<div class="dz-empty">
|
||||
<i class="bx bx-images"></i>
|
||||
<p>Trascina qui le immagini oppure <span class="dz-browse">sfoglia</span></p>
|
||||
<small>Più immagini insieme · JPG, PNG, WEBP o GIF · max 5 MB l'una</small>
|
||||
</div>
|
||||
<input type="file" id="gallery-file" accept="image/*" multiple hidden>
|
||||
</div>
|
||||
<div class="gallery-grid" id="gallery-grid"></div>
|
||||
<input type="hidden" name="gallery" id="gallery-input" value="<?php echo e($val['gallery']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Costo</label>
|
||||
<input type="number" step="0.01" min="0" class="form-control" name="cost" value="<?php echo e($val['cost']); ?>">
|
||||
<div class="hint">0 = gratuito.</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Valuta</label>
|
||||
<select name="currency" class="form-select">
|
||||
<?php foreach (['EUR', 'USD', 'GBP', 'CHF'] as $cur) : ?>
|
||||
<option value="<?php echo $cur; ?>" <?php echo $val['currency'] === $cur ? 'selected' : ''; ?>><?php echo $cur; ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Posti totali *</label>
|
||||
<input type="number" min="1" class="form-control" name="max_seats" required value="<?php echo e($val['max_seats']); ?>" placeholder="Es. 15">
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Max posti per persona</label>
|
||||
<input type="number" min="1" class="form-control" name="max_per_booking" value="<?php echo e($val['max_per_booking']); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Colore card</label>
|
||||
<input type="color" class="form-control" name="color" value="<?php echo e($val['color']); ?>">
|
||||
</div>
|
||||
<div class="col-md-5 mb-3">
|
||||
<label class="form-label">Stato</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="draft" <?php echo $val['status'] === 'draft' ? 'selected' : ''; ?>>Bozza</option>
|
||||
<option value="published" <?php echo $val['status'] === 'published' ? 'selected' : ''; ?>>Pubblicato</option>
|
||||
<option value="cancelled" <?php echo $val['status'] === 'cancelled' ? 'selected' : ''; ?>>Annullato</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Lista d'attesa</label>
|
||||
<div class="form-check form-switch" style="padding-top:8px;">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="waitlist_enabled" name="waitlist_enabled" value="Y" <?php echo $val['waitlist_enabled'] ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="waitlist_enabled" style="font-weight:400;">Consenti iscrizione a evento pieno</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apertura programmata -->
|
||||
<div class="row">
|
||||
<div class="col-md-5 mb-3">
|
||||
<label class="form-label">Apertura prenotazioni</label>
|
||||
<div class="form-check form-switch" style="padding-top:8px;">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="schedule_enabled" name="schedule_enabled" value="Y" <?php echo $val['schedule_enabled'] ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="schedule_enabled" style="font-weight:400;">Apri in una data futura (programmato)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3" id="publish-at-wrap" style="<?php echo $val['schedule_enabled'] ? '' : 'display:none;'; ?>">
|
||||
<label class="form-label">Data/ora apertura</label>
|
||||
<input type="text" class="form-control" id="publish_at" name="publish_at" value="<?php echo e($val['publish_at']); ?>" placeholder="Quando aprire le prenotazioni" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint" style="margin-top:-8px;">Se attivo, l'evento resta "Programmato" (visibile ma non prenotabile) finché il cron non lo apre alla data indicata.</p>
|
||||
|
||||
<!-- Promemoria e lista d'attesa (override per evento) -->
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Promemoria automatico</label>
|
||||
<select name="reminder_enabled" class="form-select">
|
||||
<option value="">Predefinito (scuola: <?php echo $defReminderEnabled ? 'attivo' : 'disattivo'; ?>)</option>
|
||||
<option value="Y" <?php echo $val['reminder_enabled'] === 'Y' ? 'selected' : ''; ?>>Attivo</option>
|
||||
<option value="N" <?php echo $val['reminder_enabled'] === 'N' ? 'selected' : ''; ?>>Disattivo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Promemoria: ore prima</label>
|
||||
<input type="number" min="1" max="168" class="form-control" name="reminder_hours" value="<?php echo e($val['reminder_hours']); ?>" placeholder="Default: <?php echo $defReminderHours; ?>">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Scadenza offerta lista d'attesa (ore)</label>
|
||||
<input type="number" min="1" max="336" class="form-control" name="offer_expiry_hours" value="<?php echo e($val['offer_expiry_hours']); ?>" placeholder="Default: <?php echo $defOfferHours; ?>">
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint" style="margin-top:-8px;">Lascia vuoto o "Predefinito" per usare le impostazioni generali (Opzioni Eventi).</p>
|
||||
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<a href="events.php" class="btn btn-light btn-back"><i class="bx bx-arrow-back"></i> Indietro</a>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
<i class="bx bx-save"></i> <?php echo $isEdit ? 'Salva modifiche' : 'Crea evento'; ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
/* ---------- Date/ora con flatpickr (fine = inizio + 1h) ---------- */
|
||||
var fpOpts = {
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
minuteIncrement: 15,
|
||||
dateFormat: "Y-m-d H:i",
|
||||
altInput: true,
|
||||
altFormat: "l j F Y, H:i",
|
||||
locale: "it"
|
||||
};
|
||||
|
||||
var endPicker = flatpickr("#ends_at", fpOpts);
|
||||
flatpickr("#publish_at", fpOpts);
|
||||
document.getElementById('schedule_enabled').addEventListener('change', function() {
|
||||
document.getElementById('publish-at-wrap').style.display = this.checked ? '' : 'none';
|
||||
});
|
||||
var startPicker = flatpickr("#starts_at", Object.assign({}, fpOpts, {
|
||||
onChange: function(dates) {
|
||||
if (!dates[0]) return;
|
||||
endPicker.set('minDate', dates[0]);
|
||||
var end = endPicker.selectedDates[0];
|
||||
if (!end || end <= dates[0]) {
|
||||
endPicker.setDate(new Date(dates[0].getTime() + 60 * 60 * 1000), false);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
/* ---------- Sede / indirizzo ---------- */
|
||||
var addressInput = document.getElementById('address-input');
|
||||
var latInput = document.getElementById('lat-input');
|
||||
var lngInput = document.getElementById('lng-input');
|
||||
var geoHint = document.getElementById('geo-hint');
|
||||
var locationSelect = document.getElementById('location-select');
|
||||
var currentAddress = <?php echo json_encode($val['address'], JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
function clearCoords() {
|
||||
latInput.value = '';
|
||||
lngInput.value = '';
|
||||
}
|
||||
|
||||
// Dichiarazione (hoisted): usata anche in fase di preselezione qui sotto.
|
||||
function applyLocation() {
|
||||
if (!locationSelect) return;
|
||||
var opt = locationSelect.options[locationSelect.selectedIndex];
|
||||
if (locationSelect.value === 'custom') {
|
||||
// Indirizzo personalizzato: campo vuoto, nessuna coordinata finché non premi "Calcola"
|
||||
addressInput.value = '';
|
||||
clearCoords();
|
||||
geoHint.textContent = 'Scrivi l\'indirizzo e premi "Calcola" per le coordinate.';
|
||||
addressInput.focus();
|
||||
} else {
|
||||
// Sede salvata: indirizzo e coordinate REALI della sede
|
||||
addressInput.value = opt.getAttribute('data-address') || '';
|
||||
latInput.value = opt.getAttribute('data-lat') || '';
|
||||
lngInput.value = opt.getAttribute('data-lng') || '';
|
||||
geoHint.textContent = (latInput.value && lngInput.value) ?
|
||||
'Coordinate della sede caricate.' :
|
||||
'Questa sede non ha coordinate salvate: premi "Calcola".';
|
||||
}
|
||||
}
|
||||
|
||||
if (locationSelect) {
|
||||
var matched = null;
|
||||
Array.prototype.forEach.call(locationSelect.options, function(opt) {
|
||||
if (opt.value !== 'custom' && currentAddress !== '' && opt.getAttribute('data-address') === currentAddress) {
|
||||
matched = opt.value;
|
||||
}
|
||||
});
|
||||
if (currentAddress === '') {
|
||||
locationSelect.selectedIndex = 0; // nuovo evento -> sede di default (scuola)
|
||||
applyLocation();
|
||||
} else if (matched) {
|
||||
locationSelect.value = matched; // l'indirizzo salvato combacia con una sede
|
||||
applyLocation();
|
||||
} else {
|
||||
locationSelect.value = 'custom'; // indirizzo personalizzato salvato: NON toccare i valori esistenti
|
||||
}
|
||||
locationSelect.addEventListener('change', applyLocation);
|
||||
}
|
||||
|
||||
// Se modifichi l'indirizzo a mano, le vecchie coordinate non valgono più:
|
||||
// le azzero subito, così non restano valori "di default" fuorvianti.
|
||||
addressInput.addEventListener('input', function() {
|
||||
clearCoords();
|
||||
geoHint.textContent = 'Indirizzo modificato: premi "Calcola" per aggiornare le coordinate.';
|
||||
});
|
||||
|
||||
/* ---------- Geocoding indirizzo -> lat/lng ---------- */
|
||||
document.getElementById('btn-geocode').addEventListener('click', function() {
|
||||
var q = addressInput.value.trim();
|
||||
if (q === '') {
|
||||
addressInput.focus();
|
||||
return;
|
||||
}
|
||||
geoHint.textContent = 'Ricerca coordinate…';
|
||||
fetch('event_geocode.php?q=' + encodeURIComponent(q))
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
latInput.value = data.lat;
|
||||
lngInput.value = data.lng;
|
||||
latInput.classList.add('coord-flash');
|
||||
lngInput.classList.add('coord-flash');
|
||||
setTimeout(function() {
|
||||
latInput.classList.remove('coord-flash');
|
||||
lngInput.classList.remove('coord-flash');
|
||||
}, 900);
|
||||
geoHint.innerHTML = '<span style="color:#1a7f52;font-weight:600;">✓ Coordinate aggiornate:</span> ' +
|
||||
data.lat + ', ' + data.lng + (data.display ? ' — ' + data.display : '');
|
||||
} else {
|
||||
geoHint.textContent = data.error || 'Indirizzo non trovato.';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
geoHint.textContent = 'Errore durante il geocoding.';
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- Editor Quill ---------- */
|
||||
var quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{
|
||||
'header': [2, 3, false]
|
||||
}],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{
|
||||
'list': 'ordered'
|
||||
}, {
|
||||
'list': 'bullet'
|
||||
}],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
]
|
||||
}
|
||||
});
|
||||
var initialHtml = <?php echo json_encode($val['description'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;
|
||||
if (initialHtml && initialHtml.trim() !== '') {
|
||||
quill.clipboard.dangerouslyPasteHTML(initialHtml);
|
||||
}
|
||||
|
||||
/* ---------- Copertina drag&drop ---------- */
|
||||
var dz = document.getElementById('cover-dropzone');
|
||||
var fileInput = document.getElementById('cover-file');
|
||||
var dzEmpty = document.getElementById('dz-empty');
|
||||
var dzPreview = document.getElementById('dz-preview');
|
||||
var dzProgress = document.getElementById('dz-progress');
|
||||
var dzImg = document.getElementById('dz-img');
|
||||
var coverHidden = document.getElementById('cover_image');
|
||||
|
||||
dz.addEventListener('click', function(ev) {
|
||||
if (ev.target.id === 'dz-remove' || ev.target.closest('#dz-remove')) return;
|
||||
fileInput.click();
|
||||
});
|
||||
['dragenter', 'dragover'].forEach(function(t) {
|
||||
dz.addEventListener(t, function(ev) {
|
||||
ev.preventDefault();
|
||||
dz.classList.add('dragover');
|
||||
});
|
||||
});
|
||||
['dragleave', 'drop'].forEach(function(t) {
|
||||
dz.addEventListener(t, function(ev) {
|
||||
ev.preventDefault();
|
||||
dz.classList.remove('dragover');
|
||||
});
|
||||
});
|
||||
dz.addEventListener('drop', function(ev) {
|
||||
if (ev.dataTransfer.files.length) uploadCover(ev.dataTransfer.files[0]);
|
||||
});
|
||||
fileInput.addEventListener('change', function() {
|
||||
if (fileInput.files.length) uploadCover(fileInput.files[0]);
|
||||
});
|
||||
document.getElementById('dz-remove').addEventListener('click', function() {
|
||||
coverHidden.value = '';
|
||||
dzImg.src = '';
|
||||
dzPreview.style.display = 'none';
|
||||
dzEmpty.style.display = '';
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
function uploadCover(file) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
Swal.fire('Formato non valido', 'Seleziona un file immagine.', 'warning');
|
||||
return;
|
||||
}
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
Swal.fire('File troppo grande', "L'immagine supera i 5 MB.", 'warning');
|
||||
return;
|
||||
}
|
||||
dzEmpty.style.display = 'none';
|
||||
dzPreview.style.display = 'none';
|
||||
dzProgress.style.display = 'block';
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('cover', file);
|
||||
fetch('event_upload_cover.php', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
dzProgress.style.display = 'none';
|
||||
if (data.ok) {
|
||||
coverHidden.value = data.path;
|
||||
dzImg.src = data.path + '?t=' + Date.now();
|
||||
dzPreview.style.display = 'block';
|
||||
} else {
|
||||
dzEmpty.style.display = '';
|
||||
Swal.fire('Errore', data.error || 'Caricamento non riuscito.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
dzProgress.style.display = 'none';
|
||||
dzEmpty.style.display = '';
|
||||
Swal.fire('Errore', 'Caricamento non riuscito.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Galleria foto (multi-upload) ---------- */
|
||||
var galleryInput = document.getElementById('gallery-input');
|
||||
var galleryGrid = document.getElementById('gallery-grid');
|
||||
var galleryFile = document.getElementById('gallery-file');
|
||||
var galleryDz = document.getElementById('gallery-dropzone');
|
||||
var galleryItems = [];
|
||||
try {
|
||||
galleryItems = JSON.parse(galleryInput.value || '[]');
|
||||
if (!Array.isArray(galleryItems)) galleryItems = [];
|
||||
} catch (e) {
|
||||
galleryItems = [];
|
||||
}
|
||||
|
||||
function renderGallery() {
|
||||
galleryGrid.innerHTML = '';
|
||||
galleryItems.forEach(function(path, idx) {
|
||||
var cell = document.createElement('div');
|
||||
cell.className = 'gallery-cell';
|
||||
var img = document.createElement('img');
|
||||
img.src = path;
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'gallery-remove';
|
||||
btn.setAttribute('data-idx', idx);
|
||||
btn.innerHTML = '×';
|
||||
cell.appendChild(img);
|
||||
cell.appendChild(btn);
|
||||
galleryGrid.appendChild(cell);
|
||||
});
|
||||
galleryInput.value = JSON.stringify(galleryItems);
|
||||
}
|
||||
renderGallery();
|
||||
|
||||
galleryGrid.addEventListener('click', function(ev) {
|
||||
var btn = ev.target.closest('.gallery-remove');
|
||||
if (!btn) return;
|
||||
galleryItems.splice(parseInt(btn.getAttribute('data-idx'), 10), 1);
|
||||
renderGallery();
|
||||
});
|
||||
galleryDz.addEventListener('click', function() {
|
||||
galleryFile.click();
|
||||
});
|
||||
['dragenter', 'dragover'].forEach(function(t) {
|
||||
galleryDz.addEventListener(t, function(ev) {
|
||||
ev.preventDefault();
|
||||
galleryDz.classList.add('dragover');
|
||||
});
|
||||
});
|
||||
['dragleave', 'drop'].forEach(function(t) {
|
||||
galleryDz.addEventListener(t, function(ev) {
|
||||
ev.preventDefault();
|
||||
galleryDz.classList.remove('dragover');
|
||||
});
|
||||
});
|
||||
galleryDz.addEventListener('drop', function(ev) {
|
||||
if (ev.dataTransfer.files.length) uploadGallery(ev.dataTransfer.files);
|
||||
});
|
||||
galleryFile.addEventListener('change', function() {
|
||||
if (galleryFile.files.length) uploadGallery(galleryFile.files);
|
||||
});
|
||||
|
||||
function uploadGallery(files) {
|
||||
Array.prototype.forEach.call(files, function(file) {
|
||||
if (!file.type.startsWith('image/')) return;
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
Swal.fire('File troppo grande', file.name + ' supera i 5 MB.', 'warning');
|
||||
return;
|
||||
}
|
||||
var fd = new FormData();
|
||||
fd.append('cover', file); // stesso handler della copertina
|
||||
fetch('event_upload_cover.php', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
galleryItems.push(data.path);
|
||||
renderGallery();
|
||||
} else {
|
||||
Swal.fire('Errore', data.error || 'Caricamento non riuscito.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Caricamento non riuscito.', 'error');
|
||||
});
|
||||
});
|
||||
galleryFile.value = '';
|
||||
}
|
||||
|
||||
/* ---------- Slug suggerito dal titolo ---------- */
|
||||
(function() {
|
||||
var titleInput = document.querySelector('input[name="title"]');
|
||||
var slugInput = document.querySelector('input[name="slug"]');
|
||||
if (!titleInput || !slugInput) return;
|
||||
// In modifica lo slug esiste già: lo consideriamo "toccato" e non lo sovrascriviamo.
|
||||
var slugEdited = slugInput.value.trim() !== '';
|
||||
|
||||
function slugify(s) {
|
||||
return s.toString().normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
}
|
||||
slugInput.addEventListener('input', function() {
|
||||
slugEdited = slugInput.value.trim() !== '';
|
||||
});
|
||||
titleInput.addEventListener('input', function() {
|
||||
if (!slugEdited) slugInput.value = slugify(titleInput.value);
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---------- Submit ---------- */
|
||||
document.getElementById('event-form').addEventListener('submit', function() {
|
||||
var html = quill.root.innerHTML;
|
||||
document.getElementById('description-input').value = (html === '<p><br></p>') ? '' : html;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Geocoding indirizzo -> coordinate. Proxy server-side verso OpenStreetMap /
|
||||
* Nominatim (gratuito, senza API key). Ritorna JSON:
|
||||
* { ok:true, lat:45.66, lng:8.79, display:"..." } oppure { ok:false, error:"..." }
|
||||
*
|
||||
* Nota: Nominatim chiede max 1 richiesta/secondo e uno User-Agent identificativo
|
||||
* (impostato sotto). Quando avrai la chiave Google Maps si può passare alla
|
||||
* Geocoding API di Google per maggiore precisione, cambiando solo questo file.
|
||||
*/
|
||||
if (function_exists('ob_get_level') && ob_get_level()) {
|
||||
ob_clean();
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$q = trim($_GET['q'] ?? '');
|
||||
if ($q === '') {
|
||||
echo json_encode(['ok' => false, 'error' => 'Indirizzo mancante.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$url = 'https://nominatim.openstreetmap.org/search?format=json&limit=1&addressdetails=0&q=' . rawurlencode($q);
|
||||
$ua = 'YogiBook/1.0 (info@yogasoul.it)';
|
||||
$json = false;
|
||||
|
||||
if (function_exists('curl_init')) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_USERAGENT => $ua,
|
||||
CURLOPT_TIMEOUT => 8,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
]);
|
||||
$json = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
if ($json === false) {
|
||||
$ctx = stream_context_create(['http' => [
|
||||
'method' => 'GET',
|
||||
'header' => "User-Agent: {$ua}\r\n",
|
||||
'timeout' => 8,
|
||||
]]);
|
||||
$json = @file_get_contents($url, false, $ctx);
|
||||
}
|
||||
|
||||
$data = $json ? json_decode($json, true) : null;
|
||||
|
||||
if (!empty($data[0]['lat']) && !empty($data[0]['lon'])) {
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'lat' => round((float) $data[0]['lat'], 7),
|
||||
'lng' => round((float) $data[0]['lon'], 7),
|
||||
'display' => $data[0]['display_name'] ?? '',
|
||||
]);
|
||||
} else {
|
||||
echo json_encode(['ok' => false, 'error' => 'Indirizzo non trovato. Prova a essere più specifico.']);
|
||||
}
|
||||
exit();
|
||||
@@ -0,0 +1,471 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Modulo Eventi - gestione Sedi (event_locations): elenco, aggiungi, modifica,
|
||||
* elimina e "imposta come predefinita".
|
||||
* - event_locations.php -> elenco + form nuovo
|
||||
* - event_locations.php?edit=ID -> form in modifica
|
||||
* - event_locations.php?delete=ID -> elimina (con conferma via modale)
|
||||
* - event_locations.php?makedefault=ID -> imposta predefinita
|
||||
* Il salvataggio (insert/update) passa da event_locations_save.php.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
// Azioni inline (prima di ogni output)
|
||||
if (isset($_GET['delete'])) {
|
||||
$delId = (int) $_GET['delete'];
|
||||
$pdo->prepare("DELETE FROM event_locations WHERE id = ?")->execute([$delId]);
|
||||
header('Location: event_locations.php?message=deleted');
|
||||
exit();
|
||||
}
|
||||
if (isset($_GET['makedefault'])) {
|
||||
$defId = (int) $_GET['makedefault'];
|
||||
// Una sola predefinita: Y su questa, N su tutte le altre
|
||||
$pdo->prepare("UPDATE event_locations SET is_default = IF(id = ?, 'Y', 'N'), updated_at = NOW()")->execute([$defId]);
|
||||
header('Location: event_locations.php?message=default');
|
||||
exit();
|
||||
}
|
||||
|
||||
$editId = isset($_GET['edit']) ? (int) $_GET['edit'] : 0;
|
||||
$edit = null;
|
||||
if ($editId > 0) {
|
||||
$st = $pdo->prepare("SELECT * FROM event_locations WHERE id = ?");
|
||||
$st->execute([$editId]);
|
||||
$edit = $st->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$tableMissing = false;
|
||||
$locations = [];
|
||||
try {
|
||||
$locations = $pdo->query("SELECT * FROM event_locations ORDER BY is_default DESC, name ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $ex) {
|
||||
$tableMissing = true;
|
||||
}
|
||||
|
||||
$message = $_GET['message'] ?? '';
|
||||
|
||||
$val = [
|
||||
'id' => $edit['id'] ?? 0,
|
||||
'name' => $edit['name'] ?? '',
|
||||
'address' => $edit['address'] ?? '',
|
||||
'lat' => $edit['lat'] ?? '',
|
||||
'lng' => $edit['lng'] ?? '',
|
||||
'is_default' => ($edit['is_default'] ?? 'N') === 'Y',
|
||||
];
|
||||
$isEdit = $editId > 0 && $edit;
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Sedi</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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e4e9;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #1ebf73;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.12);
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.btn-geocode {
|
||||
border: 1px solid #e2e4e9;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #2f7be7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-geocode:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.coord-flash {
|
||||
border-color: #1ebf73 !important;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.18) !important;
|
||||
transition: box-shadow .2s;
|
||||
}
|
||||
|
||||
.loc-table thead th {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: #6b7280;
|
||||
border-bottom: 2px solid #eef0f3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.loc-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.loc-name {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.default-badge {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
background: #e7f7ee;
|
||||
color: #1a7f52;
|
||||
}
|
||||
|
||||
.btn-act {
|
||||
border: 1px solid #e2e4e9;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-act:hover {
|
||||
background: #f3f4f6;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.btn-act.danger {
|
||||
color: #c0392b;
|
||||
border-color: #f2c9c4;
|
||||
}
|
||||
|
||||
.btn-act.danger:hover {
|
||||
background: #fdecea;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 10px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 34px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
color: #d5d9e0;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
border-radius: 8px;
|
||||
padding: 10px 26px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</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">Sedi</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<?php include('include/languageselection.php'); ?>
|
||||
<?php include('include/profiletopbar.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<?php include('include/sidebar.php'); ?>
|
||||
<header class="ishorizontal-topbar">
|
||||
<div class="navbar-header">
|
||||
<div class="d-flex"></div>
|
||||
</div>
|
||||
<div class="topnav">
|
||||
<div class="container-fluid">
|
||||
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid">
|
||||
|
||||
<?php if ($message === 'success') : ?>
|
||||
<div class="alert alert-success" role="alert">Sede salvata.</div>
|
||||
<?php elseif ($message === 'deleted') : ?>
|
||||
<div class="alert alert-warning" role="alert">Sede eliminata.</div>
|
||||
<?php elseif ($message === 'default') : ?>
|
||||
<div class="alert alert-success" role="alert">Sede predefinita aggiornata.</div>
|
||||
<?php elseif ($message === 'error') : ?>
|
||||
<div class="alert alert-danger" role="alert">Errore: nome e indirizzo sono obbligatori.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($tableMissing) : ?>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
La tabella <code>event_locations</code> non esiste ancora. Esegui la migration Phinx <code>CreateEventLocationsTable</code> e ricarica.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<!-- Form -->
|
||||
<div class="col-12">
|
||||
<div class="card admin-card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="section-title"><?php echo $isEdit ? 'Modifica sede' : 'Nuova sede'; ?></div>
|
||||
<p class="section-lead">Le sedi salvate compaiono nella tendina "Sede" del form evento.</p>
|
||||
|
||||
<form action="event_locations_save.php" method="POST" id="loc-form">
|
||||
<input type="hidden" name="id" value="<?php echo (int) $val['id']; ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nome sede *</label>
|
||||
<input type="text" class="form-control" name="name" required
|
||||
value="<?php echo e($val['name']); ?>" placeholder="Es. Studio YogaSoul">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Indirizzo *</label>
|
||||
<div class="d-flex gap-2">
|
||||
<input type="text" class="form-control" id="address-input" name="address" required
|
||||
value="<?php echo e($val['address']); ?>" placeholder="Via, numero, città">
|
||||
<button type="button" class="btn-geocode" id="btn-geocode"><i class="bx bx-map-pin"></i> Calcola</button>
|
||||
</div>
|
||||
<div class="hint" id="geo-hint">Premi "Calcola" per ottenere le coordinate dall'indirizzo.</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">Latitudine</label>
|
||||
<input type="text" class="form-control" id="lat-input" name="lat" value="<?php echo e($val['lat']); ?>" placeholder="45.6600">
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">Longitudine</label>
|
||||
<input type="text" class="form-control" id="lng-input" name="lng" value="<?php echo e($val['lng']); ?>" placeholder="8.7900">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="is_default" name="is_default" value="Y" <?php echo $val['is_default'] ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="is_default">Imposta come sede predefinita</label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-save"><i class="bx bx-save"></i> <?php echo $isEdit ? 'Salva' : 'Aggiungi'; ?></button>
|
||||
<?php if ($isEdit) : ?>
|
||||
<a href="event_locations.php" class="btn btn-light btn-save">Annulla</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Elenco -->
|
||||
<div class="col-12">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Sedi salvate</div>
|
||||
<p class="section-lead">Rubrica delle sedi riutilizzabili.</p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table loc-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sede</th>
|
||||
<th>Indirizzo</th>
|
||||
<th>Coordinate</th>
|
||||
<th class="text-end">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($locations)) : ?>
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
Nessuna sede salvata. Aggiungine una dal form a sinistra.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else :
|
||||
foreach ($locations as $loc) :
|
||||
$hasCoords = $loc['lat'] !== null && $loc['lng'] !== null && $loc['lat'] !== '' && $loc['lng'] !== '';
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="loc-name"><?php echo e($loc['name']); ?></span>
|
||||
<?php if ($loc['is_default'] === 'Y') : ?>
|
||||
<span class="default-badge"><i class="bx bxs-star"></i> Predefinita</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo e($loc['address']); ?></td>
|
||||
<td><?php echo $hasCoords ? e($loc['lat']) . ', ' . e($loc['lng']) : '<span style="color:#9ca3af;">—</span>'; ?></td>
|
||||
<td class="text-end">
|
||||
<a href="event_locations.php?edit=<?php echo (int) $loc['id']; ?>" class="btn-act"><i class="bx bx-edit"></i> Modifica</a>
|
||||
<?php if ($loc['is_default'] !== 'Y') : ?>
|
||||
<a href="event_locations.php?makedefault=<?php echo (int) $loc['id']; ?>" class="btn-act"><i class="bx bx-star"></i> Predefinita</a>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn-act danger btn-del"
|
||||
data-id="<?php echo (int) $loc['id']; ?>"
|
||||
data-name="<?php echo e($loc['name']); ?>">
|
||||
<i class="bx bx-trash"></i> Elimina
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/libs/metismenujs/metismenujs.min.js"></script>
|
||||
<script src="assets/libs/simplebar/simplebar.min.js"></script>
|
||||
<script src="assets/libs/eva-icons/eva.min.js"></script>
|
||||
<script src="assets/js/app.js"></script>
|
||||
|
||||
<script>
|
||||
var addressInput = document.getElementById('address-input');
|
||||
var latInput = document.getElementById('lat-input');
|
||||
var lngInput = document.getElementById('lng-input');
|
||||
var geoHint = document.getElementById('geo-hint');
|
||||
|
||||
// Modifica indirizzo -> le vecchie coordinate non valgono più
|
||||
addressInput.addEventListener('input', function() {
|
||||
latInput.value = '';
|
||||
lngInput.value = '';
|
||||
geoHint.textContent = 'Indirizzo modificato: premi "Calcola" per aggiornare le coordinate.';
|
||||
});
|
||||
|
||||
document.getElementById('btn-geocode').addEventListener('click', function() {
|
||||
var q = addressInput.value.trim();
|
||||
if (q === '') {
|
||||
addressInput.focus();
|
||||
return;
|
||||
}
|
||||
geoHint.textContent = 'Ricerca coordinate…';
|
||||
fetch('event_geocode.php?q=' + encodeURIComponent(q))
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
latInput.value = data.lat;
|
||||
lngInput.value = data.lng;
|
||||
latInput.classList.add('coord-flash');
|
||||
lngInput.classList.add('coord-flash');
|
||||
setTimeout(function() {
|
||||
latInput.classList.remove('coord-flash');
|
||||
lngInput.classList.remove('coord-flash');
|
||||
}, 900);
|
||||
geoHint.innerHTML = '<span style="color:#1a7f52;font-weight:600;">✓ Coordinate aggiornate:</span> ' +
|
||||
data.lat + ', ' + data.lng + (data.display ? ' — ' + data.display : '');
|
||||
} else {
|
||||
geoHint.textContent = data.error || 'Indirizzo non trovato.';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
geoHint.textContent = 'Errore durante il geocoding.';
|
||||
});
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
$('.btn-del').on('click', function() {
|
||||
var id = $(this).data('id');
|
||||
var name = $(this).data('name');
|
||||
Swal.fire({
|
||||
title: 'Eliminare la sede?',
|
||||
html: 'Stai per eliminare <strong>' + $('<div>').text(name).html() + '</strong>.<br><small>Gli eventi che la usavano mantengono il loro indirizzo.</small>',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Elimina',
|
||||
cancelButtonText: 'Chiudi',
|
||||
confirmButtonColor: '#e74c3c',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = 'event_locations.php?delete=' + id;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Salvataggio sede (insert/update) da event_locations.php.
|
||||
* Se "predefinita" è spuntata, la imposta come unica default.
|
||||
*/
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: event_locations.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$id = isset($_POST['id']) ? (int) $_POST['id'] : 0;
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$isDefault = (isset($_POST['is_default']) && $_POST['is_default'] === 'Y') ? 'Y' : 'N';
|
||||
|
||||
$lat = ($_POST['lat'] ?? '') !== '' ? (float) $_POST['lat'] : null;
|
||||
$lng = ($_POST['lng'] ?? '') !== '' ? (float) $_POST['lng'] : null;
|
||||
|
||||
if ($name === '' || $address === '') {
|
||||
header('Location: event_locations.php?message=error');
|
||||
exit();
|
||||
}
|
||||
|
||||
try {
|
||||
if ($id > 0) {
|
||||
$stmt = $pdo->prepare("UPDATE event_locations
|
||||
SET name = :name, address = :address, lat = :lat, lng = :lng,
|
||||
is_default = :is_default, updated_at = NOW()
|
||||
WHERE id = :id");
|
||||
$stmt->execute([
|
||||
':name' => $name,
|
||||
':address' => $address,
|
||||
':lat' => $lat,
|
||||
':lng' => $lng,
|
||||
':is_default' => $isDefault,
|
||||
':id' => $id,
|
||||
]);
|
||||
$savedId = $id;
|
||||
} else {
|
||||
$stmt = $pdo->prepare("INSERT INTO event_locations (name, address, lat, lng, is_default, created_at, updated_at)
|
||||
VALUES (:name, :address, :lat, :lng, :is_default, NOW(), NOW())");
|
||||
$stmt->execute([
|
||||
':name' => $name,
|
||||
':address' => $address,
|
||||
':lat' => $lat,
|
||||
':lng' => $lng,
|
||||
':is_default' => $isDefault,
|
||||
]);
|
||||
$savedId = (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// Una sola predefinita
|
||||
if ($isDefault === 'Y') {
|
||||
$pdo->prepare("UPDATE event_locations SET is_default = IF(id = ?, 'Y', 'N'), updated_at = NOW()")->execute([$savedId]);
|
||||
}
|
||||
|
||||
header('Location: event_locations.php?message=success');
|
||||
exit();
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[event_locations_save] ' . $ex->getMessage());
|
||||
header('Location: event_locations.php?message=error');
|
||||
exit();
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Opzioni Eventi (lato scuola): indirizzo per le copie, quali email mettere in
|
||||
* copia (per tipo), e impostazioni promemoria. Salva in event_options_save.php.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$opt = $pdo->query("SELECT * FROM `option` LIMIT 1")->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
$message = $_GET['message'] ?? '';
|
||||
|
||||
$ccEmail = $opt['event_notify_email'] ?? '';
|
||||
$reminderH = (int) ($opt['event_reminder_hours'] ?? 24);
|
||||
$offerH = (int) ($opt['event_offer_expiry_hours'] ?? 24);
|
||||
function isY($opt, $k)
|
||||
{
|
||||
return ($opt[$k] ?? 'N') === 'Y';
|
||||
}
|
||||
|
||||
$types = [
|
||||
['event_cc_booking', 'Prenotazione', 'Quando qualcuno prenota (o entra in lista d\'attesa).'],
|
||||
['event_cc_promotion', 'Promozione lista d\'attesa', 'Quando una persona in attesa viene promossa.'],
|
||||
['event_cc_user_cancel', 'Disdetta utente', 'Quando un iscritto annulla la sua prenotazione.'],
|
||||
['event_cc_event_cancel', 'Annullo evento', 'Quando annulli un intero evento (a tutti gli iscritti).'],
|
||||
['event_cc_reminder', 'Promemoria', 'Il promemoria inviato prima dell\'evento.'],
|
||||
];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Opzioni Eventi</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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e4e9;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #1ebf73;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.12);
|
||||
}
|
||||
|
||||
.opt-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.opt-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.opt-label {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.opt-help {
|
||||
color: #9ca3af;
|
||||
font-size: 13px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.form-switch .form-check-input {
|
||||
width: 3em;
|
||||
height: 1.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.save-bar {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
</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">Opzioni Eventi</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<?php include('include/languageselection.php'); ?>
|
||||
<?php include('include/profiletopbar.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<?php include('include/sidebar.php'); ?>
|
||||
<header class="ishorizontal-topbar">
|
||||
<div class="navbar-header">
|
||||
<div class="d-flex"></div>
|
||||
</div>
|
||||
<div class="topnav">
|
||||
<div class="container-fluid">
|
||||
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid">
|
||||
<?php if ($message === 'success') : ?>
|
||||
<div class="alert alert-success" role="alert">Opzioni salvate.</div>
|
||||
<?php elseif ($message === 'error') : ?>
|
||||
<div class="alert alert-danger" role="alert">Errore durante il salvataggio.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="event_options_save.php" method="POST">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card admin-card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Copie alla scuola</div>
|
||||
<p class="section-lead">Scegli su quali email l'insegnante/scuola riceve una copia (CC).</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email della scuola per le copie</label>
|
||||
<input type="email" class="form-control" name="event_notify_email" value="<?php echo e($ccEmail); ?>" placeholder="info@yogasoul.it">
|
||||
<div class="opt-help">Se vuota, non viene inviata nessuna copia anche se le voci sotto sono attive.</div>
|
||||
</div>
|
||||
|
||||
<?php foreach ($types as $t) : ?>
|
||||
<div class="opt-row">
|
||||
<div>
|
||||
<div class="opt-label"><?php echo e($t[1]); ?></div>
|
||||
<div class="opt-help"><?php echo e($t[2]); ?></div>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" name="<?php echo e($t[0]); ?>" value="Y" <?php echo isY($opt, $t[0]) ? 'checked' : ''; ?>>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Promemoria</div>
|
||||
<p class="section-lead">Invio automatico del promemoria agli iscritti prima dell'evento (richiede il cron, vedi setup).</p>
|
||||
|
||||
<div class="opt-row">
|
||||
<div>
|
||||
<div class="opt-label">Attiva promemoria</div>
|
||||
<div class="opt-help">Invia un'email di promemoria agli iscritti confermati.</div>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" name="event_reminder_enabled" value="Y" <?php echo isY($opt, 'event_reminder_enabled') ? 'checked' : ''; ?>>
|
||||
</div>
|
||||
</div>
|
||||
<div class="opt-row">
|
||||
<div>
|
||||
<div class="opt-label">Ore prima dell'evento</div>
|
||||
<div class="opt-help">Quante ore prima dell'inizio inviare il promemoria.</div>
|
||||
</div>
|
||||
<div style="width:120px;">
|
||||
<input type="number" min="1" max="168" class="form-control" name="event_reminder_hours" value="<?php echo e($reminderH); ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="opt-row">
|
||||
<div>
|
||||
<div class="opt-label">Scadenza offerta lista d'attesa (ore)</div>
|
||||
<div class="opt-help">Default: quanto tempo una persona promossa ha per riconfermare prima che il posto passi al prossimo. Modificabile per singolo evento.</div>
|
||||
</div>
|
||||
<div style="width:120px;">
|
||||
<input type="number" min="1" max="336" class="form-control" name="event_offer_expiry_hours" value="<?php echo e($offerH); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save-bar">
|
||||
<button type="submit" class="btn btn-success"><i class="fas fa-save me-1"></i> Salva opzioni</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Salvataggio Opzioni Eventi (email/copie/promemoria) nella tabella `option`.
|
||||
*/
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: event_options.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function yn(string $k): string
|
||||
{
|
||||
return (isset($_POST[$k]) && $_POST[$k] === 'Y') ? 'Y' : 'N';
|
||||
}
|
||||
|
||||
$ccEmail = trim($_POST['event_notify_email'] ?? '');
|
||||
$reminderH = max(1, min(168, (int) ($_POST['event_reminder_hours'] ?? 24)));
|
||||
$offerH = max(1, min(336, (int) ($_POST['event_offer_expiry_hours'] ?? 24)));
|
||||
|
||||
try {
|
||||
// Individua la riga singola di configurazione
|
||||
$idption = (int) $pdo->query("SELECT idption FROM `option` ORDER BY idption ASC LIMIT 1")->fetchColumn();
|
||||
|
||||
$sql = "UPDATE `option` SET
|
||||
event_notify_email = :cc_email,
|
||||
event_cc_booking = :cc_booking,
|
||||
event_cc_promotion = :cc_promotion,
|
||||
event_cc_user_cancel = :cc_user_cancel,
|
||||
event_cc_event_cancel = :cc_event_cancel,
|
||||
event_cc_reminder = :cc_reminder,
|
||||
event_reminder_enabled = :rem_enabled,
|
||||
event_reminder_hours = :rem_hours,
|
||||
event_offer_expiry_hours = :offer_hours
|
||||
WHERE idption = :idption";
|
||||
$pdo->prepare($sql)->execute([
|
||||
':cc_email' => $ccEmail !== '' ? $ccEmail : null,
|
||||
':cc_booking' => yn('event_cc_booking'),
|
||||
':cc_promotion' => yn('event_cc_promotion'),
|
||||
':cc_user_cancel' => yn('event_cc_user_cancel'),
|
||||
':cc_event_cancel' => yn('event_cc_event_cancel'),
|
||||
':cc_reminder' => yn('event_cc_reminder'),
|
||||
':rem_enabled' => yn('event_reminder_enabled'),
|
||||
':rem_hours' => $reminderH,
|
||||
':offer_hours' => $offerH,
|
||||
':idption' => $idption,
|
||||
]);
|
||||
|
||||
header('Location: event_options.php?message=success');
|
||||
exit();
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[event_options_save] ' . $ex->getMessage());
|
||||
header('Location: event_options.php?message=error');
|
||||
exit();
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/event_waitlist.php');
|
||||
require_once('include/event_mailer.php');
|
||||
|
||||
/**
|
||||
* Modulo Eventi - elenco partecipanti di un evento.
|
||||
* Mostra riepilogo (posti, iscrizioni, incasso) e la tabella delle prenotazioni.
|
||||
* Gestisce anche l'annullamento della singola iscrizione via ?cancel_booking=ID.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function initials(string $first, string $last): string
|
||||
{
|
||||
$ini = strtoupper(mb_substr(trim($first), 0, 1) . mb_substr(trim($last), 0, 1));
|
||||
return $ini !== '' ? $ini : '?';
|
||||
}
|
||||
|
||||
function avatarColor(string $seed): string
|
||||
{
|
||||
$palette = ['#1ebf73', '#2980b9', '#8e44ad', '#e67e22', '#16a085', '#c0392b', '#2c3e50', '#d35400'];
|
||||
return $palette[abs(crc32($seed)) % count($palette)];
|
||||
}
|
||||
|
||||
$eventId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
if ($eventId <= 0) {
|
||||
header('Location: events.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Annullamento singola iscrizione (deve avvenire prima di ogni output)
|
||||
if (isset($_GET['cancel_booking'])) {
|
||||
$bookingId = (int) $_GET['cancel_booking'];
|
||||
$promoted = [];
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
$pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([$eventId]);
|
||||
|
||||
$bl = $pdo->prepare("SELECT status FROM event_bookings WHERE id = ? AND event_id = ?");
|
||||
$bl->execute([$bookingId, $eventId]);
|
||||
$bStatus = $bl->fetchColumn();
|
||||
|
||||
if ($bStatus !== false && $bStatus !== 'cancelled') {
|
||||
$wasOccupying = in_array($bStatus, ['confirmed', 'pending', 'offered'], true);
|
||||
$pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ? AND event_id = ?")
|
||||
->execute([$bookingId, $eventId]);
|
||||
if ($wasOccupying) {
|
||||
// libera posti -> offre alla prima in lista d'attesa
|
||||
$promoted = promoteFromWaitlist($pdo, $eventId);
|
||||
}
|
||||
}
|
||||
$pdo->commit();
|
||||
|
||||
// Email di promozione (E, con riconferma) ai nuovi "offered"
|
||||
if (!empty($promoted)) {
|
||||
$ev = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$ev->execute([$eventId]);
|
||||
$evRow = $ev->fetch(PDO::FETCH_ASSOC);
|
||||
if ($evRow) {
|
||||
foreach ($promoted as $pr) {
|
||||
try {
|
||||
yse_mail_promotion($pr, $evRow);
|
||||
} catch (Throwable $me) {
|
||||
error_log('[promo mail] ' . $me->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('[participants cancel] ' . $ex->getMessage());
|
||||
}
|
||||
header('Location: event_participants.php?id=' . $eventId . '&message=booking_cancelled');
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$stmt->execute([$eventId]);
|
||||
$event = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$event) {
|
||||
header('Location: events.php?message=error');
|
||||
exit();
|
||||
}
|
||||
|
||||
$bStmt = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? ORDER BY created_at DESC, id DESC");
|
||||
$bStmt->execute([$eventId]);
|
||||
$bookings = $bStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Riepilogo: posti occupati = confermate/pending/offerte (in riconferma); lista d'attesa a parte
|
||||
$bookedSeats = 0;
|
||||
$waitingSeats = 0;
|
||||
$activeCount = 0;
|
||||
$revenue = 0.0;
|
||||
foreach ($bookings as $b) {
|
||||
if (in_array($b['status'], ['confirmed', 'pending', 'offered'], true)) {
|
||||
$bookedSeats += (int) $b['seats'];
|
||||
$activeCount++;
|
||||
$revenue += (float) $b['amount'];
|
||||
} elseif ($b['status'] === 'waiting') {
|
||||
$waitingSeats += (int) $b['seats'];
|
||||
}
|
||||
}
|
||||
$maxSeats = (int) $event['max_seats'];
|
||||
$message = $_GET['message'] ?? '';
|
||||
|
||||
function bookingBadge(string $status): array
|
||||
{
|
||||
switch ($status) {
|
||||
case 'confirmed':
|
||||
return ['Confermata', '#e7f7ee', '#1a7f52'];
|
||||
case 'cancelled':
|
||||
return ['Annullata', '#fdecea', '#c0392b'];
|
||||
case 'waiting':
|
||||
return ["Lista d'attesa", '#eef2fb', '#3a5bb8'];
|
||||
case 'offered':
|
||||
return ['Da riconfermare', '#fff4e5', '#b26a00'];
|
||||
default:
|
||||
return ['In attesa', '#fff4e5', '#b26a00'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Partecipanti</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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
background: #f7f8fa;
|
||||
border-radius: 12px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.part-table thead th {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: #6b7280;
|
||||
border-bottom: 2px solid #eef0f3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.part-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.user-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.guest-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
background: #eef0f3;
|
||||
border-radius: 10px;
|
||||
padding: 1px 7px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.btn-cancel-b {
|
||||
border: 1px solid #f2c9c4;
|
||||
background: #fff;
|
||||
color: #c0392b;
|
||||
border-radius: 8px;
|
||||
padding: 5px 11px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-cancel-b:hover {
|
||||
background: #fdecea;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 44px 10px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 34px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
color: #d5d9e0;
|
||||
}
|
||||
|
||||
.yes-mark {
|
||||
color: #1a7f52;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.no-mark {
|
||||
color: #c0392b;
|
||||
font-weight: 700;
|
||||
}
|
||||
</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">Partecipanti</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<?php include('include/languageselection.php'); ?>
|
||||
<?php include('include/profiletopbar.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<?php include('include/sidebar.php'); ?>
|
||||
<header class="ishorizontal-topbar">
|
||||
<div class="navbar-header">
|
||||
<div class="d-flex"></div>
|
||||
</div>
|
||||
<div class="topnav">
|
||||
<div class="container-fluid">
|
||||
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid">
|
||||
|
||||
<?php if ($message === 'booking_cancelled') : ?>
|
||||
<div class="alert alert-warning" role="alert">Iscrizione annullata.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card admin-card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="section-title">Partecipanti</div>
|
||||
<p class="section-lead mb-0">
|
||||
<strong style="color:#1f2937;"><?php echo e($event['title']); ?></strong>
|
||||
· <?php echo e(date('d-m-Y H:i', strtotime($event['starts_at']))); ?>
|
||||
</p>
|
||||
</div>
|
||||
<a href="events.php" class="btn btn-light" style="border-radius:8px;font-weight:600;"><i class="bx bx-arrow-back"></i> Tutti gli eventi</a>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo $bookedSeats; ?> / <?php echo $maxSeats; ?></div>
|
||||
<div class="stat-label">Posti prenotati</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo $waitingSeats; ?></div>
|
||||
<div class="stat-label">In lista d'attesa</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo $activeCount; ?></div>
|
||||
<div class="stat-label">Iscrizioni attive</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo e(number_format($revenue, 2, ',', '.')); ?> <?php echo e($event['currency']); ?></div>
|
||||
<div class="stat-label">Incasso stimato</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table part-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Partecipante</th>
|
||||
<th>Email</th>
|
||||
<th>Telefono</th>
|
||||
<th>Posti</th>
|
||||
<th>Marketing</th>
|
||||
<th>Iscritto il</th>
|
||||
<th>Stato</th>
|
||||
<th class="text-end">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($bookings)) : ?>
|
||||
<tr>
|
||||
<td colspan="8">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-users"></i>
|
||||
Ancora nessun iscritto a questo evento.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else :
|
||||
foreach ($bookings as $b) :
|
||||
// Il nome può essere un unico campo: proviamo a spezzarlo per le iniziali
|
||||
$nameParts = preg_split('/\s+/', trim($b['name']), 2);
|
||||
$first = $nameParts[0] ?? '';
|
||||
$last = $nameParts[1] ?? '';
|
||||
$isGuest = empty($b['user_id']);
|
||||
[$stLabel, $stBg, $stColor] = bookingBadge($b['status']);
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-cell">
|
||||
<div class="avatar" style="background: <?php echo avatarColor($b['name']); ?>;">
|
||||
<?php echo e(initials($first, $last)); ?>
|
||||
</div>
|
||||
<span class="user-name"><?php echo e($b['name']); ?>
|
||||
<?php if ($isGuest) : ?><span class="guest-tag">ospite</span><?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><?php echo e($b['email']); ?></td>
|
||||
<td><?php echo e($b['phone'] ?: '—'); ?></td>
|
||||
<td><?php echo (int) $b['seats']; ?></td>
|
||||
<td>
|
||||
<?php if (($b['marketing_opt_in'] ?? 'N') === 'Y') : ?>
|
||||
<span class="yes-mark">Sì</span>
|
||||
<?php else : ?>
|
||||
<span class="no-mark">No</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo e($b['created_at'] ? date('d-m-Y', strtotime($b['created_at'])) : '—'); ?></td>
|
||||
<td><span class="status-badge" style="background: <?php echo $stBg; ?>; color: <?php echo $stColor; ?>;"><?php echo e($stLabel); ?></span></td>
|
||||
<td class="text-end">
|
||||
<?php if ($b['status'] !== 'cancelled') : ?>
|
||||
<button type="button" class="btn-cancel-b btn-cancel-booking"
|
||||
data-id="<?php echo (int) $b['id']; ?>"
|
||||
data-name="<?php echo e($b['name']); ?>">
|
||||
<i class="bx bx-block"></i> Annulla
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<span style="color:#9ca3af;font-size:13px;">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/libs/metismenujs/metismenujs.min.js"></script>
|
||||
<script src="assets/libs/simplebar/simplebar.min.js"></script>
|
||||
<script src="assets/libs/eva-icons/eva.min.js"></script>
|
||||
<script src="assets/js/app.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.btn-cancel-booking').on('click', function() {
|
||||
var id = $(this).data('id');
|
||||
var name = $(this).data('name');
|
||||
Swal.fire({
|
||||
title: 'Annullare iscrizione?',
|
||||
html: 'Stai per annullare l\'iscrizione di <strong>' + $('<div>').text(name).html() + '</strong>.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Annulla iscrizione',
|
||||
cancelButtonText: 'Chiudi',
|
||||
confirmButtonColor: '#e74c3c',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = 'event_participants.php?id=<?php echo $eventId; ?>&cancel_booking=' + id;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/event_mailer.php');
|
||||
|
||||
/**
|
||||
* Promemoria manuale: apre un editor col testo prefissato (modificabile) e il
|
||||
* punto di ritrovo come link mappa (prefill dal luogo evento). Invio da
|
||||
* event_reminder_send.php a tutti gli iscritti confermati.
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$eventId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
if ($eventId <= 0) {
|
||||
header('Location: events.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$stmt->execute([$eventId]);
|
||||
$event = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$event) {
|
||||
header('Location: events.php?message=error');
|
||||
exit();
|
||||
}
|
||||
|
||||
$cnt = $pdo->prepare("SELECT COUNT(*) FROM event_bookings WHERE event_id = ? AND status = 'confirmed'");
|
||||
$cnt->execute([$eventId]);
|
||||
$recipients = (int) $cnt->fetchColumn();
|
||||
|
||||
$defaultMsg = "Ti ricordiamo il tuo prossimo appuntamento. Ti aspettiamo, a presto!";
|
||||
$meetingLink = yse_maps_link($event); // prefill: link mappa derivato dall'indirizzo/coordinate
|
||||
$message = $_GET['message'] ?? '';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Promemoria</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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e4e9;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #1ebf73;
|
||||
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.12);
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.recap {
|
||||
background: #f7f8fa;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
</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">Promemoria</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<?php include('include/languageselection.php'); ?>
|
||||
<?php include('include/profiletopbar.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<?php include('include/sidebar.php'); ?>
|
||||
<header class="ishorizontal-topbar">
|
||||
<div class="navbar-header">
|
||||
<div class="d-flex"></div>
|
||||
</div>
|
||||
<div class="topnav">
|
||||
<div class="container-fluid">
|
||||
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid">
|
||||
<?php if ($message === 'sent') : ?>
|
||||
<div class="alert alert-success" role="alert">Promemoria inviato.</div>
|
||||
<?php elseif ($message === 'error') : ?>
|
||||
<div class="alert alert-danger" role="alert">Errore durante l'invio.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Invia promemoria</div>
|
||||
<p class="section-lead">Controlla il testo e il punto di ritrovo, poi invia agli iscritti confermati.</p>
|
||||
|
||||
<div class="recap">
|
||||
<strong style="color:#1f2937;"><?php echo e($event['title']); ?></strong> · <?php echo e(date('d-m-Y H:i', strtotime($event['starts_at']))); ?><br>
|
||||
Destinatari confermati: <strong><?php echo $recipients; ?></strong>
|
||||
</div>
|
||||
|
||||
<form action="event_reminder_send.php" method="POST" id="rem-form">
|
||||
<input type="hidden" name="id" value="<?php echo (int) $eventId; ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Messaggio</label>
|
||||
<textarea class="form-control" name="message" rows="5"><?php echo e($defaultMsg); ?></textarea>
|
||||
<div class="hint">Data, ora e luogo dell'evento vengono aggiunti automaticamente sotto al messaggio.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Punto di ritrovo (link mappa)</label>
|
||||
<input type="url" class="form-control" name="meeting_link" value="<?php echo e($meetingLink); ?>" placeholder="Incolla qui il link di Google Maps del punto di ritrovo">
|
||||
<div class="hint">Prefissato dal luogo dell'evento. Puoi incollare un link di Google Maps più preciso (es. la condivisione della posizione). Lascia vuoto per non includerlo.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<a href="events.php" class="btn btn-light" style="border-radius:8px;font-weight:600;"><i class="bx bx-arrow-back"></i> Indietro</a>
|
||||
<button type="submit" class="btn btn-success" style="border-radius:8px;font-weight:600;" <?php echo $recipients === 0 ? 'disabled' : ''; ?>>
|
||||
<i class="bx bx-send"></i> Invia a <?php echo $recipients; ?> iscritt<?php echo $recipients === 1 ? 'o' : 'i'; ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
document.getElementById('rem-form').addEventListener('submit', function(ev) {
|
||||
ev.preventDefault();
|
||||
var form = this;
|
||||
Swal.fire({
|
||||
title: 'Inviare il promemoria?',
|
||||
text: 'Verrà inviato a <?php echo $recipients; ?> iscritti confermati.',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Invia',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#1ebf73',
|
||||
reverseButtons: true
|
||||
}).then(function(r) {
|
||||
if (r.isConfirmed) form.submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/event_mailer.php');
|
||||
|
||||
/**
|
||||
* Invio del promemoria manuale a tutti gli iscritti confermati di un evento.
|
||||
*/
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: events.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$eventId = (int) ($_POST['id'] ?? 0);
|
||||
$message = trim($_POST['message'] ?? '');
|
||||
$meetingLink = trim($_POST['meeting_link'] ?? '');
|
||||
|
||||
if ($eventId <= 0) {
|
||||
header('Location: events.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Accetta solo link http/https come punto di ritrovo
|
||||
if ($meetingLink !== '' && !preg_match('#^https?://#i', $meetingLink)) {
|
||||
$meetingLink = '';
|
||||
}
|
||||
|
||||
try {
|
||||
$es = $pdo->prepare("SELECT * FROM events WHERE id = ?");
|
||||
$es->execute([$eventId]);
|
||||
$event = $es->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$event) {
|
||||
header('Location: events.php?message=error');
|
||||
exit();
|
||||
}
|
||||
|
||||
$ps = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? AND status = 'confirmed'");
|
||||
$ps->execute([$eventId]);
|
||||
$participants = $ps->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($participants as $p) {
|
||||
try {
|
||||
yse_mail_reminder($p, $event, $message, $meetingLink);
|
||||
} catch (Throwable $me) {
|
||||
error_log('[reminder send] ' . $me->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->prepare("UPDATE events SET reminder_sent_at = NOW() WHERE id = ?")->execute([$eventId]);
|
||||
|
||||
header('Location: event_reminder.php?id=' . $eventId . '&message=sent');
|
||||
exit();
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[event_reminder_send] ' . $ex->getMessage());
|
||||
header('Location: event_reminder.php?id=' . $eventId . '&message=error');
|
||||
exit();
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Handler salvataggio evento (insert + update). Riceve il POST di event_form.php,
|
||||
* genera lo slug se mancante, valida i campi e fa redirect con ?message=.
|
||||
*/
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: events.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
// --- ID insegnante loggato -------------------------------------------------
|
||||
// headscript.php popola $iduserlogin da Auth::user()->present()->id
|
||||
$currentUserId = $iduserlogin ?? ($_SESSION['iduserlogin'] ?? null);
|
||||
|
||||
// --- Helper ----------------------------------------------------------------
|
||||
function slugify(string $text): string
|
||||
{
|
||||
$text = trim($text);
|
||||
if (function_exists('iconv')) {
|
||||
$conv = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text);
|
||||
if ($conv !== false) {
|
||||
$text = $conv;
|
||||
}
|
||||
}
|
||||
$text = strtolower($text);
|
||||
$text = preg_replace('/[^a-z0-9]+/', '-', $text);
|
||||
$text = trim($text, '-');
|
||||
return $text !== '' ? $text : 'evento';
|
||||
}
|
||||
|
||||
function uniqueSlug(PDO $pdo, string $base, int $excludeId): string
|
||||
{
|
||||
$slug = $base;
|
||||
$i = 2;
|
||||
while (true) {
|
||||
if ($excludeId > 0) {
|
||||
$st = $pdo->prepare("SELECT COUNT(*) FROM events WHERE slug = ? AND id <> ?");
|
||||
$st->execute([$slug, $excludeId]);
|
||||
} else {
|
||||
$st = $pdo->prepare("SELECT COUNT(*) FROM events WHERE slug = ?");
|
||||
$st->execute([$slug]);
|
||||
}
|
||||
if ((int) $st->fetchColumn() === 0) {
|
||||
return $slug;
|
||||
}
|
||||
$slug = $base . '-' . $i;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
function toDateTime(?string $v): ?string
|
||||
{
|
||||
if (!$v) {
|
||||
return null;
|
||||
}
|
||||
$v = str_replace('T', ' ', $v);
|
||||
$ts = strtotime($v);
|
||||
return $ts ? date('Y-m-d H:i:s', $ts) : null;
|
||||
}
|
||||
|
||||
// --- Lettura e validazione input -------------------------------------------
|
||||
$id = isset($_POST['id']) ? (int) $_POST['id'] : 0;
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
|
||||
$startsAt = toDateTime($_POST['starts_at'] ?? null);
|
||||
$maxSeats = (int) ($_POST['max_seats'] ?? 0);
|
||||
|
||||
$redirectOnError = $id > 0 ? "event_form.php?id={$id}&message=error" : "event_form.php?message=error";
|
||||
if ($title === '' || $startsAt === null || $maxSeats < 1) {
|
||||
header("Location: {$redirectOnError}");
|
||||
exit();
|
||||
}
|
||||
|
||||
$slugInput = trim($_POST['slug'] ?? '');
|
||||
$slugBase = slugify($slugInput !== '' ? $slugInput : $title);
|
||||
$slug = uniqueSlug($pdo, $slugBase, $id);
|
||||
|
||||
$endsAt = toDateTime($_POST['ends_at'] ?? null);
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$organizer = trim($_POST['organizer_name'] ?? '');
|
||||
$description = $_POST['description'] ?? '';
|
||||
$cost = round((float) ($_POST['cost'] ?? 0), 2);
|
||||
$maxPerBooking = max(1, (int) ($_POST['max_per_booking'] ?? 1));
|
||||
$coverImage = trim($_POST['cover_image'] ?? '');
|
||||
|
||||
$lat = ($_POST['lat'] ?? '') !== '' ? (float) $_POST['lat'] : null;
|
||||
$lng = ($_POST['lng'] ?? '') !== '' ? (float) $_POST['lng'] : null;
|
||||
|
||||
$currency = strtoupper(trim($_POST['currency'] ?? 'EUR'));
|
||||
if (!in_array($currency, ['EUR', 'USD', 'GBP', 'CHF'], true)) {
|
||||
$currency = 'EUR';
|
||||
}
|
||||
|
||||
$color = trim($_POST['color'] ?? '#4CAF50');
|
||||
if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) {
|
||||
$color = '#4CAF50';
|
||||
}
|
||||
|
||||
$status = $_POST['status'] ?? 'draft';
|
||||
if (!in_array($status, ['draft', 'published', 'cancelled'], true)) {
|
||||
$status = 'draft';
|
||||
}
|
||||
|
||||
// Apertura programmata: se attiva con data futura -> stato 'scheduled' + publish_at
|
||||
$scheduleEnabled = isset($_POST['schedule_enabled']) && $_POST['schedule_enabled'] === 'Y';
|
||||
$publishAt = toDateTime($_POST['publish_at'] ?? null);
|
||||
if ($scheduleEnabled && $publishAt && strtotime($publishAt) > time()) {
|
||||
$status = 'scheduled';
|
||||
} else {
|
||||
$publishAt = null; // niente programmazione (o data già passata)
|
||||
}
|
||||
|
||||
// Campi facoltativi (testo)
|
||||
$instructions = trim($_POST['instructions'] ?? '');
|
||||
$whatToBring = trim($_POST['what_to_bring'] ?? '');
|
||||
$suitableFor = trim($_POST['suitable_for'] ?? '');
|
||||
|
||||
$waitlistEnabled = (isset($_POST['waitlist_enabled']) && $_POST['waitlist_enabled'] === 'Y') ? 'Y' : 'N';
|
||||
|
||||
// Override per-evento (NULL = usa default scuola)
|
||||
$reminderEnabled = (isset($_POST['reminder_enabled']) && in_array($_POST['reminder_enabled'], ['Y', 'N'], true)) ? $_POST['reminder_enabled'] : null;
|
||||
$reminderHours = (($_POST['reminder_hours'] ?? '') !== '') ? max(1, min(168, (int) $_POST['reminder_hours'])) : null;
|
||||
$offerHours = (($_POST['offer_expiry_hours'] ?? '') !== '') ? max(1, min(336, (int) $_POST['offer_expiry_hours'])) : null;
|
||||
|
||||
// Galleria: array JSON di path -> validazione e re-encode pulito
|
||||
$gallery = null;
|
||||
$galleryRaw = $_POST['gallery'] ?? '';
|
||||
if ($galleryRaw !== '') {
|
||||
$decoded = json_decode($galleryRaw, true);
|
||||
if (is_array($decoded)) {
|
||||
$clean = [];
|
||||
foreach ($decoded as $p) {
|
||||
if (is_string($p) && $p !== '') {
|
||||
$clean[] = $p;
|
||||
}
|
||||
}
|
||||
$gallery = !empty($clean) ? json_encode(array_values($clean), JSON_UNESCAPED_SLASHES) : null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Insert / Update -------------------------------------------------------
|
||||
try {
|
||||
if ($id > 0) {
|
||||
$sql = "UPDATE events SET
|
||||
title = :title, slug = :slug, description = :description,
|
||||
instructions = :instructions, what_to_bring = :what_to_bring, suitable_for = :suitable_for,
|
||||
organizer_name = :organizer_name, starts_at = :starts_at, ends_at = :ends_at,
|
||||
address = :address, lat = :lat, lng = :lng, cost = :cost, currency = :currency,
|
||||
max_seats = :max_seats, max_per_booking = :max_per_booking, color = :color,
|
||||
waitlist_enabled = :waitlist_enabled,
|
||||
reminder_enabled = :reminder_enabled, reminder_hours = :reminder_hours, offer_expiry_hours = :offer_expiry_hours,
|
||||
cover_image = :cover_image, gallery = :gallery, status = :status, publish_at = :publish_at, updated_at = NOW()
|
||||
WHERE id = :id";
|
||||
$params = [':id' => $id];
|
||||
} else {
|
||||
$sql = "INSERT INTO events
|
||||
(user_id, title, slug, description, instructions, what_to_bring, suitable_for,
|
||||
organizer_name, starts_at, ends_at,
|
||||
address, lat, lng, cost, currency, max_seats, max_per_booking, color,
|
||||
waitlist_enabled,
|
||||
reminder_enabled, reminder_hours, offer_expiry_hours,
|
||||
cover_image, gallery, status, publish_at, created_at, updated_at)
|
||||
VALUES
|
||||
(:user_id, :title, :slug, :description, :instructions, :what_to_bring, :suitable_for,
|
||||
:organizer_name, :starts_at, :ends_at,
|
||||
:address, :lat, :lng, :cost, :currency, :max_seats, :max_per_booking, :color,
|
||||
:waitlist_enabled,
|
||||
:reminder_enabled, :reminder_hours, :offer_expiry_hours,
|
||||
:cover_image, :gallery, :status, :publish_at, NOW(), NOW())";
|
||||
$params = [':user_id' => $currentUserId];
|
||||
}
|
||||
|
||||
$params += [
|
||||
':title' => $title,
|
||||
':slug' => $slug,
|
||||
':description' => $description !== '' ? $description : null,
|
||||
':instructions' => $instructions !== '' ? $instructions : null,
|
||||
':what_to_bring' => $whatToBring !== '' ? $whatToBring : null,
|
||||
':suitable_for' => $suitableFor !== '' ? $suitableFor : null,
|
||||
':organizer_name' => $organizer !== '' ? $organizer : null,
|
||||
':starts_at' => $startsAt,
|
||||
':ends_at' => $endsAt,
|
||||
':address' => $address !== '' ? $address : null,
|
||||
':lat' => $lat,
|
||||
':lng' => $lng,
|
||||
':cost' => $cost,
|
||||
':currency' => $currency,
|
||||
':max_seats' => $maxSeats,
|
||||
':max_per_booking' => $maxPerBooking,
|
||||
':waitlist_enabled' => $waitlistEnabled,
|
||||
':reminder_enabled' => $reminderEnabled,
|
||||
':reminder_hours' => $reminderHours,
|
||||
':offer_expiry_hours' => $offerHours,
|
||||
':color' => $color,
|
||||
':cover_image' => $coverImage !== '' ? $coverImage : null,
|
||||
':gallery' => $gallery,
|
||||
':publish_at' => $publishAt,
|
||||
':status' => $status,
|
||||
];
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
header('Location: events.php?message=success');
|
||||
exit();
|
||||
} catch (Throwable $ex) {
|
||||
error_log('[event_save] ' . $ex->getMessage());
|
||||
header("Location: {$redirectOnError}");
|
||||
exit();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Upload copertina evento (chiamato in AJAX dalla dropzone di event_form.php).
|
||||
* Valida tipo/dimensione, salva in assets/uploads/events/ e ritorna JSON:
|
||||
* { ok:true, path:"assets/uploads/events/xxxx.jpg" } oppure { ok:false, error:"..." }
|
||||
*/
|
||||
if (function_exists('ob_get_level') && ob_get_level()) {
|
||||
ob_clean(); // pulisce eventuali whitespace dell'include prima del JSON
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$fail = function (string $msg): void {
|
||||
echo json_encode(['ok' => false, 'error' => $msg]);
|
||||
exit();
|
||||
};
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['cover'])) {
|
||||
$fail('Richiesta non valida.');
|
||||
}
|
||||
|
||||
$file = $_FILES['cover'];
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
$fail('Errore nel caricamento (codice ' . (int) $file['error'] . ').');
|
||||
}
|
||||
if ($file['size'] > 5 * 1024 * 1024) {
|
||||
$fail("L'immagine supera i 5 MB.");
|
||||
}
|
||||
|
||||
// Validazione MIME reale (non ci si fida dell'estensione)
|
||||
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'];
|
||||
$info = @getimagesize($file['tmp_name']);
|
||||
$mime = $info['mime'] ?? '';
|
||||
if (!isset($allowed[$mime])) {
|
||||
$fail('Formato non supportato. Usa JPG, PNG, WEBP o GIF.');
|
||||
}
|
||||
$ext = $allowed[$mime];
|
||||
|
||||
$relDir = 'assets/uploads/events';
|
||||
$absDir = __DIR__ . '/' . $relDir;
|
||||
if (!is_dir($absDir)) {
|
||||
@mkdir($absDir, 0775, true);
|
||||
}
|
||||
if (!is_dir($absDir) || !is_writable($absDir)) {
|
||||
$fail('Cartella non scrivibile: ' . $relDir . ' (crea la cartella e dai permessi di scrittura).');
|
||||
}
|
||||
|
||||
try {
|
||||
$rand = bin2hex(random_bytes(6));
|
||||
} catch (Throwable $e) {
|
||||
$rand = uniqid('', true);
|
||||
}
|
||||
$name = date('Ymd_His') . '_' . $rand . '.' . $ext;
|
||||
$dest = $absDir . '/' . $name;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $dest)) {
|
||||
$fail('Impossibile salvare il file.');
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true, 'path' => $relDir . '/' . $name]);
|
||||
exit();
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lista d'attesa — promozione automatica.
|
||||
*
|
||||
* IMPORTANTE: chiamare DENTRO una transazione in cui la riga dell'evento è già
|
||||
* bloccata (SELECT ... FOR UPDATE), tipicamente subito dopo aver liberato posti
|
||||
* (cancellazione di una prenotazione confermata). Così due operazioni simultanee
|
||||
* non promuovono oltre la capienza.
|
||||
*
|
||||
* Promuove in ordine di arrivo (FIFO) le prenotazioni 'waiting' che entrano nei
|
||||
* posti liberi e si ferma alla prima che non ci sta, per rispettare la coda
|
||||
* (non "salta" chi è davanti).
|
||||
*
|
||||
* @return array<int,array> le prenotazioni promosse (per inviare le email nello Step 4)
|
||||
*/
|
||||
if (!function_exists('promoteFromWaitlist')) {
|
||||
function promoteFromWaitlist(PDO $pdo, int $eventId): array
|
||||
{
|
||||
$ev = $pdo->prepare("SELECT max_seats FROM events WHERE id = ?");
|
||||
$ev->execute([$eventId]);
|
||||
$maxSeats = (int) $ev->fetchColumn();
|
||||
if ($maxSeats <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$os = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings
|
||||
WHERE event_id = ? AND status IN ('confirmed','pending')");
|
||||
$os->execute([$eventId]);
|
||||
$free = $maxSeats - (int) $os->fetchColumn();
|
||||
if ($free <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ws = $pdo->prepare("SELECT * FROM event_bookings
|
||||
WHERE event_id = ? AND status = 'waiting'
|
||||
ORDER BY created_at ASC, id ASC");
|
||||
$ws->execute([$eventId]);
|
||||
$waiting = $ws->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$promoted = [];
|
||||
$upd = $pdo->prepare("UPDATE event_bookings SET status = 'confirmed', updated_at = NOW() WHERE id = ?");
|
||||
foreach ($waiting as $w) {
|
||||
$need = (int) $w['seats'];
|
||||
if ($need <= $free) {
|
||||
$upd->execute([$w['id']]);
|
||||
$free -= $need;
|
||||
$w['status'] = 'confirmed';
|
||||
$promoted[] = $w;
|
||||
if ($free <= 0) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break; // rispetta la coda
|
||||
}
|
||||
}
|
||||
return $promoted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
/**
|
||||
* Modulo Eventi - elenco lato insegnante (stile Amelia).
|
||||
* Mostra gli eventi raggruppati per data con posti prenotati, stato e azioni:
|
||||
* - Partecipanti -> event_participants.php
|
||||
* - Modifica -> event_form.php?id=
|
||||
* - Annulla -> modale (SweetAlert2) che chiede se inviare la mail di
|
||||
* annullamento a tutti i partecipanti, poi event_delete.php
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$message = $_GET['message'] ?? '';
|
||||
|
||||
// Posti prenotati = SOMMA dei posti delle prenotazioni non annullate.
|
||||
$sql = "SELECT e.*,
|
||||
COALESCE(b.booked, 0) AS booked,
|
||||
COALESCE(b.bookings_count, 0) AS bookings_count,
|
||||
COALESCE(w.waiting, 0) AS waiting,
|
||||
u.first_name AS creator_first,
|
||||
u.last_name AS creator_last
|
||||
FROM events e
|
||||
LEFT JOIN (
|
||||
SELECT event_id,
|
||||
SUM(seats) AS booked,
|
||||
COUNT(*) AS bookings_count
|
||||
FROM event_bookings
|
||||
WHERE status IN ('confirmed','pending','offered')
|
||||
GROUP BY event_id
|
||||
) b ON b.event_id = e.id
|
||||
LEFT JOIN (
|
||||
SELECT event_id, SUM(seats) AS waiting
|
||||
FROM event_bookings
|
||||
WHERE status = 'waiting'
|
||||
GROUP BY event_id
|
||||
) w ON w.event_id = e.id
|
||||
LEFT JOIN auth_users u ON u.id = e.user_id
|
||||
ORDER BY e.starts_at ASC";
|
||||
$events = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$mesi = [
|
||||
1 => 'gennaio',
|
||||
2 => 'febbraio',
|
||||
3 => 'marzo',
|
||||
4 => 'aprile',
|
||||
5 => 'maggio',
|
||||
6 => 'giugno',
|
||||
7 => 'luglio',
|
||||
8 => 'agosto',
|
||||
9 => 'settembre',
|
||||
10 => 'ottobre',
|
||||
11 => 'novembre',
|
||||
12 => 'dicembre'
|
||||
];
|
||||
|
||||
function dataItaliana(string $dt, array $mesi): string
|
||||
{
|
||||
$t = strtotime($dt);
|
||||
return date('j', $t) . ' ' . $mesi[(int) date('n', $t)] . ' ' . date('Y', $t);
|
||||
}
|
||||
|
||||
/** @return array [label, bgColor, textColor] */
|
||||
function statusBadge(string $status): array
|
||||
{
|
||||
switch ($status) {
|
||||
case 'published':
|
||||
return ['Aperto', '#e7f7ee', '#1a7f52'];
|
||||
case 'scheduled':
|
||||
return ['Programmato', '#eef2fb', '#3a5bb8'];
|
||||
case 'cancelled':
|
||||
return ['Annullato', '#fdecea', '#c0392b'];
|
||||
default:
|
||||
return ['Bozza', '#eef0f3', '#6b7280'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Eventi</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="YogiBook - Prenotazione facile YogaSoul" name="description" />
|
||||
<link rel="shortcut icon" href="assets/images/favicon.ico">
|
||||
<link href="assets/css/bootstrap.min.css" id="bootstrap-style" rel="stylesheet" type="text/css" />
|
||||
<link href="assets/css/icons.min.css" rel="stylesheet" type="text/css" />
|
||||
<link href="assets/css/app.min.css" id="app-style" rel="stylesheet" type="text/css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.events-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.btn-new-event {
|
||||
background: #2f7be7;
|
||||
border: none;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 9px 18px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-new-event:hover {
|
||||
filter: brightness(0.94);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.events-table thead th {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: #6b7280;
|
||||
border-bottom: 2px solid #eef0f3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.events-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.date-group td {
|
||||
background: #f7f8fa;
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
font-size: 13px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.event-name {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.event-thumb {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.event-thumb.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.event-id {
|
||||
color: #9ca3af;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.seats-pill {
|
||||
display: inline-block;
|
||||
background: #eef6f1;
|
||||
color: #1a7f52;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.seats-pill.full {
|
||||
background: #fdecea;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.wait-pill {
|
||||
display: inline-block;
|
||||
background: #eef2fb;
|
||||
color: #3a5bb8;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
padding: 2px 9px;
|
||||
border-radius: 20px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.id-chip {
|
||||
display: inline-block;
|
||||
background: #eef0f3;
|
||||
color: #4b5563;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
padding: 1px 8px;
|
||||
border-radius: 6px;
|
||||
margin-left: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.btn-act {
|
||||
border: 1px solid #e2e4e9;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-act:hover {
|
||||
background: #f3f4f6;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.btn-act.danger {
|
||||
color: #c0392b;
|
||||
border-color: #f2c9c4;
|
||||
}
|
||||
|
||||
.btn-act.danger:hover {
|
||||
background: #fdecea;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 44px 10px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 34px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
color: #d5d9e0;
|
||||
}
|
||||
</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">Eventi</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<?php include('include/languageselection.php'); ?>
|
||||
<?php include('include/profiletopbar.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<?php include('include/sidebar.php'); ?>
|
||||
<header class="ishorizontal-topbar">
|
||||
<div class="navbar-header">
|
||||
<div class="d-flex"></div>
|
||||
</div>
|
||||
<div class="topnav">
|
||||
<div class="container-fluid">
|
||||
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid">
|
||||
|
||||
<?php if ($message === 'success') : ?>
|
||||
<div class="alert alert-success" role="alert">Evento salvato con successo.</div>
|
||||
<?php elseif ($message === 'cancelled') : ?>
|
||||
<div class="alert alert-warning" role="alert">Evento annullato.</div>
|
||||
<?php elseif ($message === 'error') : ?>
|
||||
<div class="alert alert-danger" role="alert">Si è verificato un errore. Controlla i dati e riprova.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="events-head">
|
||||
<div>
|
||||
<div class="section-title">Eventi</div>
|
||||
<p class="section-lead mb-0">Ciao <?php echo e($firstname ?? ''); ?>, gestisci qui i tuoi eventi: crea, modifica, vedi i partecipanti o annulla.</p>
|
||||
</div>
|
||||
<a href="event_form.php" class="btn btn-new-event"><i class="bx bx-plus align-middle"></i> Nuovo evento</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive mt-3">
|
||||
<table class="table events-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Orario</th>
|
||||
<th>Nome</th>
|
||||
<th>Organizzatore</th>
|
||||
<th>Prenotato</th>
|
||||
<th>Stato</th>
|
||||
<th class="text-end">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($events)) : ?>
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-calendar-plus"></i>
|
||||
Nessun evento ancora. Crea il primo con "Nuovo evento".
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else :
|
||||
$currentDate = null;
|
||||
foreach ($events as $ev) :
|
||||
$d = date('Y-m-d', strtotime($ev['starts_at']));
|
||||
if ($d !== $currentDate) :
|
||||
$currentDate = $d; ?>
|
||||
<tr class="date-group">
|
||||
<td colspan="6"><?php echo e(dataItaliana($ev['starts_at'], $mesi)); ?></td>
|
||||
</tr>
|
||||
<?php endif;
|
||||
|
||||
$organizer = trim((string) $ev['organizer_name']) !== ''
|
||||
? $ev['organizer_name']
|
||||
: trim(($ev['creator_first'] ?? '') . ' ' . ($ev['creator_last'] ?? ''));
|
||||
if ($organizer === '') {
|
||||
$organizer = '—';
|
||||
}
|
||||
|
||||
$booked = (int) $ev['booked'];
|
||||
$maxSeats = (int) $ev['max_seats'];
|
||||
$isFull = $maxSeats > 0 && $booked >= $maxSeats;
|
||||
[$stLabel, $stBg, $stColor] = statusBadge($ev['status']);
|
||||
|
||||
$timeLabel = date('H:i', strtotime($ev['starts_at']));
|
||||
if (!empty($ev['ends_at'])) {
|
||||
$timeLabel .= ' - ' . date('H:i', strtotime($ev['ends_at']));
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo e($timeLabel); ?></td>
|
||||
<td>
|
||||
<div class="event-name">
|
||||
<?php if (!empty($ev['cover_image'])) : ?>
|
||||
<img class="event-thumb" src="<?php echo e($ev['cover_image']); ?>" alt="">
|
||||
<?php else : ?>
|
||||
<span class="event-thumb placeholder" style="background: <?php echo e($ev['color']); ?>22;"><i class="bx bx-image" style="color: <?php echo e($ev['color']); ?>;"></i></span>
|
||||
<?php endif; ?>
|
||||
<span class="color-dot" style="background: <?php echo e($ev['color']); ?>;"></span>
|
||||
<span><?php echo e($ev['title']); ?> <span class="id-chip">ID <?php echo (int) $ev['id']; ?></span></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><?php echo e($organizer); ?></td>
|
||||
<td>
|
||||
<span class="seats-pill <?php echo $isFull ? 'full' : ''; ?>">
|
||||
<?php echo $booked; ?> / <?php echo $maxSeats; ?>
|
||||
</span>
|
||||
<?php if ((int) $ev['waiting'] > 0) : ?>
|
||||
<span class="wait-pill" title="In lista d'attesa">+<?php echo (int) $ev['waiting']; ?> in lista</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge" style="background: <?php echo $stBg; ?>; color: <?php echo $stColor; ?>;"><?php echo e($stLabel); ?></span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn-act btn-shortcode" data-code='[yogasoul_eventi ids="<?php echo (int) $ev['id']; ?>"]'><i class="bx bx-code-alt"></i> Shortcode</button>
|
||||
<a href="event_preview.php?id=<?php echo (int) $ev['id']; ?>" target="_blank" class="btn-act"><i class="bx bx-show"></i> Anteprima</a>
|
||||
<a href="event_participants.php?id=<?php echo (int) $ev['id']; ?>" class="btn-act"><i class="bx bx-group"></i> Partecipanti</a>
|
||||
<a href="event_form.php?id=<?php echo (int) $ev['id']; ?>" class="btn-act"><i class="bx bx-edit"></i> Modifica</a>
|
||||
<?php if ($ev['status'] === 'published' && strtotime($ev['starts_at']) > time()) : ?>
|
||||
<a href="event_reminder.php?id=<?php echo (int) $ev['id']; ?>" class="btn-act"><i class="bx bx-bell"></i> Promemoria</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($ev['status'] !== 'cancelled') : ?>
|
||||
<button type="button" class="btn-act danger btn-annulla"
|
||||
data-id="<?php echo (int) $ev['id']; ?>"
|
||||
data-title="<?php echo e($ev['title']); ?>"
|
||||
data-booked="<?php echo $booked; ?>">
|
||||
<i class="bx bx-block"></i> Annulla
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/libs/metismenujs/metismenujs.min.js"></script>
|
||||
<script src="assets/libs/simplebar/simplebar.min.js"></script>
|
||||
<script src="assets/libs/eva-icons/eva.min.js"></script>
|
||||
<script src="assets/js/app.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Copia shortcode
|
||||
$('.btn-shortcode').on('click', function() {
|
||||
var code = $(this).data('code');
|
||||
var btn = $(this);
|
||||
var original = btn.html();
|
||||
|
||||
function done() {
|
||||
btn.html('<i class="bx bx-check"></i> Copiato!');
|
||||
setTimeout(function() {
|
||||
btn.html(original);
|
||||
}, 1500);
|
||||
}
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(code).then(done).catch(function() {
|
||||
window.prompt('Copia lo shortcode:', code);
|
||||
});
|
||||
} else {
|
||||
window.prompt('Copia lo shortcode:', code);
|
||||
}
|
||||
});
|
||||
|
||||
$('.btn-annulla').on('click', function() {
|
||||
var id = $(this).data('id');
|
||||
var title = $(this).data('title');
|
||||
var booked = parseInt($(this).data('booked'), 10) || 0;
|
||||
|
||||
Swal.fire({
|
||||
title: "Annullare l'evento?",
|
||||
html: '<p style="margin-bottom:12px;">Stai per annullare <strong>' + $('<div>').text(title).html() + '</strong>.</p>' +
|
||||
'<div style="text-align:left; background:#f7f8fa; border-radius:8px; padding:12px;">' +
|
||||
' <label style="display:flex; align-items:flex-start; gap:8px; cursor:pointer; margin:0;">' +
|
||||
' <input type="checkbox" id="notify-check" ' + (booked > 0 ? 'checked' : '') + '>' +
|
||||
' <span>Invia email di annullamento ai partecipanti' +
|
||||
(booked > 0 ? ' <strong>(' + booked + ' post' + (booked === 1 ? 'o' : 'i') + ' prenotat' + (booked === 1 ? 'o' : 'i') + ')</strong>' : ' <em>(nessun iscritto)</em>') +
|
||||
'</span>' +
|
||||
' </label>' +
|
||||
'</div>',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Annulla evento',
|
||||
cancelButtonText: 'Chiudi',
|
||||
confirmButtonColor: '#e74c3c',
|
||||
reverseButtons: true,
|
||||
preConfirm: function() {
|
||||
return document.getElementById('notify-check').checked ? 1 : 0;
|
||||
}
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = 'event_delete.php?id=' + id + '¬ify=' + result.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Motore email del modulo Eventi (PHPMailer, legge le MAIL_* dal .env).
|
||||
*
|
||||
* Requisito una tantum: composer require phpmailer/phpmailer
|
||||
*
|
||||
* Builder per i 5 tipi:
|
||||
* A) yse_mail_booking() prenotazione (confermata o lista d'attesa)
|
||||
* B) yse_mail_user_cancel() disdetta fatta dall'utente
|
||||
* C) yse_mail_event_cancel() evento annullato dall'insegnante (a ciascun iscritto)
|
||||
* E) yse_mail_promotion() promozione da lista d'attesa (con RICONFERMA)
|
||||
* D) yse_mail_reminder() promemoria prima dell'evento
|
||||
*
|
||||
* Le copie alla scuola (CC) sono decise per-tipo dalla pagina "Opzioni Eventi".
|
||||
* L'indirizzo di copia è option.event_notify_email.
|
||||
*/
|
||||
|
||||
// Assicura l'autoload (di norma già caricato da db-functions.php)
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
$__al = dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
if (is_file($__al)) {
|
||||
require_once $__al;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined('YB_PUBLIC_BASE')) {
|
||||
define('YB_PUBLIC_BASE', 'https://yogibook.yogasoul.it/');
|
||||
}
|
||||
|
||||
/* ---------- Opzioni ---------- */
|
||||
function yse_mail_options(): array
|
||||
{
|
||||
static $opt = null;
|
||||
if ($opt !== null) {
|
||||
return $opt;
|
||||
}
|
||||
$row = [];
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
$row = $pdo->query("SELECT * FROM `option` LIMIT 1")->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
} catch (Throwable $e) {
|
||||
$row = [];
|
||||
}
|
||||
$opt = [
|
||||
'cc_email' => trim((string) ($row['event_notify_email'] ?? '')),
|
||||
'cc' => [
|
||||
'booking' => ($row['event_cc_booking'] ?? 'N') === 'Y',
|
||||
'user_cancel' => ($row['event_cc_user_cancel'] ?? 'N') === 'Y',
|
||||
'event_cancel' => ($row['event_cc_event_cancel'] ?? 'N') === 'Y',
|
||||
'reminder' => ($row['event_cc_reminder'] ?? 'N') === 'Y',
|
||||
'promotion' => ($row['event_cc_promotion'] ?? 'N') === 'Y',
|
||||
],
|
||||
];
|
||||
return $opt;
|
||||
}
|
||||
|
||||
/* ---------- Invio base ---------- */
|
||||
function yse_send(string $to, string $toName, string $subject, string $html, bool $ccSchool): bool
|
||||
{
|
||||
$env = $_ENV;
|
||||
try {
|
||||
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $env['MAIL_HOST'] ?? '';
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $env['MAIL_USERNAME'] ?? '';
|
||||
$mail->Password = $env['MAIL_PASSWORD'] ?? '';
|
||||
$enc = strtolower($env['MAIL_ENCRYPTION'] ?? 'ssl');
|
||||
$mail->SMTPSecure = ($enc === 'tls')
|
||||
? \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS
|
||||
: \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
|
||||
$mail->Port = (int) ($env['MAIL_PORT'] ?? 465);
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
$mail->setFrom($env['MAIL_FROM_ADDRESS'] ?? 'info@yogasoul.it', $env['MAIL_FROM_NAME'] ?? 'YogiBook');
|
||||
$mail->addAddress($to, $toName);
|
||||
|
||||
if ($ccSchool) {
|
||||
$ccEmail = yse_mail_options()['cc_email'];
|
||||
if ($ccEmail !== '' && strcasecmp($ccEmail, $to) !== 0) {
|
||||
$mail->addCC($ccEmail);
|
||||
}
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $html;
|
||||
$mail->AltBody = trim(preg_replace(
|
||||
'/\n{3,}/',
|
||||
"\n\n",
|
||||
strip_tags(str_replace(['</p>', '<br>', '<br/>', '<br />', '</div>'], "\n", $html))
|
||||
));
|
||||
|
||||
return $mail->send();
|
||||
} catch (Throwable $e) {
|
||||
error_log('[event mail] ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Helper contenuto ---------- */
|
||||
function yse_h($v): string
|
||||
{
|
||||
return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function yse_fmt_datetime(array $event): string
|
||||
{
|
||||
$mesi = [
|
||||
1 => 'gennaio',
|
||||
2 => 'febbraio',
|
||||
3 => 'marzo',
|
||||
4 => 'aprile',
|
||||
5 => 'maggio',
|
||||
6 => 'giugno',
|
||||
7 => 'luglio',
|
||||
8 => 'agosto',
|
||||
9 => 'settembre',
|
||||
10 => 'ottobre',
|
||||
11 => 'novembre',
|
||||
12 => 'dicembre'
|
||||
];
|
||||
$t = strtotime($event['starts_at']);
|
||||
$s = (int) date('j', $t) . ' ' . $mesi[(int) date('n', $t)] . ' ' . date('Y', $t) . ' · ' . date('H:i', $t);
|
||||
if (!empty($event['ends_at'])) {
|
||||
$s .= '–' . date('H:i', strtotime($event['ends_at']));
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
function yse_maps_link(array $event): string
|
||||
{
|
||||
$q = trim((string) ($event['address'] ?? ''));
|
||||
if ($q === '' && !empty($event['lat']) && !empty($event['lng'])) {
|
||||
$q = $event['lat'] . ',' . $event['lng'];
|
||||
}
|
||||
return $q !== '' ? 'https://www.google.com/maps/search/?api=1&query=' . rawurlencode($q) : '';
|
||||
}
|
||||
|
||||
function yse_event_info_block(array $event): string
|
||||
{
|
||||
$accent = (isset($event['color']) && preg_match('/^#[0-9a-fA-F]{6}$/', $event['color'])) ? $event['color'] : '#4CAF50';
|
||||
$when = yse_h(yse_fmt_datetime($event));
|
||||
$addr = trim((string) ($event['address'] ?? ''));
|
||||
$maps = yse_maps_link($event);
|
||||
|
||||
$rows = '<tr><td style="padding:6px 0;color:#8c837c;width:90px;">Quando</td><td style="padding:6px 0;color:#2b2622;font-weight:600;">' . $when . '</td></tr>';
|
||||
if ($addr !== '') {
|
||||
$mapsLink = $maps !== '' ? ' <a href="' . yse_h($maps) . '" style="color:' . $accent . ';">apri mappa</a>' : '';
|
||||
$rows .= '<tr><td style="padding:6px 0;color:#8c837c;">Dove</td><td style="padding:6px 0;color:#2b2622;font-weight:600;">' . yse_h($addr) . $mapsLink . '</td></tr>';
|
||||
}
|
||||
|
||||
$info = '';
|
||||
foreach ([['suitable_for', 'Per chi è adatto'], ['what_to_bring', 'Cosa portare'], ['instructions', 'Istruzioni']] as $f) {
|
||||
$val = trim((string) ($event[$f[0]] ?? ''));
|
||||
if ($val !== '') {
|
||||
$info .= '<p style="margin:10px 0 0;"><strong style="color:#2b2622;">' . $f[1] . ':</strong><br>' . nl2br(yse_h($val)) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
$html = '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-size:14px;border-collapse:collapse;margin:10px 0;">' . $rows . '</table>';
|
||||
if ($info !== '') {
|
||||
$html .= '<div style="background:#faf8f6;border-radius:10px;padding:12px 14px;font-size:14px;color:#4a433d;">' . $info . '</div>';
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
function yse_email_layout(array $event, string $heading, string $bodyHtml): string
|
||||
{
|
||||
$accent = (isset($event['color']) && preg_match('/^#[0-9a-fA-F]{6}$/', $event['color'])) ? $event['color'] : '#4CAF50';
|
||||
$from = $_ENV['MAIL_FROM_NAME'] ?? 'YogaSoul';
|
||||
$title = yse_h($event['title'] ?? '');
|
||||
return '
|
||||
<div style="background:#f2efec;padding:24px 0;font-family:Arial,Helvetica,sans-serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td align="center">
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:#fff;border-radius:16px;overflow:hidden;">
|
||||
<tr><td style="background:' . $accent . ';height:6px;"></td></tr>
|
||||
<tr><td style="padding:28px 30px 6px;">
|
||||
<div style="font-size:13px;letter-spacing:.05em;text-transform:uppercase;color:#8c837c;">' . yse_h($from) . '</div>
|
||||
<h1 style="margin:6px 0 2px;font-size:22px;color:#2b2622;">' . $heading . '</h1>
|
||||
<div style="font-size:17px;color:' . $accent . ';font-weight:600;">' . $title . '</div>
|
||||
</td></tr>
|
||||
<tr><td style="padding:10px 30px 26px;color:#4a433d;font-size:15px;line-height:1.6;">' . $bodyHtml . '</td></tr>
|
||||
<tr><td style="padding:16px 30px;background:#faf8f6;color:#9ca3af;font-size:12px;">Questa email è stata inviata da ' . yse_h($from) . '.</td></tr>
|
||||
</table>
|
||||
</td></tr></table>
|
||||
</div>';
|
||||
}
|
||||
|
||||
function yse_button(string $url, string $label, string $accent): string
|
||||
{
|
||||
return '<a href="' . yse_h($url) . '" style="display:inline-block;background:' . $accent . ';color:#fff;text-decoration:none;font-weight:600;padding:12px 22px;border-radius:10px;">' . yse_h($label) . '</a>';
|
||||
}
|
||||
|
||||
function yse_cancel_url(array $booking): string
|
||||
{
|
||||
return YB_PUBLIC_BASE . 'apiwp/cancel.php?token=' . rawurlencode($booking['token']);
|
||||
}
|
||||
|
||||
function yse_confirm_url(array $booking): string
|
||||
{
|
||||
return YB_PUBLIC_BASE . 'apiwp/confirm.php?token=' . rawurlencode($booking['token']);
|
||||
}
|
||||
|
||||
/* ---------- Builder per tipo ---------- */
|
||||
|
||||
/** A) Prenotazione (confermata o lista d'attesa) */
|
||||
function yse_mail_booking(array $booking, array $event): bool
|
||||
{
|
||||
$accent = $event['color'] ?? '#4CAF50';
|
||||
$waiting = ($booking['status'] ?? '') === 'waiting';
|
||||
$parts = explode(' ', trim($booking['name']));
|
||||
$name = $parts[0] ?? '';
|
||||
|
||||
if ($waiting) {
|
||||
$heading = "Sei in lista d'attesa";
|
||||
$intro = '<p>Ciao ' . yse_h($name) . ", ti abbiamo messo in lista d'attesa per questo evento. Ti avviseremo appena si libera un posto.</p>";
|
||||
} else {
|
||||
$heading = 'Prenotazione confermata';
|
||||
$intro = '<p>Ciao ' . yse_h($name) . ', la tua prenotazione è confermata. Ecco i dettagli:</p>';
|
||||
}
|
||||
|
||||
$body = $intro;
|
||||
$body .= yse_event_info_block($event);
|
||||
$body .= '<p style="margin-top:8px;">Posti prenotati: <strong>' . (int) $booking['seats'] . '</strong></p>';
|
||||
$body .= '<p style="margin:22px 0 6px;">Se non puoi più partecipare, puoi annullare qui:</p>';
|
||||
$body .= '<p>' . yse_button(yse_cancel_url($booking), 'Annulla prenotazione', $accent) . '</p>';
|
||||
|
||||
$subject = ($waiting ? "Lista d'attesa: " : 'Prenotazione confermata: ') . $event['title'];
|
||||
$html = yse_email_layout($event, $heading, $body);
|
||||
return yse_send($booking['email'], $booking['name'], $subject, $html, yse_mail_options()['cc']['booking']);
|
||||
}
|
||||
|
||||
/** B) Disdetta fatta dall'utente */
|
||||
function yse_mail_user_cancel(array $booking, array $event): bool
|
||||
{
|
||||
$parts = explode(' ', trim($booking['name']));
|
||||
$name = $parts[0] ?? '';
|
||||
$body = '<p>Ciao ' . yse_h($name) . ', la tua prenotazione per questo evento è stata <strong>annullata</strong>.</p>';
|
||||
$body .= yse_event_info_block($event);
|
||||
$body .= '<p style="margin-top:14px;">Ci dispiace non vederti questa volta. A presto!</p>';
|
||||
|
||||
$subject = 'Prenotazione annullata: ' . $event['title'];
|
||||
$html = yse_email_layout($event, 'Prenotazione annullata', $body);
|
||||
return yse_send($booking['email'], $booking['name'], $subject, $html, yse_mail_options()['cc']['user_cancel']);
|
||||
}
|
||||
|
||||
/** C) Evento annullato dall'insegnante (a ciascun iscritto) */
|
||||
function yse_mail_event_cancel(array $booking, array $event): bool
|
||||
{
|
||||
$parts = explode(' ', trim($booking['name']));
|
||||
$name = $parts[0] ?? '';
|
||||
$body = '<p>Ciao ' . yse_h($name) . ", ci dispiace: l'evento è stato <strong>annullato</strong>.</p>";
|
||||
$body .= yse_event_info_block($event);
|
||||
$body .= '<p style="margin-top:14px;">La tua prenotazione è stata annullata automaticamente. Per informazioni, rispondi pure a questa email.</p>';
|
||||
|
||||
$subject = 'Evento annullato: ' . $event['title'];
|
||||
$html = yse_email_layout($event, 'Evento annullato', $body);
|
||||
return yse_send($booking['email'], $booking['name'], $subject, $html, yse_mail_options()['cc']['event_cancel']);
|
||||
}
|
||||
|
||||
/** E) Promozione da lista d'attesa — con RICONFERMA */
|
||||
function yse_mail_promotion(array $booking, array $event): bool
|
||||
{
|
||||
$accent = $event['color'] ?? '#4CAF50';
|
||||
$parts = explode(' ', trim($booking['name']));
|
||||
$name = $parts[0] ?? '';
|
||||
$body = '<p>Ciao ' . yse_h($name) . ", buone notizie: si è liberato un posto e tocca a te!</p>";
|
||||
$body .= yse_event_info_block($event);
|
||||
$body .= '<p style="margin:22px 0 6px;"><strong>Conferma entro breve</strong> per tenere il posto:</p>';
|
||||
$body .= '<p>' . yse_button(yse_confirm_url($booking), 'Confermo, ci sarò', $accent) . '</p>';
|
||||
$body .= '<p style="margin-top:16px;font-size:13px;color:#8c837c;">Se non puoi partecipare, <a href="' . yse_h(yse_cancel_url($booking)) . '" style="color:' . $accent . ';">annulla qui</a> così offriamo il posto a un\'altra persona.</p>';
|
||||
|
||||
$subject = 'Si è liberato un posto: ' . $event['title'];
|
||||
$html = yse_email_layout($event, 'Un posto per te', $body);
|
||||
return yse_send($booking['email'], $booking['name'], $subject, $html, yse_mail_options()['cc']['promotion']);
|
||||
}
|
||||
|
||||
/** D) Promemoria prima dell'evento (testo e punto di ritrovo opzionali) */
|
||||
function yse_mail_reminder(array $booking, array $event, string $message = '', string $meetingLink = ''): bool
|
||||
{
|
||||
$accent = $event['color'] ?? '#4CAF50';
|
||||
$parts = explode(' ', trim($booking['name']));
|
||||
$name = $parts[0] ?? '';
|
||||
$note = $message !== '' ? nl2br(yse_h($message)) : 'Ti ricordiamo il tuo prossimo appuntamento.';
|
||||
|
||||
$body = '<p>Ciao ' . yse_h($name) . ',<br>' . $note . '</p>';
|
||||
$body .= yse_event_info_block($event);
|
||||
if ($meetingLink !== '') {
|
||||
$body .= '<p style="margin:16px 0 6px;"><strong>Punto di ritrovo:</strong> <a href="' . yse_h($meetingLink) . '" style="color:' . $accent . ';">apri la posizione sulla mappa</a></p>';
|
||||
}
|
||||
$body .= '<p style="margin:22px 0 6px;">Se non puoi più partecipare, avvisaci annullando:</p>';
|
||||
$body .= '<p>' . yse_button(yse_cancel_url($booking), 'Annulla prenotazione', $accent) . '</p>';
|
||||
|
||||
$subject = 'Promemoria: ' . $event['title'];
|
||||
$html = yse_email_layout($event, 'Ci vediamo presto', $body);
|
||||
return yse_send($booking['email'], $booking['name'], $subject, $html, yse_mail_options()['cc']['reminder']);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lista d'attesa — promozione automatica.
|
||||
*
|
||||
* IMPORTANTE: chiamare DENTRO una transazione in cui la riga dell'evento è già
|
||||
* bloccata (SELECT ... FOR UPDATE), tipicamente subito dopo aver liberato posti
|
||||
* (cancellazione di una prenotazione confermata). Così due operazioni simultanee
|
||||
* non promuovono oltre la capienza.
|
||||
*
|
||||
* Promuove in ordine di arrivo (FIFO) le prenotazioni 'waiting' che entrano nei
|
||||
* posti liberi e si ferma alla prima che non ci sta, per rispettare la coda
|
||||
* (non "salta" chi è davanti).
|
||||
*
|
||||
* @return array<int,array> le prenotazioni promosse (per inviare le email nello Step 4)
|
||||
*/
|
||||
if (!function_exists('promoteFromWaitlist')) {
|
||||
function promoteFromWaitlist(PDO $pdo, int $eventId): array
|
||||
{
|
||||
$ev = $pdo->prepare("SELECT max_seats FROM events WHERE id = ?");
|
||||
$ev->execute([$eventId]);
|
||||
$maxSeats = (int) $ev->fetchColumn();
|
||||
if ($maxSeats <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$os = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings
|
||||
WHERE event_id = ? AND status IN ('confirmed','pending','offered')");
|
||||
$os->execute([$eventId]);
|
||||
$free = $maxSeats - (int) $os->fetchColumn();
|
||||
if ($free <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ws = $pdo->prepare("SELECT * FROM event_bookings
|
||||
WHERE event_id = ? AND status = 'waiting'
|
||||
ORDER BY created_at ASC, id ASC");
|
||||
$ws->execute([$eventId]);
|
||||
$waiting = $ws->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$promoted = [];
|
||||
// Promozione = "offerta": tiene il posto ma richiede riconferma (confirm.php)
|
||||
$upd = $pdo->prepare("UPDATE event_bookings SET status = 'offered', offered_at = NOW(), updated_at = NOW() WHERE id = ?");
|
||||
foreach ($waiting as $w) {
|
||||
$need = (int) $w['seats'];
|
||||
if ($need <= $free) {
|
||||
$upd->execute([$w['id']]);
|
||||
$free -= $need;
|
||||
$w['status'] = 'offered';
|
||||
$promoted[] = $w;
|
||||
if ($free <= 0) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break; // rispetta la coda
|
||||
}
|
||||
}
|
||||
return $promoted;
|
||||
}
|
||||
}
|
||||
+77
-24
@@ -70,57 +70,110 @@
|
||||
</li>
|
||||
|
||||
<?php if (Auth::user()->hasRole('Admin')): ?>
|
||||
<li class="menu-title" data-key="t-applications">Amministrazione</li>
|
||||
|
||||
<li>
|
||||
<a href="admin-services.php" class="">
|
||||
<i class="bx bx-edit-alt icon nav-icon"></i>
|
||||
<span class="menu-item" data-key="t-email">Servizi</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Classi & Prenotazioni -->
|
||||
<li class="menu-title" data-key="t-admin-classi">Classi & Prenotazioni</li>
|
||||
|
||||
<li>
|
||||
<a href="adminpanel.php" class="">
|
||||
<i class="bx bxs-school icon nav-icon"></i>
|
||||
<span class="menu-item" data-key="t-email">Classi</span>
|
||||
<span class="menu-item">Classi</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="admin-services.php" class="">
|
||||
<i class="bx bx-edit-alt icon nav-icon"></i>
|
||||
<span class="menu-item">Servizi</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="pendingbookedclass.php" class="">
|
||||
<i class="bx bx-time-five icon nav-icon"></i>
|
||||
<span class="menu-item" data-key="t-email">Pending/Waiting</span>
|
||||
<span class="menu-item">Pending/Waiting</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="dayoff.php" class="">
|
||||
<i class="bx bx-power-off icon nav-icon"></i>
|
||||
<span class="menu-item" data-key="t-email">Day Off</span>
|
||||
<span class="menu-item">Day Off</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Eventi -->
|
||||
<li class="menu-title" data-key="t-admin-eventi">Eventi</li>
|
||||
|
||||
<li>
|
||||
<a href="events.php" class="">
|
||||
<i class="bx bx-calendar-star icon nav-icon"></i>
|
||||
<span class="menu-item">Eventi</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="event_locations.php" class="">
|
||||
<i class="bx bx-map icon nav-icon"></i>
|
||||
<span class="menu-item">Sedi</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="event_options.php" class="">
|
||||
<i class="bx bx-map icon nav-icon"></i>
|
||||
<span class="menu-item">Opzione Eventi</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Ordini -->
|
||||
<li class="menu-title" data-key="t-admin-ordini">Ordini</li>
|
||||
|
||||
<li>
|
||||
<a href="orderbooklist.php" class="">
|
||||
<i class="bx bx-list-check icon nav-icon"></i>
|
||||
<span class="menu-item" data-key="t-email">Order Pending</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="certificati.php" class="">
|
||||
<i class="bx bx-certification icon nav-icon"></i>
|
||||
<span class="menu-item" data-key="t-email">Certificati</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="situationusers.php" class="">
|
||||
<i class="bx bx-bar-chart-alt-2 icon nav-icon"></i>
|
||||
<span class="menu-item" data-key="t-email">Situazione</span>
|
||||
<span class="menu-item">Order Pending</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="ordersummary.php" class="">
|
||||
<i class="bx bx-receipt icon nav-icon"></i>
|
||||
<span class="menu-item" data-key="t-email">Sommario Ordini</span>
|
||||
<span class="menu-item">Sommario Ordini</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Report & Certificati -->
|
||||
<li class="menu-title" data-key="t-admin-report">Report & Certificati</li>
|
||||
|
||||
<li>
|
||||
<a href="situationusers.php" class="">
|
||||
<i class="bx bx-bar-chart-alt-2 icon nav-icon"></i>
|
||||
<span class="menu-item">Situazione</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="certificati.php" class="">
|
||||
<i class="bx bx-certification icon nav-icon"></i>
|
||||
<span class="menu-item">Certificati</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="statistiche.php" class="">
|
||||
<i class="bx bx-certification icon nav-icon"></i>
|
||||
<span class="menu-item">Statistiche</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Impostazioni -->
|
||||
<li class="menu-title" data-key="t-admin-settings">Impostazioni</li>
|
||||
|
||||
<li>
|
||||
<a href="optionsschool.php" class="">
|
||||
<i class="bx bx-slider-alt icon nav-icon"></i>
|
||||
<span class="menu-item">Opzioni Scuola</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="insegnanti.php" class="">
|
||||
<i class="bx bx-slider-alt icon nav-icon"></i>
|
||||
<span class="menu-item">Insegnanti</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Template (demo) -->
|
||||
<li class="menu-title" data-key="t-applications">Template</li>
|
||||
|
||||
<li>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Helper per la gestione della lista d'attesa (waiting list).
|
||||
*/
|
||||
|
||||
/**
|
||||
* La scuola ha attivato la lista d'attesa?
|
||||
* Legge l'unico record della tabella option.
|
||||
*/
|
||||
function isWaitingListEnabled(mysqli $conn): bool
|
||||
{
|
||||
$res = $conn->query("SELECT waitinglist_enabled FROM `option` LIMIT 1");
|
||||
if ($res && ($row = $res->fetch_assoc())) {
|
||||
return $row['waitinglist_enabled'] === 'Y';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capienza, posti impegnati (booked + pending) e posti liberi di una classe.
|
||||
*
|
||||
* @return array{maxcapacity:int, occupied:int, free:int}
|
||||
*/
|
||||
function getClassOccupancy(mysqli $conn, int $idserviceschedule): array
|
||||
{
|
||||
// Capienza (da service via serviceschedule)
|
||||
$cap = 0;
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT s.maxcapacity
|
||||
FROM serviceschedule ss
|
||||
JOIN service s ON ss.idservice = s.idservice
|
||||
WHERE ss.idserviceschedule = ?"
|
||||
);
|
||||
$stmt->bind_param("i", $idserviceschedule);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($rowCap = $res->fetch_assoc()) {
|
||||
$cap = (int) $rowCap['maxcapacity'];
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
// Posti impegnati: booked + pending
|
||||
$occupied = 0;
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT COUNT(*) AS occupied
|
||||
FROM bookingclass
|
||||
WHERE idserviceschedule = ?
|
||||
AND status IN ('booked', 'pending')"
|
||||
);
|
||||
$stmt->bind_param("i", $idserviceschedule);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($rowOcc = $res->fetch_assoc()) {
|
||||
$occupied = (int) $rowOcc['occupied'];
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
$free = $cap - $occupied;
|
||||
|
||||
return [
|
||||
'maxcapacity' => $cap,
|
||||
'occupied' => $occupied,
|
||||
'free' => $free < 0 ? 0 : $free,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide lo stato di una nuova prenotazione:
|
||||
* - 'pending' -> c'è posto (flusso normale, insegnante approva)
|
||||
* - 'waiting' -> classe piena E waiting list attiva
|
||||
* - 'full' -> classe piena E waiting list disattivata (NON prenotabile)
|
||||
*/
|
||||
function decideBookingStatus(mysqli $conn, int $idserviceschedule): string
|
||||
{
|
||||
$occ = getClassOccupancy($conn, $idserviceschedule);
|
||||
|
||||
if ($occ['free'] > 0) {
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
return isWaitingListEnabled($conn) ? 'waiting' : 'full';
|
||||
}
|
||||
|
||||
/**
|
||||
* Posizione (1-based) di un utente nella coda waiting di una classe.
|
||||
* 0 se non è in coda.
|
||||
*/
|
||||
function getWaitingPosition(mysqli $conn, int $idserviceschedule, int $iduser): int
|
||||
{
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT COUNT(*) AS pos
|
||||
FROM bookingclass
|
||||
WHERE idserviceschedule = ?
|
||||
AND status = 'waiting'
|
||||
AND created_at <= (
|
||||
SELECT created_at FROM bookingclass
|
||||
WHERE idserviceschedule = ? AND iduser = ? AND status = 'waiting'
|
||||
ORDER BY created_at ASC LIMIT 1
|
||||
)"
|
||||
);
|
||||
$stmt->bind_param("iii", $idserviceschedule, $idserviceschedule, $iduser);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
$pos = 0;
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
$pos = (int) $row['pos'];
|
||||
}
|
||||
$stmt->close();
|
||||
return $pos;
|
||||
}
|
||||
@@ -0,0 +1,783 @@
|
||||
<?php ob_start();
|
||||
require_once('include/headscript.php');
|
||||
/**
|
||||
* YogiBook - Gestione insegnanti (CRUD)
|
||||
* Tabella: employee
|
||||
* - un solo titolare (is_owner = 'Y'), non eliminabile né disattivabile
|
||||
* - notifiche email attivabili/disattivabili
|
||||
* - foto opzionale (JPG/PNG/WEBP, max 3 MB) in assets/images/teachers/
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
if (!function_exists('e')) {
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
if (empty($_SESSION['csrf_teachers'])) {
|
||||
$_SESSION['csrf_teachers'] = bin2hex(random_bytes(16));
|
||||
}
|
||||
$csrf = $_SESSION['csrf_teachers'];
|
||||
|
||||
define('PHOTO_DIR_REL', 'assets/images/teachers/');
|
||||
$photoDirAbs = __DIR__ . '/' . PHOTO_DIR_REL;
|
||||
|
||||
/* ================================================================ helper */
|
||||
|
||||
function flashRedirect(string $type, string $msg): void
|
||||
{
|
||||
$_SESSION['teachers_flash'] = [$type, $msg];
|
||||
header('Location: insegnanti.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
function teacherInitials(string $first, string $last): string
|
||||
{
|
||||
$ini = mb_strtoupper(mb_substr(trim($first), 0, 1) . mb_substr(trim($last), 0, 1));
|
||||
return $ini !== '' ? $ini : '?';
|
||||
}
|
||||
|
||||
function teacherColor(string $seed): string
|
||||
{
|
||||
$palette = ['#1ebf73', '#2980b9', '#8e44ad', '#e67e22', '#16a085', '#c0392b', '#2c3e50', '#b08d57'];
|
||||
return $palette[abs(crc32($seed)) % count($palette)];
|
||||
}
|
||||
|
||||
function deletePhotoFile(string $dir, ?string $name): void
|
||||
{
|
||||
if ($name && preg_match('/^teacher_[a-f0-9]{16}\.(jpg|png|webp)$/', $name) && is_file($dir . $name)) {
|
||||
@unlink($dir . $name);
|
||||
}
|
||||
}
|
||||
|
||||
/** Ritorna il nome file salvato, null se nessun file caricato. Lancia RuntimeException se non valido. */
|
||||
function handlePhotoUpload(array $file, string $dir): ?string
|
||||
{
|
||||
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
|
||||
return null;
|
||||
}
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new RuntimeException('Errore nel caricamento della foto.');
|
||||
}
|
||||
if ($file['size'] > 3 * 1024 * 1024) {
|
||||
throw new RuntimeException('La foto supera i 3 MB.');
|
||||
}
|
||||
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
|
||||
$ext = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'][$mime] ?? null;
|
||||
if (!$ext || @getimagesize($file['tmp_name']) === false) {
|
||||
throw new RuntimeException('Formato foto non valido: usa JPG, PNG o WEBP.');
|
||||
}
|
||||
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
|
||||
throw new RuntimeException('La cartella delle foto non è scrivibile.');
|
||||
}
|
||||
$name = 'teacher_' . bin2hex(random_bytes(8)) . '.' . $ext;
|
||||
if (!move_uploaded_file($file['tmp_name'], $dir . $name)) {
|
||||
throw new RuntimeException('Impossibile salvare la foto.');
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
function findTeacher(PDO $pdo, int $id): ?array
|
||||
{
|
||||
$st = $pdo->prepare("SELECT * FROM employee WHERE idemployee = :id");
|
||||
$st->execute([':id' => $id]);
|
||||
$row = $st->fetch();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/* ================================================================ azioni POST */
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!hash_equals($csrf, (string) ($_POST['csrf'] ?? ''))) {
|
||||
flashRedirect('danger', 'Sessione scaduta, riprova.');
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
|
||||
try {
|
||||
/* ---------- crea / modifica */
|
||||
if ($action === 'save') {
|
||||
$name = trim((string) ($_POST['nameemployee'] ?? ''));
|
||||
$surname = trim((string) ($_POST['surnameemployee'] ?? ''));
|
||||
$email = trim((string) ($_POST['email'] ?? ''));
|
||||
$phone = trim((string) ($_POST['phone'] ?? ''));
|
||||
$notify = isset($_POST['notifications']) ? 'Y' : 'N';
|
||||
$active = isset($_POST['active']) ? 'Y' : 'N';
|
||||
|
||||
if ($name === '' || $surname === '') {
|
||||
flashRedirect('danger', 'Nome e cognome sono obbligatori.');
|
||||
}
|
||||
if (mb_strlen($name) > 100 || mb_strlen($surname) > 100) {
|
||||
flashRedirect('danger', 'Nome o cognome troppo lunghi.');
|
||||
}
|
||||
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
flashRedirect('danger', 'Indirizzo email non valido.');
|
||||
}
|
||||
if ($phone !== '' && !preg_match('/^[0-9 +().\/-]{5,40}$/', $phone)) {
|
||||
flashRedirect('danger', 'Numero di telefono non valido.');
|
||||
}
|
||||
if ($email !== '') {
|
||||
$dup = $pdo->prepare("SELECT COUNT(*) AS c FROM employee WHERE email = :email AND idemployee <> :id");
|
||||
$dup->execute([':email' => $email, ':id' => $id]);
|
||||
if ((int) $dup->fetch()['c'] > 0) {
|
||||
flashRedirect('danger', 'Questa email è già usata da un altro insegnante.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($id > 0) {
|
||||
$current = findTeacher($pdo, $id);
|
||||
if (!$current) {
|
||||
flashRedirect('danger', 'Insegnante non trovato.');
|
||||
}
|
||||
if ($current['is_owner'] === 'Y' && $active === 'N') {
|
||||
flashRedirect('danger', 'Il titolare non può essere disattivato.');
|
||||
}
|
||||
|
||||
$photo = $current['photo'];
|
||||
$newPhoto = handlePhotoUpload($_FILES['photo'] ?? [], $photoDirAbs);
|
||||
if ($newPhoto !== null) {
|
||||
deletePhotoFile($photoDirAbs, $photo);
|
||||
$photo = $newPhoto;
|
||||
} elseif (isset($_POST['remove_photo'])) {
|
||||
deletePhotoFile($photoDirAbs, $photo);
|
||||
$photo = null;
|
||||
}
|
||||
|
||||
$st = $pdo->prepare("UPDATE employee SET nameemployee = :n, surnameemployee = :s, email = :e,
|
||||
phone = :p, photo = :ph, notifications = :nt, active = :a
|
||||
WHERE idemployee = :id");
|
||||
$st->execute([
|
||||
':n' => $name,
|
||||
':s' => $surname,
|
||||
':e' => $email ?: null,
|
||||
':p' => $phone ?: null,
|
||||
':ph' => $photo,
|
||||
':nt' => $notify,
|
||||
':a' => $active,
|
||||
':id' => $id,
|
||||
]);
|
||||
flashRedirect('success', 'Insegnante aggiornato.');
|
||||
}
|
||||
|
||||
// nuovo
|
||||
$photo = handlePhotoUpload($_FILES['photo'] ?? [], $photoDirAbs);
|
||||
$hasOwner = (int) $pdo->query("SELECT COUNT(*) AS c FROM employee WHERE is_owner = 'Y'")->fetch()['c'] > 0;
|
||||
|
||||
$st = $pdo->prepare("INSERT INTO employee
|
||||
(nameemployee, surnameemployee, email, phone, photo, is_owner, notifications, active, idlocation)
|
||||
VALUES (:n, :s, :e, :p, :ph, :o, :nt, :a, 0)");
|
||||
$st->execute([
|
||||
':n' => $name,
|
||||
':s' => $surname,
|
||||
':e' => $email ?: null,
|
||||
':p' => $phone ?: null,
|
||||
':ph' => $photo,
|
||||
':o' => $hasOwner ? 'N' : 'Y',
|
||||
':nt' => $notify,
|
||||
':a' => $hasOwner ? $active : 'Y',
|
||||
]);
|
||||
flashRedirect('success', 'Insegnante aggiunto' . ($hasOwner ? '.' : ' e impostato come titolare.'));
|
||||
}
|
||||
|
||||
/* ---------- imposta titolare */
|
||||
if ($action === 'set_owner' && $id > 0) {
|
||||
if (!findTeacher($pdo, $id)) {
|
||||
flashRedirect('danger', 'Insegnante non trovato.');
|
||||
}
|
||||
$pdo->beginTransaction();
|
||||
$pdo->exec("UPDATE employee SET is_owner = 'N' WHERE is_owner = 'Y'");
|
||||
$st = $pdo->prepare("UPDATE employee SET is_owner = 'Y', active = 'Y' WHERE idemployee = :id");
|
||||
$st->execute([':id' => $id]);
|
||||
$pdo->commit();
|
||||
flashRedirect('success', 'Titolare aggiornato.');
|
||||
}
|
||||
|
||||
/* ---------- notifiche on/off */
|
||||
if ($action === 'toggle_notifications' && $id > 0) {
|
||||
$st = $pdo->prepare("UPDATE employee SET notifications = IF(notifications = 'Y', 'N', 'Y') WHERE idemployee = :id");
|
||||
$st->execute([':id' => $id]);
|
||||
flashRedirect('success', 'Notifiche aggiornate.');
|
||||
}
|
||||
|
||||
/* ---------- elimina */
|
||||
if ($action === 'delete' && $id > 0) {
|
||||
$t = findTeacher($pdo, $id);
|
||||
if (!$t) {
|
||||
flashRedirect('danger', 'Insegnante non trovato.');
|
||||
}
|
||||
if ($t['is_owner'] === 'Y') {
|
||||
flashRedirect('danger', 'Il titolare non può essere eliminato. Imposta prima un altro titolare.');
|
||||
}
|
||||
$cnt = $pdo->prepare("SELECT COUNT(*) AS c FROM service WHERE idteacher = :id");
|
||||
$cnt->execute([':id' => $id]);
|
||||
$nServices = (int) $cnt->fetch()['c'];
|
||||
if ($nServices > 0) {
|
||||
flashRedirect('danger', 'Impossibile eliminare: ha ' . $nServices . ' classi assegnate. Riassegnale oppure disattiva l\'insegnante.');
|
||||
}
|
||||
$pdo->beginTransaction();
|
||||
$pdo->prepare("DELETE FROM dayoff WHERE idteacher = :id")->execute([':id' => $id]);
|
||||
$pdo->prepare("DELETE FROM employee WHERE idemployee = :id")->execute([':id' => $id]);
|
||||
$pdo->commit();
|
||||
deletePhotoFile($photoDirAbs, $t['photo']);
|
||||
flashRedirect('success', 'Insegnante eliminato.');
|
||||
}
|
||||
|
||||
flashRedirect('danger', 'Azione non valida.');
|
||||
} catch (RuntimeException $ex) {
|
||||
flashRedirect('danger', $ex->getMessage());
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('insegnanti.php: ' . $ex->getMessage());
|
||||
flashRedirect('danger', 'Errore durante il salvataggio. Riprova.');
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================ lettura */
|
||||
|
||||
$flash = $_SESSION['teachers_flash'] ?? null;
|
||||
unset($_SESSION['teachers_flash']);
|
||||
|
||||
$teachers = $pdo->query("SELECT e.*,
|
||||
(SELECT COUNT(*) FROM service s WHERE s.idteacher = e.idemployee) AS nservices
|
||||
FROM employee e
|
||||
ORDER BY e.is_owner DESC, e.active DESC, e.surnameemployee, e.nameemployee")->fetchAll();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Insegnanti</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="YogiBook - Prenotazione facile YogaSoul" name="description" />
|
||||
<meta content="Advanced Creative Solutions" name="author" />
|
||||
<link rel="shortcut icon" href="assets/images/favicon.ico">
|
||||
<link href="assets/css/bootstrap.min.css" id="bootstrap-style" rel="stylesheet" type="text/css" />
|
||||
<link href="assets/css/icons.min.css" rel="stylesheet" type="text/css" />
|
||||
<link href="assets/css/app.min.css" id="app-style" rel="stylesheet" type="text/css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<style>
|
||||
.teachers-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.teachers-head h5 {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.teacher-card {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 3px 16px rgba(0, 0, 0, 0.06);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
height: calc(100% - 20px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 5px solid #eef0f3;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.teacher-card:hover {
|
||||
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.teacher-card.owner {
|
||||
border-top-color: #E1D0B8;
|
||||
}
|
||||
|
||||
.teacher-card.inactive {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.teacher-top {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.teacher-avatar {
|
||||
flex: 0 0 64px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
border: 3px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.teacher-name {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.t-badge {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
margin-top: 5px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.t-badge.owner {
|
||||
background: #E1D0B8;
|
||||
color: #5a4630;
|
||||
}
|
||||
|
||||
.t-badge.inactive {
|
||||
background: #eef0f3;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.teacher-info {
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
margin-bottom: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.teacher-info i {
|
||||
width: 18px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.teacher-info a {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.teacher-info .missing {
|
||||
color: #c0c4cc;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.teacher-foot {
|
||||
margin-top: auto;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.notify-form .form-check-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notify-form .form-check-input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notify-form .form-check-input:checked {
|
||||
background-color: #1ebf73;
|
||||
border-color: #1ebf73;
|
||||
}
|
||||
|
||||
.notify-warning {
|
||||
font-size: 11px;
|
||||
color: #e67e22;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.t-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.t-btn {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 6px 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s;
|
||||
}
|
||||
|
||||
.t-btn:hover {
|
||||
filter: brightness(0.93);
|
||||
}
|
||||
|
||||
.t-btn.edit {
|
||||
background: #eef2f7;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.t-btn.owner {
|
||||
background: #f5eee4;
|
||||
color: #8a6a3f;
|
||||
}
|
||||
|
||||
.t-btn.del {
|
||||
background: #fdecea;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.photo-preview {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background: #f3f4f6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #9ca3af;
|
||||
font-size: 28px;
|
||||
border: 3px solid #eef0f3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.photo-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.empty-teachers {
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
padding: 50px 10px;
|
||||
}
|
||||
</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">Insegnanti</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<?php include('include/languageselection.php'); ?>
|
||||
<?php include('include/profiletopbar.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<?php include('include/sidebar.php'); ?>
|
||||
<header class="ishorizontal-topbar">
|
||||
<div class="navbar-header">
|
||||
<div class="d-flex"></div>
|
||||
</div>
|
||||
<div class="topnav">
|
||||
<div class="container-fluid">
|
||||
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid">
|
||||
|
||||
<?php if ($flash) : ?>
|
||||
<div class="alert alert-<?php echo e($flash[0]); ?> alert-dismissible fade show" role="alert">
|
||||
<?php echo e($flash[1]); ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="teachers-head">
|
||||
<div>
|
||||
<h5>Insegnanti della scuola</h5>
|
||||
<small class="text-muted"><?php echo count($teachers); ?> insegnant<?php echo count($teachers) === 1 ? 'e' : 'i'; ?></small>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="openTeacherModal(null)">
|
||||
<i class="fas fa-user-plus me-1"></i> Nuovo insegnante
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (empty($teachers)) : ?>
|
||||
<div class="empty-teachers">
|
||||
<i class="fas fa-chalkboard-teacher fa-2x mb-2"></i>
|
||||
<p>Nessun insegnante. Aggiungi il primo: diventerà automaticamente il titolare.</p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="row">
|
||||
<?php foreach ($teachers as $t) :
|
||||
$tid = (int) $t['idemployee'];
|
||||
$isOwner = $t['is_owner'] === 'Y';
|
||||
$isActive = $t['active'] === 'Y';
|
||||
$fullName = trim($t['nameemployee'] . ' ' . $t['surnameemployee']);
|
||||
$photoUrl = $t['photo'] ? PHOTO_DIR_REL . $t['photo'] : '';
|
||||
$payload = [
|
||||
'id' => $tid,
|
||||
'nameemployee' => $t['nameemployee'],
|
||||
'surnameemployee' => $t['surnameemployee'],
|
||||
'email' => $t['email'],
|
||||
'phone' => $t['phone'],
|
||||
'photo' => $photoUrl,
|
||||
'notifications' => $t['notifications'] === 'Y',
|
||||
'active' => $isActive,
|
||||
'is_owner' => $isOwner,
|
||||
];
|
||||
?>
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<div class="teacher-card <?php echo $isOwner ? 'owner' : ''; ?> <?php echo $isActive ? '' : 'inactive'; ?>">
|
||||
<div class="teacher-top">
|
||||
<?php if ($photoUrl) : ?>
|
||||
<img class="teacher-avatar" src="<?php echo e($photoUrl); ?>" alt="<?php echo e($fullName); ?>">
|
||||
<?php else : ?>
|
||||
<div class="teacher-avatar" style="background:<?php echo teacherColor($fullName); ?>;"><?php echo e(teacherInitials($t['nameemployee'], $t['surnameemployee'])); ?></div>
|
||||
<?php endif; ?>
|
||||
<div>
|
||||
<div class="teacher-name"><?php echo e($fullName); ?></div>
|
||||
<?php if ($isOwner) : ?><span class="t-badge owner"><i class="fas fa-crown me-1"></i>Titolare</span><?php endif; ?>
|
||||
<?php if (!$isActive) : ?><span class="t-badge inactive">Non attivo</span><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="teacher-info"><i class="far fa-envelope"></i>
|
||||
<?php echo $t['email'] ? '<a href="mailto:' . e($t['email']) . '">' . e($t['email']) . '</a>' : '<span class="missing">email non inserita</span>'; ?>
|
||||
</div>
|
||||
<div class="teacher-info"><i class="fas fa-phone"></i>
|
||||
<?php echo $t['phone'] ? '<a href="tel:' . e(preg_replace('/[^0-9+]/', '', $t['phone'])) . '">' . e($t['phone']) . '</a>' : '<span class="missing">telefono non inserito</span>'; ?>
|
||||
</div>
|
||||
<div class="teacher-info"><i class="fas fa-layer-group"></i>
|
||||
<?php echo (int) $t['nservices']; ?> class<?php echo (int) $t['nservices'] === 1 ? 'e' : 'i'; ?> assegnat<?php echo (int) $t['nservices'] === 1 ? 'a' : 'e'; ?>
|
||||
</div>
|
||||
|
||||
<div class="teacher-foot">
|
||||
<form method="post" class="notify-form">
|
||||
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
|
||||
<input type="hidden" name="action" value="toggle_notifications">
|
||||
<input type="hidden" name="id" value="<?php echo $tid; ?>">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="notify<?php echo $tid; ?>"
|
||||
<?php echo $t['notifications'] === 'Y' ? 'checked' : ''; ?> onchange="this.form.submit()">
|
||||
<label class="form-check-label" for="notify<?php echo $tid; ?>">Notifiche email</label>
|
||||
</div>
|
||||
<?php if ($t['notifications'] === 'Y' && !$t['email']) : ?>
|
||||
<div class="notify-warning"><i class="fas fa-exclamation-triangle"></i> manca l'email</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="t-actions">
|
||||
<button type="button" class="t-btn edit" title="Modifica"
|
||||
data-teacher="<?php echo e(json_encode($payload)); ?>"
|
||||
onclick="openTeacherModal(JSON.parse(this.dataset.teacher))">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
<?php if (!$isOwner) : ?>
|
||||
<form method="post" class="confirm-form"
|
||||
data-title="Impostare come titolare?"
|
||||
data-text="<?php echo e($fullName); ?> diventerà il titolare della scuola al posto dell'attuale.">
|
||||
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
|
||||
<input type="hidden" name="action" value="set_owner">
|
||||
<input type="hidden" name="id" value="<?php echo $tid; ?>">
|
||||
<button type="submit" class="t-btn owner" title="Imposta come titolare"><i class="fas fa-crown"></i></button>
|
||||
</form>
|
||||
<form method="post" class="confirm-form" data-danger="1"
|
||||
data-title="Eliminare <?php echo e($fullName); ?>?"
|
||||
data-text="L'operazione non è reversibile.">
|
||||
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?php echo $tid; ?>">
|
||||
<button type="submit" class="t-btn del" title="Elimina"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ MODALE CREA / MODIFICA -->
|
||||
<div class="modal fade" id="teacherModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form class="modal-content" method="post" enctype="multipart/form-data" id="teacherForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="teacherModalTitle">Nuovo insegnante</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
|
||||
<input type="hidden" name="action" value="save">
|
||||
<input type="hidden" name="id" id="t_id" value="0">
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<div class="photo-preview" id="t_preview"><i class="fas fa-user"></i></div>
|
||||
<div class="flex-grow-1">
|
||||
<label class="form-label mb-1">Foto <small class="text-muted">(facoltativa, JPG/PNG/WEBP, max 3 MB)</small></label>
|
||||
<input type="file" name="photo" id="t_photo" class="form-control form-control-sm" accept="image/jpeg,image/png,image/webp">
|
||||
<div class="form-check mt-2" id="t_remove_wrap" style="display:none;">
|
||||
<input class="form-check-input" type="checkbox" name="remove_photo" id="t_remove">
|
||||
<label class="form-check-label" for="t_remove">Rimuovi foto attuale</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label mb-1">Nome *</label>
|
||||
<input type="text" name="nameemployee" id="t_name" class="form-control" maxlength="100" required>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label mb-1">Cognome *</label>
|
||||
<input type="text" name="surnameemployee" id="t_surname" class="form-control" maxlength="100" required>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label mb-1">Email</label>
|
||||
<input type="email" name="email" id="t_email" class="form-control" maxlength="150">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label mb-1">Telefono</label>
|
||||
<input type="text" name="phone" id="t_phone" class="form-control" maxlength="40">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="notifications" id="t_notify" checked>
|
||||
<label class="form-check-label" for="t_notify">Riceve notifiche email</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="active" id="t_active" checked>
|
||||
<label class="form-check-label" for="t_active">Attivo</label>
|
||||
</div>
|
||||
<div class="text-muted" id="t_owner_note" style="font-size:12px;display:none;">
|
||||
<i class="fas fa-crown me-1" style="color:#b08d57;"></i> È il titolare: non può essere disattivato.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-save me-1"></i> Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
var teacherModal = null;
|
||||
|
||||
function setPreview(url) {
|
||||
var box = document.getElementById('t_preview');
|
||||
box.innerHTML = url ? '<img src="' + url + '" alt="">' : '<i class="fas fa-user"></i>';
|
||||
}
|
||||
|
||||
function openTeacherModal(t) {
|
||||
var isEdit = !!t;
|
||||
document.getElementById('teacherModalTitle').textContent = isEdit ? 'Modifica insegnante' : 'Nuovo insegnante';
|
||||
document.getElementById('t_id').value = isEdit ? t.id : 0;
|
||||
document.getElementById('t_name').value = isEdit ? (t.nameemployee || '') : '';
|
||||
document.getElementById('t_surname').value = isEdit ? (t.surnameemployee || '') : '';
|
||||
document.getElementById('t_email').value = isEdit ? (t.email || '') : '';
|
||||
document.getElementById('t_phone').value = isEdit ? (t.phone || '') : '';
|
||||
document.getElementById('t_notify').checked = isEdit ? t.notifications : true;
|
||||
|
||||
var active = document.getElementById('t_active');
|
||||
active.checked = isEdit ? t.active : true;
|
||||
active.disabled = isEdit && t.is_owner;
|
||||
document.getElementById('t_owner_note').style.display = (isEdit && t.is_owner) ? 'block' : 'none';
|
||||
|
||||
document.getElementById('t_photo').value = '';
|
||||
document.getElementById('t_remove').checked = false;
|
||||
document.getElementById('t_remove_wrap').style.display = (isEdit && t.photo) ? 'block' : 'none';
|
||||
setPreview(isEdit ? t.photo : '');
|
||||
|
||||
if (!teacherModal) teacherModal = new bootstrap.Modal(document.getElementById('teacherModal'));
|
||||
teacherModal.show();
|
||||
}
|
||||
|
||||
// il titolare ha "Attivo" disabilitato: lo riabilitiamo al submit così il valore viene inviato
|
||||
document.getElementById('teacherForm').addEventListener('submit', function() {
|
||||
document.getElementById('t_active').disabled = false;
|
||||
});
|
||||
|
||||
// anteprima foto + controllo dimensione
|
||||
document.getElementById('t_photo').addEventListener('change', function() {
|
||||
var f = this.files[0];
|
||||
if (!f) return;
|
||||
if (f.size > 3 * 1024 * 1024) {
|
||||
Swal.fire('Foto troppo grande', 'La foto deve essere al massimo di 3 MB.', 'warning');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(ev) {
|
||||
setPreview(ev.target.result);
|
||||
};
|
||||
reader.readAsDataURL(f);
|
||||
document.getElementById('t_remove').checked = false;
|
||||
});
|
||||
|
||||
// conferma per titolare / elimina
|
||||
document.querySelectorAll('.confirm-form').forEach(function(form) {
|
||||
form.addEventListener('submit', function(ev) {
|
||||
if (form.dataset.confirmed === '1') return;
|
||||
ev.preventDefault();
|
||||
var danger = form.dataset.danger === '1';
|
||||
Swal.fire({
|
||||
title: form.dataset.title,
|
||||
text: form.dataset.text,
|
||||
icon: danger ? 'warning' : 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: danger ? '#e74c3c' : '#b08d57',
|
||||
cancelButtonColor: '#6b7280',
|
||||
confirmButtonText: danger ? 'Sì, elimina' : 'Sì, conferma',
|
||||
cancelButtonText: 'Annulla'
|
||||
}).then(function(r) {
|
||||
if (r.isConfirmed) {
|
||||
form.dataset.confirmed = '1';
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
if ($conn->connect_error) {
|
||||
header("Location: userpanel.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
$iduserlogin = (int) $iduserlogin;
|
||||
$idbookingclass = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
|
||||
if ($idbookingclass > 0) {
|
||||
// Cancella solo se è una riga waiting DELL'utente loggato (sicurezza)
|
||||
$stmt = $conn->prepare(
|
||||
"DELETE FROM bookingclass
|
||||
WHERE idbookingclass = ? AND iduser = ? AND status = 'waiting'"
|
||||
);
|
||||
$stmt->bind_param("ii", $idbookingclass, $iduserlogin);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
header("Location: userpanel.php?leftwaiting=Y");
|
||||
exit();
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: options.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$waiting = isset($_POST['waitinglist_enabled']) ? 'Y' : 'N';
|
||||
$showTeacher = isset($_POST['show_teacher']) ? 'Y' : 'N';
|
||||
$maxCancel = max(0, (int) ($_POST['maxbeforetimecancell'] ?? 0));
|
||||
|
||||
try {
|
||||
$exists = (int) $pdo->query("SELECT COUNT(*) AS c FROM `option`")->fetch()['c'] > 0;
|
||||
|
||||
if ($exists) {
|
||||
$st = $pdo->prepare("UPDATE `option`
|
||||
SET waitinglist_enabled = :w, show_teacher = :s, maxbeforetimecancell = :m
|
||||
LIMIT 1");
|
||||
} else {
|
||||
$st = $pdo->prepare("INSERT INTO `option` (waitinglist_enabled, show_teacher, maxbeforetimecancell)
|
||||
VALUES (:w, :s, :m)");
|
||||
}
|
||||
$st->execute([':w' => $waiting, ':s' => $showTeacher, ':m' => (string) $maxCancel]);
|
||||
|
||||
header('Location: optionsschool.php?message=success');
|
||||
} catch (Throwable $e) {
|
||||
error_log('options_save: ' . $e->getMessage());
|
||||
header('Location: options.php?message=error');
|
||||
}
|
||||
exit;
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
if ($conn->connect_error) {
|
||||
die("Connessione fallita: " . $conn->connect_error);
|
||||
}
|
||||
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$message = $_GET['message'] ?? 'n';
|
||||
|
||||
// Carica l'unico record di option
|
||||
$opt = $conn->query("SELECT * FROM `option` LIMIT 1")->fetch_assoc();
|
||||
$conn->close();
|
||||
|
||||
$waitingEnabled = ($opt['waitinglist_enabled'] ?? 'N') === 'Y';
|
||||
$maxCancel = $opt['maxbeforetimecancell'] ?? '0';
|
||||
$showTeacher = ($opt['show_teacher'] ?? 'N') === 'Y';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Opzioni</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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<style>
|
||||
.admin-card {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.admin-card .card-body {
|
||||
padding: 26px 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section-lead {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.option-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.option-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.option-help {
|
||||
color: #9ca3af;
|
||||
font-size: 13px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.form-switch .form-check-input {
|
||||
width: 3em;
|
||||
height: 1.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.save-bar {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
</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">Opzioni</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<?php include('include/languageselection.php'); ?>
|
||||
<?php include('include/profiletopbar.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<?php include('include/sidebar.php'); ?>
|
||||
<header class="ishorizontal-topbar">
|
||||
<div class="navbar-header">
|
||||
<div class="d-flex"></div>
|
||||
</div>
|
||||
<div class="topnav">
|
||||
<div class="container-fluid">
|
||||
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid">
|
||||
<?php if ($message === 'success') : ?>
|
||||
<div class="alert alert-success" role="alert">Impostazioni salvate con successo.</div>
|
||||
<?php elseif ($message === 'error') : ?>
|
||||
<div class="alert alert-danger" role="alert">Errore durante il salvataggio.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Impostazioni prenotazioni</div>
|
||||
<p class="section-lead">Configura il comportamento del sistema di prenotazione.</p>
|
||||
|
||||
<form action="options_save.php" method="POST">
|
||||
|
||||
<div class="option-row">
|
||||
<div>
|
||||
<div class="option-label">Lista d'attesa</div>
|
||||
<div class="option-help">Se attiva, quando una classe è piena gli utenti possono mettersi in coda. Alla liberazione di un posto verranno gestiti in ordine di arrivo.</div>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="waitinglist_enabled" name="waitinglist_enabled" value="Y" <?php echo $waitingEnabled ? 'checked' : ''; ?>>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-row">
|
||||
<div>
|
||||
<div class="option-label">Mostra insegnante nelle classi</div>
|
||||
<div class="option-help">Se attiva, nelle card delle classi viene indicato il nome (e la foto) dell'insegnante.</div>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="show_teacher" name="show_teacher" value="Y" <?php echo $showTeacher ? 'checked' : ''; ?>>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option-row">
|
||||
<div>
|
||||
<div class="option-label">Ore minime per l'annullamento</div>
|
||||
<div class="option-help">Quante ore prima dell'inizio di una lezione è ancora possibile annullarla.</div>
|
||||
</div>
|
||||
<div style="width:120px;">
|
||||
<input type="number" min="0" class="form-control" name="maxbeforetimecancell" value="<?php echo e($maxCancel); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save-bar">
|
||||
<button type="submit" class="btn btn-success"><i class="fas fa-save me-1"></i> Salva impostazioni</button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+527
-321
@@ -1,299 +1,170 @@
|
||||
<?php require_once('include/headscript.php'); ?>
|
||||
|
||||
<?php
|
||||
// Creazione della connessione
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
/**
|
||||
* Riepilogo ordini (amministratore)
|
||||
* - filtro Attivi / Scaduti (server)
|
||||
* - ricerca istantanea per nome, cognome, email, n. ordine, classe (client)
|
||||
* - modifica al click di: scadenza, max riprogrammazioni, riprogrammazioni effettuate
|
||||
*/
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
// Verifica della connessione
|
||||
if ($conn->connect_error) {
|
||||
die("Connessione fallita: " . $conn->connect_error);
|
||||
}
|
||||
|
||||
// Gestione del messaggio GET
|
||||
if (isset($_GET['message'])) {
|
||||
$message = $_GET['message'];
|
||||
} else {
|
||||
$message = 'n';
|
||||
}
|
||||
|
||||
// ID dell'insegnante (per il form DayOff)
|
||||
$idTeacher = 1;
|
||||
|
||||
// Gestione dell'inserimento del DayOff
|
||||
if (isset($_POST['submit'])) {
|
||||
$dayoffdate = $_POST['dayoff'];
|
||||
$insertQuery = "INSERT INTO dayoff (idteacher, dayoffdate) VALUES (?, ?)";
|
||||
$stmt = $conn->prepare($insertQuery);
|
||||
$stmt->bind_param("is", $idTeacher, $dayoffdate);
|
||||
if ($stmt->execute()) {
|
||||
$message = 'success_dayoff';
|
||||
} else {
|
||||
$message = 'error_dayoff';
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
// Gestione del filtro per gli ordini
|
||||
$filter = isset($_GET['filter']) ? $_GET['filter'] : 'all';
|
||||
$today = date("Y-m-d");
|
||||
$whereClause = "";
|
||||
if ($filter == 'active') {
|
||||
$whereClause = "WHERE expireon > '$today'";
|
||||
} elseif ($filter == 'expired') {
|
||||
$whereClause = "WHERE expireon <= '$today'";
|
||||
}
|
||||
|
||||
// Query SQL per recuperare tutti gli ordini con join su service
|
||||
$query = "SELECT o.idorderbook, o.first_name, o.last_name, s.servicename, o.expireon, o.status, o.nticket
|
||||
FROM orderbook o
|
||||
LEFT JOIN service s ON o.idservice = s.idservice
|
||||
$whereClause
|
||||
ORDER BY o.idorderbook DESC";
|
||||
$result = $conn->query($query);
|
||||
$orders = [];
|
||||
if ($result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$orders[] = $row;
|
||||
if (!function_exists('e')) {
|
||||
function e($value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
// Query SQL per i DayOff
|
||||
$query_dayoff = "SELECT iddayoff, dayoffdate FROM dayoff WHERE idteacher = '1'";
|
||||
$result_dayoff = $conn->query($query_dayoff);
|
||||
$documents = [];
|
||||
if ($result_dayoff->num_rows > 0) {
|
||||
while ($row = $result_dayoff->fetch_assoc()) {
|
||||
$documents[] = $row;
|
||||
function reschedBadge($max, $done): string
|
||||
{
|
||||
$done = (int) $done;
|
||||
if ($max === null || $max === '') {
|
||||
return '<span class="badge bg-light text-dark">' . $done . '</span>';
|
||||
}
|
||||
$max = (int) $max;
|
||||
$cls = $done >= $max ? 'bg-danger' : ($done > 0 ? 'bg-warning text-dark' : 'bg-success');
|
||||
return '<span class="badge ' . $cls . '">' . $done . ' / ' . $max . '</span>';
|
||||
}
|
||||
|
||||
$message = $_GET['message'] ?? 'n';
|
||||
|
||||
$filter = $_GET['filter'] ?? 'all';
|
||||
if (!in_array($filter, ['all', 'active', 'expired'], true)) {
|
||||
$filter = 'all';
|
||||
}
|
||||
$q = trim((string) ($_GET['q'] ?? ''));
|
||||
$today = date('Y-m-d');
|
||||
|
||||
$where = '';
|
||||
$params = [];
|
||||
if ($filter === 'active') {
|
||||
$where = 'WHERE o.expireon > :today';
|
||||
$params[':today'] = $today;
|
||||
} elseif ($filter === 'expired') {
|
||||
$where = 'WHERE o.expireon <= :today';
|
||||
$params[':today'] = $today;
|
||||
}
|
||||
|
||||
$sql = "SELECT o.idorderbook, o.order_id, o.first_name, o.last_name, o.order_billing_email,
|
||||
s.servicename, o.expireon, o.status, o.nticket, o.maxreschedule, o.reprogrammed
|
||||
FROM orderbook o
|
||||
LEFT JOIN service s ON o.idservice = s.idservice
|
||||
$where
|
||||
ORDER BY o.idorderbook DESC";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$orders = $stmt->fetchAll();
|
||||
|
||||
$qParam = $q !== '' ? '&q=' . urlencode($q) : '';
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>YogiBook - Prenotazioni YogaSoul</title>
|
||||
<title>YogiBook - Riepilogo Ordini</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="YogiBook - Prenotazione facile YogaSOul" name="description" />
|
||||
<meta content="YogiBook - Prenotazione facile YogaSoul" name="description" />
|
||||
<meta content="Advanced Creative Solutions" name="author" />
|
||||
<link rel="shortcut icon" href="assets/images/favicon.ico">
|
||||
<link href="assets/css/bootstrap.min.css" id="bootstrap-style" rel="stylesheet" type="text/css" />
|
||||
<link href="assets/css/icons.min.css" rel="stylesheet" type="text/css" />
|
||||
<link href="assets/css/app.min.css" id="app-style" rel="stylesheet" type="text/css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
|
||||
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
// Inizializzazione del Datepicker per i campi DayOff
|
||||
$("#dayoff").datepicker({
|
||||
dateFormat: "yy-mm-dd",
|
||||
minDate: 0
|
||||
});
|
||||
|
||||
// Inizializzazione del Datepicker per i campi di scadenza editabili
|
||||
$(document).on('focus', '.expiryDateInput', function() {
|
||||
$(this).datepicker({
|
||||
dateFormat: "yy-mm-dd",
|
||||
minDate: 0,
|
||||
onSelect: function(dateText) {
|
||||
let orderId = $(this).data('order-id');
|
||||
console.log("Datepicker selezionato - idorderbook: " + orderId + ", newExpiry: " + dateText);
|
||||
confirmUpdate(orderId, dateText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Rendere il campo scadenza editabile al click
|
||||
$(document).on('click', '.expiry-date', function() {
|
||||
let orderId = $(this).data('order-id');
|
||||
let currentDate = $(this).text().trim();
|
||||
console.log("Clic su expiry-date - idorderbook: " + orderId + ", currentDate: " + currentDate);
|
||||
if (!orderId) {
|
||||
Swal.fire('Errore!', 'ID ordine non valido.', 'error');
|
||||
return;
|
||||
}
|
||||
$(this).html(`<input type="text" class="form-control expiryDateInput" data-order-id="${orderId}" value="${currentDate}" />`);
|
||||
$(this).find('.expiryDateInput').focus();
|
||||
});
|
||||
|
||||
// Funzione per confermare l'aggiornamento della scadenza
|
||||
function confirmUpdate(orderId, newExpiry) {
|
||||
let originalCell = $(`td.expiry-date[data-order-id="${orderId}"]`);
|
||||
let originalDate = originalCell.text().trim();
|
||||
console.log("Invio AJAX - idorderbook: " + orderId + ", new_expiry: " + newExpiry);
|
||||
|
||||
Swal.fire({
|
||||
title: "Sei sicuro?",
|
||||
text: "La data di scadenza verrà modificata!",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Sì, modifica!",
|
||||
cancelButtonText: "Annulla"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajax({
|
||||
url: 'update_expiry.php',
|
||||
type: 'POST',
|
||||
data: {
|
||||
order_id: orderId,
|
||||
new_expiry: newExpiry,
|
||||
update_expiry: 1
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
console.log("Risposta AJAX:", response);
|
||||
if (response.status === 'success') {
|
||||
Swal.fire('Successo!', response.message, 'success');
|
||||
originalCell.text(newExpiry);
|
||||
} else {
|
||||
Swal.fire('Errore!', response.message, 'error');
|
||||
originalCell.text(originalDate);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Errore AJAX:', {
|
||||
status: status,
|
||||
error: error,
|
||||
responseText: xhr.responseText
|
||||
});
|
||||
Swal.fire('Errore!', 'Errore durante la richiesta al server: ' + error, 'error');
|
||||
originalCell.text(originalDate);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
originalCell.text(originalDate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Funzione per confermare la cancellazione
|
||||
function confirmDelete(id, deletePageUrl) {
|
||||
Swal.fire({
|
||||
title: "Sei sicuro?",
|
||||
text: "Questa prenotazione verrà cancellata definitivamente! Ricordati poi di riprogrammare la tua lezione!",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#d33",
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonText: "Sì, cancella!",
|
||||
cancelButtonText: "Annulla"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = `deleteclass.php?id=${id}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.custom-card {
|
||||
margin: 10px auto;
|
||||
.orders-toolbar {
|
||||
display: flex;
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
background-color: white;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.custom-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.custom-date-box {
|
||||
flex: 1;
|
||||
background-color: red;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
font-size: 60px;
|
||||
font-weight: bold;
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.custom-day {
|
||||
line-height: 1;
|
||||
.orders-search {
|
||||
margin-left: auto;
|
||||
position: relative;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.custom-month {
|
||||
font-size: 28px;
|
||||
.orders-search i {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.custom-event-details {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 20px;
|
||||
background-color: lightblue;
|
||||
.orders-search input {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.custom-heading {
|
||||
margin-top: 0;
|
||||
font-size: 24px;
|
||||
#searchCount {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
min-height: 20px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.custom-paragraph {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.custom-actions {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.custom-card.expanded .custom-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.custom-action-button {
|
||||
background-color: #f0f0f0;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
.editable {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.custom-action-button:hover {
|
||||
background-color: #e0e0e0;
|
||||
.editable:hover {
|
||||
background-color: #eef6f1 !important;
|
||||
box-shadow: inset 0 0 0 1px #cfe8db;
|
||||
}
|
||||
|
||||
.expiry-date {
|
||||
#ordersTable th.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.expiry-date:hover {
|
||||
background-color: #f0f0f0;
|
||||
#ordersTable th.sortable:hover {
|
||||
color: #1ebf73;
|
||||
}
|
||||
|
||||
#ordersTable th.sortable::after {
|
||||
content: "\2195";
|
||||
margin-left: 5px;
|
||||
font-size: 11px;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
#ordersTable th.sortable.asc::after {
|
||||
content: "\25B2";
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#ordersTable th.sortable.desc::after {
|
||||
content: "\25BC";
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.num-input {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.expiryDateInput {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.edit-hint {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.custom-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.custom-date-box,
|
||||
.custom-event-details {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.custom-event-time {
|
||||
font-size: 24px;
|
||||
.orders-search {
|
||||
margin-left: 0;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -314,91 +185,118 @@ if ($result_dayoff->num_rows > 0) {
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<?php include('include/languageselection.php'); ?>
|
||||
<div class="dropdown d-inline-block">
|
||||
<button type="button" class="btn header-item noti-icon" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="bx bx-search icon-sm align-middle"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-end p-0">
|
||||
<form class="p-2">
|
||||
<div class="search-box">
|
||||
<div class="position-relative">
|
||||
<input type="text" class="form-control rounded bg-light border-0" placeholder="Search...">
|
||||
<i class="bx bx-search search-icon"></i>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php include('include/profiletopbar.php'); ?>
|
||||
</div>
|
||||
<?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>
|
||||
</header>
|
||||
<?php include('include/sidebar.php'); ?>
|
||||
<header class="ishorizontal-topbar">
|
||||
<div class="navbar-header">
|
||||
<div class="d-flex"></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="topnav">
|
||||
<div class="container-fluid">
|
||||
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5>Riepilogo Ordini (Amministratore)</h5>
|
||||
<p>Tutti gli ordini registrati</p>
|
||||
<div class="mb-3">
|
||||
<a href="?filter=all" class="btn btn-secondary <?php echo $filter == 'all' ? 'active' : ''; ?>">Tutti</a>
|
||||
<a href="?filter=active" class="btn btn-success <?php echo $filter == 'active' ? 'active' : ''; ?>">Attivi</a>
|
||||
<a href="?filter=expired" class="btn btn-danger <?php echo $filter == 'expired' ? 'active' : ''; ?>">Scaduti</a>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Stato</th>
|
||||
<th>Numero Ordine</th>
|
||||
<th>Nome</th>
|
||||
<th>Cognome</th>
|
||||
<th>Nome Classe</th>
|
||||
<th>Numero Ticket</th>
|
||||
<th>Scadenza</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($orders as $order) {
|
||||
$isActive = ($order["expireon"] > $today);
|
||||
$badgeClass = $isActive ? 'badge bg-success' : 'badge bg-danger';
|
||||
$badgeText = $isActive ? 'Attivo' : 'Scaduto';
|
||||
?>
|
||||
<div class="main-content">
|
||||
<div class="page-content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5>Riepilogo Ordini (Amministratore)</h5>
|
||||
<p>Tutti gli ordini registrati</p>
|
||||
|
||||
<div class="orders-toolbar">
|
||||
<a href="?filter=all<?php echo e($qParam); ?>" data-filter="all" class="filter-link btn btn-secondary <?php echo $filter === 'all' ? 'active' : ''; ?>">Tutti</a>
|
||||
<a href="?filter=active<?php echo e($qParam); ?>" data-filter="active" class="filter-link btn btn-success <?php echo $filter === 'active' ? 'active' : ''; ?>">Attivi</a>
|
||||
<a href="?filter=expired<?php echo e($qParam); ?>" data-filter="expired" class="filter-link btn btn-danger <?php echo $filter === 'expired' ? 'active' : ''; ?>">Scaduti</a>
|
||||
|
||||
<div class="orders-search">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="search" id="orderSearch" class="form-control" autocomplete="off"
|
||||
placeholder="Cerca nome, cognome, email, n. ordine…" value="<?php echo e($q); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="searchCount"></div>
|
||||
<div class="edit-hint"><i class="fas fa-pen me-1"></i> Clicca su scadenza, max riprogr. o riprogrammate per modificarle.</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped mb-0" id="ordersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><span class="<?php echo $badgeClass; ?>"><?php echo $badgeText; ?></span></td>
|
||||
<td><?php echo $order["idorderbook"]; ?></td>
|
||||
<td><?php echo $order["first_name"]; ?></td>
|
||||
<td><?php echo $order["last_name"]; ?></td>
|
||||
<td><?php echo $order["servicename"]; ?></td>
|
||||
<td><?php echo $order["nticket"]; ?></td>
|
||||
<td class="expiry-date" data-order-id="<?php echo $order['idorderbook']; ?>"><?php echo $order["expireon"]; ?></td>
|
||||
<td><?php echo $order["status"]; ?></td>
|
||||
<th class="sortable" data-type="text">Stato</th>
|
||||
<th class="sortable desc" data-type="num">N. Ordine</th>
|
||||
<th class="sortable" data-type="text">Nome</th>
|
||||
<th class="sortable" data-type="text">Cognome</th>
|
||||
<th class="sortable" data-type="text">Classe</th>
|
||||
<th class="sortable" data-type="num">Ticket</th>
|
||||
<th class="sortable" data-type="date">Scadenza</th>
|
||||
<th class="sortable" data-type="num">Max riprogr.</th>
|
||||
<th class="sortable" data-type="num">Riprogrammate</th>
|
||||
<th class="sortable" data-type="text">Status</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($orders as $order) :
|
||||
$id = (int) $order['idorderbook'];
|
||||
$isActive = ($order['expireon'] !== null && $order['expireon'] > $today);
|
||||
$max = $order['maxreschedule'];
|
||||
$done = (int) $order['reprogrammed'];
|
||||
$search = implode(' ', [
|
||||
$order['first_name'],
|
||||
$order['last_name'],
|
||||
$order['order_billing_email'],
|
||||
$id,
|
||||
$order['order_id'],
|
||||
$order['servicename'],
|
||||
]);
|
||||
?>
|
||||
<tr class="order-row" data-search="<?php echo e($search); ?>">
|
||||
<td class="state-cell">
|
||||
<span class="badge <?php echo $isActive ? 'bg-success' : 'bg-danger'; ?>"><?php echo $isActive ? 'Attivo' : 'Scaduto'; ?></span>
|
||||
</td>
|
||||
<td><?php echo $id; ?></td>
|
||||
<td><?php echo e($order['first_name']); ?></td>
|
||||
<td><?php echo e($order['last_name']); ?></td>
|
||||
<td><?php echo e($order['servicename']); ?></td>
|
||||
<td><?php echo e($order['nticket']); ?></td>
|
||||
<td class="expiry-date editable" data-order-id="<?php echo $id; ?>" title="Clicca per modificare"><?php echo e($order['expireon']); ?></td>
|
||||
<td class="num-edit editable" data-order-id="<?php echo $id; ?>" data-field="maxreschedule"
|
||||
data-value="<?php echo $max === null ? '' : (int) $max; ?>" title="Clicca per modificare">
|
||||
<?php echo $max === null ? '<span class="text-muted">–</span>' : (int) $max; ?>
|
||||
</td>
|
||||
<td class="num-edit editable" data-order-id="<?php echo $id; ?>" data-field="reprogrammed"
|
||||
data-value="<?php echo $done; ?>" title="Clicca per modificare">
|
||||
<?php echo reschedBadge($max, $done); ?>
|
||||
</td>
|
||||
<td><?php echo e($order['status']); ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="confirmDeleteOrder(<?php echo $id; ?>)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<tr id="noResults" style="display:none;">
|
||||
<td colspan="11" class="text-center text-muted py-4">Nessun ordine trovato.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -407,6 +305,314 @@ if ($result_dayoff->num_rows > 0) {
|
||||
<script src="assets/libs/simplebar/simplebar.min.js"></script>
|
||||
<script src="assets/libs/eva-icons/eva.min.js"></script>
|
||||
<script src="assets/js/app.js"></script>
|
||||
|
||||
<script>
|
||||
function confirmDeleteOrder(id) {
|
||||
Swal.fire({
|
||||
title: "Sei sicuro?",
|
||||
text: "Verranno cancellati l'ordine e TUTTE le prenotazioni collegate a questa persona. L'azione è irreversibile!",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#d33",
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonText: "Sì, cancella tutto!",
|
||||
cancelButtonText: "Annulla"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = 'deleteorder.php?id=' + id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(function() {
|
||||
var today = '<?php echo $today; ?>';
|
||||
|
||||
/* ===================== RICERCA ===================== */
|
||||
function norm(s) {
|
||||
return (s || '').toString().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
|
||||
$('#ordersTable tr.order-row').each(function() {
|
||||
$(this).data('hay', norm($(this).attr('data-search')));
|
||||
});
|
||||
|
||||
function applySearch() {
|
||||
var raw = $('#orderSearch').val().trim();
|
||||
var terms = norm(raw).split(/\s+/).filter(Boolean);
|
||||
var visible = 0;
|
||||
|
||||
$('#ordersTable tr.order-row').each(function() {
|
||||
var hay = $(this).data('hay');
|
||||
var ok = terms.every(function(t) {
|
||||
return hay.indexOf(t) !== -1;
|
||||
});
|
||||
$(this).toggle(ok);
|
||||
if (ok) visible++;
|
||||
});
|
||||
|
||||
$('#noResults').toggle(visible === 0);
|
||||
$('#searchCount').text(terms.length ? visible + (visible === 1 ? ' ordine trovato' : ' ordini trovati') : '');
|
||||
|
||||
// i pulsanti Tutti/Attivi/Scaduti mantengono la ricerca
|
||||
$('.filter-link').each(function() {
|
||||
$(this).attr('href', '?filter=' + $(this).data('filter') + (raw ? '&q=' + encodeURIComponent(raw) : ''));
|
||||
});
|
||||
|
||||
var url = new URL(window.location.href);
|
||||
if (raw) url.searchParams.set('q', raw);
|
||||
else url.searchParams.delete('q');
|
||||
url.searchParams.delete('message');
|
||||
history.replaceState(null, '', url.toString());
|
||||
}
|
||||
|
||||
$('#orderSearch').on('input', applySearch);
|
||||
applySearch();
|
||||
|
||||
/* ===================== ORDINAMENTO ===================== */
|
||||
var sortCol = 1,
|
||||
sortDir = -1; // di partenza: N. Ordine decrescente, come la query
|
||||
|
||||
function cellValue(row, idx, type) {
|
||||
var td = $(row).children().eq(idx);
|
||||
if (td.find('input').length) {
|
||||
// cella in modifica: usa il valore originale
|
||||
var orig = td.data('original');
|
||||
if (typeof orig === 'undefined') orig = td.attr('data-value');
|
||||
td = $('<td>').text(orig || '');
|
||||
}
|
||||
var v = td.attr('data-value');
|
||||
if (typeof v === 'undefined') v = td.text();
|
||||
v = String(v).trim();
|
||||
if (v === '' || v === '–') return null;
|
||||
if (type === 'num') {
|
||||
var num = parseFloat(v.replace(',', '.'));
|
||||
return isNaN(num) ? null : num;
|
||||
}
|
||||
if (type === 'date') return v; // formato AAAA-MM-GG: ordinabile come testo
|
||||
return norm(v);
|
||||
}
|
||||
|
||||
$('#ordersTable thead').on('click', 'th.sortable', function() {
|
||||
var th = $(this);
|
||||
var idx = th.index();
|
||||
var type = th.data('type');
|
||||
|
||||
sortDir = (sortCol === idx) ? -sortDir : 1;
|
||||
sortCol = idx;
|
||||
|
||||
$('#ordersTable thead th.sortable').removeClass('asc desc');
|
||||
th.addClass(sortDir === 1 ? 'asc' : 'desc');
|
||||
|
||||
var rows = $('#ordersTable tbody tr.order-row').get();
|
||||
rows.sort(function(a, b) {
|
||||
var va = cellValue(a, idx, type);
|
||||
var vb = cellValue(b, idx, type);
|
||||
if (va === null && vb === null) return 0;
|
||||
if (va === null) return 1; // vuoti sempre in fondo
|
||||
if (vb === null) return -1;
|
||||
if (va < vb) return -sortDir;
|
||||
if (va > vb) return sortDir;
|
||||
return 0;
|
||||
});
|
||||
$('#noResults').before(rows);
|
||||
});
|
||||
|
||||
/* ===================== SCADENZA ===================== */
|
||||
$(document).on('click', 'td.expiry-date', function() {
|
||||
var cell = $(this);
|
||||
if (cell.find('input').length) return;
|
||||
|
||||
var orderId = cell.data('order-id');
|
||||
var current = cell.text().trim();
|
||||
cell.data('original', current).data('selected', false);
|
||||
|
||||
var input = $('<input type="text" class="form-control form-control-sm expiryDateInput" readonly>').val(current);
|
||||
cell.empty().append(input);
|
||||
|
||||
input.datepicker({
|
||||
dateFormat: 'yy-mm-dd',
|
||||
minDate: 0,
|
||||
onSelect: function(dateText) {
|
||||
cell.data('selected', true);
|
||||
confirmExpiry(cell, orderId, dateText);
|
||||
},
|
||||
onClose: function() {
|
||||
setTimeout(function() {
|
||||
if (!cell.data('selected')) cell.text(cell.data('original'));
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
input.datepicker('show');
|
||||
});
|
||||
|
||||
function confirmExpiry(cell, orderId, newExpiry) {
|
||||
var original = cell.data('original');
|
||||
if (newExpiry === original) {
|
||||
cell.text(original);
|
||||
return;
|
||||
}
|
||||
Swal.fire({
|
||||
title: "Sei sicuro?",
|
||||
html: "La scadenza passerà a <strong>" + newExpiry + "</strong>.",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Sì, modifica!",
|
||||
cancelButtonText: "Annulla"
|
||||
}).then(function(result) {
|
||||
if (!result.isConfirmed) {
|
||||
cell.text(original);
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: 'update_expiry.php',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
order_id: orderId,
|
||||
new_expiry: newExpiry,
|
||||
update_expiry: 1
|
||||
}
|
||||
}).done(function(res) {
|
||||
if (res.status === 'success') {
|
||||
cell.text(newExpiry);
|
||||
var active = newExpiry > today;
|
||||
cell.closest('tr').find('.state-cell').html(
|
||||
'<span class="badge ' + (active ? 'bg-success' : 'bg-danger') + '">' + (active ? 'Attivo' : 'Scaduto') + '</span>'
|
||||
);
|
||||
Swal.fire('Successo!', res.message, 'success');
|
||||
} else {
|
||||
cell.text(original);
|
||||
Swal.fire('Errore!', res.message, 'error');
|
||||
}
|
||||
}).fail(function(xhr, status, error) {
|
||||
console.error('Errore AJAX:', xhr.responseText);
|
||||
cell.text(original);
|
||||
Swal.fire('Errore!', 'Errore durante la richiesta al server: ' + error, 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ============ MAX RIPROGRAMMAZIONI / RIPROGRAMMATE ============ */
|
||||
function badgeHtml(max, done) {
|
||||
done = parseInt(done, 10) || 0;
|
||||
if (max === null || max === '' || typeof max === 'undefined') {
|
||||
return '<span class="badge bg-light text-dark">' + done + '</span>';
|
||||
}
|
||||
max = parseInt(max, 10);
|
||||
var cls = done >= max ? 'bg-danger' : (done > 0 ? 'bg-warning text-dark' : 'bg-success');
|
||||
return '<span class="badge ' + cls + '">' + done + ' / ' + max + '</span>';
|
||||
}
|
||||
|
||||
function renderResched(row, max, done) {
|
||||
var maxCell = row.find('td[data-field="maxreschedule"]');
|
||||
var doneCell = row.find('td[data-field="reprogrammed"]');
|
||||
var hasMax = !(max === null || max === '' || typeof max === 'undefined');
|
||||
maxCell.attr('data-value', hasMax ? max : '').html(hasMax ? max : '<span class="text-muted">–</span>');
|
||||
doneCell.attr('data-value', done).html(badgeHtml(max, done));
|
||||
}
|
||||
|
||||
$(document).on('click', 'td.num-edit', function() {
|
||||
var cell = $(this);
|
||||
if (cell.find('input').length) return;
|
||||
|
||||
var field = cell.data('field');
|
||||
var orderId = cell.data('order-id');
|
||||
var current = String(cell.attr('data-value'));
|
||||
var input = $('<input type="number" min="0" max="999" step="1" class="form-control form-control-sm num-input">').val(current);
|
||||
|
||||
cell.data('html', cell.html()).empty().append(input);
|
||||
input.trigger('focus').trigger('select');
|
||||
|
||||
var finished = false;
|
||||
|
||||
function finish(save) {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
var val = String(input.val()).trim();
|
||||
if (!save || val === '' || val === current) {
|
||||
cell.html(cell.data('html'));
|
||||
return;
|
||||
}
|
||||
if (!/^\d{1,3}$/.test(val)) {
|
||||
cell.html(cell.data('html'));
|
||||
Swal.fire('Valore non valido', 'Inserisci un numero intero da 0 a 999.', 'warning');
|
||||
return;
|
||||
}
|
||||
confirmNumber(cell, orderId, field, parseInt(val, 10));
|
||||
}
|
||||
|
||||
input.on('keydown', function(ev) {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
finish(true);
|
||||
} else if (ev.key === 'Escape') {
|
||||
finish(false);
|
||||
}
|
||||
});
|
||||
input.on('blur', function() {
|
||||
finish(true);
|
||||
});
|
||||
});
|
||||
|
||||
function confirmNumber(cell, orderId, field, value) {
|
||||
var label = field === 'maxreschedule' ? 'Max riprogrammazioni' : 'Riprogrammate';
|
||||
Swal.fire({
|
||||
title: 'Confermi la modifica?',
|
||||
html: label + ': <strong>' + value + '</strong>',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Sì, modifica!',
|
||||
cancelButtonText: 'Annulla'
|
||||
}).then(function(result) {
|
||||
if (!result.isConfirmed) {
|
||||
cell.html(cell.data('html'));
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: 'update_reschedule.php',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
order_id: orderId,
|
||||
field: field,
|
||||
value: value
|
||||
}
|
||||
}).done(function(res) {
|
||||
if (res.status === 'success') {
|
||||
renderResched(cell.closest('tr'), res.maxreschedule, res.reprogrammed);
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Aggiornato',
|
||||
timer: 1200,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else {
|
||||
cell.html(cell.data('html'));
|
||||
Swal.fire('Errore!', res.message || 'Aggiornamento non riuscito.', 'error');
|
||||
}
|
||||
}).fail(function(xhr) {
|
||||
console.error('Errore AJAX:', xhr.responseText);
|
||||
cell.html(cell.data('html'));
|
||||
Swal.fire('Errore!', 'Errore di comunicazione con il server.', 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php if ($message === 'success_delete') { ?>
|
||||
<script>
|
||||
Swal.fire('Fatto!', 'Ordine e prenotazioni cancellati.', 'success');
|
||||
</script>
|
||||
<?php } elseif ($message === 'error_delete') { ?>
|
||||
<script>
|
||||
Swal.fire('Errore!', 'Errore durante la cancellazione.', 'error');
|
||||
</script>
|
||||
<?php } ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,3 +1,4 @@
|
||||
[15-Oct-2025 15:13:11 UTC] Tentativo di aggiornamento AJAX - idorderbook: 1, new_expiry: 2025-10-16
|
||||
[15-Oct-2025 15:13:19 UTC] Tentativo di aggiornamento AJAX - idorderbook: 1, new_expiry: 2025-10-15
|
||||
[15-Oct-2025 15:13:58 UTC] Tentativo di aggiornamento AJAX - idorderbook: 1, new_expiry: 2025-10-30
|
||||
[24-Sep-2026 09:19:47 UTC] Tentativo di aggiornamento AJAX - idorderbook: 7, new_expiry: 2026-11-02
|
||||
|
||||
@@ -103,7 +103,11 @@ if ($is_openflexy || $service_schedule_result->num_rows > 0) {
|
||||
$idservice = $schedule_row["idservice"];
|
||||
$idserviceschedule = $schedule_row["idserviceschedule"];
|
||||
|
||||
$querychk = "SELECT * FROM bookingclass WHERE iduser = ? AND idserviceschedule = ?";
|
||||
$querychk = "SELECT 1 FROM bookingclass
|
||||
WHERE iduser = ?
|
||||
AND idserviceschedule = ?
|
||||
AND status <> 'cancelled'
|
||||
LIMIT 1";
|
||||
$stmt_chk = $conn->prepare($querychk);
|
||||
$stmt_chk->bind_param("ii", $selected_user_id, $idserviceschedule);
|
||||
$stmt_chk->execute();
|
||||
@@ -119,7 +123,10 @@ if ($is_openflexy || $service_schedule_result->num_rows > 0) {
|
||||
$maxcapacity = $row['maxcapacity'];
|
||||
$stmt_maxcapacity->close();
|
||||
|
||||
$bookingclass_count_query = "SELECT COUNT(*) as total_records FROM bookingclass WHERE idserviceschedule = ?";
|
||||
$bookingclass_count_query = "SELECT COUNT(*) AS total_records
|
||||
FROM bookingclass
|
||||
WHERE idserviceschedule = ?
|
||||
AND status = 'booked'";
|
||||
$stmt_count = $conn->prepare($bookingclass_count_query);
|
||||
$stmt_count->bind_param("i", $idserviceschedule);
|
||||
$stmt_count->execute();
|
||||
|
||||
+11
-5
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once __DIR__ . '/class/mailer.php';
|
||||
require_once('include/waitinglist.php');
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
@@ -53,12 +54,15 @@ $stmt->bind_param("i", $idpreviousbooking);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
// Inserisci la nuova prenotazione (pending)
|
||||
// Decide lo stato: pending se c'è posto, waiting se la classe è piena
|
||||
$bookingStatus = decideBookingStatus($conn, $idnewbooking);
|
||||
|
||||
// Inserisci la nuova prenotazione
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO bookingclass (idserviceschedule, iduser, prevbookingstart, idprevserviceschedule, idservice, idorder, bookingstart, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')"
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param("iisiiss", $idnewbooking, $iduser, $prevBookingStart, $idPrevServiceSchedule, $idservicenew, $globalidorder, $newtime);
|
||||
$stmt->bind_param("iisiisss", $idnewbooking, $iduser, $prevBookingStart, $idPrevServiceSchedule, $idservicenew, $globalidorder, $newtime, $bookingStatus);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
@@ -83,9 +87,11 @@ $stmtUser->close();
|
||||
|
||||
$newtimeformat = date("d-m-Y H:i", strtotime($newtime));
|
||||
|
||||
$redirectFlag = ($bookingStatus === 'waiting') ? 'waiting=Y' : 'reprogram=Y';
|
||||
|
||||
// Se manca l'email, completa comunque il flusso senza inviare
|
||||
if ($emailuser === '') {
|
||||
header("Location: userpanel.php?reprogram=Y");
|
||||
header("Location: userpanel.php?$redirectFlag");
|
||||
exit();
|
||||
}
|
||||
|
||||
@@ -138,5 +144,5 @@ try {
|
||||
error_log("Errore invio mail riprogrammazione (iduser $iduser): " . $mail->ErrorInfo);
|
||||
}
|
||||
|
||||
header("Location: userpanel.php?reprogram=Y");
|
||||
header("Location: userpanel.php?$redirectFlag");
|
||||
exit();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Aggiorna maxreschedule o reprogrammed di un ordine (chiamato via AJAX dal riepilogo ordini).
|
||||
* Risponde sempre in JSON: {status, message, maxreschedule, reprogrammed}
|
||||
*/
|
||||
ob_start();
|
||||
require_once('include/headscript.php');
|
||||
ob_end_clean(); // scarta eventuale output di headscript, così il JSON resta pulito
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
function jsonOut(array $data, int $code = 200): void
|
||||
{
|
||||
http_response_code($code);
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonOut(['status' => 'error', 'message' => 'Metodo non consentito.'], 405);
|
||||
}
|
||||
|
||||
$allowedFields = ['maxreschedule', 'reprogrammed'];
|
||||
$field = $_POST['field'] ?? '';
|
||||
$orderId = (int) ($_POST['order_id'] ?? 0);
|
||||
$value = (string) ($_POST['value'] ?? '');
|
||||
|
||||
if (!in_array($field, $allowedFields, true)) {
|
||||
jsonOut(['status' => 'error', 'message' => 'Campo non valido.'], 400);
|
||||
}
|
||||
if ($orderId <= 0) {
|
||||
jsonOut(['status' => 'error', 'message' => 'ID ordine non valido.'], 400);
|
||||
}
|
||||
if (!preg_match('/^\d{1,3}$/', $value)) {
|
||||
jsonOut(['status' => 'error', 'message' => 'Inserisci un numero intero da 0 a 999.'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
$check = $pdo->prepare("SELECT idorderbook FROM orderbook WHERE idorderbook = :id");
|
||||
$check->execute([':id' => $orderId]);
|
||||
if (!$check->fetch()) {
|
||||
jsonOut(['status' => 'error', 'message' => 'Ordine non trovato.'], 404);
|
||||
}
|
||||
|
||||
// $field è da whitelist, quindi sicuro da inserire nella query
|
||||
$upd = $pdo->prepare("UPDATE orderbook SET `$field` = :v WHERE idorderbook = :id");
|
||||
$upd->execute([':v' => (int) $value, ':id' => $orderId]);
|
||||
|
||||
$row = $pdo->prepare("SELECT maxreschedule, reprogrammed FROM orderbook WHERE idorderbook = :id");
|
||||
$row->execute([':id' => $orderId]);
|
||||
$r = $row->fetch();
|
||||
|
||||
jsonOut([
|
||||
'status' => 'success',
|
||||
'message' => 'Valore aggiornato.',
|
||||
'maxreschedule' => $r['maxreschedule'] === null ? null : (int) $r['maxreschedule'],
|
||||
'reprogrammed' => (int) $r['reprogrammed'],
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('update_reschedule: ' . $e->getMessage());
|
||||
jsonOut(['status' => 'error', 'message' => 'Errore durante il salvataggio.'], 500);
|
||||
}
|
||||
+185
-3
@@ -59,7 +59,7 @@ $stmtBooked->execute([
|
||||
$bookedRows = $stmtBooked->fetchAll();
|
||||
|
||||
// Lezioni in attesa di conferma (pending) dell'utente, nel mese visualizzato
|
||||
$sqlPending = "SELECT bookingclass.*, service.servicename, service.colorclass, serviceschedule.dateschedule
|
||||
$sqlPending = "SELECT bookingclass.*, service.servicename, service.colorclass, service.idteacher, serviceschedule.dateschedule
|
||||
FROM bookingclass
|
||||
LEFT JOIN service ON bookingclass.idservice = service.idservice
|
||||
LEFT JOIN serviceschedule ON bookingclass.idserviceschedule = serviceschedule.idserviceschedule
|
||||
@@ -75,6 +75,40 @@ $stmtPending->execute([
|
||||
]);
|
||||
$pendingRows = $stmtPending->fetchAll();
|
||||
|
||||
// Lezioni in lista d'attesa (waiting) dell'utente, nel mese visualizzato
|
||||
$sqlWaiting = "SELECT bookingclass.*, service.servicename, service.colorclass, service.maxcapacity, service.idteacher, serviceschedule.dateschedule
|
||||
FROM bookingclass
|
||||
LEFT JOIN service ON bookingclass.idservice = service.idservice
|
||||
LEFT JOIN serviceschedule ON bookingclass.idserviceschedule = serviceschedule.idserviceschedule
|
||||
WHERE bookingclass.iduser = :iduser
|
||||
AND bookingclass.status = 'waiting'
|
||||
AND serviceschedule.dateschedule BETWEEN :monthStart AND DATE_ADD(:monthEnd, INTERVAL 1 DAY)
|
||||
ORDER BY serviceschedule.dateschedule";
|
||||
$stmtWaiting = $pdo->prepare($sqlWaiting);
|
||||
$stmtWaiting->execute([
|
||||
':iduser' => $iduserlogin,
|
||||
':monthStart' => $currentMonthStart,
|
||||
':monthEnd' => $currentMonthEnd,
|
||||
]);
|
||||
$waitingRows = $stmtWaiting->fetchAll();
|
||||
|
||||
// Posizione in coda per ciascuna lezione waiting (1-based, ordine per created_at)
|
||||
$waitingPositions = [];
|
||||
foreach ($waitingRows as $wrow) {
|
||||
$sqlPos = "SELECT COUNT(*) AS pos
|
||||
FROM bookingclass
|
||||
WHERE idserviceschedule = :sched
|
||||
AND status = 'waiting'
|
||||
AND created_at <= :mycreated";
|
||||
$stmtPos = $pdo->prepare($sqlPos);
|
||||
$stmtPos->execute([
|
||||
':sched' => (int) $wrow['idserviceschedule'],
|
||||
':mycreated' => $wrow['created_at'],
|
||||
]);
|
||||
$rowPos = $stmtPos->fetch();
|
||||
$waitingPositions[(int) $wrow['idbookingclass']] = (int) ($rowPos['pos'] ?? 0);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 3) Somma dei ticket acquistati dall'utente
|
||||
* ---------------------------------------------------------------------- */
|
||||
@@ -123,6 +157,18 @@ $pending = (int) ($rowCounts['pending'] ?? 0);
|
||||
|
||||
$toprogram = $totalTickets - $passedRecords - $futureRecords - $pending - $lost;
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 4b) Insegnanti nelle card (solo se attivo nelle Opzioni)
|
||||
* ---------------------------------------------------------------------- */
|
||||
$optShow = $pdo->query("SELECT show_teacher FROM `option` LIMIT 1")->fetch();
|
||||
$showTeacher = ($optShow['show_teacher'] ?? 'N') === 'Y';
|
||||
|
||||
$teachersById = [];
|
||||
if ($showTeacher) {
|
||||
foreach ($pdo->query("SELECT idemployee, nameemployee, surnameemployee, photo FROM employee")->fetchAll() as $t) {
|
||||
$teachersById[(int) $t['idemployee']] = $t;
|
||||
}
|
||||
}
|
||||
/* -------------------------------------------------------------------------
|
||||
* 5) Pre-caricamento limiti di riprogrammazione per gli ordini coinvolti
|
||||
* (una sola query invece di una per ogni card, dentro il loop)
|
||||
@@ -157,6 +203,22 @@ function e($value): string
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function teacherLine(array $row, bool $show, array $teachersById): string
|
||||
{
|
||||
if (!$show) return '';
|
||||
$t = $teachersById[(int) ($row['idteacher'] ?? 0)] ?? null;
|
||||
if (!$t) return '';
|
||||
|
||||
$name = trim($t['nameemployee'] . ' ' . $t['surnameemployee']);
|
||||
if (!empty($t['photo'])) {
|
||||
$avatar = '<img class="teacher-avatar-sm" src="assets/images/teachers/' . e($t['photo']) . '" alt="">';
|
||||
} else {
|
||||
$ini = mb_strtoupper(mb_substr($t['nameemployee'], 0, 1) . mb_substr($t['surnameemployee'], 0, 1));
|
||||
$avatar = '<span class="teacher-avatar-sm">' . e($ini) . '</span>';
|
||||
}
|
||||
return '<p class="custom-paragraph teacher-line">' . $avatar . 'Insegnante: <strong>' . e($name) . '</strong></p>';
|
||||
}
|
||||
|
||||
$italianMonths = [
|
||||
"January" => "Gennaio",
|
||||
"February" => "Febbraio",
|
||||
@@ -233,8 +295,8 @@ $italianMonths = [
|
||||
|
||||
.custom-date-box {
|
||||
flex: 0 0 110px;
|
||||
background: #1ebf73;
|
||||
color: #fff;
|
||||
background: #E1D0B8;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -271,6 +333,33 @@ $italianMonths = [
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.teacher-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 4px 0 6px;
|
||||
}
|
||||
|
||||
.teacher-line strong {
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.teacher-avatar-sm {
|
||||
flex: 0 0 26px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background: #E1D0B8;
|
||||
color: #5a4630;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-paragraph {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
@@ -541,6 +630,27 @@ $italianMonths = [
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if (isset($_GET['waiting'])) { ?>
|
||||
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-hourglass-half me-2"></i>
|
||||
La classe era piena: sei stata/o inserita/o in <strong>lista d'attesa</strong>. Se si libera un posto verrai avvisata/o.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if (isset($_GET['leftwaiting'])) { ?>
|
||||
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check me-2"></i>
|
||||
Sei uscita/o dalla lista d'attesa.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if (isset($_GET['full'])) { ?>
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-ban me-2"></i>
|
||||
La classe è al completo e la lista d'attesa non è attiva. Prova con un altro orario.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if (isset($_GET['deleted'])) { ?>
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="mdi mdi-check-all me-2"></i>
|
||||
@@ -626,6 +736,7 @@ $italianMonths = [
|
||||
<span style="display:inline-block;background:#fff4d6;color:#8a6d00;font-size:11px;font-weight:700;padding:2px 9px;border-radius:20px;margin-left:6px;vertical-align:middle;">In attesa</span>
|
||||
</h2>
|
||||
<p class="custom-paragraph">Quando: <?php echo e($pWhen); ?></p>
|
||||
<?php echo teacherLine($prow, $showTeacher, $teachersById); ?>
|
||||
<p class="custom-paragraph">Luogo: via Valassina 62/B Seregno - Sala Contesto Yoga</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -636,6 +747,59 @@ $italianMonths = [
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($waitingRows)) : ?>
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 style="color:#8a6d00;margin-bottom:14px;">
|
||||
<i class="fas fa-hourglass-half me-1"></i> In lista d'attesa
|
||||
</h5>
|
||||
<?php foreach ($waitingRows as $wrow) :
|
||||
$wDateObj = new DateTime($wrow['dateschedule']);
|
||||
$wDay = $wDateObj->format("d");
|
||||
$wMonth = $italianMonths[$wDateObj->format("F")];
|
||||
$wWhen = $wDateObj->format("d-m-Y H:i");
|
||||
$wColor = $wrow['colorclass'] ?? '#1ebf73';
|
||||
$wName = $wrow['servicename'] ?? '';
|
||||
$wPos = $waitingPositions[(int) $wrow['idbookingclass']] ?? 0;
|
||||
?>
|
||||
<div class="custom-card pending-card">
|
||||
<div class="custom-date-box" style="background:#9C9C9C;">
|
||||
<div class="custom-day"><?php echo e($wDay); ?></div>
|
||||
<div class="custom-month"><?php echo e($wMonth); ?></div>
|
||||
</div>
|
||||
<div class="custom-event-details" style="border-left-color:<?php echo e($wColor); ?>">
|
||||
<h2 class="custom-heading">
|
||||
<?php echo e($wName); ?>
|
||||
<span style="display:inline-block;background:#eee;color:#555;font-size:11px;font-weight:700;padding:2px 9px;border-radius:20px;margin-left:6px;vertical-align:middle;">In coda</span>
|
||||
</h2>
|
||||
<p class="custom-paragraph">Quando: <?php echo e($wWhen); ?></p>
|
||||
<?php echo teacherLine($wrow, $showTeacher, $teachersById); ?>
|
||||
<p class="custom-paragraph">Luogo: via Valassina 62/B Seregno - Sala Contesto Yoga</p>
|
||||
<?php if ($wPos > 0) : ?>
|
||||
<p class="custom-paragraph" style="font-weight:700;color:#8a6d00;">
|
||||
<i class="fas fa-list-ol me-1"></i> La tua posizione in coda: <?php echo e($wPos); ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<p class="custom-paragraph" style="font-size:13px;color:#9ca3af;">
|
||||
Se si libera un posto verrai avvisata/o. Non serve fare nulla.
|
||||
</p>
|
||||
<div style="margin-top:8px;">
|
||||
<button class="custom-action-button" style="background:#fbeef3;color:#d1698a;"
|
||||
onclick="confirmLeaveWaiting(<?php echo (int) $wrow['idbookingclass']; ?>)">
|
||||
<i class="fas fa-times-circle"></i> Esci dalla lista d'attesa
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div class="card">
|
||||
@@ -694,6 +858,7 @@ $italianMonths = [
|
||||
<div class="custom-event-details" style="border-left-color:<?php echo e($colorclass); ?>">
|
||||
<h2 class="custom-heading"><?php echo e($servicename); ?></h2>
|
||||
<p class="custom-paragraph">Quando: <?php echo e($newDateFormat); ?></p>
|
||||
<?php echo teacherLine($row, $showTeacher, $teachersById); ?>
|
||||
<p class="custom-paragraph">Luogo: via Valassina 62/B Seregno - Sala Contesto Yoga</p>
|
||||
<div class="custom-actions">
|
||||
<button class="custom-action-button" onclick="addToCalendar(this)" data-eventname="<?php echo e($servicename); ?>" data-eventdate="<?php echo e($newDateFormat); ?>">
|
||||
@@ -741,6 +906,23 @@ $italianMonths = [
|
||||
const outlookCalendarLink = `webcal://outlook.live.com/calendar/0/deeplink/compose?path=/calendar/action/compose&subject=${encodeURIComponent(eventName)}&startdt=${encodeURIComponent(eventDate)}`;
|
||||
window.open(outlookCalendarLink, '_blank');
|
||||
}
|
||||
|
||||
function confirmLeaveWaiting(id) {
|
||||
Swal.fire({
|
||||
title: "Uscire dalla lista d'attesa?",
|
||||
text: "Verrai rimossa/o dalla coda per questa lezione.",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#d1698a",
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonText: "Sì, esci",
|
||||
cancelButtonText: "Annulla"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = "leavewaiting.php?id=" + id;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,153 @@
|
||||
/* YogaSoul Eventi — stili widget.
|
||||
Tutti i selettori sono prefissati con .yse-widget per alzare la specificità
|
||||
sopra le regole del tema WordPress (evita che tema "vinca" su colori/font). */
|
||||
|
||||
.yse-widget {
|
||||
--yse-ink: #2b2622;
|
||||
--yse-muted: #8c837c;
|
||||
--yse-line: #ece7e2;
|
||||
--yse-card: #ffffff;
|
||||
max-width: 1140px; /* largo come Amelia; su schermi piccoli riempie il contenitore */
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
font-family: inherit;
|
||||
color: var(--yse-ink);
|
||||
line-height: 1.55;
|
||||
text-align: left;
|
||||
}
|
||||
.yse-widget, .yse-widget * { box-sizing: border-box; }
|
||||
|
||||
/* --- Reset mirati contro il tema --- */
|
||||
.yse-widget button {
|
||||
font-family: inherit;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
text-transform: none !important;
|
||||
letter-spacing: normal !important;
|
||||
min-height: 0 !important;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.yse-widget button:hover { filter: none; }
|
||||
.yse-widget input {
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.yse-widget h3, .yse-widget h4, .yse-widget p { margin: 0; padding: 0; }
|
||||
.yse-widget img { max-width: 100%; height: auto; }
|
||||
.yse-widget a { text-decoration: none; }
|
||||
.yse-widget svg { vertical-align: middle; }
|
||||
|
||||
.yse-empty { text-align: center; color: var(--yse-muted); padding: 26px; }
|
||||
|
||||
.yse-widget .yse-card {
|
||||
--yse-accent: #4CAF50;
|
||||
--yse-accent-soft: color-mix(in srgb, var(--yse-accent) 10%, white);
|
||||
--yse-accent-ink: color-mix(in srgb, var(--yse-accent) 62%, black);
|
||||
background: var(--yse-card);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 14px 40px rgba(43, 38, 34, 0.10);
|
||||
overflow: hidden;
|
||||
}
|
||||
.yse-widget .yse-card + .yse-card { margin-top: 24px; }
|
||||
|
||||
/* Header compatto */
|
||||
.yse-widget .yse-chead { display: flex; gap: 18px; padding: 20px 26px; align-items: flex-start; }
|
||||
.yse-widget .yse-datechip { flex-shrink: 0; border: 1px solid var(--yse-line); border-radius: 12px; padding: 7px 12px 5px; text-align: center; min-width: 58px; }
|
||||
.yse-widget .yse-datechip .yse-m { font-size: 11px; font-weight: 600; letter-spacing: .12em; color: var(--yse-accent-ink); }
|
||||
.yse-widget .yse-datechip .yse-d { font-family: inherit; font-size: 24px; font-weight: 600; color: var(--yse-ink); line-height: 1; }
|
||||
.yse-widget .yse-main { flex: 1; min-width: 0; }
|
||||
.yse-widget .yse-title-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
.yse-widget .yse-title-row h3 { font-family: inherit; font-weight: 500; font-size: 23px; margin: 0; color: var(--yse-ink); }
|
||||
.yse-widget .yse-toggle { background: none !important; border: none !important; color: var(--yse-accent-ink) !important; font-weight: 600; font-size: 13px; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; padding: 2px; border-radius: 0; }
|
||||
.yse-widget .yse-toggle svg { width: 15px; height: 15px; transition: transform .2s; }
|
||||
.yse-widget .yse-toggle.yse-on svg { transform: rotate(180deg); }
|
||||
.yse-widget .yse-meta { display: flex; flex-wrap: wrap; gap: 6px 22px; margin-top: 10px; }
|
||||
.yse-widget .yse-item { display: flex; align-items: center; gap: 7px; color: var(--yse-muted); font-size: 14px; }
|
||||
.yse-widget .yse-item svg { width: 17px; height: 17px; color: var(--yse-accent-ink); flex-shrink: 0; }
|
||||
.yse-widget .yse-item strong { color: var(--yse-ink); font-weight: 600; }
|
||||
.yse-widget .yse-price { flex-shrink: 0; align-self: flex-start; background: var(--yse-accent) !important; color: #fff; font-weight: 600; font-size: 15px; padding: 8px 15px; border-radius: 999px; }
|
||||
|
||||
/* Dettagli espandibili */
|
||||
.yse-widget .yse-details { display: none; border-top: 1px solid var(--yse-line); }
|
||||
.yse-widget .yse-details.yse-open { display: block; }
|
||||
.yse-widget .yse-hero { position: relative; height: 300px; background: linear-gradient(135deg, var(--yse-accent-ink), var(--yse-accent)); }
|
||||
.yse-widget .yse-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.yse-widget .yse-emblem { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,.35); }
|
||||
.yse-widget .yse-emblem svg { width: 78px; height: 78px; }
|
||||
.yse-widget .yse-body { padding: 24px 26px; }
|
||||
.yse-widget .yse-section { margin-top: 26px; }
|
||||
.yse-widget .yse-body h4 { font-family: inherit; font-weight: 600; font-size: 19px; color: var(--yse-ink); margin: 0 0 10px; display: flex; align-items: center; gap: 9px; }
|
||||
.yse-widget .yse-body h4 svg { width: 20px; height: 20px; color: var(--yse-accent-ink); }
|
||||
.yse-widget .yse-desc { font-size: 15.5px; color: #4a433d; }
|
||||
.yse-widget .yse-desc p { margin: 0 0 10px; }
|
||||
.yse-widget .yse-desc img { max-width: 100%; border-radius: 10px; }
|
||||
.yse-widget .yse-detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-top: 24px; }
|
||||
.yse-widget .yse-detail { background: var(--yse-accent-soft); border: 1px solid color-mix(in srgb, var(--yse-accent) 18%, white); border-radius: 14px; padding: 15px 16px; }
|
||||
.yse-widget .yse-dh { display: flex; align-items: center; gap: 8px; font-weight: 600; color: var(--yse-accent-ink); margin-bottom: 6px; font-size: 14px; }
|
||||
.yse-widget .yse-dh svg { width: 17px; height: 17px; }
|
||||
.yse-widget .yse-dt { font-size: 14px; color: #4a433d; }
|
||||
.yse-widget .yse-gallery { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.yse-widget .yse-gallery img { width: 108px; height: 108px; object-fit: cover; border-radius: 12px; cursor: pointer; transition: transform .15s, box-shadow .15s; }
|
||||
.yse-widget .yse-gallery img:hover { transform: translateY(-2px); box-shadow: 0 8px 18px rgba(0,0,0,.15); }
|
||||
.yse-widget .yse-map { border: 0; width: 100%; height: 220px; border-radius: 14px; display: block; }
|
||||
|
||||
/* Booking */
|
||||
.yse-widget .yse-booking { padding: 22px 26px; border-top: 1px solid var(--yse-line); }
|
||||
.yse-widget .yse-soldout { text-align: center; font-weight: 600; color: #c0392b; }
|
||||
.yse-widget .yse-wl-banner { background: color-mix(in srgb, var(--yse-accent) 12%, white); color: var(--yse-accent-ink); border-radius: 12px; padding: 12px 14px; font-size: 13.5px; margin-bottom: 16px; }
|
||||
.yse-widget .yse-bcompact { display: flex; align-items: flex-end; gap: 22px; flex-wrap: wrap; }
|
||||
.yse-widget .yse-btitle { font-family: inherit; font-weight: 600; font-size: 19px; color: var(--yse-ink); margin-right: auto; align-self: center; }
|
||||
.yse-widget .yse-lbl { font-size: 12px; color: var(--yse-muted); text-transform: uppercase; letter-spacing: .05em; margin-bottom: 5px; }
|
||||
.yse-widget .yse-stepper { display: flex; align-items: center; border: 1px solid var(--yse-line); border-radius: 11px; overflow: hidden; background: #fff; }
|
||||
.yse-widget .yse-stepper button { background: #fff !important; width: 44px; height: 46px; font-size: 22px; color: var(--yse-accent-ink) !important; cursor: pointer; }
|
||||
.yse-widget .yse-stepper button:disabled { color: #cfc8c2 !important; cursor: not-allowed; }
|
||||
.yse-widget .yse-num { width: 54px; text-align: center; font-size: 17px; font-weight: 600; }
|
||||
.yse-widget .yse-total .yse-val { font-family: inherit; font-size: 26px; font-weight: 600; color: var(--yse-accent-ink); }
|
||||
.yse-widget .yse-cta { background: var(--yse-accent) !important; color: #fff !important; font-size: 15px; font-weight: 600; border-radius: 11px; cursor: pointer; transition: filter .15s; }
|
||||
.yse-widget .yse-cta:hover { filter: brightness(.94) !important; }
|
||||
.yse-widget .yse-cta:disabled { background: #cfc8c2 !important; cursor: not-allowed; }
|
||||
.yse-widget .yse-open { padding: 14px 26px; }
|
||||
.yse-widget .yse-full { width: 100%; padding: 15px; margin-top: 6px; }
|
||||
.yse-widget .yse-bform { display: none; margin-top: 20px; padding-top: 20px; border-top: 1px dashed var(--yse-line); }
|
||||
.yse-widget .yse-bform.yse-open { display: block; }
|
||||
.yse-widget .yse-frow { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.yse-widget .yse-field { margin-bottom: 12px; }
|
||||
.yse-widget .yse-field label { display: block; font-size: 13px; font-weight: 600; color: var(--yse-ink); margin-bottom: 5px; }
|
||||
.yse-widget .yse-field input { border: 1px solid var(--yse-line) !important; border-radius: 10px; padding: 12px 13px; font-size: 15px; background: #fff !important; color: var(--yse-ink) !important; }
|
||||
.yse-widget .yse-field input:focus { outline: none; border-color: var(--yse-accent) !important; box-shadow: 0 0 0 3px color-mix(in srgb, var(--yse-accent) 22%, white) !important; }
|
||||
.yse-widget .yse-consent { font-size: 13px; color: #4a433d; display: flex; gap: 9px; align-items: flex-start; margin-bottom: 9px; }
|
||||
.yse-widget .yse-consent input { width: auto; margin-top: 3px; }
|
||||
.yse-widget .yse-hp { position: absolute !important; left: -9999px !important; width: 1px !important; height: 1px !important; }
|
||||
.yse-widget .yse-error { color: #c0392b; font-size: 13px; margin-top: 8px; min-height: 18px; }
|
||||
.yse-widget .yse-done { text-align: center; padding: 10px 4px; }
|
||||
.yse-widget .yse-done .yse-ok { width: 54px; height: 54px; border-radius: 50%; background: var(--yse-accent); color: #fff; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px; }
|
||||
.yse-widget .yse-done h3 { margin: 0 0 6px; font-family: inherit; font-size: 21px; }
|
||||
.yse-widget .yse-done p { color: #4a433d; font-size: 14px; margin: 2px 0; }
|
||||
|
||||
/* Lightbox (montato su <body>, fuori da .yse-widget) */
|
||||
.yse-lightbox { position: fixed; inset: 0; background: rgba(24,19,16,.9); display: none; align-items: center; justify-content: center; z-index: 99999; padding: 24px; }
|
||||
.yse-lightbox.yse-open { display: flex; }
|
||||
.yse-lightbox img { max-width: 92vw; max-height: 86vh; border-radius: 12px; }
|
||||
.yse-lightbox .yse-lbclose { position: absolute; top: 18px; right: 22px; color: #fff !important; font-size: 34px; cursor: pointer; background: none !important; border: none !important; line-height: 1; padding: 0; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.yse-widget .yse-hero { height: 210px; }
|
||||
.yse-widget .yse-title-row h3 { font-size: 20px; }
|
||||
.yse-widget .yse-frow { grid-template-columns: 1fr; }
|
||||
.yse-widget .yse-btitle { width: 100%; margin-bottom: 4px; }
|
||||
.yse-widget .yse-chead, .yse-widget .yse-booking, .yse-widget .yse-body { padding-left: 18px; padding-right: 18px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { .yse-widget * { transition: none !important; } }
|
||||
|
||||
/* Miniatura copertina nella testata compatta */
|
||||
.yse-widget .yse-thumb { width: 76px; height: 76px; border-radius: 12px; object-fit: cover; flex-shrink: 0; }
|
||||
@media (max-width: 600px) { .yse-widget .yse-thumb { width: 60px; height: 60px; } }
|
||||
|
||||
/* Evento programmato (prenotazioni non ancora aperte) */
|
||||
.yse-widget .yse-scheduled { text-align: center; font-weight: 600; color: var(--yse-accent-ink); background: color-mix(in srgb, var(--yse-accent) 12%, white); border-radius: 12px; padding: 12px 14px; }
|
||||
@@ -0,0 +1,246 @@
|
||||
/* YogaSoul Eventi — interazioni widget */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function fmtMoney(v, sym) {
|
||||
if (v <= 0) return "Gratuito";
|
||||
return (
|
||||
sym +
|
||||
v.toLocaleString("it-IT", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function apiBase(el) {
|
||||
var w = el.closest(".yse-widget");
|
||||
return (w && w.getAttribute("data-api")) || "";
|
||||
}
|
||||
|
||||
function initCard(card) {
|
||||
var api = apiBase(card);
|
||||
var eid = parseInt(card.getAttribute("data-eid"), 10);
|
||||
var cost = parseFloat(card.getAttribute("data-cost")) || 0;
|
||||
var sym = card.getAttribute("data-sym") || "€";
|
||||
var maxQty = parseInt(card.getAttribute("data-maxqty"), 10) || 0;
|
||||
var waitMode = card.getAttribute("data-waitmode") === "1";
|
||||
|
||||
// Toggle dettagli
|
||||
var toggle = card.querySelector(".yse-toggle");
|
||||
if (toggle) {
|
||||
toggle.addEventListener("click", function () {
|
||||
var det = card.querySelector(".yse-details");
|
||||
if (!det) return;
|
||||
var open = det.classList.toggle("yse-open");
|
||||
toggle.classList.toggle("yse-on", open);
|
||||
toggle.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
var lbl = toggle.querySelector(".yse-tlabel");
|
||||
if (lbl) lbl.textContent = open ? "Chiudi" : "Dettagli";
|
||||
});
|
||||
}
|
||||
|
||||
var booking = card.querySelector(".yse-booking");
|
||||
var numEl = card.querySelector(".yse-num");
|
||||
if (!numEl) return; // esaurito senza lista d'attesa: nessuna prenotazione
|
||||
|
||||
var totEl = card.querySelector(".yse-total .yse-val");
|
||||
var minus = card.querySelector(".yse-minus");
|
||||
var plus = card.querySelector(".yse-plus");
|
||||
var openBtn = card.querySelector(".yse-open");
|
||||
var form = card.querySelector(".yse-bform");
|
||||
var submit = card.querySelector(".yse-submit");
|
||||
var qty = 1;
|
||||
|
||||
function refresh() {
|
||||
numEl.textContent = qty;
|
||||
if (totEl) totEl.textContent = fmtMoney(qty * cost, sym);
|
||||
minus.disabled = qty <= 1;
|
||||
plus.disabled = qty >= maxQty;
|
||||
if (submit) {
|
||||
submit.textContent = waitMode
|
||||
? "Iscriviti alla lista d'attesa"
|
||||
: "Conferma prenotazione" +
|
||||
(cost > 0 ? " · " + fmtMoney(qty * cost, sym) : "");
|
||||
}
|
||||
}
|
||||
minus.addEventListener("click", function () {
|
||||
if (qty > 1) {
|
||||
qty--;
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
plus.addEventListener("click", function () {
|
||||
if (qty < maxQty) {
|
||||
qty++;
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
refresh();
|
||||
|
||||
openBtn.addEventListener("click", function () {
|
||||
form.classList.add("yse-open");
|
||||
openBtn.style.display = "none";
|
||||
var n = card.querySelector(".yse-name");
|
||||
if (n) n.focus();
|
||||
});
|
||||
|
||||
submit.addEventListener("click", function () {
|
||||
var name = card.querySelector(".yse-name").value.trim();
|
||||
var surname = card.querySelector(".yse-surname").value.trim();
|
||||
var email = card.querySelector(".yse-email").value.trim();
|
||||
var privacy = card.querySelector(".yse-c-privacy").checked;
|
||||
var mkt = card.querySelector(".yse-c-mkt").checked;
|
||||
var hp = card.querySelector(".yse-hp").value;
|
||||
var err = card.querySelector(".yse-error");
|
||||
|
||||
if (!name || !surname) {
|
||||
err.textContent = "Inserisci nome e cognome.";
|
||||
return;
|
||||
}
|
||||
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
||||
err.textContent = "Inserisci un'email valida.";
|
||||
return;
|
||||
}
|
||||
if (!privacy) {
|
||||
err.textContent = "Devi accettare l'informativa privacy.";
|
||||
return;
|
||||
}
|
||||
err.textContent = "";
|
||||
|
||||
submit.disabled = true;
|
||||
submit.textContent = "Invio in corso…";
|
||||
|
||||
var body = new URLSearchParams();
|
||||
body.set("event_id", eid);
|
||||
body.set("name", name);
|
||||
body.set("surname", surname);
|
||||
body.set("email", email);
|
||||
body.set("seats", qty);
|
||||
body.set("consent", "1");
|
||||
body.set("marketing", mkt ? "1" : "0");
|
||||
body.set("website", hp); // honeypot
|
||||
|
||||
fetch(api + "/book.php", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type":
|
||||
"application/x-www-form-urlencoded;charset=UTF-8",
|
||||
},
|
||||
body: body.toString(),
|
||||
})
|
||||
.then(function (r) {
|
||||
return r.json().then(function (j) {
|
||||
return { status: r.status, j: j };
|
||||
});
|
||||
})
|
||||
.then(function (res) {
|
||||
var j = res.j || {};
|
||||
if (j.ok) {
|
||||
showDone(booking, j, name, surname, email);
|
||||
return;
|
||||
}
|
||||
// Errori gestiti
|
||||
submit.disabled = false;
|
||||
refresh();
|
||||
if (j.code === "partial") {
|
||||
err.textContent =
|
||||
j.error ||
|
||||
"Sono rimasti solo " + j.remaining + " posti.";
|
||||
if (
|
||||
typeof j.remaining === "number" &&
|
||||
j.remaining > 0
|
||||
) {
|
||||
maxQty = j.remaining;
|
||||
if (qty > maxQty) {
|
||||
qty = maxQty;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err.textContent =
|
||||
j.error || "Prenotazione non riuscita. Riprova.";
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
submit.disabled = false;
|
||||
refresh();
|
||||
err.textContent = "Errore di rete. Riprova.";
|
||||
});
|
||||
});
|
||||
|
||||
function showDone(container, j, name, surname, email) {
|
||||
var waiting = j.status === "waiting";
|
||||
var title = waiting
|
||||
? "Sei in lista d'attesa"
|
||||
: "Prenotazione confermata!";
|
||||
var msg =
|
||||
j.message ||
|
||||
(waiting
|
||||
? "Ti avviseremo appena si libera un posto."
|
||||
: "Ti abbiamo inviato una email di riepilogo.");
|
||||
container.innerHTML =
|
||||
'<div class="yse-done">' +
|
||||
' <div class="yse-ok"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" width="24" height="24">' +
|
||||
' <path d="M20 6 9 17l-5-5"/></svg></div>' +
|
||||
" <h3>" +
|
||||
title +
|
||||
"</h3>" +
|
||||
" <p><strong>" +
|
||||
escapeHtml(name + " " + surname) +
|
||||
"</strong> · " +
|
||||
escapeHtml(email) +
|
||||
"</p>" +
|
||||
" <p>" +
|
||||
escapeHtml(msg) +
|
||||
"</p>" +
|
||||
"</div>";
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
var d = document.createElement("div");
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// Lightbox condiviso
|
||||
function ensureLightbox() {
|
||||
var lb = document.querySelector(".yse-lightbox");
|
||||
if (lb) return lb;
|
||||
lb = document.createElement("div");
|
||||
lb.className = "yse-lightbox";
|
||||
lb.innerHTML =
|
||||
'<button class="yse-lbclose" aria-label="Chiudi">×</button><img src="" alt="">';
|
||||
document.body.appendChild(lb);
|
||||
function close() {
|
||||
lb.classList.remove("yse-open");
|
||||
lb.querySelector("img").src = "";
|
||||
}
|
||||
lb.addEventListener("click", function (e) {
|
||||
if (e.target === lb || e.target.classList.contains("yse-lbclose"))
|
||||
close();
|
||||
});
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") close();
|
||||
});
|
||||
return lb;
|
||||
}
|
||||
|
||||
function init() {
|
||||
document.querySelectorAll(".yse-card").forEach(initCard);
|
||||
document.querySelectorAll(".yse-gallery img").forEach(function (img) {
|
||||
img.addEventListener("click", function () {
|
||||
var lb = ensureLightbox();
|
||||
lb.querySelector("img").src = img.getAttribute("data-full");
|
||||
lb.classList.add("yse-open");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: YogaSoul Eventi
|
||||
* Description: Mostra gli eventi di YogiBook con card di prenotazione (lista d'attesa inclusa). Shortcode: [yogasoul_eventi], [yogasoul_eventi ids="12,15"], [yogasoul_eventi id="12"], [yogasoul_eventi limit="3"].
|
||||
* Version: 1.0.0
|
||||
* Author: YogaSoul
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base dell'API YogiBook. Sovrascrivibile in wp-config.php con:
|
||||
* define('YSE_API_BASE', 'https://yogibook.yogasoul.it/apiwp');
|
||||
*/
|
||||
if (!defined('YSE_API_BASE')) {
|
||||
define('YSE_API_BASE', 'https://yogibook.yogasoul.it/apiwp');
|
||||
}
|
||||
|
||||
/* Registra gli asset (accodati solo quando lo shortcode è in pagina).
|
||||
Nessun Google Font: il widget eredita il carattere del tema. */
|
||||
add_action('wp_enqueue_scripts', function () {
|
||||
wp_register_style('yse-eventi', plugins_url('assets/yogasoul-eventi.css', __FILE__), [], '1.0.1');
|
||||
wp_register_script('yse-eventi', plugins_url('assets/yogasoul-eventi.js', __FILE__), [], '1.0.0', true);
|
||||
});
|
||||
|
||||
add_shortcode('yogasoul_eventi', 'yse_render_shortcode');
|
||||
|
||||
function yse_render_shortcode($atts)
|
||||
{
|
||||
$atts = shortcode_atts([
|
||||
'ids' => '',
|
||||
'id' => '',
|
||||
'limit' => '',
|
||||
], $atts, 'yogasoul_eventi');
|
||||
|
||||
wp_enqueue_style('yse-eventi');
|
||||
wp_enqueue_script('yse-eventi');
|
||||
|
||||
// Costruisce l'URL dell'endpoint lista
|
||||
$query = [];
|
||||
if ($atts['id'] !== '') {
|
||||
$query['id'] = (int) $atts['id'];
|
||||
}
|
||||
if ($atts['ids'] !== '') {
|
||||
$clean = preg_replace('/[^0-9,]/', '', $atts['ids']);
|
||||
if ($clean !== '') {
|
||||
$query['ids'] = $clean;
|
||||
}
|
||||
}
|
||||
if ($atts['limit'] !== '') {
|
||||
$query['limit'] = (int) $atts['limit'];
|
||||
}
|
||||
$url = YSE_API_BASE . '/events.php' . (!empty($query) ? '?' . http_build_query($query) : '');
|
||||
|
||||
// Lettura server-side (niente CORS) + anti-cache
|
||||
$resp = wp_remote_get($url . (strpos($url, '?') !== false ? '&' : '?') . '_=' . time(), [
|
||||
'timeout' => 8,
|
||||
'headers' => ['Cache-Control' => 'no-cache'],
|
||||
]);
|
||||
if (is_wp_error($resp) || (int) wp_remote_retrieve_response_code($resp) !== 200) {
|
||||
return '<div class="yse-widget"><p class="yse-empty">Eventi non disponibili al momento.</p></div>';
|
||||
}
|
||||
$data = json_decode(wp_remote_retrieve_body($resp), true);
|
||||
if (!is_array($data) || empty($data['ok'])) {
|
||||
return '<div class="yse-widget"><p class="yse-empty">Eventi non disponibili al momento.</p></div>';
|
||||
}
|
||||
if (empty($data['events'])) {
|
||||
return '<div class="yse-widget"><p class="yse-empty">Nessun evento in programma al momento.</p></div>';
|
||||
}
|
||||
|
||||
$out = '<div class="yse-widget" data-api="' . esc_attr(YSE_API_BASE) . '">';
|
||||
foreach ($data['events'] as $ev) {
|
||||
$out .= yse_render_card($ev);
|
||||
}
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
/* Icona SVG inline (stroke = currentColor) */
|
||||
function yse_icon($name)
|
||||
{
|
||||
$p = [
|
||||
'user' => '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
||||
'pin' => '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/>',
|
||||
'clock' => '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
||||
'heart' => '<path d="M20.8 5.6a5 5 0 0 0-7-.1L12 7l-1.8-1.5a5 5 0 1 0-6.6 7.5L12 21l8.4-8a5 5 0 0 0 .4-7.4z"/>',
|
||||
'bag' => '<path d="M6 7h12l1 13H5L6 7z"/><path d="M9 7a3 3 0 0 1 6 0"/>',
|
||||
'info' => '<circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 8h.01"/>',
|
||||
'img' => '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-5-5L5 21"/>',
|
||||
'chev' => '<path d="m6 9 6 6 6-6"/>',
|
||||
];
|
||||
$inner = $p[$name] ?? '';
|
||||
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">' . $inner . '</svg>';
|
||||
}
|
||||
|
||||
/* Rende una singola card evento */
|
||||
function yse_render_card($ev)
|
||||
{
|
||||
$mesiAbbr = [1 => 'GEN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAG', 6 => 'GIU', 7 => 'LUG', 8 => 'AGO', 9 => 'SET', 10 => 'OTT', 11 => 'NOV', 12 => 'DIC'];
|
||||
$mesi = [1 => 'gennaio', 2 => 'febbraio', 3 => 'marzo', 4 => 'aprile', 5 => 'maggio', 6 => 'giugno', 7 => 'luglio', 8 => 'agosto', 9 => 'settembre', 10 => 'ottobre', 11 => 'novembre', 12 => 'dicembre'];
|
||||
|
||||
$id = (int) $ev['id'];
|
||||
$cost = (float) $ev['cost'];
|
||||
$sym = (string) $ev['currency_symbol'];
|
||||
$price = $cost > 0 ? $sym . number_format($cost, 2, ',', '.') : 'Gratuito';
|
||||
$color = (isset($ev['color']) && preg_match('/^#[0-9a-fA-F]{6}$/', $ev['color'])) ? $ev['color'] : '#4CAF50';
|
||||
|
||||
$soldOut = !empty($ev['sold_out']);
|
||||
$waitOn = !empty($ev['waitlist_enabled']);
|
||||
$waitMode = $soldOut && $waitOn;
|
||||
$hardFull = $soldOut && !$waitOn;
|
||||
$maxPer = max(1, (int) $ev['max_per_booking']);
|
||||
$remaining = (int) $ev['remaining'];
|
||||
$maxQty = $soldOut ? ($waitMode ? $maxPer : 0) : max(1, min($maxPer, $remaining));
|
||||
|
||||
$startTs = strtotime($ev['starts_at']);
|
||||
$dayNum = (int) date('j', $startTs);
|
||||
$monAbbr = $mesiAbbr[(int) date('n', $startTs)];
|
||||
$dateLong = $dayNum . ' ' . $mesi[(int) date('n', $startTs)] . ' ' . date('Y', $startTs);
|
||||
$timeLabel = date('H:i', $startTs) . (!empty($ev['ends_at']) ? '–' . date('H:i', strtotime($ev['ends_at'])) : '');
|
||||
|
||||
$address = trim((string) ($ev['address'] ?? ''));
|
||||
$organizer = trim((string) ($ev['organizer'] ?? ''));
|
||||
$mapQuery = $address !== '' ? $address : ((isset($ev['lat'], $ev['lng']) && $ev['lat'] && $ev['lng']) ? ($ev['lat'] . ',' . $ev['lng']) : '');
|
||||
|
||||
$gallery = is_array($ev['gallery'] ?? null) ? $ev['gallery'] : [];
|
||||
$hasDetails = trim((string) ($ev['description'] ?? '')) !== ''
|
||||
|| trim((string) ($ev['suitable_for'] ?? '')) !== ''
|
||||
|| trim((string) ($ev['what_to_bring'] ?? '')) !== ''
|
||||
|| trim((string) ($ev['instructions'] ?? '')) !== ''
|
||||
|| !empty($ev['cover_image']) || !empty($gallery) || $mapQuery !== '';
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="yse-card" style="--yse-accent: <?php echo esc_attr($color); ?>;"
|
||||
data-eid="<?php echo $id; ?>"
|
||||
data-cost="<?php echo esc_attr($cost); ?>"
|
||||
data-sym="<?php echo esc_attr($sym); ?>"
|
||||
data-maxqty="<?php echo (int) $maxQty; ?>"
|
||||
data-waitmode="<?php echo $waitMode ? 1 : 0; ?>">
|
||||
|
||||
<!-- HEADER COMPATTO -->
|
||||
<div class="yse-chead">
|
||||
<div class="yse-datechip">
|
||||
<div class="yse-m"><?php echo esc_html($monAbbr); ?></div>
|
||||
<div class="yse-d"><?php echo $dayNum; ?></div>
|
||||
</div>
|
||||
<?php if (!empty($ev['cover_image'])) : ?>
|
||||
<img class="yse-thumb" src="<?php echo esc_url($ev['cover_image']); ?>" alt="<?php echo esc_attr($ev['title']); ?>">
|
||||
<?php endif; ?>
|
||||
<div class="yse-main">
|
||||
<div class="yse-title-row">
|
||||
<h3><?php echo esc_html($ev['title']); ?></h3>
|
||||
<?php if ($hasDetails) : ?>
|
||||
<button type="button" class="yse-toggle" aria-expanded="false">
|
||||
<span class="yse-tlabel">Dettagli</span><?php echo yse_icon('chev'); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="yse-meta">
|
||||
<span class="yse-item"><?php echo yse_icon('user'); ?> Capacità <strong><?php echo (int) $ev['occupied']; ?>/<?php echo (int) $ev['max_seats']; ?></strong></span>
|
||||
<?php if ($address !== '') : ?>
|
||||
<span class="yse-item"><?php echo yse_icon('pin'); ?> <?php echo esc_html($address); ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="yse-item"><?php echo yse_icon('clock'); ?> <?php echo esc_html($dateLong); ?> · <?php echo esc_html($timeLabel); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yse-price"><?php echo esc_html($price); ?></div>
|
||||
</div>
|
||||
|
||||
<!-- DETTAGLI ESPANDIBILI -->
|
||||
<?php if ($hasDetails) : ?>
|
||||
<div class="yse-details">
|
||||
<div class="yse-hero <?php echo empty($ev['cover_image']) ? 'yse-nocover' : ''; ?>">
|
||||
<?php if (!empty($ev['cover_image'])) : ?>
|
||||
<img src="<?php echo esc_url($ev['cover_image']); ?>" alt="<?php echo esc_attr($ev['title']); ?>">
|
||||
<?php else : ?>
|
||||
<div class="yse-emblem"><?php echo yse_icon('img'); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="yse-body">
|
||||
<?php if (trim((string) ($ev['description'] ?? '')) !== '') : ?>
|
||||
<div class="yse-desc"><?php echo wp_kses_post($ev['description']); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (trim((string) ($ev['suitable_for'] ?? '')) !== '' || trim((string) ($ev['what_to_bring'] ?? '')) !== '' || trim((string) ($ev['instructions'] ?? '')) !== '') : ?>
|
||||
<div class="yse-detail-grid">
|
||||
<?php if (trim((string) ($ev['suitable_for'] ?? '')) !== '') : ?>
|
||||
<div class="yse-detail"><div class="yse-dh"><?php echo yse_icon('heart'); ?> Per chi è adatto</div><div class="yse-dt"><?php echo nl2br(esc_html($ev['suitable_for'])); ?></div></div>
|
||||
<?php endif; ?>
|
||||
<?php if (trim((string) ($ev['what_to_bring'] ?? '')) !== '') : ?>
|
||||
<div class="yse-detail"><div class="yse-dh"><?php echo yse_icon('bag'); ?> Cosa portare</div><div class="yse-dt"><?php echo nl2br(esc_html($ev['what_to_bring'])); ?></div></div>
|
||||
<?php endif; ?>
|
||||
<?php if (trim((string) ($ev['instructions'] ?? '')) !== '') : ?>
|
||||
<div class="yse-detail"><div class="yse-dh"><?php echo yse_icon('info'); ?> Istruzioni</div><div class="yse-dt"><?php echo nl2br(esc_html($ev['instructions'])); ?></div></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($gallery)) : ?>
|
||||
<div class="yse-section">
|
||||
<h4><?php echo yse_icon('img'); ?> Galleria</h4>
|
||||
<div class="yse-gallery">
|
||||
<?php foreach ($gallery as $g) : ?>
|
||||
<img src="<?php echo esc_url($g); ?>" alt="" data-full="<?php echo esc_url($g); ?>">
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($mapQuery !== '') : ?>
|
||||
<div class="yse-section">
|
||||
<h4><?php echo yse_icon('pin'); ?> Dove</h4>
|
||||
<iframe class="yse-map" loading="lazy" src="https://www.google.com/maps?q=<?php echo rawurlencode($mapQuery); ?>&output=embed"></iframe>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- BOOKING -->
|
||||
<div class="yse-booking">
|
||||
<?php if (!empty($ev['scheduled'])) :
|
||||
$pt = !empty($ev['publish_at']) ? strtotime($ev['publish_at']) : 0;
|
||||
$ptLabel = $pt ? ((int) date('j', $pt) . ' ' . $mesi[(int) date('n', $pt)] . ' ' . date('Y', $pt) . ' alle ' . date('H:i', $pt)) : '';
|
||||
?>
|
||||
<div class="yse-scheduled">Prenotazioni aperte dal <strong><?php echo esc_html($ptLabel); ?></strong></div>
|
||||
<?php elseif ($hardFull) : ?>
|
||||
<div class="yse-soldout">Evento esaurito — non ci sono più posti disponibili.</div>
|
||||
<?php else : ?>
|
||||
<?php if ($waitMode) :
|
||||
$waitingCount = (int) ($ev['waiting'] ?? 0);
|
||||
$waitingTxt = $waitingCount > 0
|
||||
? ' Al momento ' . ($waitingCount === 1 ? "c'è 1 persona" : "ci sono {$waitingCount} persone") . ' in coda.'
|
||||
: '';
|
||||
?>
|
||||
<div class="yse-wl-banner">Posti esauriti — puoi iscriverti alla <strong>lista d'attesa</strong>: ti avviseremo se si libera un posto.<?php echo esc_html($waitingTxt); ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="yse-bcompact">
|
||||
<div class="yse-btitle"><?php echo $waitMode ? "Lista d'attesa" : 'Prenota questo evento'; ?></div>
|
||||
<div class="yse-bblock">
|
||||
<div class="yse-lbl">Persone</div>
|
||||
<div class="yse-stepper">
|
||||
<button type="button" class="yse-minus">−</button>
|
||||
<span class="yse-num">1</span>
|
||||
<button type="button" class="yse-plus">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yse-bblock yse-total">
|
||||
<div class="yse-lbl">Totale</div>
|
||||
<div class="yse-val"><?php echo esc_html($price); ?></div>
|
||||
</div>
|
||||
<button type="button" class="yse-cta yse-open"><?php echo $waitMode ? "Lista d'attesa" : 'Prenota'; ?></button>
|
||||
</div>
|
||||
|
||||
<div class="yse-bform">
|
||||
<div class="yse-frow">
|
||||
<div class="yse-field"><label>Nome *</label><input type="text" class="yse-name" placeholder="Il tuo nome"></div>
|
||||
<div class="yse-field"><label>Cognome *</label><input type="text" class="yse-surname" placeholder="Il tuo cognome"></div>
|
||||
</div>
|
||||
<div class="yse-field"><label>Email *</label><input type="email" class="yse-email" placeholder="latua@email.it"></div>
|
||||
<label class="yse-consent"><input type="checkbox" class="yse-c-privacy"> <span>Ho letto e accetto l'informativa privacy. *</span></label>
|
||||
<label class="yse-consent"><input type="checkbox" class="yse-c-mkt"> <span>Desidero ricevere aggiornamenti e novità via email.</span></label>
|
||||
<!-- honeypot anti-bot -->
|
||||
<input type="text" class="yse-hp" name="website" tabindex="-1" autocomplete="off">
|
||||
<button type="button" class="yse-cta yse-full yse-submit"><?php echo $waitMode ? "Iscriviti alla lista d'attesa" : 'Conferma prenotazione'; ?></button>
|
||||
<div class="yse-error"></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
Reference in New Issue
Block a user