added mail to events
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,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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/include/event_mailer.php';
|
||||
|
||||
/**
|
||||
* Endpoint pubblico PRENOTAZIONE (POST).
|
||||
@@ -98,7 +99,7 @@ try {
|
||||
|
||||
// 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')");
|
||||
WHERE event_id = ? AND status IN ('confirmed','pending','offered')");
|
||||
$os->execute([$eventId]);
|
||||
$occupied = (int) $os->fetchColumn();
|
||||
$remaining = max(0, $maxSeats - $occupied);
|
||||
@@ -159,7 +160,13 @@ try {
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// TODO Step 4: email di conferma / lista d'attesa all'iscritto (usando $token).
|
||||
// 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.'
|
||||
|
||||
+298
-1
@@ -1,6 +1,7 @@
|
||||
<?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).
|
||||
@@ -25,6 +26,7 @@ if ($token !== '') {
|
||||
|
||||
$state = 'form'; // form | notfound | already | toolate | done
|
||||
$promotedCount = 0;
|
||||
$didCancel = false;
|
||||
|
||||
if (!$booking) {
|
||||
$state = 'notfound';
|
||||
@@ -51,11 +53,306 @@ if (!$booking) {
|
||||
if ($wasOccupying) {
|
||||
$promoted = promoteFromWaitlist($pdo, (int) $booking['eid']);
|
||||
$promotedCount = count($promoted);
|
||||
// TODO Step 4: email "sei stato promosso" ai $promoted
|
||||
// Nota: l'email di promozione (E, con riconferma) si attiva nel secondo tempo.
|
||||
}
|
||||
$didCancel = true;
|
||||
}
|
||||
$pdo->commit();
|
||||
$state = 'done';
|
||||
|
||||
// Email di conferma disdetta all'utente (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);
|
||||
}
|
||||
} 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';
|
||||
}
|
||||
}
|
||||
|
||||
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><?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();
|
||||
|
||||
@@ -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>
|
||||
@@ -49,7 +49,7 @@ try {
|
||||
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')
|
||||
FROM event_bookings WHERE status IN ('confirmed','pending','offered')
|
||||
GROUP BY event_id
|
||||
) b ON b.event_id = e.id
|
||||
LEFT JOIN (
|
||||
|
||||
@@ -41,7 +41,11 @@ try {
|
||||
// 3) Notifica (opzionale). Il template vero arriva nello step successivo.
|
||||
if ($notify && !empty($participants)) {
|
||||
foreach ($participants as $p) {
|
||||
sendEventCancellationMail($p['email'], $p['name'], $event);
|
||||
try {
|
||||
yse_mail_event_cancel($p, $event);
|
||||
} catch (Throwable $mailEx) {
|
||||
error_log('[event_delete mail] ' . $mailEx->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ if ($id > 0) {
|
||||
$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 = [];
|
||||
@@ -68,6 +71,9 @@ $val = [
|
||||
'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'] ?? '',
|
||||
];
|
||||
|
||||
$isEdit = $id > 0;
|
||||
@@ -549,6 +555,27 @@ $pageTitle = $isEdit ? 'Modifica evento' : 'Nuovo evento';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?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.
|
||||
@@ -35,6 +36,7 @@ if ($eventId <= 0) {
|
||||
// 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]);
|
||||
@@ -44,15 +46,31 @@ if (isset($_GET['cancel_booking'])) {
|
||||
$bStatus = $bl->fetchColumn();
|
||||
|
||||
if ($bStatus !== false && $bStatus !== 'cancelled') {
|
||||
$wasOccupying = in_array($bStatus, ['confirmed', 'pending'], true);
|
||||
$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 -> promuove la prima in lista d'attesa
|
||||
promoteFromWaitlist($pdo, $eventId);
|
||||
// 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();
|
||||
@@ -75,13 +93,13 @@ $bStmt = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? ORDER BY
|
||||
$bStmt->execute([$eventId]);
|
||||
$bookings = $bStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Riepilogo: i posti occupati sono solo confermate/pending; la lista d'attesa a parte
|
||||
// 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'], true)) {
|
||||
if (in_array($b['status'], ['confirmed', 'pending', 'offered'], true)) {
|
||||
$bookedSeats += (int) $b['seats'];
|
||||
$activeCount++;
|
||||
$revenue += (float) $b['amount'];
|
||||
@@ -101,6 +119,8 @@ function bookingBadge(string $status): array
|
||||
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'];
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ if (!$ev) {
|
||||
exit();
|
||||
}
|
||||
|
||||
$bs = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings WHERE event_id = ? AND status IN ('confirmed','pending')");
|
||||
$bs = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings WHERE event_id = ? AND status IN ('confirmed','pending','offered')");
|
||||
$bs->execute([$id]);
|
||||
$booked = (int) $bs->fetchColumn();
|
||||
$maxSeats = (int) $ev['max_seats'];
|
||||
@@ -186,6 +186,14 @@ function textBlock(?string $t): string
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chead-thumb {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chead .main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -702,6 +710,9 @@ function textBlock(?string $t): string
|
||||
<div class="m"><?php echo e($monAbbr); ?></div>
|
||||
<div class="d"><?php echo $dayNum; ?></div>
|
||||
</div>
|
||||
<?php if (!empty($ev['cover_image'])) : ?>
|
||||
<img class="chead-thumb" src="<?php echo e($ev['cover_image']); ?>" alt="<?php echo e($ev['title']); ?>">
|
||||
<?php endif; ?>
|
||||
<div class="main">
|
||||
<div class="title-row">
|
||||
<h1><?php echo e($ev['title']); ?></h1>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -112,6 +112,11 @@ $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'] ?? '';
|
||||
@@ -138,6 +143,7 @@ try {
|
||||
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, updated_at = NOW()
|
||||
WHERE id = :id";
|
||||
$params = [':id' => $id];
|
||||
@@ -147,12 +153,14 @@ try {
|
||||
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, 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, NOW(), NOW())";
|
||||
$params = [':user_id' => $currentUserId];
|
||||
}
|
||||
@@ -175,6 +183,9 @@ try {
|
||||
':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,
|
||||
|
||||
+4
-1
@@ -30,7 +30,7 @@ $sql = "SELECT e.*,
|
||||
SUM(seats) AS booked,
|
||||
COUNT(*) AS bookings_count
|
||||
FROM event_bookings
|
||||
WHERE status <> 'cancelled'
|
||||
WHERE status IN ('confirmed','pending','offered')
|
||||
GROUP BY event_id
|
||||
) b ON b.event_id = e.id
|
||||
LEFT JOIN auth_users u ON u.id = e.user_id
|
||||
@@ -391,6 +391,9 @@ function statusBadge(string $status): array
|
||||
<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']; ?>"
|
||||
|
||||
+286
-45
@@ -1,58 +1,299 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* STUB mailer del modulo Eventi.
|
||||
* Motore email del modulo Eventi (PHPMailer, legge le MAIL_* dal .env).
|
||||
*
|
||||
* Per ora NON invia davvero: registra solo un log, così tutto il flusso
|
||||
* (annullamento + scelta "invia mail") è già funzionante end-to-end.
|
||||
* Nello step successivo definiamo il TEMPLATE e attiviamo l'invio reale via
|
||||
* PHPMailer, leggendo le variabili MAIL_* dal .env che hai già.
|
||||
* Requisito una tantum: composer require phpmailer/phpmailer
|
||||
*
|
||||
* Config disponibile nel .env:
|
||||
* MAIL_HOST=mail.yogasoul.it MAIL_PORT=465 MAIL_ENCRYPTION=ssl
|
||||
* MAIL_USERNAME=info@yogasoul.it MAIL_PASSWORD=...
|
||||
* MAIL_FROM_ADDRESS=info@yogasoul.it MAIL_FROM_NAME="YogiBook"
|
||||
* 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.
|
||||
*/
|
||||
|
||||
if (!function_exists('sendEventCancellationMail')) {
|
||||
/**
|
||||
* @param string $toEmail Email destinatario
|
||||
* @param string $toName Nome destinatario
|
||||
* @param array $event Riga completa della tabella events
|
||||
*/
|
||||
function sendEventCancellationMail(string $toEmail, string $toName, array $event): bool
|
||||
// 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
|
||||
{
|
||||
// ------------------------------------------------------------------
|
||||
// STEP SUCCESSIVO: sostituire il log qui sotto con l'invio reale.
|
||||
// Scheletro PHPMailer (richiede: composer require phpmailer/phpmailer
|
||||
// e un modo per leggere il .env, es. vlucas/phpdotenv oppure il
|
||||
// bootstrap Laravel già presente nel progetto):
|
||||
//
|
||||
// use PHPMailer\PHPMailer\PHPMailer;
|
||||
// $mail = new PHPMailer(true);
|
||||
// $mail->isSMTP();
|
||||
// $mail->Host = getenv('MAIL_HOST');
|
||||
// $mail->SMTPAuth = true;
|
||||
// $mail->Username = getenv('MAIL_USERNAME');
|
||||
// $mail->Password = getenv('MAIL_PASSWORD');
|
||||
// $mail->SMTPSecure = getenv('MAIL_ENCRYPTION'); // 'ssl'
|
||||
// $mail->Port = (int) getenv('MAIL_PORT'); // 465
|
||||
// $mail->setFrom(getenv('MAIL_FROM_ADDRESS'), getenv('MAIL_FROM_NAME'));
|
||||
// $mail->addAddress($toEmail, $toName);
|
||||
// $mail->isHTML(true);
|
||||
// $mail->Subject = 'Evento annullato: ' . $event['title'];
|
||||
// $mail->Body = '...TEMPLATE HTML...';
|
||||
// return $mail->send();
|
||||
// ------------------------------------------------------------------
|
||||
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;
|
||||
}
|
||||
|
||||
error_log(sprintf(
|
||||
'[event mail] STUB annullamento -> %s (%s) per evento #%d "%s"',
|
||||
$toEmail,
|
||||
$toName,
|
||||
(int) $event['id'],
|
||||
$event['title'] ?? ''
|
||||
/* ---------- 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 true;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,12 @@
|
||||
<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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user