nodebb-plugin-onekite-calendar 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -14
- package/lib/admin.js +8 -8
- package/lib/api.js +66 -148
- package/lib/constants.js +5 -2
- package/lib/controllers.js +2 -2
- package/lib/db.js +5 -5
- package/lib/discord.js +1 -1
- package/lib/email.js +18 -5
- package/lib/helloassoWebhook.js +4 -4
- package/lib/scheduler.js +6 -6
- package/lib/widgets.js +12 -1
- package/library.js +69 -38
- package/package.json +1 -1
- package/plugin.json +10 -4
- package/public/admin.js +42 -19
- package/public/client.js +22 -22
- package/templates/admin/plugins/{onekite-calendar.tpl → calendar-onekite.tpl} +1 -1
- package/templates/{onekite-calendar.tpl → calendar-onekite.tpl} +0 -11
- /package/templates/emails/{onekite-calendar_approved.tpl → calendar-onekite_approved.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_expired.tpl → calendar-onekite_expired.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_paid.tpl → calendar-onekite_paid.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_pending.tpl → calendar-onekite_pending.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_refused.tpl → calendar-onekite_refused.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_reminder.tpl → calendar-onekite_reminder.tpl} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 1.1.0
|
|
4
|
-
- Ultra perf clean : JS chargé uniquement sur les pages/plugin concernées (plus de scripts globaux)
|
|
5
|
-
- Suppression legacy supplémentaire : clés DB renommées en onekite-calendar:* (aucune donnée conservée)
|
|
6
|
-
- Nettoyages internes (variables globales JS, routes/paramètres uniformisés)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## 1.0.8
|
|
10
|
-
- Suppression du legacy (namespace unique onekite-calendar, ACP/API/widgets)
|
|
11
|
-
- Suppression des fallbacks inutiles
|
|
12
|
-
|
|
1
|
+
# Changelog – nodebb-plugin-onekite-calendar
|
|
13
2
|
|
|
14
3
|
## 1.0.6
|
|
15
4
|
- Refactor interne : centralisation de l’envoi d’emails et du logging (opt-in)
|
|
16
|
-
-
|
|
5
|
+
- Compatibilité : ajout d’alias de routes "onekite-calendar" en plus de "calendar-onekite" (API + ACP)
|
|
17
6
|
|
|
18
7
|
## 1.0.5
|
|
19
8
|
- Widget : points colorés selon le type (mêmes couleurs que le calendrier)
|
|
@@ -43,7 +32,7 @@
|
|
|
43
32
|
- Correctifs divers de stabilité/performance (dont crash au démarrage)
|
|
44
33
|
|
|
45
34
|
## 1.0.0
|
|
46
|
-
- Première version stable du plugin onekite
|
|
35
|
+
- Première version stable du plugin calendar-onekite
|
|
47
36
|
- Gestion des réservations de matériel avec contrôle de disponibilité
|
|
48
37
|
- Calendrier FullCalendar via CDN
|
|
49
38
|
- Validation / refus des demandes depuis l’ACP
|
package/lib/admin.js
CHANGED
|
@@ -50,18 +50,18 @@ const ADMIN_PRIV = 'admin:settings';
|
|
|
50
50
|
const admin = {};
|
|
51
51
|
|
|
52
52
|
admin.renderAdmin = async function (req, res) {
|
|
53
|
-
res.render('admin/plugins/onekite
|
|
53
|
+
res.render('admin/plugins/calendar-onekite', {
|
|
54
54
|
title: 'Calendar Onekite',
|
|
55
55
|
});
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
admin.getSettings = async function (req, res) {
|
|
59
|
-
const settings = await meta.settings.get('onekite
|
|
59
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
60
60
|
res.json(settings || {});
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
admin.saveSettings = async function (req, res) {
|
|
64
|
-
await meta.settings.set('onekite
|
|
64
|
+
await meta.settings.set('calendar-onekite', req.body || {});
|
|
65
65
|
res.json({ ok: true });
|
|
66
66
|
};
|
|
67
67
|
|
|
@@ -97,7 +97,7 @@ admin.approveReservation = async function (req, res) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// Create HelloAsso payment link if configured
|
|
100
|
-
const settings = await meta.settings.get('onekite
|
|
100
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
101
101
|
const env = settings.helloassoEnv || 'prod';
|
|
102
102
|
const token = await helloasso.getAccessToken({
|
|
103
103
|
env,
|
|
@@ -111,7 +111,7 @@ admin.approveReservation = async function (req, res) {
|
|
|
111
111
|
const totalAmount = Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
112
112
|
const base = forumBaseUrl();
|
|
113
113
|
const returnUrl = base ? `${base}/calendar` : '';
|
|
114
|
-
const webhookUrl = base ? `${base}/plugins/onekite
|
|
114
|
+
const webhookUrl = base ? `${base}/plugins/calendar-onekite/helloasso` : '';
|
|
115
115
|
const year = new Date(Number(r.start)).getFullYear();
|
|
116
116
|
const intent = await helloasso.createCheckoutIntent({
|
|
117
117
|
env,
|
|
@@ -154,7 +154,7 @@ admin.approveReservation = async function (req, res) {
|
|
|
154
154
|
const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
|
|
155
155
|
? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
|
|
156
156
|
: '';
|
|
157
|
-
await sendEmail('
|
|
157
|
+
await sendEmail('calendar-onekite_approved', requester.email, 'Location matériel - Réservation validée', {
|
|
158
158
|
uid: parseInt(r.uid, 10),
|
|
159
159
|
username: requester.username,
|
|
160
160
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
@@ -189,7 +189,7 @@ admin.refuseReservation = async function (req, res) {
|
|
|
189
189
|
try {
|
|
190
190
|
const requester = await user.getUserFields(r.uid, ['username', 'email']);
|
|
191
191
|
if (requester && requester.email) {
|
|
192
|
-
await sendEmail('
|
|
192
|
+
await sendEmail('calendar-onekite_refused', requester.email, 'Location matériel - Réservation refusée', {
|
|
193
193
|
uid: parseInt(r.uid, 10),
|
|
194
194
|
username: requester.username,
|
|
195
195
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
@@ -360,7 +360,7 @@ admin.exportAccountingCsv = async function (req, res) {
|
|
|
360
360
|
}
|
|
361
361
|
const csv = lines.join('\n');
|
|
362
362
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
363
|
-
res.setHeader('Content-Disposition', 'attachment; filename="onekite-
|
|
363
|
+
res.setHeader('Content-Disposition', 'attachment; filename="calendar-onekite-accounting.csv"');
|
|
364
364
|
return res.send(csv);
|
|
365
365
|
};
|
|
366
366
|
|
package/lib/api.js
CHANGED
|
@@ -13,41 +13,6 @@ const dbLayer = require('./db');
|
|
|
13
13
|
const { sendEmail } = require('./email');
|
|
14
14
|
const log = require('./log');
|
|
15
15
|
|
|
16
|
-
// Ultra-perf: short-lived in-memory cache for the events feed.
|
|
17
|
-
// We cache a "base" event list without per-user privileges, then decorate
|
|
18
|
-
// it per request. This reduces DB load when many users/widgets refresh.
|
|
19
|
-
const EVENTS_CACHE = new Map();
|
|
20
|
-
const EVENTS_CACHE_TTL_MS = 30 * 1000;
|
|
21
|
-
const EVENTS_CACHE_MAX = 50;
|
|
22
|
-
|
|
23
|
-
function eventsCacheKey(startTs, endTs) {
|
|
24
|
-
// Keep the key stable and short.
|
|
25
|
-
return `${Math.max(0, Number(startTs) || 0)}:${Math.max(0, Number(endTs) || 0)}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function getEventsCache(key) {
|
|
29
|
-
const hit = EVENTS_CACHE.get(key);
|
|
30
|
-
if (!hit) return null;
|
|
31
|
-
if ((Date.now() - hit.ts) > EVENTS_CACHE_TTL_MS) {
|
|
32
|
-
EVENTS_CACHE.delete(key);
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
return hit.data;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function setEventsCache(key, data) {
|
|
39
|
-
EVENTS_CACHE.set(key, { ts: Date.now(), data });
|
|
40
|
-
if (EVENTS_CACHE.size > EVENTS_CACHE_MAX) {
|
|
41
|
-
// Drop oldest entries.
|
|
42
|
-
const entries = Array.from(EVENTS_CACHE.entries());
|
|
43
|
-
entries.sort((a, b) => (a[1]?.ts || 0) - (b[1]?.ts || 0));
|
|
44
|
-
const toDrop = Math.max(1, EVENTS_CACHE.size - EVENTS_CACHE_MAX);
|
|
45
|
-
for (let i = 0; i < toDrop; i++) {
|
|
46
|
-
EVENTS_CACHE.delete(entries[i][0]);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
16
|
// Fast membership check without N calls to groups.isMember.
|
|
52
17
|
// NodeBB's groups.getUserGroups([uid]) returns an array (per uid) of group objects.
|
|
53
18
|
// We compare against both group slugs and names to be tolerant with older settings.
|
|
@@ -336,7 +301,7 @@ api.getEvents = async function (req, res) {
|
|
|
336
301
|
const startTs = toTs(req.query.start) || 0;
|
|
337
302
|
const endTs = toTs(req.query.end) || (Date.now() + 365 * 24 * 3600 * 1000);
|
|
338
303
|
|
|
339
|
-
const settings = await meta.settings.get('onekite
|
|
304
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
340
305
|
const canMod = req.uid ? await canValidate(req.uid, settings) : false;
|
|
341
306
|
const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings) : false;
|
|
342
307
|
const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings) : false;
|
|
@@ -344,89 +309,20 @@ api.getEvents = async function (req, res) {
|
|
|
344
309
|
// Fetch a wider window because an event can start before the query range
|
|
345
310
|
// and still overlap.
|
|
346
311
|
const wideStart = Math.max(0, startTs - 366 * 24 * 3600 * 1000);
|
|
347
|
-
|
|
348
|
-
const cacheKey = eventsCacheKey(startTs, endTs);
|
|
349
|
-
let base = getEventsCache(cacheKey);
|
|
350
|
-
if (!base) {
|
|
351
|
-
const baseOut = [];
|
|
352
|
-
|
|
353
|
-
const ids = await dbLayer.listReservationIdsByStartRange(wideStart, endTs, 5000);
|
|
354
|
-
// Batch fetch = major perf win when there are many reservations.
|
|
355
|
-
const reservations = await dbLayer.getReservations(ids);
|
|
356
|
-
for (const r of (reservations || [])) {
|
|
357
|
-
if (!r) continue;
|
|
358
|
-
if (!['pending', 'awaiting_payment', 'paid'].includes(r.status)) continue;
|
|
359
|
-
const rStart = parseInt(r.start, 10);
|
|
360
|
-
const rEnd = parseInt(r.end, 10);
|
|
361
|
-
if (!(rStart < endTs && startTs < rEnd)) continue;
|
|
362
|
-
|
|
363
|
-
const evs = eventsFor(r);
|
|
364
|
-
const hasPay = (r.status === 'awaiting_payment' && r.paymentUrl && (/^https?:\/\//i).test(String(r.paymentUrl)));
|
|
365
|
-
for (const ev of (evs || [])) {
|
|
366
|
-
const p = ev.extendedProps || {};
|
|
367
|
-
baseOut.push({
|
|
368
|
-
id: ev.id,
|
|
369
|
-
title: ev.title,
|
|
370
|
-
backgroundColor: ev.backgroundColor,
|
|
371
|
-
borderColor: ev.borderColor,
|
|
372
|
-
textColor: ev.textColor,
|
|
373
|
-
allDay: ev.allDay,
|
|
374
|
-
start: ev.start,
|
|
375
|
-
end: ev.end,
|
|
376
|
-
extendedProps: {
|
|
377
|
-
type: 'reservation',
|
|
378
|
-
rid: p.rid,
|
|
379
|
-
status: p.status,
|
|
380
|
-
uid: p.uid,
|
|
381
|
-
_username: r.username ? String(r.username) : '',
|
|
382
|
-
_hasPayment: !!hasPay,
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Special events
|
|
389
|
-
try {
|
|
390
|
-
const specialIds = await dbLayer.listSpecialIdsByStartRange(wideStart, endTs, 5000);
|
|
391
|
-
const specials = await dbLayer.getSpecialEvents(specialIds);
|
|
392
|
-
for (const sev of (specials || [])) {
|
|
393
|
-
if (!sev) continue;
|
|
394
|
-
const sStart = parseInt(sev.start, 10);
|
|
395
|
-
const sEnd = parseInt(sev.end, 10);
|
|
396
|
-
if (!(sStart < endTs && startTs < sEnd)) continue;
|
|
397
|
-
const full = eventsForSpecial(sev);
|
|
398
|
-
baseOut.push({
|
|
399
|
-
id: full.id,
|
|
400
|
-
title: full.title,
|
|
401
|
-
allDay: full.allDay,
|
|
402
|
-
start: full.start,
|
|
403
|
-
end: full.end,
|
|
404
|
-
backgroundColor: full.backgroundColor,
|
|
405
|
-
borderColor: full.borderColor,
|
|
406
|
-
textColor: full.textColor,
|
|
407
|
-
extendedProps: {
|
|
408
|
-
type: 'special',
|
|
409
|
-
eid: sev.eid,
|
|
410
|
-
uid: sev.uid,
|
|
411
|
-
_username: sev.username ? String(sev.username) : '',
|
|
412
|
-
},
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
} catch (e) {
|
|
416
|
-
// ignore
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
base = baseOut;
|
|
420
|
-
setEventsCache(cacheKey, baseOut);
|
|
421
|
-
}
|
|
422
|
-
|
|
312
|
+
const ids = await dbLayer.listReservationIdsByStartRange(wideStart, endTs, 5000);
|
|
423
313
|
const out = [];
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
314
|
+
// Batch fetch = major perf win when there are many reservations.
|
|
315
|
+
const reservations = await dbLayer.getReservations(ids);
|
|
316
|
+
for (const r of (reservations || [])) {
|
|
317
|
+
if (!r) continue;
|
|
318
|
+
// Only show active statuses
|
|
319
|
+
if (!['pending', 'awaiting_payment', 'paid'].includes(r.status)) continue;
|
|
320
|
+
const rStart = parseInt(r.start, 10);
|
|
321
|
+
const rEnd = parseInt(r.end, 10);
|
|
322
|
+
if (!(rStart < endTs && startTs < rEnd)) continue; // overlap check
|
|
323
|
+
const evs = eventsFor(r);
|
|
324
|
+
for (const ev of evs) {
|
|
325
|
+
const p = ev.extendedProps || {};
|
|
430
326
|
const minimal = {
|
|
431
327
|
id: ev.id,
|
|
432
328
|
title: ev.title,
|
|
@@ -444,30 +340,53 @@ api.getEvents = async function (req, res) {
|
|
|
444
340
|
canModerate: canMod,
|
|
445
341
|
},
|
|
446
342
|
};
|
|
447
|
-
|
|
448
|
-
if (
|
|
343
|
+
// Only expose username on the event list to owner/moderators.
|
|
344
|
+
if (r.username && ((req.uid && String(req.uid) === String(r.uid)) || canMod)) {
|
|
345
|
+
minimal.extendedProps.username = String(r.username);
|
|
346
|
+
}
|
|
347
|
+
// Let the UI decide if a "Payer" button might exist, without exposing the URL in list.
|
|
348
|
+
if (r.status === 'awaiting_payment' && r.paymentUrl && (/^https?:\/\//i).test(String(r.paymentUrl))) {
|
|
349
|
+
if ((req.uid && String(req.uid) === String(r.uid)) || canMod) {
|
|
350
|
+
minimal.extendedProps.hasPayment = true;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
449
353
|
out.push(minimal);
|
|
450
|
-
}
|
|
451
|
-
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Special events
|
|
358
|
+
try {
|
|
359
|
+
const specialIds = await dbLayer.listSpecialIdsByStartRange(wideStart, endTs, 5000);
|
|
360
|
+
const specials = await dbLayer.getSpecialEvents(specialIds);
|
|
361
|
+
for (const sev of (specials || [])) {
|
|
362
|
+
if (!sev) continue;
|
|
363
|
+
const sStart = parseInt(sev.start, 10);
|
|
364
|
+
const sEnd = parseInt(sev.end, 10);
|
|
365
|
+
if (!(sStart < endTs && startTs < sEnd)) continue;
|
|
366
|
+
const full = eventsForSpecial(sev);
|
|
452
367
|
const minimal = {
|
|
453
|
-
id:
|
|
454
|
-
title:
|
|
455
|
-
allDay:
|
|
456
|
-
start:
|
|
457
|
-
end:
|
|
458
|
-
backgroundColor:
|
|
459
|
-
borderColor:
|
|
460
|
-
textColor:
|
|
368
|
+
id: full.id,
|
|
369
|
+
title: full.title,
|
|
370
|
+
allDay: full.allDay,
|
|
371
|
+
start: full.start,
|
|
372
|
+
end: full.end,
|
|
373
|
+
backgroundColor: full.backgroundColor,
|
|
374
|
+
borderColor: full.borderColor,
|
|
375
|
+
textColor: full.textColor,
|
|
461
376
|
extendedProps: {
|
|
462
377
|
type: 'special',
|
|
463
|
-
eid:
|
|
378
|
+
eid: sev.eid,
|
|
464
379
|
canCreateSpecial: canSpecialCreate,
|
|
465
380
|
canDeleteSpecial: canSpecialDelete,
|
|
466
381
|
},
|
|
467
382
|
};
|
|
468
|
-
if (
|
|
383
|
+
if (sev.username && (canMod || canSpecialDelete || (req.uid && String(req.uid) === String(sev.uid)))) {
|
|
384
|
+
minimal.extendedProps.username = String(sev.username);
|
|
385
|
+
}
|
|
469
386
|
out.push(minimal);
|
|
470
387
|
}
|
|
388
|
+
} catch (e) {
|
|
389
|
+
// ignore
|
|
471
390
|
}
|
|
472
391
|
|
|
473
392
|
// Stable ordering -> stable ETag
|
|
@@ -482,8 +401,7 @@ api.getEvents = async function (req, res) {
|
|
|
482
401
|
|
|
483
402
|
const etag = computeEtag(out);
|
|
484
403
|
res.setHeader('ETag', etag);
|
|
485
|
-
|
|
486
|
-
res.setHeader('Cache-Control', 'private, max-age=15, must-revalidate');
|
|
404
|
+
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
|
|
487
405
|
if (String(req.headers['if-none-match'] || '') === etag) {
|
|
488
406
|
return res.status(304).end();
|
|
489
407
|
}
|
|
@@ -495,7 +413,7 @@ api.getReservationDetails = async function (req, res) {
|
|
|
495
413
|
const uid = req.uid;
|
|
496
414
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
497
415
|
|
|
498
|
-
const settings = await meta.settings.get('onekite
|
|
416
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
499
417
|
const canMod = await canValidate(uid, settings);
|
|
500
418
|
|
|
501
419
|
const rid = String(req.params.rid || '').trim();
|
|
@@ -537,7 +455,7 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
537
455
|
const uid = req.uid;
|
|
538
456
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
539
457
|
|
|
540
|
-
const settings = await meta.settings.get('onekite
|
|
458
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
541
459
|
const canMod = await canValidate(uid, settings);
|
|
542
460
|
const canSpecialDelete = await canDeleteSpecial(uid, settings);
|
|
543
461
|
|
|
@@ -566,7 +484,7 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
566
484
|
};
|
|
567
485
|
|
|
568
486
|
api.getCapabilities = async function (req, res) {
|
|
569
|
-
const settings = await meta.settings.get('onekite
|
|
487
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
570
488
|
const uid = req.uid || 0;
|
|
571
489
|
const canMod = uid ? await canValidate(uid, settings) : false;
|
|
572
490
|
res.json({
|
|
@@ -577,7 +495,7 @@ api.getCapabilities = async function (req, res) {
|
|
|
577
495
|
};
|
|
578
496
|
|
|
579
497
|
api.createSpecialEvent = async function (req, res) {
|
|
580
|
-
const settings = await meta.settings.get('onekite
|
|
498
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
581
499
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
582
500
|
const ok = await canCreateSpecial(req.uid, settings);
|
|
583
501
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
@@ -613,7 +531,7 @@ api.createSpecialEvent = async function (req, res) {
|
|
|
613
531
|
};
|
|
614
532
|
|
|
615
533
|
api.deleteSpecialEvent = async function (req, res) {
|
|
616
|
-
const settings = await meta.settings.get('onekite
|
|
534
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
617
535
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
618
536
|
const ok = await canDeleteSpecial(req.uid, settings);
|
|
619
537
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
@@ -624,7 +542,7 @@ api.deleteSpecialEvent = async function (req, res) {
|
|
|
624
542
|
};
|
|
625
543
|
|
|
626
544
|
api.getItems = async function (req, res) {
|
|
627
|
-
const settings = await meta.settings.get('onekite
|
|
545
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
628
546
|
|
|
629
547
|
const env = settings.helloassoEnv || 'prod';
|
|
630
548
|
const token = await helloasso.getAccessToken({
|
|
@@ -665,7 +583,7 @@ api.createReservation = async function (req, res) {
|
|
|
665
583
|
const uid = req.uid;
|
|
666
584
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
667
585
|
|
|
668
|
-
const settings = await meta.settings.get('onekite
|
|
586
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
669
587
|
const startPreview = toTs(req.body.start);
|
|
670
588
|
const ok = await canRequest(uid, settings, startPreview);
|
|
671
589
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
@@ -762,7 +680,7 @@ api.createReservation = async function (req, res) {
|
|
|
762
680
|
|
|
763
681
|
for (const md of (usersData || [])) {
|
|
764
682
|
if (md && md.email) {
|
|
765
|
-
await sendEmail('
|
|
683
|
+
await sendEmail('calendar-onekite_pending', md.email, 'Location matériel - Demande de réservation', {
|
|
766
684
|
username: md.username,
|
|
767
685
|
requester: requester.username,
|
|
768
686
|
itemName: itemsLabel,
|
|
@@ -801,7 +719,7 @@ api.createReservation = async function (req, res) {
|
|
|
801
719
|
api.approveReservation = async function (req, res) {
|
|
802
720
|
const uid = req.uid;
|
|
803
721
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
804
|
-
const settings = await meta.settings.get('onekite
|
|
722
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
805
723
|
const ok = await canValidate(uid, settings);
|
|
806
724
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
807
725
|
|
|
@@ -828,7 +746,7 @@ api.approveReservation = async function (req, res) {
|
|
|
828
746
|
}
|
|
829
747
|
// Create HelloAsso payment link on validation
|
|
830
748
|
try {
|
|
831
|
-
const settings2 = await meta.settings.get('onekite
|
|
749
|
+
const settings2 = await meta.settings.get('calendar-onekite');
|
|
832
750
|
const token = await helloasso.getAccessToken({ env: settings2.helloassoEnv || 'prod', clientId: settings2.helloassoClientId, clientSecret: settings2.helloassoClientSecret });
|
|
833
751
|
const payer = await user.getUserFields(r.uid, ['email']);
|
|
834
752
|
const year = yearFromTs(r.start);
|
|
@@ -880,7 +798,7 @@ api.approveReservation = async function (req, res) {
|
|
|
880
798
|
const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
|
|
881
799
|
? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
|
|
882
800
|
: '';
|
|
883
|
-
await sendEmail('
|
|
801
|
+
await sendEmail('calendar-onekite_approved', requester.email, 'Location matériel - Réservation validée', {
|
|
884
802
|
username: requester.username,
|
|
885
803
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
886
804
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
@@ -904,7 +822,7 @@ api.approveReservation = async function (req, res) {
|
|
|
904
822
|
api.refuseReservation = async function (req, res) {
|
|
905
823
|
const uid = req.uid;
|
|
906
824
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
907
|
-
const settings = await meta.settings.get('onekite
|
|
825
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
908
826
|
const ok = await canValidate(uid, settings);
|
|
909
827
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
910
828
|
|
|
@@ -919,7 +837,7 @@ api.refuseReservation = async function (req, res) {
|
|
|
919
837
|
|
|
920
838
|
const requester = await user.getUserFields(r.uid, ['username', 'email']);
|
|
921
839
|
if (requester && requester.email) {
|
|
922
|
-
await sendEmail('
|
|
840
|
+
await sendEmail('calendar-onekite_refused', requester.email, 'Location matériel - Demande de réservation', {
|
|
923
841
|
username: requester.username,
|
|
924
842
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
925
843
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
@@ -939,7 +857,7 @@ api.cancelReservation = async function (req, res) {
|
|
|
939
857
|
const uid = req.uid;
|
|
940
858
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
941
859
|
|
|
942
|
-
const settings = await meta.settings.get('onekite
|
|
860
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
943
861
|
const rid = String(req.params.rid || '').trim();
|
|
944
862
|
if (!rid) return res.status(400).json({ error: 'missing-rid' });
|
|
945
863
|
|
package/lib/constants.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// Keep backward compatibility with historical route namespaces.
|
|
4
|
+
// New plugin name: nodebb-plugin-onekite-calendar
|
|
5
|
+
|
|
4
6
|
module.exports = {
|
|
7
|
+
LEGACY_NAMESPACE: 'calendar-onekite',
|
|
5
8
|
NAMESPACE: 'onekite-calendar',
|
|
6
|
-
WIDGET_ID: 'onekite-
|
|
9
|
+
WIDGET_ID: 'calendar-onekite-twoweeks',
|
|
7
10
|
};
|
package/lib/controllers.js
CHANGED
package/lib/db.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const db = require.main.require('./src/database');
|
|
4
4
|
|
|
5
|
-
const KEY_ZSET = 'onekite
|
|
6
|
-
const KEY_OBJ = (rid) => `onekite
|
|
7
|
-
const KEY_CHECKOUT_INTENT_TO_RID = 'onekite
|
|
5
|
+
const KEY_ZSET = 'calendar-onekite:reservations';
|
|
6
|
+
const KEY_OBJ = (rid) => `calendar-onekite:reservation:${rid}`;
|
|
7
|
+
const KEY_CHECKOUT_INTENT_TO_RID = 'calendar-onekite:helloasso:checkoutIntentToRid';
|
|
8
8
|
|
|
9
9
|
// Special events (non-reservation events shown in a different colour)
|
|
10
|
-
const KEY_SPECIAL_ZSET = 'onekite
|
|
11
|
-
const KEY_SPECIAL_OBJ = (eid) => `onekite
|
|
10
|
+
const KEY_SPECIAL_ZSET = 'calendar-onekite:special';
|
|
11
|
+
const KEY_SPECIAL_OBJ = (eid) => `calendar-onekite:special:${eid}`;
|
|
12
12
|
|
|
13
13
|
// Helpers
|
|
14
14
|
function reservationKey(rid) {
|
package/lib/discord.js
CHANGED
|
@@ -37,7 +37,7 @@ function postWebhook(webhookUrl, payload) {
|
|
|
37
37
|
headers: {
|
|
38
38
|
'Content-Type': 'application/json',
|
|
39
39
|
'Content-Length': body.length,
|
|
40
|
-
'User-Agent': 'nodebb-plugin-onekite
|
|
40
|
+
'User-Agent': 'nodebb-plugin-calendar-onekite',
|
|
41
41
|
},
|
|
42
42
|
}, (res) => {
|
|
43
43
|
const ok = res.statusCode && res.statusCode >= 200 && res.statusCode < 300;
|
package/lib/email.js
CHANGED
|
@@ -8,17 +8,30 @@ function defaultLanguage() {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Send a transactional email using NodeBB's emailer
|
|
11
|
+
* Send a transactional email using NodeBB's emailer.
|
|
12
|
+
*
|
|
13
|
+
* We intentionally do not log here (per project preferences).
|
|
12
14
|
*/
|
|
13
15
|
async function sendEmail(template, toEmail, subject, data) {
|
|
14
16
|
if (!toEmail) return;
|
|
15
|
-
if (!emailer || typeof emailer.sendToEmail !== 'function') {
|
|
16
|
-
throw new Error('Emailer not available (sendToEmail missing)');
|
|
17
|
-
}
|
|
18
17
|
|
|
19
18
|
const language = defaultLanguage();
|
|
20
19
|
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
21
|
-
|
|
20
|
+
|
|
21
|
+
// Common signature: sendToEmail(template, email, language, params)
|
|
22
|
+
if (emailer && typeof emailer.sendToEmail === 'function') {
|
|
23
|
+
await emailer.sendToEmail(template, toEmail, language, params);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Fallbacks for older builds
|
|
28
|
+
if (emailer && typeof emailer.send === 'function') {
|
|
29
|
+
if (emailer.send.length >= 4) {
|
|
30
|
+
await emailer.send(template, toEmail, language, params);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
await emailer.send(template, toEmail, params);
|
|
34
|
+
}
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
module.exports = {
|
package/lib/helloassoWebhook.js
CHANGED
|
@@ -20,10 +20,10 @@ const dbLayer = require('./db');
|
|
|
20
20
|
const helloasso = require('./helloasso');
|
|
21
21
|
const discord = require('./discord');
|
|
22
22
|
|
|
23
|
-
const SETTINGS_KEY = 'onekite
|
|
23
|
+
const SETTINGS_KEY = 'calendar-onekite';
|
|
24
24
|
|
|
25
25
|
// Replay protection: store processed payment ids.
|
|
26
|
-
const PROCESSED_KEY = 'onekite
|
|
26
|
+
const PROCESSED_KEY = 'calendar-onekite:helloasso:processedPayments';
|
|
27
27
|
|
|
28
28
|
async function sendEmail(template, toEmail, subject, data) {
|
|
29
29
|
const uidFromData = data && Number.isInteger(data.uid) ? data.uid : null;
|
|
@@ -348,14 +348,14 @@ async function handler(req, res, next) {
|
|
|
348
348
|
// Real-time notify: refresh calendars for all viewers (owner + validators/admins)
|
|
349
349
|
try {
|
|
350
350
|
if (io && io.sockets && typeof io.sockets.emit === 'function') {
|
|
351
|
-
io.sockets.emit('event:onekite
|
|
351
|
+
io.sockets.emit('event:calendar-onekite.reservationUpdated', { rid: r.rid, status: r.status });
|
|
352
352
|
}
|
|
353
353
|
} catch (e) {}
|
|
354
354
|
|
|
355
355
|
// Notify requester
|
|
356
356
|
const requester = await user.getUserFields(r.uid, ['username', 'email']);
|
|
357
357
|
if (requester && requester.email) {
|
|
358
|
-
await sendEmail('
|
|
358
|
+
await sendEmail('calendar-onekite_paid', requester.email, 'Location matériel - Paiement reçu', {
|
|
359
359
|
uid: parseInt(r.uid, 10),
|
|
360
360
|
username: requester.username,
|
|
361
361
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
package/lib/scheduler.js
CHANGED
|
@@ -16,7 +16,7 @@ function getSetting(settings, key, fallback) {
|
|
|
16
16
|
|
|
17
17
|
// Pending holds: short lock after a user creates a request (defaults to 5 minutes)
|
|
18
18
|
async function expirePending() {
|
|
19
|
-
const settings = await meta.settings.get('onekite
|
|
19
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
20
20
|
const holdMins = parseInt(getSetting(settings, 'pendingHoldMinutes', '5'), 10) || 5;
|
|
21
21
|
const now = Date.now();
|
|
22
22
|
|
|
@@ -44,7 +44,7 @@ async function expirePending() {
|
|
|
44
44
|
// - We send a reminder after `paymentHoldMinutes` (default 60)
|
|
45
45
|
// - We expire (and remove) after `2 * paymentHoldMinutes`
|
|
46
46
|
async function processAwaitingPayment() {
|
|
47
|
-
const settings = await meta.settings.get('onekite
|
|
47
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
48
48
|
const holdMins = parseInt(
|
|
49
49
|
getSetting(settings, 'paymentHoldMinutes', getSetting(settings, 'holdMinutes', '60')),
|
|
50
50
|
10
|
|
@@ -108,7 +108,7 @@ async function processAwaitingPayment() {
|
|
|
108
108
|
|
|
109
109
|
if (!r.reminderSent && now >= reminderAt && now < expireAt) {
|
|
110
110
|
// Send reminder once (guarded across clustered NodeBB processes)
|
|
111
|
-
const reminderKey = 'onekite
|
|
111
|
+
const reminderKey = 'calendar-onekite:email:reminderSent';
|
|
112
112
|
const first = await db.setAdd(reminderKey, rid);
|
|
113
113
|
if (!first) {
|
|
114
114
|
// another process already sent it
|
|
@@ -119,7 +119,7 @@ async function processAwaitingPayment() {
|
|
|
119
119
|
}
|
|
120
120
|
const u = await user.getUserFields(r.uid, ['username', 'email']);
|
|
121
121
|
if (u && u.email) {
|
|
122
|
-
await sendEmail('
|
|
122
|
+
await sendEmail('calendar-onekite_reminder', u.email, 'Location matériel - Rappel', {
|
|
123
123
|
username: u.username,
|
|
124
124
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
125
125
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
@@ -138,12 +138,12 @@ async function processAwaitingPayment() {
|
|
|
138
138
|
if (now >= expireAt) {
|
|
139
139
|
// Expire: remove reservation so it disappears from calendar and frees items
|
|
140
140
|
// Guard email send across clustered NodeBB processes
|
|
141
|
-
const expiredKey = 'onekite
|
|
141
|
+
const expiredKey = 'calendar-onekite:email:expiredSent';
|
|
142
142
|
const firstExpired = await db.setAdd(expiredKey, rid);
|
|
143
143
|
const shouldEmail = !!firstExpired;
|
|
144
144
|
const u = await user.getUserFields(r.uid, ['username', 'email']);
|
|
145
145
|
if (shouldEmail && u && u.email) {
|
|
146
|
-
await sendEmail('
|
|
146
|
+
await sendEmail('calendar-onekite_expired', u.email, 'Location matériel - Rappel', {
|
|
147
147
|
username: u.username,
|
|
148
148
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
149
149
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
package/lib/widgets.js
CHANGED
|
@@ -40,7 +40,7 @@ widgets.defineWidgets = async function (widgetData) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
list.push({
|
|
43
|
-
widget: 'onekite-
|
|
43
|
+
widget: 'calendar-onekite-twoweeks',
|
|
44
44
|
name: 'Calendrier Onekite',
|
|
45
45
|
description: 'Affiche la semaine courante + la semaine suivante (FullCalendar via CDN).',
|
|
46
46
|
content: '',
|
|
@@ -54,11 +54,14 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
54
54
|
const id = makeDomId();
|
|
55
55
|
const calUrl = widgetCalendarUrl();
|
|
56
56
|
const apiBase = forumBaseUrl();
|
|
57
|
+
// Prefer the new namespace, but keep a transparent fallback to the legacy one.
|
|
57
58
|
const eventsEndpoint = `${apiBase}/api/v3/plugins/onekite-calendar/events`;
|
|
59
|
+
const legacyEventsEndpoint = `${apiBase}/api/v3/plugins/calendar-onekite/events`;
|
|
58
60
|
|
|
59
61
|
const idJson = JSON.stringify(id);
|
|
60
62
|
const calUrlJson = JSON.stringify(calUrl);
|
|
61
63
|
const eventsEndpointJson = JSON.stringify(eventsEndpoint);
|
|
64
|
+
const legacyEventsEndpointJson = JSON.stringify(legacyEventsEndpoint);
|
|
62
65
|
|
|
63
66
|
const html = `
|
|
64
67
|
<div class="onekite-twoweeks">
|
|
@@ -75,6 +78,7 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
75
78
|
const containerId = ${idJson};
|
|
76
79
|
const calUrl = ${calUrlJson};
|
|
77
80
|
const eventsEndpoint = ${eventsEndpointJson};
|
|
81
|
+
const legacyEventsEndpoint = ${legacyEventsEndpointJson};
|
|
78
82
|
|
|
79
83
|
function loadOnce(tag, attrs) {
|
|
80
84
|
return new Promise((resolve, reject) => {
|
|
@@ -150,6 +154,13 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
150
154
|
const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
|
|
151
155
|
const url1 = eventsEndpoint + '?' + qs.toString();
|
|
152
156
|
fetch(url1, { credentials: 'same-origin' })
|
|
157
|
+
.then((r) => {
|
|
158
|
+
if (r && r.status === 404) {
|
|
159
|
+
const url2 = legacyEventsEndpoint + '?' + qs.toString();
|
|
160
|
+
return fetch(url2, { credentials: 'same-origin' });
|
|
161
|
+
}
|
|
162
|
+
return r;
|
|
163
|
+
})
|
|
153
164
|
.then((r) => r.json())
|
|
154
165
|
.then((json) => successCallback(json || []))
|
|
155
166
|
.catch((e) => failureCallback(e));
|
package/library.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// We use NodeBB's route helpers for page routes so the rendered page includes
|
|
4
|
+
// the normal client bundle (ajaxify/requirejs). The helper signatures differ
|
|
5
|
+
// across NodeBB versions, but for NodeBB v4.x the order is:
|
|
6
|
+
// setupPageRoute(router, path, middlewaresArray, handler)
|
|
7
|
+
// setupAdminPageRoute(router, path, middlewaresArray, handler)
|
|
3
8
|
const routeHelpers = require.main.require('./src/routes/helpers');
|
|
4
9
|
|
|
5
10
|
const controllers = require('./lib/controllers.js');
|
|
@@ -8,7 +13,7 @@ const admin = require('./lib/admin');
|
|
|
8
13
|
const scheduler = require('./lib/scheduler');
|
|
9
14
|
const helloassoWebhook = require('./lib/helloassoWebhook');
|
|
10
15
|
const widgets = require('./lib/widgets');
|
|
11
|
-
const { NAMESPACE } = require('./lib/constants');
|
|
16
|
+
const { LEGACY_NAMESPACE, NAMESPACE } = require('./lib/constants');
|
|
12
17
|
const bodyParser = require('body-parser');
|
|
13
18
|
|
|
14
19
|
const Plugin = {};
|
|
@@ -19,8 +24,16 @@ const mw = (...fns) => fns.filter(isFn);
|
|
|
19
24
|
Plugin.init = async function (params) {
|
|
20
25
|
const { router, middleware } = params;
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
// Build middleware arrays safely and always spread them into Express route methods.
|
|
28
|
+
// Express will throw if any callback is undefined, so we filter strictly.
|
|
29
|
+
// Auth middlewares differ slightly depending on NodeBB configuration.
|
|
30
|
+
// In v4, some installs rely on middleware.authenticate rather than exposeUid.
|
|
31
|
+
const baseExpose = mw(middleware && (middleware.authenticate || middleware.exposeUid));
|
|
32
|
+
const publicExpose = baseExpose;
|
|
33
|
+
const publicAuth = mw(middleware && (middleware.authenticate || middleware.exposeUid), middleware && middleware.ensureLoggedIn);
|
|
23
34
|
|
|
35
|
+
// Robust admin guard: avoid middleware.admin.checkPrivileges() signature differences
|
|
36
|
+
// across NodeBB versions. We treat membership in the 'administrators' group as admin.
|
|
24
37
|
const Groups = require.main.require('./src/groups');
|
|
25
38
|
async function adminOnly(req, res, next) {
|
|
26
39
|
try {
|
|
@@ -37,7 +50,6 @@ Plugin.init = async function (params) {
|
|
|
37
50
|
return next(err);
|
|
38
51
|
}
|
|
39
52
|
}
|
|
40
|
-
|
|
41
53
|
const adminMws = mw(
|
|
42
54
|
middleware && (middleware.authenticate || middleware.exposeUid),
|
|
43
55
|
middleware && middleware.ensureLoggedIn,
|
|
@@ -45,57 +57,72 @@ Plugin.init = async function (params) {
|
|
|
45
57
|
);
|
|
46
58
|
|
|
47
59
|
// Page routes (HTML)
|
|
60
|
+
// IMPORTANT: pass an ARRAY for middlewares (even if empty), otherwise
|
|
61
|
+
// setupPageRoute will throw "middlewares is not iterable".
|
|
48
62
|
routeHelpers.setupPageRoute(router, '/calendar', mw(), controllers.renderCalendar);
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
// Admin page route (keep legacy + add new)
|
|
64
|
+
routeHelpers.setupAdminPageRoute(router, `/admin/plugins/${LEGACY_NAMESPACE}`, mw(), admin.renderAdmin);
|
|
51
65
|
routeHelpers.setupAdminPageRoute(router, `/admin/plugins/${NAMESPACE}`, mw(), admin.renderAdmin);
|
|
52
66
|
|
|
53
|
-
// Public API (JSON) — NodeBB 4.x (v3 API)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// Public API (JSON) — NodeBB 4.x only (v3 API)
|
|
68
|
+
const pluginBases = [LEGACY_NAMESPACE, NAMESPACE];
|
|
69
|
+
pluginBases.forEach((ns) => {
|
|
70
|
+
router.get(`/api/v3/plugins/${ns}/events`, ...publicExpose, api.getEvents);
|
|
71
|
+
router.get(`/api/v3/plugins/${ns}/items`, ...publicExpose, api.getItems);
|
|
72
|
+
router.get(`/api/v3/plugins/${ns}/capabilities`, ...publicExpose, api.getCapabilities);
|
|
73
|
+
|
|
74
|
+
router.post(`/api/v3/plugins/${ns}/reservations`, ...publicExpose, api.createReservation);
|
|
75
|
+
router.get(`/api/v3/plugins/${ns}/reservations/:rid`, ...publicExpose, api.getReservationDetails);
|
|
76
|
+
router.put(`/api/v3/plugins/${ns}/reservations/:rid/approve`, ...publicExpose, api.approveReservation);
|
|
77
|
+
router.put(`/api/v3/plugins/${ns}/reservations/:rid/refuse`, ...publicExpose, api.refuseReservation);
|
|
78
|
+
router.put(`/api/v3/plugins/${ns}/reservations/:rid/cancel`, ...publicExpose, api.cancelReservation);
|
|
79
|
+
|
|
80
|
+
router.post(`/api/v3/plugins/${ns}/special-events`, ...publicExpose, api.createSpecialEvent);
|
|
81
|
+
router.get(`/api/v3/plugins/${ns}/special-events/:eid`, ...publicExpose, api.getSpecialEventDetails);
|
|
82
|
+
router.delete(`/api/v3/plugins/${ns}/special-events/:eid`, ...publicExpose, api.deleteSpecialEvent);
|
|
83
|
+
});
|
|
67
84
|
|
|
68
85
|
// Admin API (JSON)
|
|
69
|
-
const
|
|
86
|
+
const adminBases = [`/api/v3/admin/plugins/${LEGACY_NAMESPACE}`, `/api/v3/admin/plugins/${NAMESPACE}`];
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
adminBases.forEach((base) => {
|
|
89
|
+
router.get(`${base}/settings`, ...adminMws, admin.getSettings);
|
|
90
|
+
router.put(`${base}/settings`, ...adminMws, admin.saveSettings);
|
|
73
91
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
router.get(`${base}/pending`, ...adminMws, admin.listPending);
|
|
93
|
+
router.put(`${base}/reservations/:rid/approve`, ...adminMws, admin.approveReservation);
|
|
94
|
+
router.put(`${base}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
|
|
77
95
|
|
|
78
|
-
|
|
96
|
+
router.post(`${base}/purge`, ...adminMws, admin.purgeByYear);
|
|
97
|
+
// Accounting / exports
|
|
98
|
+
router.get(`${base}/accounting`, ...adminMws, admin.getAccounting);
|
|
99
|
+
router.get(`${base}/accounting.csv`, ...adminMws, admin.exportAccountingCsv);
|
|
100
|
+
router.post(`${base}/accounting/purge`, ...adminMws, admin.purgeAccounting);
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
|
|
102
|
+
// Purge special events by year
|
|
103
|
+
router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
|
|
104
|
+
});
|
|
85
105
|
|
|
86
|
-
// HelloAsso
|
|
106
|
+
// HelloAsso callback endpoint (hardened)
|
|
107
|
+
// - Only accepts POST
|
|
108
|
+
// - Verifies x-ha-signature (HMAC SHA-256) using the configured client secret
|
|
109
|
+
// - Basic replay protection
|
|
110
|
+
// NOTE: we capture the raw body for signature verification.
|
|
87
111
|
const helloassoJson = bodyParser.json({
|
|
88
112
|
verify: (req, _res, buf) => {
|
|
89
113
|
req.rawBody = buf;
|
|
90
114
|
},
|
|
91
115
|
type: ['application/json', 'application/*+json'],
|
|
92
116
|
});
|
|
93
|
-
|
|
117
|
+
// Accept webhook on both legacy root path and namespaced plugin path.
|
|
118
|
+
// Some reverse proxies block unknown root paths, so /plugins/... is recommended.
|
|
94
119
|
router.post('/helloasso', helloassoJson, helloassoWebhook.handler);
|
|
120
|
+
router.post(`/plugins/${LEGACY_NAMESPACE}/helloasso`, helloassoJson, helloassoWebhook.handler);
|
|
95
121
|
router.post(`/plugins/${NAMESPACE}/helloasso`, helloassoJson, helloassoWebhook.handler);
|
|
96
122
|
|
|
97
|
-
// Optional health checks
|
|
123
|
+
// Optional: health checks
|
|
98
124
|
router.get('/helloasso', (req, res) => res.json({ ok: true }));
|
|
125
|
+
router.get(`/plugins/${LEGACY_NAMESPACE}/helloasso`, (req, res) => res.json({ ok: true }));
|
|
99
126
|
router.get(`/plugins/${NAMESPACE}/helloasso`, (req, res) => res.json({ ok: true }));
|
|
100
127
|
|
|
101
128
|
scheduler.start();
|
|
@@ -104,20 +131,24 @@ Plugin.init = async function (params) {
|
|
|
104
131
|
Plugin.addAdminNavigation = async function (header) {
|
|
105
132
|
header.plugins = header.plugins || [];
|
|
106
133
|
header.plugins.push({
|
|
107
|
-
route: `/plugins/${
|
|
134
|
+
route: `/plugins/${LEGACY_NAMESPACE}`,
|
|
108
135
|
icon: 'fa-calendar',
|
|
109
|
-
name: 'Onekite
|
|
136
|
+
name: 'Calendar Onekite',
|
|
110
137
|
});
|
|
111
138
|
return header;
|
|
112
139
|
};
|
|
113
140
|
|
|
141
|
+
|
|
114
142
|
// Ensure our transactional emails always get a subject.
|
|
143
|
+
// NodeBB's Emailer.sendToEmail signature expects (template, email, language, params),
|
|
144
|
+
// so plugins typically inject/modify the subject via this hook.
|
|
115
145
|
Plugin.emailModify = async function (data) {
|
|
116
146
|
try {
|
|
117
147
|
if (!data || !data.template) return data;
|
|
118
148
|
const tpl = String(data.template);
|
|
119
|
-
if (!tpl.startsWith('
|
|
149
|
+
if (!tpl.startsWith('calendar-onekite_')) return data;
|
|
120
150
|
|
|
151
|
+
// If the caller provided a subject (we pass it in params.subject), copy it to data.subject.
|
|
121
152
|
const provided = data.params && data.params.subject ? String(data.params.subject) : '';
|
|
122
153
|
if (provided && (!data.subject || !String(data.subject).trim())) {
|
|
123
154
|
data.subject = provided;
|
|
@@ -130,4 +161,4 @@ Plugin.emailModify = async function (data) {
|
|
|
130
161
|
Plugin.defineWidgets = widgets.defineWidgets;
|
|
131
162
|
Plugin.renderTwoWeeksWidget = widgets.renderTwoWeeksWidget;
|
|
132
163
|
|
|
133
|
-
module.exports = Plugin;
|
|
164
|
+
module.exports = Plugin;
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"method": "defineWidgets"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
|
-
"hook": "filter:widget.render:onekite-
|
|
24
|
+
"hook": "filter:widget.render:calendar-onekite-twoweeks",
|
|
25
25
|
"method": "renderTwoWeeksWidget"
|
|
26
26
|
}
|
|
27
27
|
],
|
|
@@ -30,8 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
"templates": "./templates",
|
|
32
32
|
"modules": {
|
|
33
|
-
"admin/plugins/onekite
|
|
34
|
-
"
|
|
33
|
+
"../admin/plugins/calendar-onekite.js": "./public/admin.js",
|
|
34
|
+
"admin/plugins/calendar-onekite": "./public/admin.js"
|
|
35
35
|
},
|
|
36
|
-
"
|
|
36
|
+
"scripts": [
|
|
37
|
+
"public/client.js"
|
|
38
|
+
],
|
|
39
|
+
"acpScripts": [
|
|
40
|
+
"public/admin.js"
|
|
41
|
+
],
|
|
42
|
+
"version": "1.0.7"
|
|
37
43
|
}
|
package/public/admin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
define('admin/plugins/onekite
|
|
2
|
+
define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts, bootbox) {
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
// Cache of pending reservations keyed by rid so delegated click handlers
|
|
@@ -11,11 +11,11 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
|
|
|
11
11
|
// by NodeBB ACP save buttons/hooks across ajaxify navigations.
|
|
12
12
|
try {
|
|
13
13
|
const now = Date.now();
|
|
14
|
-
const last = window.
|
|
14
|
+
const last = window.oneKiteCalendarLastAlert;
|
|
15
15
|
if (last && last.type === type && last.msg === msg && (now - last.ts) < 1200) {
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
|
-
window.
|
|
18
|
+
window.oneKiteCalendarLastAlert = { type, msg, ts: now };
|
|
19
19
|
} catch (e) {}
|
|
20
20
|
try {
|
|
21
21
|
if (alerts && typeof alerts[type] === 'function') {
|
|
@@ -46,7 +46,30 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
if (!res.ok) {
|
|
49
|
-
|
|
49
|
+
// NodeBB versions differ: some expose admin APIs under /api/admin instead of /api/v3/admin
|
|
50
|
+
if (res.status === 404 && typeof url === 'string' && url.includes('/api/v3/admin/')) {
|
|
51
|
+
const altUrl = url.replace('/api/v3/admin/', '/api/admin/');
|
|
52
|
+
const res2 = await fetch(altUrl, {
|
|
53
|
+
credentials: 'same-origin',
|
|
54
|
+
headers: (() => {
|
|
55
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
56
|
+
const token =
|
|
57
|
+
(window.config && (window.config.csrf_token || window.config.csrfToken)) ||
|
|
58
|
+
(window.ajaxify && window.ajaxify.data && window.ajaxify.data.csrf_token) ||
|
|
59
|
+
(document.querySelector('meta[name="csrf-token"]') && document.querySelector('meta[name="csrf-token"]').getAttribute('content')) ||
|
|
60
|
+
(document.querySelector('meta[name="csrf_token"]') && document.querySelector('meta[name="csrf_token"]').getAttribute('content')) ||
|
|
61
|
+
(typeof app !== 'undefined' && app && app.csrfToken) ||
|
|
62
|
+
null;
|
|
63
|
+
if (token) headers['x-csrf-token'] = token;
|
|
64
|
+
return headers;
|
|
65
|
+
})(),
|
|
66
|
+
...opts,
|
|
67
|
+
});
|
|
68
|
+
if (res2.ok) {
|
|
69
|
+
return await res2.json();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const text = await res.text().catch(() => '');
|
|
50
73
|
throw new Error(`${res.status} ${text}`);
|
|
51
74
|
}
|
|
52
75
|
return await res.json();
|
|
@@ -244,36 +267,36 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
|
|
|
244
267
|
}
|
|
245
268
|
|
|
246
269
|
async function loadSettings() {
|
|
247
|
-
return await fetchJson('/api/v3/admin/plugins/onekite
|
|
270
|
+
return await fetchJson('/api/v3/admin/plugins/calendar-onekite/settings');
|
|
248
271
|
}
|
|
249
272
|
|
|
250
273
|
async function saveSettings(payload) {
|
|
251
|
-
return await fetchJson('/api/v3/admin/plugins/onekite
|
|
274
|
+
return await fetchJson('/api/v3/admin/plugins/calendar-onekite/settings', {
|
|
252
275
|
method: 'PUT',
|
|
253
276
|
body: JSON.stringify(payload),
|
|
254
277
|
});
|
|
255
278
|
}
|
|
256
279
|
|
|
257
280
|
async function loadPending() {
|
|
258
|
-
return await fetchJson('/api/v3/admin/plugins/onekite
|
|
281
|
+
return await fetchJson('/api/v3/admin/plugins/calendar-onekite/pending');
|
|
259
282
|
}
|
|
260
283
|
|
|
261
284
|
async function approve(rid, payload) {
|
|
262
|
-
return await fetchJson(`/api/v3/admin/plugins/onekite
|
|
285
|
+
return await fetchJson(`/api/v3/admin/plugins/calendar-onekite/reservations/${rid}/approve`, {
|
|
263
286
|
method: 'PUT',
|
|
264
287
|
body: JSON.stringify(payload || {}),
|
|
265
288
|
});
|
|
266
289
|
}
|
|
267
290
|
|
|
268
291
|
async function refuse(rid, payload) {
|
|
269
|
-
return await fetchJson(`/api/v3/admin/plugins/onekite
|
|
292
|
+
return await fetchJson(`/api/v3/admin/plugins/calendar-onekite/reservations/${rid}/refuse`, {
|
|
270
293
|
method: 'PUT',
|
|
271
294
|
body: JSON.stringify(payload || {}),
|
|
272
295
|
});
|
|
273
296
|
}
|
|
274
297
|
|
|
275
298
|
async function purge(year) {
|
|
276
|
-
return await fetchJson('/api/v3/admin/plugins/onekite
|
|
299
|
+
return await fetchJson('/api/v3/admin/plugins/calendar-onekite/purge', {
|
|
277
300
|
method: 'POST',
|
|
278
301
|
body: JSON.stringify({ year }),
|
|
279
302
|
});
|
|
@@ -281,12 +304,12 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
|
|
|
281
304
|
|
|
282
305
|
async function purgeSpecialEvents(year) {
|
|
283
306
|
try {
|
|
284
|
-
return await fetchJson('/api/v3/admin/plugins/onekite
|
|
307
|
+
return await fetchJson('/api/v3/admin/plugins/calendar-onekite/special-events/purge', {
|
|
285
308
|
method: 'POST',
|
|
286
309
|
body: JSON.stringify({ year }),
|
|
287
310
|
});
|
|
288
311
|
} catch (e) {
|
|
289
|
-
return await fetchJson('/api/v3/admin/plugins/onekite
|
|
312
|
+
return await fetchJson('/api/v3/admin/plugins/calendar-onekite/special-events/purge', {
|
|
290
313
|
method: 'POST',
|
|
291
314
|
body: JSON.stringify({ year }),
|
|
292
315
|
});
|
|
@@ -298,7 +321,7 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
|
|
|
298
321
|
if (from) params.set('from', from);
|
|
299
322
|
if (to) params.set('to', to);
|
|
300
323
|
const qs = params.toString();
|
|
301
|
-
return await fetchJson(`/api/v3/admin/plugins/onekite
|
|
324
|
+
return await fetchJson(`/api/v3/admin/plugins/calendar-onekite/accounting${qs ? `?${qs}` : ''}`);
|
|
302
325
|
}
|
|
303
326
|
|
|
304
327
|
async function init() {
|
|
@@ -361,19 +384,19 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
|
|
|
361
384
|
|
|
362
385
|
// Expose the latest save handler so the global delegated listener (bound once)
|
|
363
386
|
// can always call the current instance tied to the current form.
|
|
364
|
-
window.
|
|
387
|
+
window.oneKiteCalendarAdminDoSave = doSave;
|
|
365
388
|
|
|
366
389
|
// Save buttons (NodeBB header/footer "Enregistrer" + floppy icon)
|
|
367
390
|
// Bind a SINGLE delegated listener for the entire admin session.
|
|
368
391
|
const SAVE_SELECTOR = '#save, .save, [data-action="save"], .settings-save, .floating-save, .btn[data-action="save"]';
|
|
369
|
-
if (!window.
|
|
370
|
-
window.
|
|
392
|
+
if (!window.oneKiteCalendarAdminBound) {
|
|
393
|
+
window.oneKiteCalendarAdminBound = true;
|
|
371
394
|
document.addEventListener('click', (ev) => {
|
|
372
395
|
const btn = ev.target && ev.target.closest && ev.target.closest(SAVE_SELECTOR);
|
|
373
396
|
if (!btn) return;
|
|
374
397
|
// Only handle clicks while we're on this plugin page
|
|
375
398
|
if (!document.getElementById('onekite-settings-form')) return;
|
|
376
|
-
const fn = window.
|
|
399
|
+
const fn = window.oneKiteCalendarAdminDoSave;
|
|
377
400
|
if (typeof fn === 'function') fn(ev);
|
|
378
401
|
});
|
|
379
402
|
}
|
|
@@ -697,7 +720,7 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
|
|
|
697
720
|
if (accFrom && accFrom.value) params.set('from', accFrom.value);
|
|
698
721
|
if (accTo && accTo.value) params.set('to', accTo.value);
|
|
699
722
|
const qs = params.toString();
|
|
700
|
-
const url = `/api/v3/admin/plugins/onekite
|
|
723
|
+
const url = `/api/v3/admin/plugins/calendar-onekite/accounting.csv${qs ? `?${qs}` : ''}`;
|
|
701
724
|
window.open(url, '_blank');
|
|
702
725
|
});
|
|
703
726
|
|
|
@@ -710,7 +733,7 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
|
|
|
710
733
|
if (accFrom && accFrom.value) params.set('from', accFrom.value);
|
|
711
734
|
if (accTo && accTo.value) params.set('to', accTo.value);
|
|
712
735
|
const qs = params.toString();
|
|
713
|
-
const url = `/api/v3/admin/plugins/onekite
|
|
736
|
+
const url = `/api/v3/admin/plugins/calendar-onekite/accounting/purge${qs ? `?${qs}` : ''}`;
|
|
714
737
|
const res = await fetchJson(url, { method: 'POST' });
|
|
715
738
|
if (res && res.ok) {
|
|
716
739
|
showAlert('success', `Compta purgée : ${res.purged || 0} réservation(s).`);
|
package/public/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* global FullCalendar, ajaxify */
|
|
2
2
|
|
|
3
|
-
define('forum/onekite
|
|
3
|
+
define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alerts, bootbox, hooks) {
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
// Ensure small UI tweaks are applied even when themes override bootstrap defaults.
|
|
@@ -498,7 +498,7 @@ define('forum/onekite-calendar', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
498
498
|
}
|
|
499
499
|
|
|
500
500
|
async function loadCapabilities() {
|
|
501
|
-
return await fetchJson('/api/v3/plugins/onekite
|
|
501
|
+
return await fetchJson('/api/v3/plugins/calendar-onekite/capabilities');
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
// Leaflet (OpenStreetMap) helpers - loaded lazily only when needed.
|
|
@@ -706,34 +706,34 @@ function attachAddressAutocomplete(inputEl, onPick) {
|
|
|
706
706
|
|
|
707
707
|
async function loadItems() {
|
|
708
708
|
try {
|
|
709
|
-
return await fetchJson('/api/v3/plugins/onekite
|
|
709
|
+
return await fetchJson('/api/v3/plugins/calendar-onekite/items');
|
|
710
710
|
} catch (e) {
|
|
711
711
|
return [];
|
|
712
712
|
}
|
|
713
713
|
}
|
|
714
714
|
|
|
715
715
|
async function requestReservation(payload) {
|
|
716
|
-
return await fetchJson('/api/v3/plugins/onekite
|
|
716
|
+
return await fetchJson('/api/v3/plugins/calendar-onekite/reservations', {
|
|
717
717
|
method: 'POST',
|
|
718
718
|
body: JSON.stringify(payload),
|
|
719
719
|
});
|
|
720
720
|
}
|
|
721
721
|
|
|
722
722
|
async function approveReservation(rid, payload) {
|
|
723
|
-
return await fetchJson(`/api/v3/plugins/onekite
|
|
723
|
+
return await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/approve`, {
|
|
724
724
|
method: 'PUT',
|
|
725
725
|
body: JSON.stringify(payload || {}),
|
|
726
726
|
});
|
|
727
727
|
}
|
|
728
728
|
|
|
729
729
|
async function cancelReservation(rid) {
|
|
730
|
-
return await fetchJson(`/api/v3/plugins/onekite
|
|
730
|
+
return await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/cancel`, {
|
|
731
731
|
method: 'PUT',
|
|
732
732
|
});
|
|
733
733
|
}
|
|
734
734
|
|
|
735
735
|
async function refuseReservation(rid, payload) {
|
|
736
|
-
return await fetchJson(`/api/v3/plugins/onekite
|
|
736
|
+
return await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/refuse`, {
|
|
737
737
|
method: 'PUT',
|
|
738
738
|
body: JSON.stringify(payload || {}),
|
|
739
739
|
});
|
|
@@ -783,7 +783,7 @@ function toDatetimeLocalValue(date) {
|
|
|
783
783
|
let blocked = new Set();
|
|
784
784
|
try {
|
|
785
785
|
const qs = new URLSearchParams({ start: selectionInfo.startStr, end: selectionInfo.endStr });
|
|
786
|
-
const evs = await fetchJson(`/api/v3/plugins/onekite
|
|
786
|
+
const evs = await fetchJson(`/api/v3/plugins/calendar-onekite/events?${qs.toString()}`);
|
|
787
787
|
(evs || []).forEach((ev) => {
|
|
788
788
|
const st = (ev.extendedProps && ev.extendedProps.status) || '';
|
|
789
789
|
if (!['pending', 'awaiting_payment', 'approved', 'paid'].includes(st)) return;
|
|
@@ -1094,7 +1094,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1094
1094
|
window.__onekiteEventsAbort = abort;
|
|
1095
1095
|
|
|
1096
1096
|
const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
|
|
1097
|
-
const url = `/api/v3/plugins/onekite
|
|
1097
|
+
const url = `/api/v3/plugins/calendar-onekite/events?${qs.toString()}`;
|
|
1098
1098
|
const data = await fetchJsonCached(url, { signal: abort.signal });
|
|
1099
1099
|
|
|
1100
1100
|
// Prefetch adjacent range (previous/next) for snappier navigation.
|
|
@@ -1108,8 +1108,8 @@ function toDatetimeLocalValue(date) {
|
|
|
1108
1108
|
const toStr = (d) => new Date(d.getTime()).toISOString();
|
|
1109
1109
|
const qPrev = new URLSearchParams({ start: toStr(prevStart), end: toStr(prevEnd) });
|
|
1110
1110
|
const qNext = new URLSearchParams({ start: toStr(nextStart), end: toStr(nextEnd) });
|
|
1111
|
-
fetchJsonCached(`/api/v3/plugins/onekite
|
|
1112
|
-
fetchJsonCached(`/api/v3/plugins/onekite
|
|
1111
|
+
fetchJsonCached(`/api/v3/plugins/calendar-onekite/events?${qPrev.toString()}`).catch(() => {});
|
|
1112
|
+
fetchJsonCached(`/api/v3/plugins/calendar-onekite/events?${qNext.toString()}`).catch(() => {});
|
|
1113
1113
|
}
|
|
1114
1114
|
} catch (e) {}
|
|
1115
1115
|
|
|
@@ -1171,11 +1171,11 @@ function toDatetimeLocalValue(date) {
|
|
|
1171
1171
|
isDialogOpen = false;
|
|
1172
1172
|
return;
|
|
1173
1173
|
}
|
|
1174
|
-
await fetchJson('/api/v3/plugins/onekite
|
|
1174
|
+
await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1175
1175
|
method: 'POST',
|
|
1176
1176
|
body: JSON.stringify(payload),
|
|
1177
1177
|
}).catch(async () => {
|
|
1178
|
-
return await fetchJson('/api/v3/plugins/onekite
|
|
1178
|
+
return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1179
1179
|
method: 'POST',
|
|
1180
1180
|
body: JSON.stringify(payload),
|
|
1181
1181
|
});
|
|
@@ -1265,11 +1265,11 @@ function toDatetimeLocalValue(date) {
|
|
|
1265
1265
|
isDialogOpen = false;
|
|
1266
1266
|
return;
|
|
1267
1267
|
}
|
|
1268
|
-
await fetchJson('/api/v3/plugins/onekite
|
|
1268
|
+
await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1269
1269
|
method: 'POST',
|
|
1270
1270
|
body: JSON.stringify(payload),
|
|
1271
1271
|
}).catch(async () => {
|
|
1272
|
-
return await fetchJson('/api/v3/plugins/onekite
|
|
1272
|
+
return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1273
1273
|
method: 'POST',
|
|
1274
1274
|
body: JSON.stringify(payload),
|
|
1275
1275
|
});
|
|
@@ -1296,10 +1296,10 @@ function toDatetimeLocalValue(date) {
|
|
|
1296
1296
|
let p = p0;
|
|
1297
1297
|
try {
|
|
1298
1298
|
if (p0.type === 'reservation' && p0.rid) {
|
|
1299
|
-
const details = await fetchJson(`/api/v3/plugins/onekite
|
|
1299
|
+
const details = await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${encodeURIComponent(String(p0.rid))}`);
|
|
1300
1300
|
p = Object.assign({}, p0, details);
|
|
1301
1301
|
} else if (p0.type === 'special' && p0.eid) {
|
|
1302
|
-
const details = await fetchJson(`/api/v3/plugins/onekite
|
|
1302
|
+
const details = await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(String(p0.eid))}`);
|
|
1303
1303
|
p = Object.assign({}, p0, details, {
|
|
1304
1304
|
// keep backward compat with older field names used by templates below
|
|
1305
1305
|
pickupAddress: details.address || details.pickupAddress || p0.pickupAddress,
|
|
@@ -1348,7 +1348,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1348
1348
|
callback: async () => {
|
|
1349
1349
|
try {
|
|
1350
1350
|
const eid = String(p.eid || ev.id).replace(/^special:/, '');
|
|
1351
|
-
await fetchJson(`/api/v3/plugins/onekite
|
|
1351
|
+
await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}`, { method: 'DELETE' });
|
|
1352
1352
|
showAlert('success', 'Évènement supprimé.');
|
|
1353
1353
|
calendar.refetchEvents();
|
|
1354
1354
|
} catch (e) {
|
|
@@ -1667,7 +1667,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1667
1667
|
});
|
|
1668
1668
|
|
|
1669
1669
|
// Expose for live updates
|
|
1670
|
-
try { window.
|
|
1670
|
+
try { window.oneKiteCalendar = calendar; } catch (e) {}
|
|
1671
1671
|
|
|
1672
1672
|
calendar.render();
|
|
1673
1673
|
|
|
@@ -1766,7 +1766,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1766
1766
|
function autoInit(data) {
|
|
1767
1767
|
try {
|
|
1768
1768
|
const tpl = data && data.template ? data.template.name : (ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '');
|
|
1769
|
-
if (tpl === 'onekite
|
|
1769
|
+
if (tpl === 'calendar-onekite') {
|
|
1770
1770
|
init('#onekite-calendar');
|
|
1771
1771
|
}
|
|
1772
1772
|
} catch (e) {}
|
|
@@ -1786,9 +1786,9 @@ function toDatetimeLocalValue(date) {
|
|
|
1786
1786
|
try {
|
|
1787
1787
|
if (!window.__oneKiteSocketBound && typeof socket !== 'undefined' && socket && typeof socket.on === 'function') {
|
|
1788
1788
|
window.__oneKiteSocketBound = true;
|
|
1789
|
-
socket.on('event:onekite
|
|
1789
|
+
socket.on('event:calendar-onekite.reservationUpdated', function () {
|
|
1790
1790
|
try {
|
|
1791
|
-
const cal = window.
|
|
1791
|
+
const cal = window.oneKiteCalendar;
|
|
1792
1792
|
scheduleRefetch(cal);
|
|
1793
1793
|
} catch (e) {}
|
|
1794
1794
|
});
|
|
@@ -21,17 +21,6 @@
|
|
|
21
21
|
The plugin's forum script auto-initialises on the calendar page via ajaxify.
|
|
22
22
|
-->
|
|
23
23
|
|
|
24
|
-
<script>
|
|
25
|
-
// Ultra-perf: load the forum JS only on the calendar page.
|
|
26
|
-
require(['forum/onekite-calendar'], function (mod) {
|
|
27
|
-
try {
|
|
28
|
-
if (mod && typeof mod.init === 'function') {
|
|
29
|
-
mod.init('#onekite-calendar');
|
|
30
|
-
}
|
|
31
|
-
} catch (e) {}
|
|
32
|
-
});
|
|
33
|
-
</script>
|
|
34
|
-
|
|
35
24
|
|
|
36
25
|
<style>
|
|
37
26
|
/* Make the custom "Évènement" button distinct from view buttons */
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|