added cron and waiting
This commit is contained in:
@@ -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')]);
|
||||
@@ -22,7 +22,7 @@ try {
|
||||
$idsParam = trim($_GET['ids'] ?? '');
|
||||
$limit = isset($_GET['limit']) ? max(1, min(50, (int) $_GET['limit'])) : 0;
|
||||
|
||||
$where = ["e.status = 'published'", "COALESCE(e.ends_at, e.starts_at) >= NOW()"];
|
||||
$where = ["e.status IN ('published','scheduled')", "COALESCE(e.ends_at, e.starts_at) >= NOW()"];
|
||||
$params = [];
|
||||
|
||||
if ($id > 0) {
|
||||
@@ -96,6 +96,10 @@ try {
|
||||
'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'],
|
||||
|
||||
@@ -74,6 +74,8 @@ $val = [
|
||||
'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;
|
||||
@@ -555,6 +557,22 @@ $pageTitle = $isEdit ? 'Modifica evento' : 'Nuovo evento';
|
||||
</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">
|
||||
@@ -612,6 +630,10 @@ $pageTitle = $isEdit ? 'Modifica evento' : 'Nuovo evento';
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
+13
-3
@@ -105,6 +105,15 @@ 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'] ?? '');
|
||||
@@ -144,7 +153,7 @@ try {
|
||||
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()
|
||||
cover_image = :cover_image, gallery = :gallery, status = :status, publish_at = :publish_at, updated_at = NOW()
|
||||
WHERE id = :id";
|
||||
$params = [':id' => $id];
|
||||
} else {
|
||||
@@ -154,14 +163,14 @@ try {
|
||||
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)
|
||||
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, NOW(), NOW())";
|
||||
:cover_image, :gallery, :status, :publish_at, NOW(), NOW())";
|
||||
$params = [':user_id' => $currentUserId];
|
||||
}
|
||||
|
||||
@@ -189,6 +198,7 @@ try {
|
||||
':color' => $color,
|
||||
':cover_image' => $coverImage !== '' ? $coverImage : null,
|
||||
':gallery' => $gallery,
|
||||
':publish_at' => $publishAt,
|
||||
':status' => $status,
|
||||
];
|
||||
|
||||
|
||||
+61
-4
@@ -22,6 +22,7 @@ $message = $_GET['message'] ?? '';
|
||||
$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
|
||||
@@ -33,6 +34,12 @@ $sql = "SELECT e.*,
|
||||
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);
|
||||
@@ -63,11 +70,13 @@ function statusBadge(string $status): array
|
||||
{
|
||||
switch ($status) {
|
||||
case 'published':
|
||||
return ['Aperto', '#e7f7ee', '#1a7f52'];
|
||||
return ['Aperto', '#e7f7ee', '#1a7f52'];
|
||||
case 'scheduled':
|
||||
return ['Programmato', '#eef2fb', '#3a5bb8'];
|
||||
case 'cancelled':
|
||||
return ['Annullato', '#fdecea', '#c0392b'];
|
||||
return ['Annullato', '#fdecea', '#c0392b'];
|
||||
default:
|
||||
return ['Bozza', '#eef0f3', '#6b7280'];
|
||||
return ['Bozza', '#eef0f3', '#6b7280'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -209,6 +218,29 @@ function statusBadge(string $status): array
|
||||
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;
|
||||
@@ -375,7 +407,7 @@ function statusBadge(string $status): array
|
||||
<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="event-id">(ID: <?php echo (int) $ev['id']; ?>)</span></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>
|
||||
@@ -383,11 +415,15 @@ function statusBadge(string $status): array
|
||||
<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>
|
||||
@@ -428,6 +464,27 @@ function statusBadge(string $status): array
|
||||
|
||||
<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');
|
||||
|
||||
Reference in New Issue
Block a user