nodebb-plugin-calendar-onekite 1.4.3 → 1.4.5
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/library.js +265 -193
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/static/js/admin.js +25 -12
package/library.js
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* - Events + booking items (with pickupLocation per item)
|
|
6
|
-
* - Multi-day reservations
|
|
7
|
-
* - Admin validation -> HelloAsso checkout link -> payment webhook -> mark paid
|
|
8
|
-
* - Admin planning view + "My reservations" page
|
|
9
|
-
* - Settings:
|
|
10
|
-
* - Settings key: "calendar-onekite"
|
|
11
|
-
* - Admin page: /admin/plugins/calendar-onekite
|
|
12
|
-
* - Admin API: /api/admin/plugins/calendar-onekite
|
|
4
|
+
* Calendar OneKite — NodeBB v4
|
|
13
5
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
6
|
+
* Key point: NodeBB v4 client helper `api.*` calls /api/v3/...
|
|
7
|
+
* So we expose both /api/... and /api/v3/... for the same handlers.
|
|
8
|
+
*
|
|
9
|
+
* Admin page:
|
|
10
|
+
* GET /admin/plugins/calendar-onekite
|
|
11
|
+
* Ajaxify JSON:
|
|
12
|
+
* GET /api/admin/plugins/calendar-onekite
|
|
13
|
+
* GET /api/v3/admin/plugins/calendar-onekite
|
|
14
|
+
* Save settings:
|
|
15
|
+
* PUT /api/admin/plugins/calendar-onekite
|
|
16
|
+
* PUT /api/v3/admin/plugins/calendar-onekite
|
|
23
17
|
*/
|
|
24
18
|
|
|
25
19
|
const db = require.main.require('./src/database');
|
|
@@ -46,7 +40,7 @@ async function nextReservationId() {
|
|
|
46
40
|
return String(rid);
|
|
47
41
|
}
|
|
48
42
|
|
|
49
|
-
//
|
|
43
|
+
// inclusive days between dates
|
|
50
44
|
function daysBetween(start, end) {
|
|
51
45
|
const d1 = new Date(start);
|
|
52
46
|
const d2 = new Date(end);
|
|
@@ -58,6 +52,20 @@ function daysBetween(start, end) {
|
|
|
58
52
|
return Math.max(1, diff + 1);
|
|
59
53
|
}
|
|
60
54
|
|
|
55
|
+
/* -----------------------------
|
|
56
|
+
* Route helpers (factorized)
|
|
57
|
+
* ---------------------------- */
|
|
58
|
+
|
|
59
|
+
function mountApiBoth(router, method, path, ...handlers) {
|
|
60
|
+
// path must start with /admin or /calendar etc (without /api prefix)
|
|
61
|
+
// Example: mountApiBoth(router, 'get', '/admin/calendar/pending', middleware.admin.checkPrivileges, handler)
|
|
62
|
+
const m = String(method).toLowerCase();
|
|
63
|
+
if (typeof router[m] !== 'function') throw new Error(`Unsupported router method: ${method}`);
|
|
64
|
+
|
|
65
|
+
router[m]('/api' + path, ...handlers);
|
|
66
|
+
router[m]('/api/v3' + path, ...handlers);
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
/* -----------------------------
|
|
62
70
|
* Events
|
|
63
71
|
* ---------------------------- */
|
|
@@ -125,7 +133,9 @@ async function updateEvent(eid, data) {
|
|
|
125
133
|
allDay: data.allDay !== undefined ? (data.allDay ? 1 : 0) : existing.allDay,
|
|
126
134
|
location: data.location !== undefined ? String(data.location) : existing.location,
|
|
127
135
|
visibility: data.visibility !== undefined ? String(data.visibility) : existing.visibility,
|
|
128
|
-
bookingEnabled: data.bookingEnabled !== undefined
|
|
136
|
+
bookingEnabled: data.bookingEnabled !== undefined
|
|
137
|
+
? (data.bookingEnabled ? 1 : 0)
|
|
138
|
+
: (existing.bookingEnabled || 0),
|
|
129
139
|
bookingItems: JSON.stringify(bookingItems),
|
|
130
140
|
updatedAt: String(Date.now()),
|
|
131
141
|
};
|
|
@@ -185,7 +195,6 @@ async function userCanCreate(uid) {
|
|
|
185
195
|
|
|
186
196
|
const userGroupsArr = await user.getUserGroups([uid]);
|
|
187
197
|
const groups = (userGroupsArr[0] || []).map(g => (g.name || '').toLowerCase());
|
|
188
|
-
|
|
189
198
|
return groups.some(g => allowedSet.has(g));
|
|
190
199
|
}
|
|
191
200
|
|
|
@@ -193,7 +202,6 @@ async function userCanBook(uid) {
|
|
|
193
202
|
if (!uid || uid === 0) return false;
|
|
194
203
|
|
|
195
204
|
const settings = await Settings.get(SETTINGS_KEY);
|
|
196
|
-
// if not configured -> allow any logged-in user
|
|
197
205
|
if (!settings || !settings.allowedBookingGroups) return true;
|
|
198
206
|
|
|
199
207
|
const allowedSet = new Set(
|
|
@@ -206,7 +214,6 @@ async function userCanBook(uid) {
|
|
|
206
214
|
|
|
207
215
|
const userGroupsArr = await user.getUserGroups([uid]);
|
|
208
216
|
const groups = (userGroupsArr[0] || []).map(g => (g.name || '').toLowerCase());
|
|
209
|
-
|
|
210
217
|
return groups.some(g => allowedSet.has(g));
|
|
211
218
|
}
|
|
212
219
|
|
|
@@ -220,7 +227,7 @@ async function setRsvp(eid, uid, status) {
|
|
|
220
227
|
if (!event) throw new Error('Event not found');
|
|
221
228
|
|
|
222
229
|
const parseList = (str) => {
|
|
223
|
-
try { return JSON.parse(str || '[]'); } catch
|
|
230
|
+
try { return JSON.parse(str || '[]'); } catch { return []; }
|
|
224
231
|
};
|
|
225
232
|
|
|
226
233
|
let yes = parseList(event.rsvpYes);
|
|
@@ -293,6 +300,173 @@ async function renderAdminPage(req, res) {
|
|
|
293
300
|
}
|
|
294
301
|
}
|
|
295
302
|
|
|
303
|
+
/* -----------------------------
|
|
304
|
+
* Admin handlers (shared)
|
|
305
|
+
* ---------------------------- */
|
|
306
|
+
|
|
307
|
+
async function adminGetPending(req, res) {
|
|
308
|
+
try {
|
|
309
|
+
const result = [];
|
|
310
|
+
const eids = await db.getSortedSetRange(EVENTS_SET_KEY, 0, -1);
|
|
311
|
+
|
|
312
|
+
for (const eid of eids) {
|
|
313
|
+
const event = await getEvent(eid);
|
|
314
|
+
if (!event) continue;
|
|
315
|
+
|
|
316
|
+
const reservations = JSON.parse(event.reservations || '[]');
|
|
317
|
+
const pending = reservations.filter(r => r.status === 'pending_admin');
|
|
318
|
+
if (pending.length) result.push({ event, reservations: pending });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
res.json(result);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
res.status(500).json({ error: err.message });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function adminValidateReservation(req, res) {
|
|
328
|
+
try {
|
|
329
|
+
const rid = req.params.rid;
|
|
330
|
+
const eids = await db.getSortedSetRange(EVENTS_SET_KEY, 0, -1);
|
|
331
|
+
|
|
332
|
+
let targetEvent = null;
|
|
333
|
+
let reservation = null;
|
|
334
|
+
|
|
335
|
+
for (const eid of eids) {
|
|
336
|
+
const event = await getEvent(eid);
|
|
337
|
+
if (!event) continue;
|
|
338
|
+
|
|
339
|
+
const reservations = JSON.parse(event.reservations || '[]');
|
|
340
|
+
const r = reservations.find(rr => rr.rid === rid);
|
|
341
|
+
if (r) {
|
|
342
|
+
targetEvent = event;
|
|
343
|
+
reservation = r;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!reservation) return res.status(404).json({ error: 'Réservation introuvable' });
|
|
349
|
+
if (reservation.status !== 'pending_admin') return res.status(400).json({ error: 'Réservation déjà traitée' });
|
|
350
|
+
|
|
351
|
+
reservation.status = 'awaiting_payment';
|
|
352
|
+
|
|
353
|
+
const resList = JSON.parse(targetEvent.reservations || '[]');
|
|
354
|
+
const idx = resList.findIndex(r => r.rid === rid);
|
|
355
|
+
resList[idx] = reservation;
|
|
356
|
+
targetEvent.reservations = JSON.stringify(resList);
|
|
357
|
+
|
|
358
|
+
await db.setObject(getEventKey(targetEvent.eid), targetEvent);
|
|
359
|
+
|
|
360
|
+
const amount = computePrice(targetEvent, reservation);
|
|
361
|
+
const checkoutUrl = await helloAsso.createHelloAssoCheckoutIntent({
|
|
362
|
+
eid: reservation.eid,
|
|
363
|
+
rid,
|
|
364
|
+
uid: reservation.uid,
|
|
365
|
+
itemId: reservation.itemId,
|
|
366
|
+
quantity: reservation.quantity,
|
|
367
|
+
amount,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
await emailer.send('calendar-reservation-approved', reservation.uid, {
|
|
372
|
+
subject: 'Votre réservation a été validée',
|
|
373
|
+
eventTitle: targetEvent.title,
|
|
374
|
+
itemName: reservation.itemId,
|
|
375
|
+
quantity: reservation.quantity,
|
|
376
|
+
checkoutUrl,
|
|
377
|
+
pickupLocation: reservation.pickupLocation || 'Non précisé',
|
|
378
|
+
dateStart: reservation.dateStart,
|
|
379
|
+
dateEnd: reservation.dateEnd,
|
|
380
|
+
days: reservation.days || 1,
|
|
381
|
+
});
|
|
382
|
+
} catch (e) {
|
|
383
|
+
console.warn('[calendar-onekite] email reservation-approved error:', e.message);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
res.json({ success: true, checkoutUrl });
|
|
387
|
+
} catch (err) {
|
|
388
|
+
res.status(500).json({ error: err.message });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function adminCancelReservation(req, res) {
|
|
393
|
+
try {
|
|
394
|
+
const rid = req.params.rid;
|
|
395
|
+
const eids = await db.getSortedSetRange(EVENTS_SET_KEY, 0, -1);
|
|
396
|
+
|
|
397
|
+
let found = false;
|
|
398
|
+
|
|
399
|
+
for (const eid of eids) {
|
|
400
|
+
const event = await getEvent(eid);
|
|
401
|
+
if (!event) continue;
|
|
402
|
+
|
|
403
|
+
const reservations = JSON.parse(event.reservations || '[]');
|
|
404
|
+
const rIndex = reservations.findIndex(rr => rr.rid === rid);
|
|
405
|
+
if (rIndex !== -1) {
|
|
406
|
+
reservations[rIndex].status = 'cancelled';
|
|
407
|
+
event.reservations = JSON.stringify(reservations);
|
|
408
|
+
await db.setObject(getEventKey(event.eid), event);
|
|
409
|
+
found = true;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!found) return res.status(404).json({ error: 'Réservation introuvable' });
|
|
415
|
+
res.json({ success: true });
|
|
416
|
+
} catch (err) {
|
|
417
|
+
res.status(500).json({ error: err.message });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function adminGetPlanning(req, res) {
|
|
422
|
+
try {
|
|
423
|
+
const now = new Date();
|
|
424
|
+
const eids = await db.getSortedSetRangeByScore(EVENTS_SET_KEY, 0, -1, now.getTime(), '+inf');
|
|
425
|
+
const rows = [];
|
|
426
|
+
|
|
427
|
+
for (const eid of eids) {
|
|
428
|
+
const event = await getEvent(eid);
|
|
429
|
+
if (!event) continue;
|
|
430
|
+
|
|
431
|
+
const items = JSON.parse(event.bookingItems || '[]');
|
|
432
|
+
const reservations = JSON.parse(event.reservations || '[]');
|
|
433
|
+
|
|
434
|
+
reservations
|
|
435
|
+
.filter(r => r.status === 'pending_admin' || r.status === 'awaiting_payment' || r.status === 'paid')
|
|
436
|
+
.forEach(r => {
|
|
437
|
+
const item = items.find(i => i.id === r.itemId);
|
|
438
|
+
rows.push({
|
|
439
|
+
eid: event.eid,
|
|
440
|
+
eventTitle: event.title,
|
|
441
|
+
itemId: r.itemId,
|
|
442
|
+
itemName: item ? item.name : r.itemId,
|
|
443
|
+
uid: r.uid,
|
|
444
|
+
quantity: r.quantity,
|
|
445
|
+
dateStart: r.dateStart,
|
|
446
|
+
dateEnd: r.dateEnd,
|
|
447
|
+
days: r.days || daysBetween(r.dateStart, r.dateEnd),
|
|
448
|
+
status: r.status,
|
|
449
|
+
pickupLocation: r.pickupLocation || 'Non précisé',
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
rows.sort((a, b) => new Date(a.dateStart) - new Date(b.dateStart));
|
|
455
|
+
res.json(rows);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
res.status(500).json({ error: err.message });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function adminSaveSettings(req, res) {
|
|
462
|
+
try {
|
|
463
|
+
await Settings.set(SETTINGS_KEY, req.body);
|
|
464
|
+
res.json({ status: 'ok' });
|
|
465
|
+
} catch (err) {
|
|
466
|
+
res.status(500).json({ error: err.message });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
296
470
|
/* -----------------------------
|
|
297
471
|
* Plugin init
|
|
298
472
|
* ---------------------------- */
|
|
@@ -311,6 +485,7 @@ Plugin.init = async function (params) {
|
|
|
311
485
|
// Admin planning page
|
|
312
486
|
router.get('/admin/calendar/planning', middleware.admin.buildHeader, renderPlanningPage);
|
|
313
487
|
router.get('/api/admin/calendar/planning/page', renderPlanningPage);
|
|
488
|
+
router.get('/api/v3/admin/calendar/planning/page', renderPlanningPage);
|
|
314
489
|
|
|
315
490
|
/* -----------------------------
|
|
316
491
|
* Events API
|
|
@@ -327,6 +502,18 @@ Plugin.init = async function (params) {
|
|
|
327
502
|
}
|
|
328
503
|
});
|
|
329
504
|
|
|
505
|
+
// Optional v3 mirror for future usage with api.get on front pages
|
|
506
|
+
router.get('/api/v3/calendar/events', async (req, res) => {
|
|
507
|
+
try {
|
|
508
|
+
const { start, end } = req.query;
|
|
509
|
+
if (!start || !end) return res.status(400).json({ error: 'Missing start/end' });
|
|
510
|
+
const events = await getEventsBetween(start, end);
|
|
511
|
+
res.json(events);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
res.status(500).json({ error: err.message });
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
330
517
|
router.post('/api/calendar/event', middleware.ensureLoggedIn, async (req, res) => {
|
|
331
518
|
try {
|
|
332
519
|
const uid = req.user?.uid || 0;
|
|
@@ -392,7 +579,7 @@ Plugin.init = async function (params) {
|
|
|
392
579
|
});
|
|
393
580
|
|
|
394
581
|
/* -----------------------------
|
|
395
|
-
* Client permissions
|
|
582
|
+
* Client permissions
|
|
396
583
|
* ---------------------------- */
|
|
397
584
|
|
|
398
585
|
router.get('/api/calendar/permissions/create', async (req, res) => {
|
|
@@ -408,7 +595,7 @@ Plugin.init = async function (params) {
|
|
|
408
595
|
});
|
|
409
596
|
|
|
410
597
|
/* -----------------------------
|
|
411
|
-
*
|
|
598
|
+
* Booking (multi-day)
|
|
412
599
|
* ---------------------------- */
|
|
413
600
|
|
|
414
601
|
router.post('/api/calendar/event/:eid/book', middleware.ensureLoggedIn, async (req, res) => {
|
|
@@ -440,7 +627,6 @@ Plugin.init = async function (params) {
|
|
|
440
627
|
|
|
441
628
|
const allRes = JSON.parse(event.reservations || '[]');
|
|
442
629
|
|
|
443
|
-
// Stock check: sum overlapping reservations (pending_admin/awaiting_payment/paid) for the same item
|
|
444
630
|
const overlapping = allRes.filter(r => {
|
|
445
631
|
if (r.itemId !== itemId) return false;
|
|
446
632
|
if (r.status === 'cancelled') return false;
|
|
@@ -482,7 +668,6 @@ Plugin.init = async function (params) {
|
|
|
482
668
|
|
|
483
669
|
await db.setObject(getEventKey(eid), event);
|
|
484
670
|
|
|
485
|
-
// Email: request created
|
|
486
671
|
try {
|
|
487
672
|
await emailer.send('calendar-reservation-created', uid, {
|
|
488
673
|
subject: 'Votre demande de réservation a été envoyée',
|
|
@@ -510,133 +695,7 @@ Plugin.init = async function (params) {
|
|
|
510
695
|
});
|
|
511
696
|
|
|
512
697
|
/* -----------------------------
|
|
513
|
-
*
|
|
514
|
-
* ---------------------------- */
|
|
515
|
-
|
|
516
|
-
router.get('/api/admin/calendar/pending', middleware.admin.checkPrivileges, async (req, res) => {
|
|
517
|
-
try {
|
|
518
|
-
const result = [];
|
|
519
|
-
const eids = await db.getSortedSetRange(EVENTS_SET_KEY, 0, -1);
|
|
520
|
-
|
|
521
|
-
for (const eid of eids) {
|
|
522
|
-
const event = await getEvent(eid);
|
|
523
|
-
if (!event) continue;
|
|
524
|
-
|
|
525
|
-
const reservations = JSON.parse(event.reservations || '[]');
|
|
526
|
-
const pending = reservations.filter(r => r.status === 'pending_admin');
|
|
527
|
-
if (pending.length) result.push({ event, reservations: pending });
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
res.json(result);
|
|
531
|
-
} catch (err) {
|
|
532
|
-
res.status(500).json({ error: err.message });
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
/* -----------------------------
|
|
537
|
-
* Admin: validate reservation -> awaiting_payment + HelloAsso checkout
|
|
538
|
-
* ---------------------------- */
|
|
539
|
-
|
|
540
|
-
router.post('/api/admin/calendar/reservation/:rid/validate', middleware.admin.checkPrivileges, async (req, res) => {
|
|
541
|
-
try {
|
|
542
|
-
const rid = req.params.rid;
|
|
543
|
-
const eids = await db.getSortedSetRange(EVENTS_SET_KEY, 0, -1);
|
|
544
|
-
|
|
545
|
-
let targetEvent = null;
|
|
546
|
-
let reservation = null;
|
|
547
|
-
|
|
548
|
-
for (const eid of eids) {
|
|
549
|
-
const event = await getEvent(eid);
|
|
550
|
-
if (!event) continue;
|
|
551
|
-
|
|
552
|
-
const reservations = JSON.parse(event.reservations || '[]');
|
|
553
|
-
const r = reservations.find(rr => rr.rid === rid);
|
|
554
|
-
if (r) {
|
|
555
|
-
targetEvent = event;
|
|
556
|
-
reservation = r;
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (!reservation) return res.status(404).json({ error: 'Réservation introuvable' });
|
|
562
|
-
if (reservation.status !== 'pending_admin') return res.status(400).json({ error: 'Réservation déjà traitée' });
|
|
563
|
-
|
|
564
|
-
reservation.status = 'awaiting_payment';
|
|
565
|
-
|
|
566
|
-
const resList = JSON.parse(targetEvent.reservations || '[]');
|
|
567
|
-
const idx = resList.findIndex(r => r.rid === rid);
|
|
568
|
-
resList[idx] = reservation;
|
|
569
|
-
targetEvent.reservations = JSON.stringify(resList);
|
|
570
|
-
|
|
571
|
-
await db.setObject(getEventKey(targetEvent.eid), targetEvent);
|
|
572
|
-
|
|
573
|
-
const amount = computePrice(targetEvent, reservation);
|
|
574
|
-
const checkoutUrl = await helloAsso.createHelloAssoCheckoutIntent({
|
|
575
|
-
eid: reservation.eid,
|
|
576
|
-
rid,
|
|
577
|
-
uid: reservation.uid,
|
|
578
|
-
itemId: reservation.itemId,
|
|
579
|
-
quantity: reservation.quantity,
|
|
580
|
-
amount,
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
try {
|
|
584
|
-
await emailer.send('calendar-reservation-approved', reservation.uid, {
|
|
585
|
-
subject: 'Votre réservation a été validée',
|
|
586
|
-
eventTitle: targetEvent.title,
|
|
587
|
-
itemName: reservation.itemId,
|
|
588
|
-
quantity: reservation.quantity,
|
|
589
|
-
checkoutUrl,
|
|
590
|
-
pickupLocation: reservation.pickupLocation || 'Non précisé',
|
|
591
|
-
dateStart: reservation.dateStart,
|
|
592
|
-
dateEnd: reservation.dateEnd,
|
|
593
|
-
days: reservation.days || 1,
|
|
594
|
-
});
|
|
595
|
-
} catch (e) {
|
|
596
|
-
console.warn('[calendar-onekite] email reservation-approved error:', e.message);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
res.json({ success: true, checkoutUrl });
|
|
600
|
-
} catch (err) {
|
|
601
|
-
res.status(500).json({ error: err.message });
|
|
602
|
-
}
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
/* -----------------------------
|
|
606
|
-
* Admin: cancel reservation
|
|
607
|
-
* ---------------------------- */
|
|
608
|
-
|
|
609
|
-
router.post('/api/admin/calendar/reservation/:rid/cancel', middleware.admin.checkPrivileges, async (req, res) => {
|
|
610
|
-
try {
|
|
611
|
-
const rid = req.params.rid;
|
|
612
|
-
const eids = await db.getSortedSetRange(EVENTS_SET_KEY, 0, -1);
|
|
613
|
-
|
|
614
|
-
let found = false;
|
|
615
|
-
|
|
616
|
-
for (const eid of eids) {
|
|
617
|
-
const event = await getEvent(eid);
|
|
618
|
-
if (!event) continue;
|
|
619
|
-
|
|
620
|
-
const reservations = JSON.parse(event.reservations || '[]');
|
|
621
|
-
const rIndex = reservations.findIndex(rr => rr.rid === rid);
|
|
622
|
-
if (rIndex !== -1) {
|
|
623
|
-
reservations[rIndex].status = 'cancelled';
|
|
624
|
-
event.reservations = JSON.stringify(reservations);
|
|
625
|
-
await db.setObject(getEventKey(event.eid), event);
|
|
626
|
-
found = true;
|
|
627
|
-
break;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (!found) return res.status(404).json({ error: 'Réservation introuvable' });
|
|
632
|
-
res.json({ success: true });
|
|
633
|
-
} catch (err) {
|
|
634
|
-
res.status(500).json({ error: err.message });
|
|
635
|
-
}
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
/* -----------------------------
|
|
639
|
-
* HelloAsso webhook (marks reservation paid)
|
|
698
|
+
* HelloAsso webhook
|
|
640
699
|
* ---------------------------- */
|
|
641
700
|
|
|
642
701
|
router.post('/api/calendar/helloasso/webhook', async (req, res) => {
|
|
@@ -651,7 +710,6 @@ Plugin.init = async function (params) {
|
|
|
651
710
|
const custom = order.customFields || {};
|
|
652
711
|
const eid = String(custom.eid || '');
|
|
653
712
|
const rid = String(custom.rid || '');
|
|
654
|
-
|
|
655
713
|
if (!eid || !rid) return res.status(400).json({ error: 'Missing eid/rid in customFields' });
|
|
656
714
|
|
|
657
715
|
const event = await getEvent(eid);
|
|
@@ -664,7 +722,6 @@ Plugin.init = async function (params) {
|
|
|
664
722
|
const reservation = reservations[rIndex];
|
|
665
723
|
if (reservation.status === 'paid') return res.json({ ok: true });
|
|
666
724
|
|
|
667
|
-
// Update optional aggregate reserved count
|
|
668
725
|
const items = JSON.parse(event.bookingItems || '[]');
|
|
669
726
|
const itemIndex = items.findIndex(i => i.id === reservation.itemId);
|
|
670
727
|
if (itemIndex !== -1) {
|
|
@@ -705,24 +762,43 @@ Plugin.init = async function (params) {
|
|
|
705
762
|
});
|
|
706
763
|
|
|
707
764
|
/* -----------------------------
|
|
708
|
-
* Admin plugin page + settings API
|
|
709
|
-
* IMPORTANT: keep these routes EXACTLY aligned with your admin.js
|
|
765
|
+
* Admin plugin page + settings API (factorized v3 mounting)
|
|
710
766
|
* ---------------------------- */
|
|
711
767
|
|
|
712
768
|
router.get('/admin/plugins/calendar-onekite', middleware.admin.buildHeader, renderAdminPage);
|
|
713
|
-
router.get('/api/admin/plugins/calendar-onekite', renderAdminPage);
|
|
714
769
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
770
|
+
// Ajaxify GET JSON (must exist under /api/v3 on NodeBB v4)
|
|
771
|
+
mountApiBoth(router, 'get', '/admin/plugins/calendar-onekite', middleware.admin.checkPrivileges, renderAdminPage);
|
|
772
|
+
|
|
773
|
+
// Save settings (PUT)
|
|
774
|
+
mountApiBoth(router, 'put', '/admin/plugins/calendar-onekite', middleware.admin.checkPrivileges, adminSaveSettings);
|
|
775
|
+
|
|
776
|
+
/* -----------------------------
|
|
777
|
+
* Admin endpoints (pending/validate/cancel/planning) — both /api and /api/v3
|
|
778
|
+
* ---------------------------- */
|
|
779
|
+
|
|
780
|
+
mountApiBoth(router, 'get', '/admin/calendar/pending', middleware.admin.checkPrivileges, adminGetPending);
|
|
781
|
+
|
|
782
|
+
mountApiBoth(
|
|
783
|
+
router,
|
|
784
|
+
'post',
|
|
785
|
+
'/admin/calendar/reservation/:rid/validate',
|
|
786
|
+
middleware.admin.checkPrivileges,
|
|
787
|
+
adminValidateReservation
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
mountApiBoth(
|
|
791
|
+
router,
|
|
792
|
+
'post',
|
|
793
|
+
'/admin/calendar/reservation/:rid/cancel',
|
|
794
|
+
middleware.admin.checkPrivileges,
|
|
795
|
+
adminCancelReservation
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
mountApiBoth(router, 'get', '/admin/calendar/planning', middleware.admin.checkPrivileges, adminGetPlanning);
|
|
723
799
|
|
|
724
800
|
/* -----------------------------
|
|
725
|
-
* My reservations API
|
|
801
|
+
* My reservations API (optional v3 mirror for api.get on front pages)
|
|
726
802
|
* ---------------------------- */
|
|
727
803
|
|
|
728
804
|
router.get('/api/calendar/my-reservations', middleware.ensureLoggedIn, async (req, res) => {
|
|
@@ -761,15 +837,14 @@ Plugin.init = async function (params) {
|
|
|
761
837
|
}
|
|
762
838
|
});
|
|
763
839
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
* ---------------------------- */
|
|
767
|
-
|
|
768
|
-
router.get('/api/admin/calendar/planning', middleware.admin.checkPrivileges, async (req, res) => {
|
|
840
|
+
router.get('/api/v3/calendar/my-reservations', middleware.ensureLoggedIn, async (req, res) => {
|
|
841
|
+
// same handler, duplicated minimally for clarity
|
|
769
842
|
try {
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
843
|
+
const uid = String(req.user?.uid || 0);
|
|
844
|
+
if (!uid || uid === '0') return res.status(403).json({ error: 'Non connecté' });
|
|
845
|
+
|
|
846
|
+
const eids = await db.getSortedSetRange(EVENTS_SET_KEY, 0, -1);
|
|
847
|
+
const result = [];
|
|
773
848
|
|
|
774
849
|
for (const eid of eids) {
|
|
775
850
|
const event = await getEvent(eid);
|
|
@@ -779,27 +854,21 @@ Plugin.init = async function (params) {
|
|
|
779
854
|
const reservations = JSON.parse(event.reservations || '[]');
|
|
780
855
|
|
|
781
856
|
reservations
|
|
782
|
-
.filter(r => r.
|
|
857
|
+
.filter(r => String(r.uid) === uid)
|
|
783
858
|
.forEach(r => {
|
|
784
859
|
const item = items.find(i => i.id === r.itemId);
|
|
785
|
-
|
|
786
|
-
|
|
860
|
+
result.push({
|
|
861
|
+
...r,
|
|
787
862
|
eventTitle: event.title,
|
|
788
|
-
|
|
863
|
+
eventStart: event.start,
|
|
864
|
+
eventEnd: event.end,
|
|
789
865
|
itemName: item ? item.name : r.itemId,
|
|
790
|
-
uid: r.uid,
|
|
791
|
-
quantity: r.quantity,
|
|
792
|
-
dateStart: r.dateStart,
|
|
793
|
-
dateEnd: r.dateEnd,
|
|
794
|
-
days: r.days || daysBetween(r.dateStart, r.dateEnd),
|
|
795
|
-
status: r.status,
|
|
796
|
-
pickupLocation: r.pickupLocation || 'Non précisé',
|
|
797
866
|
});
|
|
798
867
|
});
|
|
799
868
|
}
|
|
800
869
|
|
|
801
|
-
|
|
802
|
-
res.json(
|
|
870
|
+
result.sort((a, b) => new Date(a.dateStart) - new Date(b.dateStart));
|
|
871
|
+
res.json(result);
|
|
803
872
|
} catch (err) {
|
|
804
873
|
res.status(500).json({ error: err.message });
|
|
805
874
|
}
|
|
@@ -812,7 +881,6 @@ Plugin.init = async function (params) {
|
|
|
812
881
|
|
|
813
882
|
Plugin.addAdminNavigation = function (header) {
|
|
814
883
|
header.plugins.push({
|
|
815
|
-
// NodeBB ACP will resolve this under /admin
|
|
816
884
|
route: '/plugins/calendar-onekite',
|
|
817
885
|
icon: 'fa fa-calendar',
|
|
818
886
|
name: 'Calendar OneKite',
|
|
@@ -821,7 +889,7 @@ Plugin.addAdminNavigation = function (header) {
|
|
|
821
889
|
};
|
|
822
890
|
|
|
823
891
|
/* -----------------------------
|
|
824
|
-
*
|
|
892
|
+
* Widgets
|
|
825
893
|
* ---------------------------- */
|
|
826
894
|
|
|
827
895
|
Plugin.defineWidgets = async function (widgets) {
|
|
@@ -834,13 +902,17 @@ Plugin.defineWidgets = async function (widgets) {
|
|
|
834
902
|
return widgets;
|
|
835
903
|
};
|
|
836
904
|
|
|
905
|
+
// Supports both promise-style (v4) and callback-style (older)
|
|
837
906
|
Plugin.renderUpcomingWidget = async function (widget, callback) {
|
|
838
907
|
try {
|
|
839
908
|
const settings = (await Settings.get(SETTINGS_KEY)) || {};
|
|
840
|
-
const limit = Number(
|
|
909
|
+
const limit = Number(widget?.data?.limit || settings.limit || 5);
|
|
910
|
+
|
|
841
911
|
const events = await getUpcomingEvents(limit);
|
|
842
912
|
const html = await appRef.renderAsync('widgets/calendar-upcoming', { events });
|
|
913
|
+
|
|
843
914
|
widget.html = html;
|
|
915
|
+
|
|
844
916
|
if (typeof callback === 'function') {
|
|
845
917
|
return callback(null, widget);
|
|
846
918
|
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/static/js/admin.js
CHANGED
|
@@ -23,18 +23,21 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/admin', [
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
AdminCalendar.bindSettingsSave = function () {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
// évite les doubles binds quand ajaxify recharge la page
|
|
27
|
+
$('#calendar-onekite-save')
|
|
28
|
+
.off('click.calendarOnekite')
|
|
29
|
+
.on('click.calendarOnekite', function () {
|
|
30
|
+
const settings = {
|
|
31
|
+
allowedGroups: $('#calendar-onekite-groups').val(),
|
|
32
|
+
allowedBookingGroups: $('#calendar-onekite-book-groups').val(),
|
|
33
|
+
limit: $('#calendar-onekite-widget-limit').val(),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ✅ endpoint standard (doit exister côté serveur)
|
|
37
|
+
api.put('/admin/plugins/calendar-onekite', settings)
|
|
38
|
+
.then(() => alerts.success('Paramètres enregistrés'))
|
|
39
|
+
.catch(err => alerts.error(err?.message || err));
|
|
40
|
+
});
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
AdminCalendar.loadPending = function () {
|
|
@@ -155,5 +158,15 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/admin', [
|
|
|
155
158
|
.catch(err => alerts.error(err?.message || err));
|
|
156
159
|
};
|
|
157
160
|
|
|
161
|
+
// ✅ essentiel : relance l’init à chaque navigation ACP ajaxify
|
|
162
|
+
$(window).on('action:ajaxify.end', function () {
|
|
163
|
+
AdminCalendar.init();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// et au chargement initial
|
|
167
|
+
$(function () {
|
|
168
|
+
AdminCalendar.init();
|
|
169
|
+
});
|
|
170
|
+
|
|
158
171
|
return AdminCalendar;
|
|
159
172
|
});
|