| [8d32dcf] | 1 | /**
|
|---|
| [834bcc3] | 2 | * Show agenda (premium feature #8) — admin side.
|
|---|
| [8d32dcf] | 3 | *
|
|---|
| [834bcc3] | 4 | * GET /admin/shows -> list + add form
|
|---|
| 5 | * POST /admin/shows -> add show (optional notify email to subscribers)
|
|---|
| [8d32dcf] | 6 | * POST /admin/shows/:id/delete
|
|---|
| 7 | *
|
|---|
| [834bcc3] | 8 | * Premium + site manager. Notify email requires SMTP; without SMTP the show is
|
|---|
| 9 | * simply saved (no email sent).
|
|---|
| [8d32dcf] | 10 | */
|
|---|
| 11 |
|
|---|
| 12 | import express from 'express';
|
|---|
| 13 | import db from '../config/database.js';
|
|---|
| 14 | import { v4 as uuid } from 'uuid';
|
|---|
| 15 | import { renderPage } from '../middleware/render.js';
|
|---|
| 16 | import { requireSiteManager } from '../middleware/auth.js';
|
|---|
| 17 | import { premiumUnlocked } from '../services/PatreonService.js';
|
|---|
| 18 | import { mailerConfigured, sendMail } from '../config/mailer.js';
|
|---|
| 19 | import { confirmedFor, counts } from '../services/SubscriberService.js';
|
|---|
| [68b27a4] | 20 | import { getSetting, setSetting } from '../services/SettingsService.js';
|
|---|
| [318e1ce] | 21 | import { t, resolveLang } from '../services/i18n.js';
|
|---|
| [8d32dcf] | 22 |
|
|---|
| 23 | const router = express.Router();
|
|---|
| 24 |
|
|---|
| 25 | function esc(s) { return String(s || '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|---|
| 26 | function fullUrl(req, p) {
|
|---|
| 27 | const base = (process.env.PUBLIC_BASE_URL || ('https://' + (req.get('host') || ''))).replace(/\/$/, '');
|
|---|
| 28 | return base + (req.res.locals.siteUrlBase || '') + p;
|
|---|
| 29 | }
|
|---|
| 30 | function premiumGate(req, res, next) {
|
|---|
| [318e1ce] | 31 | if (!premiumUnlocked()) {
|
|---|
| 32 | const lang = resolveLang(req, { defaultLang: getSetting('default_lang') });
|
|---|
| 33 | return res.status(403).send(t(lang, 'aset.premium_gate', { feature: t(lang, 'admin.t_shows') }));
|
|---|
| 34 | }
|
|---|
| [8d32dcf] | 35 | next();
|
|---|
| 36 | }
|
|---|
| 37 | function render(req, res, extra = {}) {
|
|---|
| 38 | const site = res.locals.site;
|
|---|
| 39 | const shows = db.prepare('SELECT * FROM shows WHERE site_id = ? ORDER BY date DESC, time DESC').all(site.id);
|
|---|
| 40 | renderPage(req, res, 'pages/admin-shows', {
|
|---|
| [3487567] | 41 | pageTitleKey: 'admin.t_shows', bodyClass: 'on-admin',
|
|---|
| [8d32dcf] | 42 | shows, smtp: mailerConfigured(), notifyCount: confirmedFor(site.id, 'notify').length,
|
|---|
| [68b27a4] | 43 | agendaEnabled: getSetting('agenda_enabled') === '1',
|
|---|
| [8d32dcf] | 44 | ...extra,
|
|---|
| 45 | });
|
|---|
| 46 | }
|
|---|
| 47 |
|
|---|
| 48 | router.get('/', requireSiteManager, premiumGate, (req, res) => {
|
|---|
| 49 | if (!res.locals.site) return res.status(404).send('Geen site.');
|
|---|
| 50 | render(req, res);
|
|---|
| 51 | });
|
|---|
| 52 |
|
|---|
| 53 | router.post('/', requireSiteManager, premiumGate, async (req, res) => {
|
|---|
| 54 | const site = res.locals.site;
|
|---|
| 55 | if (!site) return res.status(404).send('Geen site.');
|
|---|
| 56 | const b = req.body || {};
|
|---|
| 57 | const date = (b.date || '').trim();
|
|---|
| 58 | const city = (b.city || '').trim();
|
|---|
| 59 | if (!date || !city) return render(req, res, { msg: 'Datum en plaats zijn verplicht.', msgKind: 'bad' });
|
|---|
| 60 | let ticket = (b.ticket_url || '').trim();
|
|---|
| 61 | if (ticket && !/^https?:\/\//i.test(ticket)) ticket = '';
|
|---|
| 62 |
|
|---|
| 63 | db.prepare(`INSERT INTO shows (id, site_id, date, time, city, venue, country, ticket_url, notes)
|
|---|
| 64 | VALUES (?,?,?,?,?,?,?,?,?)`).run(
|
|---|
| 65 | uuid(), site.id, date, (b.time || '').trim() || null, city, (b.venue || '').trim() || null,
|
|---|
| 66 | (b.country || '').trim() || null, ticket || null, (b.notes || '').trim() || null,
|
|---|
| 67 | );
|
|---|
| 68 |
|
|---|
| 69 | let sent = 0;
|
|---|
| 70 | if (b.notify && mailerConfigured()) {
|
|---|
| 71 | const subs = confirmedFor(site.id, 'notify');
|
|---|
| 72 | const where = city + (b.venue ? ' — ' + b.venue : '');
|
|---|
| 73 | for (const s of subs) {
|
|---|
| 74 | const unsub = fullUrl(req, '/nieuwsbrief/uitschrijven/' + s.token);
|
|---|
| 75 | try {
|
|---|
| 76 | await sendMail({
|
|---|
| 77 | to: s.email,
|
|---|
| 78 | subject: 'Nieuwe show: ' + where + ' (' + date + ')',
|
|---|
| 79 | text: (site.title || '') + ' speelt op ' + date + ' in ' + where + '.' + (ticket ? ('\nTickets: ' + ticket) : '') + '\n\nUitschrijven: ' + unsub,
|
|---|
| 80 | html: '<p><strong>' + esc(site.title) + '</strong> speelt op <strong>' + esc(date) + '</strong> in ' + esc(where) + '.</p>' +
|
|---|
| 81 | (ticket ? ('<p><a href="' + ticket + '">Tickets</a></p>') : '') +
|
|---|
| 82 | '<p style="color:#888;font-size:12px"><a href="' + unsub + '">Uitschrijven</a></p>',
|
|---|
| 83 | });
|
|---|
| 84 | sent++;
|
|---|
| [834bcc3] | 85 | } catch { /* skip */ }
|
|---|
| [8d32dcf] | 86 | }
|
|---|
| 87 | }
|
|---|
| 88 | render(req, res, { msg: 'Show toegevoegd.' + (sent ? (' Notify gestuurd naar ' + sent + ' abonnee(s).') : ''), msgKind: 'ok' });
|
|---|
| 89 | });
|
|---|
| 90 |
|
|---|
| [68b27a4] | 91 | router.post('/toggle', requireSiteManager, premiumGate, (req, res) => {
|
|---|
| [834bcc3] | 92 | // Show the agenda on the site (Agenda button in the pill + /shows page).
|
|---|
| [68b27a4] | 93 | setSetting('agenda_enabled', req.body.enabled ? '1' : '0');
|
|---|
| 94 | res.redirect((res.locals.siteUrlBase || '') + '/admin/shows');
|
|---|
| 95 | });
|
|---|
| 96 |
|
|---|
| [8d32dcf] | 97 | router.post('/:id/delete', requireSiteManager, premiumGate, (req, res) => {
|
|---|
| 98 | const site = res.locals.site;
|
|---|
| 99 | if (site) db.prepare('DELETE FROM shows WHERE id = ? AND site_id = ?').run(req.params.id, site.id);
|
|---|
| 100 | res.redirect((res.locals.siteUrlBase || '') + '/admin/shows');
|
|---|
| 101 | });
|
|---|
| 102 |
|
|---|
| 103 | export default router;
|
|---|