nodebb-plugin-calendar-onekite 1.4.4 → 1.4.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/library.js CHANGED
@@ -1,25 +1,19 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * NodeBB v4 plugin: Calendar OneKite
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
- * Templates expected:
15
- * - templates/calendar.tpl
16
- * - templates/calendar-my-reservations.tpl
17
- * - templates/admin/plugins/calendar-onekite.tpl
18
- * - templates/admin/calendar-planning.tpl
19
- * - templates/widgets/calendar-upcoming.tpl
20
- * - templates/emails/calendar-reservation-created.tpl
21
- * - templates/emails/calendar-reservation-approved.tpl
22
- * - templates/emails/calendar-payment-confirmed.tpl
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
- // number of days between two yyyy-mm-dd dates (inclusive)
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 ? (data.bookingEnabled ? 1 : 0) : (existing.bookingEnabled || 0),
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 (e) { return []; }
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 endpoints
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
- * Multi-day reservation request
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
- * Admin: pending list
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
- router.put('/api/admin/plugins/calendar-onekite', middleware.admin.checkPrivileges, async (req, res) => {
716
- try {
717
- await Settings.set(SETTINGS_KEY, req.body);
718
- res.json({ status: 'ok' });
719
- } catch (err) {
720
- res.status(500).json({ error: err.message });
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
- * Admin planning API (future reservations)
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 now = new Date();
771
- const eids = await db.getSortedSetRangeByScore(EVENTS_SET_KEY, 0, -1, now.getTime(), '+inf');
772
- const rows = [];
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.status === 'pending_admin' || r.status === 'awaiting_payment' || r.status === 'paid')
857
+ .filter(r => String(r.uid) === uid)
783
858
  .forEach(r => {
784
859
  const item = items.find(i => i.id === r.itemId);
785
- rows.push({
786
- eid: event.eid,
860
+ result.push({
861
+ ...r,
787
862
  eventTitle: event.title,
788
- itemId: r.itemId,
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
- rows.sort((a, b) => new Date(a.dateStart) - new Date(b.dateStart));
802
- res.json(rows);
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
- * Widget
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((widget && widget.data && widget.data.limit) || settings.limit || 5);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/plugin.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "Calendar Onekite",
4
4
  "description": "Calendrier + réservation de matériel + validation admin + paiement HelloAsso pour NodeBB v4",
5
5
  "url": "",
6
- "version": "1.4.4",
6
+ "version": "1.4.6",
7
7
  "library": "./library.js",
8
8
  "staticDirs": {
9
9
  "static": "static"
@@ -1,301 +1,42 @@
1
- 'use strict';
2
-
3
- $(document).ready(function () {
4
- if (!$('#calendar').length) return;
5
-
6
- const CalendarPage = {
7
-
8
- canBook: false,
9
-
10
- init: function () {
11
- this.prepareModals();
12
- this.checkPermissions();
13
- this.initCalendar();
14
- },
15
-
16
- checkPermissions: function () {
17
- $.get('/api/calendar/permissions/book').then(res => {
18
- CalendarPage.canBook = !!(res && res.allow);
19
- });
20
- },
21
-
22
- prepareModals: function () {
23
- $('.calendar-modal').hide();
24
- $('.calendar-modal .calendar-close').on('click', function () {
25
- $(this).closest('.calendar-modal').fadeOut();
26
- });
27
- },
28
-
29
- initCalendar: function () {
30
- const calendarEl = document.getElementById('calendar');
31
-
32
- const calendar = new FullCalendar.Calendar(calendarEl, {
33
- themeSystem: 'bootstrap5',
34
- initialView: 'dayGridMonth',
35
- locale: 'fr',
36
- height: 'auto',
37
- selectable: true,
38
- headerToolbar: {
39
- left: 'prev,next today',
40
- center: 'title',
41
- right: 'dayGridMonth,timeGridWeek,listWeek'
42
- },
43
-
44
- events: (info, success, fail) => {
45
- $.get('/api/calendar/events', { start: info.startStr, end: info.endStr })
46
- .then(events => {
47
- success(events.map(ev => ({
48
- id: ev.eid,
49
- title: ev.title,
50
- start: ev.start,
51
- end: ev.end,
52
- allDay: ev.allDay == 1,
53
- extendedProps: ev
54
- })));
55
- })
56
- .catch(fail);
57
- },
58
-
59
- dateClick: info => {
60
- CalendarPage.openCreateEventModal(info.dateStr);
61
- },
62
-
63
- eventClick: info => {
64
- CalendarPage.openEventModal(info.event.extendedProps);
65
- }
66
- });
67
-
68
- calendar.render();
69
-
70
- // bouton nouvel event
71
- $.get('/api/calendar/permissions/create').then(res => {
72
- if (!res || !res.allow) {
73
- $('#calendar-new-event').hide();
74
- } else {
75
- $('#calendar-new-event').on('click', () => {
76
- CalendarPage.openCreateEventModal();
77
- });
78
- }
79
- });
80
- },
81
-
82
- /* -------- MODAL EVENT -------- */
83
-
84
- openCreateEventModal: function (dateStr) {
85
- const modal = $('#calendar-event-modal');
86
-
87
- $('#event-modal-title').text('Créer un événement');
88
- $('#event-title').val('');
89
- $('#event-description').val('');
90
- const base = dateStr || new Date().toISOString().slice(0, 10);
91
- $('#event-start').val(base + 'T09:00');
92
- $('#event-end').val(base + 'T11:00');
93
- $('#event-allDay').prop('checked', false);
94
- $('#event-location').val('');
95
- $('#event-bookingEnabled').prop('checked', false);
96
- $('#booking-items').empty();
97
-
98
- $('#event-save').off('click').on('click', () => {
99
- CalendarPage.saveEvent();
100
- });
101
-
102
- $('#event-delete').hide();
103
- $('#event-reserve').hide();
104
-
105
- $('#event-add-item').off('click').on('click', () => {
106
- CalendarPage.addBookingItemRow();
107
- });
108
-
109
- modal.fadeIn();
110
- },
111
-
112
- openEventModal: function (eventData) {
113
- const modal = $('#calendar-event-modal');
114
-
115
- $('#event-modal-title').text('Événement : ' + eventData.title);
116
- $('#event-title').val(eventData.title);
117
- $('#event-description').val(eventData.description || '');
118
- $('#event-start').val(eventData.start.replace('Z', ''));
119
- $('#event-end').val(eventData.end.replace('Z', ''));
120
- $('#event-allDay').prop('checked', eventData.allDay == 1);
121
- $('#event-location').val(eventData.location || '');
122
- $('#event-bookingEnabled').prop('checked', eventData.bookingEnabled == 1);
123
-
124
- CalendarPage.renderBookingItems(eventData.bookingItems || []);
125
-
126
- $('#event-add-item').off('click').on('click', () => {
127
- CalendarPage.addBookingItemRow();
128
- });
129
-
130
- $('#event-save').off('click').on('click', () => {
131
- CalendarPage.updateEvent(eventData.eid);
132
- });
133
-
134
- $('#event-delete').show().off('click').on('click', () => {
135
- bootbox.confirm('Supprimer cet événement ?', ok => {
136
- if (!ok) return;
137
- $.ajax({ url: '/api/calendar/event/' + eventData.eid, method: 'DELETE' })
138
- .then(() => location.reload());
139
- });
140
- });
141
-
142
- if (CalendarPage.canBook && Number(eventData.bookingEnabled)) {
143
- $('#event-reserve').show().off('click').on('click', () => {
144
- CalendarPage.openReserveModal(eventData.eid);
145
- });
146
- } else {
147
- $('#event-reserve').hide();
148
- }
149
-
150
- modal.fadeIn();
151
- },
152
-
153
- saveEvent: function () {
154
- const data = CalendarPage.collectEventForm();
155
- $.ajax({
156
- url: '/api/calendar/event',
157
- method: 'POST',
158
- contentType: 'application/json',
159
- data: JSON.stringify(data),
160
- }).then(() => location.reload())
161
- .catch(err => app.alertError(err.responseJSON?.error || err.message));
162
- },
163
-
164
- updateEvent: function (eid) {
165
- const data = CalendarPage.collectEventForm();
166
- $.ajax({
167
- url: '/api/calendar/event/' + eid,
168
- method: 'PUT',
169
- contentType: 'application/json',
170
- data: JSON.stringify(data),
171
- }).then(() => location.reload())
172
- .catch(err => app.alertError(err.responseJSON?.error || err.message));
173
- },
174
-
175
- collectEventForm: function () {
176
- const items = [];
177
- $('#booking-items .booking-item-row').each(function () {
178
- const row = $(this);
179
- const total = Number(row.find('.item-total').val() || 0);
180
- if (!total) return;
181
-
182
- items.push({
183
- id: row.find('.item-id').val(),
184
- name: row.find('.item-name').val(),
185
- total,
186
- reserved: Number(row.find('.item-reserved').val() || 0),
187
- reservedTemp: Number(row.find('.item-reservedTemp').val() || 0),
188
- price: Number(row.find('.item-price').val() || 0),
189
- pickupLocation: row.find('.item-pickup').val() || ''
190
- });
191
- });
192
-
193
- return {
194
- title: $('#event-title').val(),
195
- description: $('#event-description').val(),
196
- start: $('#event-start').val(),
197
- end: $('#event-end').val(),
198
- allDay: $('#event-allDay').is(':checked'),
199
- location: $('#event-location').val(),
200
- bookingEnabled: $('#event-bookingEnabled').is(':checked'),
201
- bookingItems: items
202
- };
203
- },
204
-
205
- renderBookingItems: function (items) {
206
- const container = $('#booking-items');
207
- container.empty();
208
- (items || []).forEach(item => CalendarPage.addBookingItemRow(item));
209
- },
210
-
211
- addBookingItemRow: function (item = {}) {
212
- const row = $(`
213
- <div class="booking-item-row">
214
- <input type="text" class="item-id form-control" placeholder="ID" value="${item.id || ''}">
215
- <input type="text" class="item-name form-control" placeholder="Nom matériel" value="${item.name || ''}">
216
- <input type="number" class="item-total form-control" placeholder="Total" value="${item.total || 0}">
217
- <input type="number" class="item-price form-control" placeholder="Prix €" step="0.01" value="${item.price || 0}">
218
- <input type="text" class="item-pickup form-control" placeholder="Lieu retrait" value="${item.pickupLocation || ''}">
219
- <input type="hidden" class="item-reserved" value="${item.reserved || 0}">
220
- <input type="hidden" class="item-reservedTemp" value="${item.reservedTemp || 0}">
221
- <button class="btn btn-danger btn-sm item-remove">✕</button>
222
- </div>
223
- `);
224
-
225
- row.find('.item-remove').on('click', () => row.remove());
226
- $('#booking-items').append(row);
227
- },
228
-
229
- /* -------- MODAL RÉSERVATION -------- */
230
-
231
- openReserveModal: function (eid) {
232
- $.get('/api/calendar/event/' + eid).then(eventData => {
233
- const modal = $('#calendar-reserve-modal');
234
-
235
- $('#reserve-title').text(eventData.title);
236
-
237
- const items = eventData.bookingItems || [];
238
- if (!items.length || !Number(eventData.bookingEnabled)) {
239
- $('#reserve-items').html('<p>Aucun matériel réservable pour cet événement.</p>');
240
- $('#reserve-confirm').hide();
241
- } else {
242
- let html = '<ul>';
243
- items.forEach((item, index) => {
244
- const pickup = item.pickupLocation || 'Lieu de retrait non précisé';
245
- html += `
246
- <li>
247
- <label>
248
- <input type="radio" name="reserve-item" value="${item.id}" ${index === 0 ? 'checked' : ''}>
249
- <strong>${item.name}</strong>
250
- ${item.price ? `– ${item.price} €/jour` : ''}
251
- <br><small>📍 Retrait : ${pickup}</small>
252
- </label>
253
- </li>`;
254
- });
255
- html += '</ul>';
256
- $('#reserve-items').html(html);
257
- $('#reserve-confirm').show();
258
- }
259
-
260
- const today = new Date().toISOString().slice(0, 10);
261
- $('#reserve-start').val(today);
262
- $('#reserve-end').val(today);
263
- $('#reserve-quantity').val(1);
264
-
265
- $('#reserve-confirm').off('click').on('click', () => {
266
- CalendarPage.sendReservation(eid);
267
- });
1
+ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', [
2
+ 'api',
3
+ 'alerts',
4
+ ], function (api, alerts) {
5
+ 'use strict';
6
+
7
+ function init() {
8
+ // jQuery est fourni par NodeBB sur les pages rendues via buildHeader
9
+ const $ = window.jQuery;
10
+ if (!$) {
11
+ // fallback: on évite le crash
12
+ console.error('[calendar-onekite] jQuery not found');
13
+ return;
14
+ }
268
15
 
269
- modal.fadeIn();
16
+ // Exemple: bind bouton "nouvel événement"
17
+ $('#calendar-new-event')
18
+ .off('click.calendarOnekite')
19
+ .on('click.calendarOnekite', function () {
20
+ // TODO: ouvre ton modal / formulaire
21
+ alerts.success('Clique OK: handler actif ✅');
270
22
  });
271
- },
272
23
 
273
- sendReservation: function (eid) {
274
- const itemId = $('input[name="reserve-item"]:checked').val();
275
- const quantity = Number($('#reserve-quantity').val() || 1);
276
- const dateStart = $('#reserve-start').val();
277
- const dateEnd = $('#reserve-end').val();
24
+ // Si tu as d'autres binds / initialisation calendrier, fais-les ici
25
+ }
278
26
 
279
- if (!itemId) return app.alertError('Sélectionnez un matériel.');
280
- if (!quantity || quantity <= 0) return app.alertError('Quantité invalide.');
281
- if (!dateStart || !dateEnd) return app.alertError('Merci de renseigner les dates.');
282
- if (dateEnd < dateStart) return app.alertError('La date de fin doit être ≥ la date de début.');
27
+ // important: ajaxify SPA
28
+ $(window).on('action:ajaxify.end', function () {
29
+ // page /calendar
30
+ if (ajaxify?.data?.template?.includes('calendar')) {
31
+ init();
32
+ }
33
+ });
283
34
 
284
- $.ajax({
285
- url: '/api/calendar/event/' + eid + '/book',
286
- method: 'POST',
287
- contentType: 'application/json',
288
- data: JSON.stringify({ itemId, quantity, dateStart, dateEnd }),
289
- })
290
- .then(res => {
291
- app.alertSuccess(res.message || 'Demande envoyée.');
292
- $('#calendar-reserve-modal').fadeOut();
293
- })
294
- .catch(err => {
295
- app.alertError(err.responseJSON?.error || err.message);
296
- });
35
+ $(function () {
36
+ if (ajaxify?.data?.template?.includes('calendar')) {
37
+ init();
297
38
  }
298
- };
39
+ });
299
40
 
300
- CalendarPage.init();
41
+ return { init };
301
42
  });
@@ -26,4 +26,9 @@
26
26
  </div>
27
27
 
28
28
  <button id="calendar-onekite-save" class="btn btn-primary">Enregistrer</button>
29
- </div>
29
+ </div>
30
+ <script>
31
+ require(['plugins/nodebb-plugin-calendar-onekite/static/js/calendar'], function (Calendar) {
32
+ Calendar.init && Calendar.init();
33
+ });
34
+ </script>