nodebb-plugin-calendar-onekite 1.3.8 → 1.4.0
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,5 +1,27 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
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
|
|
13
|
+
*
|
|
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
|
|
23
|
+
*/
|
|
24
|
+
|
|
3
25
|
const db = require.main.require('./src/database');
|
|
4
26
|
const user = require.main.require('./src/user');
|
|
5
27
|
const meta = require.main.require('./src/meta');
|
|
@@ -11,6 +33,7 @@ const helloAsso = require('./helloasso');
|
|
|
11
33
|
const Plugin = {};
|
|
12
34
|
let appRef = null;
|
|
13
35
|
|
|
36
|
+
const SETTINGS_KEY = 'calendar-onekite';
|
|
14
37
|
const EVENTS_SET_KEY = 'calendar:events:start';
|
|
15
38
|
const EVENT_KEY_PREFIX = 'calendar:event:';
|
|
16
39
|
|
|
@@ -23,18 +46,21 @@ async function nextReservationId() {
|
|
|
23
46
|
return String(rid);
|
|
24
47
|
}
|
|
25
48
|
|
|
26
|
-
//
|
|
49
|
+
// number of days between two yyyy-mm-dd dates (inclusive)
|
|
27
50
|
function daysBetween(start, end) {
|
|
28
51
|
const d1 = new Date(start);
|
|
29
52
|
const d2 = new Date(end);
|
|
30
53
|
if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return 1;
|
|
54
|
+
|
|
31
55
|
const t1 = Date.UTC(d1.getFullYear(), d1.getMonth(), d1.getDate());
|
|
32
56
|
const t2 = Date.UTC(d2.getFullYear(), d2.getMonth(), d2.getDate());
|
|
33
57
|
const diff = Math.round((t2 - t1) / (1000 * 60 * 60 * 24));
|
|
34
58
|
return Math.max(1, diff + 1);
|
|
35
59
|
}
|
|
36
60
|
|
|
37
|
-
/*
|
|
61
|
+
/* -----------------------------
|
|
62
|
+
* Events
|
|
63
|
+
* ---------------------------- */
|
|
38
64
|
|
|
39
65
|
async function createEvent(data, uid) {
|
|
40
66
|
const eid = await db.incrObjectField('global', 'nextCalendarEid');
|
|
@@ -99,8 +125,7 @@ async function updateEvent(eid, data) {
|
|
|
99
125
|
allDay: data.allDay !== undefined ? (data.allDay ? 1 : 0) : existing.allDay,
|
|
100
126
|
location: data.location !== undefined ? String(data.location) : existing.location,
|
|
101
127
|
visibility: data.visibility !== undefined ? String(data.visibility) : existing.visibility,
|
|
102
|
-
bookingEnabled:
|
|
103
|
-
data.bookingEnabled !== undefined ? (data.bookingEnabled ? 1 : 0) : (existing.bookingEnabled || 0),
|
|
128
|
+
bookingEnabled: data.bookingEnabled !== undefined ? (data.bookingEnabled ? 1 : 0) : (existing.bookingEnabled || 0),
|
|
104
129
|
bookingItems: JSON.stringify(bookingItems),
|
|
105
130
|
updatedAt: String(Date.now()),
|
|
106
131
|
};
|
|
@@ -120,8 +145,10 @@ async function getEventsBetween(start, end) {
|
|
|
120
145
|
const endTs = new Date(end).getTime();
|
|
121
146
|
const eids = await db.getSortedSetRangeByScore(EVENTS_SET_KEY, 0, -1, startTs, endTs);
|
|
122
147
|
if (!eids || !eids.length) return [];
|
|
148
|
+
|
|
123
149
|
const keys = eids.map(eid => getEventKey(eid));
|
|
124
150
|
const events = await db.getObjects(keys);
|
|
151
|
+
|
|
125
152
|
return (events || []).filter(Boolean).map(ev => ({
|
|
126
153
|
...ev,
|
|
127
154
|
bookingItems: JSON.parse(ev.bookingItems || '[]'),
|
|
@@ -132,16 +159,20 @@ async function getUpcomingEvents(limit = 5) {
|
|
|
132
159
|
const now = Date.now();
|
|
133
160
|
const eids = await db.getSortedSetRangeByScore(EVENTS_SET_KEY, 0, limit - 1, now, '+inf');
|
|
134
161
|
if (!eids || !eids.length) return [];
|
|
162
|
+
|
|
135
163
|
const keys = eids.map(eid => getEventKey(eid));
|
|
136
164
|
const events = await db.getObjects(keys);
|
|
137
165
|
return (events || []).filter(Boolean);
|
|
138
166
|
}
|
|
139
167
|
|
|
140
|
-
/*
|
|
168
|
+
/* -----------------------------
|
|
169
|
+
* Permissions
|
|
170
|
+
* ---------------------------- */
|
|
141
171
|
|
|
142
172
|
async function userCanCreate(uid) {
|
|
143
173
|
if (!uid || uid === 0) return false;
|
|
144
|
-
|
|
174
|
+
|
|
175
|
+
const settings = await Settings.get(SETTINGS_KEY);
|
|
145
176
|
if (!settings || !settings.allowedGroups) return false;
|
|
146
177
|
|
|
147
178
|
const allowedSet = new Set(
|
|
@@ -153,15 +184,16 @@ async function userCanCreate(uid) {
|
|
|
153
184
|
if (!allowedSet.size) return false;
|
|
154
185
|
|
|
155
186
|
const userGroupsArr = await user.getUserGroups([uid]);
|
|
156
|
-
const groups = (userGroupsArr[0] || []).map(g => g.name.toLowerCase());
|
|
187
|
+
const groups = (userGroupsArr[0] || []).map(g => (g.name || '').toLowerCase());
|
|
188
|
+
|
|
157
189
|
return groups.some(g => allowedSet.has(g));
|
|
158
190
|
}
|
|
159
191
|
|
|
160
192
|
async function userCanBook(uid) {
|
|
161
193
|
if (!uid || uid === 0) return false;
|
|
162
194
|
|
|
163
|
-
const settings = await Settings.get(
|
|
164
|
-
//
|
|
195
|
+
const settings = await Settings.get(SETTINGS_KEY);
|
|
196
|
+
// if not configured -> allow any logged-in user
|
|
165
197
|
if (!settings || !settings.allowedBookingGroups) return true;
|
|
166
198
|
|
|
167
199
|
const allowedSet = new Set(
|
|
@@ -173,11 +205,14 @@ async function userCanBook(uid) {
|
|
|
173
205
|
if (!allowedSet.size) return true;
|
|
174
206
|
|
|
175
207
|
const userGroupsArr = await user.getUserGroups([uid]);
|
|
176
|
-
const groups = (userGroupsArr[0] || []).map(g => g.name.toLowerCase());
|
|
208
|
+
const groups = (userGroupsArr[0] || []).map(g => (g.name || '').toLowerCase());
|
|
209
|
+
|
|
177
210
|
return groups.some(g => allowedSet.has(g));
|
|
178
211
|
}
|
|
179
212
|
|
|
180
|
-
/*
|
|
213
|
+
/* -----------------------------
|
|
214
|
+
* RSVP
|
|
215
|
+
* ---------------------------- */
|
|
181
216
|
|
|
182
217
|
async function setRsvp(eid, uid, status) {
|
|
183
218
|
const key = getEventKey(eid);
|
|
@@ -213,7 +248,9 @@ async function setRsvp(eid, uid, status) {
|
|
|
213
248
|
return updated;
|
|
214
249
|
}
|
|
215
250
|
|
|
216
|
-
/*
|
|
251
|
+
/* -----------------------------
|
|
252
|
+
* Pricing
|
|
253
|
+
* ---------------------------- */
|
|
217
254
|
|
|
218
255
|
function computePrice(event, reservation) {
|
|
219
256
|
const items = JSON.parse(event.bookingItems || '[]');
|
|
@@ -221,15 +258,29 @@ function computePrice(event, reservation) {
|
|
|
221
258
|
if (!item) return 0;
|
|
222
259
|
|
|
223
260
|
const unit = Number(item.price || 0);
|
|
224
|
-
const days = reservation.days || 1;
|
|
225
|
-
return unit * reservation.quantity * days;
|
|
261
|
+
const days = Number(reservation.days || 1);
|
|
262
|
+
return unit * Number(reservation.quantity || 0) * days;
|
|
226
263
|
}
|
|
227
264
|
|
|
228
|
-
/*
|
|
265
|
+
/* -----------------------------
|
|
266
|
+
* Renderers (pages)
|
|
267
|
+
* ---------------------------- */
|
|
268
|
+
|
|
269
|
+
function renderCalendarPage(req, res) {
|
|
270
|
+
res.render('calendar', { title: 'Calendrier' });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function renderMyReservationsPage(req, res) {
|
|
274
|
+
res.render('calendar-my-reservations', { title: 'Mes réservations' });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function renderPlanningPage(req, res) {
|
|
278
|
+
res.render('admin/calendar-planning', { title: 'Planning des réservations' });
|
|
279
|
+
}
|
|
229
280
|
|
|
230
281
|
async function renderAdminPage(req, res) {
|
|
231
282
|
try {
|
|
232
|
-
const settings = (await Settings.get(
|
|
283
|
+
const settings = (await Settings.get(SETTINGS_KEY)) || {};
|
|
233
284
|
res.render('admin/plugins/calendar-onekite', {
|
|
234
285
|
title: 'Calendar OneKite',
|
|
235
286
|
settings,
|
|
@@ -242,25 +293,28 @@ async function renderAdminPage(req, res) {
|
|
|
242
293
|
}
|
|
243
294
|
}
|
|
244
295
|
|
|
245
|
-
/*
|
|
296
|
+
/* -----------------------------
|
|
297
|
+
* Plugin init
|
|
298
|
+
* ---------------------------- */
|
|
246
299
|
|
|
247
300
|
Plugin.init = async function (params) {
|
|
248
301
|
const { router, middleware } = params;
|
|
249
302
|
appRef = params.app;
|
|
250
303
|
|
|
251
|
-
|
|
252
|
-
|
|
304
|
+
// Public pages
|
|
253
305
|
router.get('/calendar', middleware.buildHeader, renderCalendarPage);
|
|
254
306
|
router.get('/api/calendar', renderCalendarPage);
|
|
255
307
|
|
|
256
308
|
router.get('/calendar/my-reservations', middleware.buildHeader, renderMyReservationsPage);
|
|
257
309
|
router.get('/api/calendar/my-reservations/page', renderMyReservationsPage);
|
|
258
310
|
|
|
259
|
-
//
|
|
311
|
+
// Admin planning page
|
|
260
312
|
router.get('/admin/calendar/planning', middleware.admin.buildHeader, renderPlanningPage);
|
|
261
313
|
router.get('/api/admin/calendar/planning/page', renderPlanningPage);
|
|
262
314
|
|
|
263
|
-
/*
|
|
315
|
+
/* -----------------------------
|
|
316
|
+
* Events API
|
|
317
|
+
* ---------------------------- */
|
|
264
318
|
|
|
265
319
|
router.get('/api/calendar/events', async (req, res) => {
|
|
266
320
|
try {
|
|
@@ -321,7 +375,9 @@ Plugin.init = async function (params) {
|
|
|
321
375
|
}
|
|
322
376
|
});
|
|
323
377
|
|
|
324
|
-
/*
|
|
378
|
+
/* -----------------------------
|
|
379
|
+
* RSVP
|
|
380
|
+
* ---------------------------- */
|
|
325
381
|
|
|
326
382
|
router.post('/api/calendar/event/:eid/rsvp', middleware.ensureLoggedIn, async (req, res) => {
|
|
327
383
|
try {
|
|
@@ -335,7 +391,9 @@ Plugin.init = async function (params) {
|
|
|
335
391
|
}
|
|
336
392
|
});
|
|
337
393
|
|
|
338
|
-
/*
|
|
394
|
+
/* -----------------------------
|
|
395
|
+
* Client permissions endpoints
|
|
396
|
+
* ---------------------------- */
|
|
339
397
|
|
|
340
398
|
router.get('/api/calendar/permissions/create', async (req, res) => {
|
|
341
399
|
const uid = req.user?.uid || 0;
|
|
@@ -349,7 +407,9 @@ Plugin.init = async function (params) {
|
|
|
349
407
|
res.json({ allow });
|
|
350
408
|
});
|
|
351
409
|
|
|
352
|
-
/*
|
|
410
|
+
/* -----------------------------
|
|
411
|
+
* Multi-day reservation request
|
|
412
|
+
* ---------------------------- */
|
|
353
413
|
|
|
354
414
|
router.post('/api/calendar/event/:eid/book', middleware.ensureLoggedIn, async (req, res) => {
|
|
355
415
|
try {
|
|
@@ -360,10 +420,12 @@ Plugin.init = async function (params) {
|
|
|
360
420
|
if (!await userCanBook(uid)) {
|
|
361
421
|
return res.status(403).json({ error: 'Vous n’êtes pas autorisé à réserver du matériel.' });
|
|
362
422
|
}
|
|
363
|
-
|
|
364
423
|
if (!dateStart || !dateEnd) {
|
|
365
424
|
return res.status(400).json({ error: 'Dates de début et de fin obligatoires.' });
|
|
366
425
|
}
|
|
426
|
+
if (String(dateEnd) < String(dateStart)) {
|
|
427
|
+
return res.status(400).json({ error: 'La date de fin doit être ≥ la date de début.' });
|
|
428
|
+
}
|
|
367
429
|
|
|
368
430
|
const event = await getEvent(eid);
|
|
369
431
|
if (!event) return res.status(404).json({ error: 'Événement introuvable' });
|
|
@@ -378,18 +440,21 @@ Plugin.init = async function (params) {
|
|
|
378
440
|
|
|
379
441
|
const allRes = JSON.parse(event.reservations || '[]');
|
|
380
442
|
|
|
443
|
+
// Stock check: sum overlapping reservations (pending_admin/awaiting_payment/paid) for the same item
|
|
381
444
|
const overlapping = allRes.filter(r => {
|
|
382
445
|
if (r.itemId !== itemId) return false;
|
|
383
446
|
if (r.status === 'cancelled') return false;
|
|
447
|
+
|
|
384
448
|
const startR = new Date(r.dateStart);
|
|
385
449
|
const endR = new Date(r.dateEnd);
|
|
386
450
|
const startN = new Date(dateStart);
|
|
387
451
|
const endN = new Date(dateEnd);
|
|
452
|
+
|
|
388
453
|
return !(endN < startR || startN > endR);
|
|
389
454
|
});
|
|
390
455
|
|
|
391
|
-
const used = overlapping.reduce((sum, r) => sum + r.quantity, 0);
|
|
392
|
-
const available = item.total - used;
|
|
456
|
+
const used = overlapping.reduce((sum, r) => sum + Number(r.quantity || 0), 0);
|
|
457
|
+
const available = Number(item.total || 0) - used;
|
|
393
458
|
if (q > available) return res.status(400).json({ error: 'Matériel indisponible pour ces dates.' });
|
|
394
459
|
|
|
395
460
|
const rid = await nextReservationId();
|
|
@@ -408,7 +473,7 @@ Plugin.init = async function (params) {
|
|
|
408
473
|
status: 'pending_admin',
|
|
409
474
|
helloAssoOrderId: null,
|
|
410
475
|
createdAt: now,
|
|
411
|
-
pickupLocation: item.pickupLocation || '',
|
|
476
|
+
pickupLocation: String(item.pickupLocation || ''),
|
|
412
477
|
};
|
|
413
478
|
|
|
414
479
|
allRes.push(reservation);
|
|
@@ -417,6 +482,7 @@ Plugin.init = async function (params) {
|
|
|
417
482
|
|
|
418
483
|
await db.setObject(getEventKey(eid), event);
|
|
419
484
|
|
|
485
|
+
// Email: request created
|
|
420
486
|
try {
|
|
421
487
|
await emailer.send('calendar-reservation-created', uid, {
|
|
422
488
|
subject: 'Votre demande de réservation a été envoyée',
|
|
@@ -443,37 +509,46 @@ Plugin.init = async function (params) {
|
|
|
443
509
|
}
|
|
444
510
|
});
|
|
445
511
|
|
|
446
|
-
/*
|
|
512
|
+
/* -----------------------------
|
|
513
|
+
* Admin: pending list
|
|
514
|
+
* ---------------------------- */
|
|
447
515
|
|
|
448
516
|
router.get('/api/admin/calendar/pending', middleware.admin.checkPrivileges, async (req, res) => {
|
|
449
517
|
try {
|
|
450
518
|
const result = [];
|
|
451
519
|
const eids = await db.getSortedSetRange(EVENTS_SET_KEY, 0, -1);
|
|
520
|
+
|
|
452
521
|
for (const eid of eids) {
|
|
453
522
|
const event = await getEvent(eid);
|
|
454
523
|
if (!event) continue;
|
|
524
|
+
|
|
455
525
|
const reservations = JSON.parse(event.reservations || '[]');
|
|
456
526
|
const pending = reservations.filter(r => r.status === 'pending_admin');
|
|
457
527
|
if (pending.length) result.push({ event, reservations: pending });
|
|
458
528
|
}
|
|
529
|
+
|
|
459
530
|
res.json(result);
|
|
460
531
|
} catch (err) {
|
|
461
532
|
res.status(500).json({ error: err.message });
|
|
462
533
|
}
|
|
463
534
|
});
|
|
464
535
|
|
|
465
|
-
/*
|
|
536
|
+
/* -----------------------------
|
|
537
|
+
* Admin: validate reservation -> awaiting_payment + HelloAsso checkout
|
|
538
|
+
* ---------------------------- */
|
|
466
539
|
|
|
467
540
|
router.post('/api/admin/calendar/reservation/:rid/validate', middleware.admin.checkPrivileges, async (req, res) => {
|
|
468
541
|
try {
|
|
469
542
|
const rid = req.params.rid;
|
|
470
543
|
const eids = await db.getSortedSetRange(EVENTS_SET_KEY, 0, -1);
|
|
544
|
+
|
|
471
545
|
let targetEvent = null;
|
|
472
546
|
let reservation = null;
|
|
473
547
|
|
|
474
548
|
for (const eid of eids) {
|
|
475
549
|
const event = await getEvent(eid);
|
|
476
550
|
if (!event) continue;
|
|
551
|
+
|
|
477
552
|
const reservations = JSON.parse(event.reservations || '[]');
|
|
478
553
|
const r = reservations.find(rr => rr.rid === rid);
|
|
479
554
|
if (r) {
|
|
@@ -487,6 +562,7 @@ Plugin.init = async function (params) {
|
|
|
487
562
|
if (reservation.status !== 'pending_admin') return res.status(400).json({ error: 'Réservation déjà traitée' });
|
|
488
563
|
|
|
489
564
|
reservation.status = 'awaiting_payment';
|
|
565
|
+
|
|
490
566
|
const resList = JSON.parse(targetEvent.reservations || '[]');
|
|
491
567
|
const idx = resList.findIndex(r => r.rid === rid);
|
|
492
568
|
resList[idx] = reservation;
|
|
@@ -526,49 +602,57 @@ Plugin.init = async function (params) {
|
|
|
526
602
|
}
|
|
527
603
|
});
|
|
528
604
|
|
|
529
|
-
/*
|
|
605
|
+
/* -----------------------------
|
|
606
|
+
* Admin: cancel reservation
|
|
607
|
+
* ---------------------------- */
|
|
530
608
|
|
|
531
609
|
router.post('/api/admin/calendar/reservation/:rid/cancel', middleware.admin.checkPrivileges, async (req, res) => {
|
|
532
610
|
try {
|
|
533
611
|
const rid = req.params.rid;
|
|
534
612
|
const eids = await db.getSortedSetRange(EVENTS_SET_KEY, 0, -1);
|
|
535
|
-
|
|
613
|
+
|
|
614
|
+
let found = false;
|
|
536
615
|
|
|
537
616
|
for (const eid of eids) {
|
|
538
617
|
const event = await getEvent(eid);
|
|
539
618
|
if (!event) continue;
|
|
619
|
+
|
|
540
620
|
const reservations = JSON.parse(event.reservations || '[]');
|
|
541
621
|
const rIndex = reservations.findIndex(rr => rr.rid === rid);
|
|
542
622
|
if (rIndex !== -1) {
|
|
543
|
-
|
|
544
|
-
reservation.status = 'cancelled';
|
|
545
|
-
reservations[rIndex] = reservation;
|
|
623
|
+
reservations[rIndex].status = 'cancelled';
|
|
546
624
|
event.reservations = JSON.stringify(reservations);
|
|
547
625
|
await db.setObject(getEventKey(event.eid), event);
|
|
626
|
+
found = true;
|
|
548
627
|
break;
|
|
549
628
|
}
|
|
550
629
|
}
|
|
551
630
|
|
|
552
|
-
if (!
|
|
631
|
+
if (!found) return res.status(404).json({ error: 'Réservation introuvable' });
|
|
553
632
|
res.json({ success: true });
|
|
554
633
|
} catch (err) {
|
|
555
634
|
res.status(500).json({ error: err.message });
|
|
556
635
|
}
|
|
557
636
|
});
|
|
558
637
|
|
|
559
|
-
/*
|
|
638
|
+
/* -----------------------------
|
|
639
|
+
* HelloAsso webhook (marks reservation paid)
|
|
640
|
+
* ---------------------------- */
|
|
560
641
|
|
|
561
642
|
router.post('/api/calendar/helloasso/webhook', async (req, res) => {
|
|
562
643
|
try {
|
|
563
644
|
const payload = req.body;
|
|
564
645
|
const order = payload.order || payload;
|
|
565
|
-
|
|
646
|
+
|
|
647
|
+
const orderId = String(order.id || '');
|
|
566
648
|
const state = order.state || order.status || '';
|
|
567
649
|
if (state !== 'Paid') return res.json({ ignored: true });
|
|
568
650
|
|
|
569
651
|
const custom = order.customFields || {};
|
|
570
|
-
const eid = String(custom.eid);
|
|
571
|
-
const rid = String(custom.rid);
|
|
652
|
+
const eid = String(custom.eid || '');
|
|
653
|
+
const rid = String(custom.rid || '');
|
|
654
|
+
|
|
655
|
+
if (!eid || !rid) return res.status(400).json({ error: 'Missing eid/rid in customFields' });
|
|
572
656
|
|
|
573
657
|
const event = await getEvent(eid);
|
|
574
658
|
if (!event) throw new Error('Event not found');
|
|
@@ -580,12 +664,13 @@ Plugin.init = async function (params) {
|
|
|
580
664
|
const reservation = reservations[rIndex];
|
|
581
665
|
if (reservation.status === 'paid') return res.json({ ok: true });
|
|
582
666
|
|
|
667
|
+
// Update optional aggregate reserved count
|
|
583
668
|
const items = JSON.parse(event.bookingItems || '[]');
|
|
584
669
|
const itemIndex = items.findIndex(i => i.id === reservation.itemId);
|
|
585
670
|
if (itemIndex !== -1) {
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
items[itemIndex] =
|
|
671
|
+
const it = items[itemIndex];
|
|
672
|
+
it.reserved = (Number(it.reserved || 0) + Number(reservation.quantity || 0));
|
|
673
|
+
items[itemIndex] = it;
|
|
589
674
|
}
|
|
590
675
|
|
|
591
676
|
reservation.status = 'paid';
|
|
@@ -619,21 +704,26 @@ Plugin.init = async function (params) {
|
|
|
619
704
|
}
|
|
620
705
|
});
|
|
621
706
|
|
|
622
|
-
/*
|
|
707
|
+
/* -----------------------------
|
|
708
|
+
* Admin plugin page + settings API
|
|
709
|
+
* IMPORTANT: keep these routes EXACTLY aligned with your admin.js
|
|
710
|
+
* ---------------------------- */
|
|
623
711
|
|
|
624
712
|
router.get('/admin/plugins/calendar-onekite', middleware.admin.buildHeader, renderAdminPage);
|
|
625
713
|
router.get('/api/admin/plugins/calendar-onekite', renderAdminPage);
|
|
626
714
|
|
|
627
715
|
router.put('/api/admin/plugins/calendar-onekite', middleware.admin.checkPrivileges, async (req, res) => {
|
|
628
716
|
try {
|
|
629
|
-
await Settings.set(
|
|
717
|
+
await Settings.set(SETTINGS_KEY, req.body);
|
|
630
718
|
res.json({ status: 'ok' });
|
|
631
719
|
} catch (err) {
|
|
632
720
|
res.status(500).json({ error: err.message });
|
|
633
721
|
}
|
|
634
722
|
});
|
|
635
723
|
|
|
636
|
-
/*
|
|
724
|
+
/* -----------------------------
|
|
725
|
+
* My reservations API
|
|
726
|
+
* ---------------------------- */
|
|
637
727
|
|
|
638
728
|
router.get('/api/calendar/my-reservations', middleware.ensureLoggedIn, async (req, res) => {
|
|
639
729
|
try {
|
|
@@ -646,10 +736,12 @@ Plugin.init = async function (params) {
|
|
|
646
736
|
for (const eid of eids) {
|
|
647
737
|
const event = await getEvent(eid);
|
|
648
738
|
if (!event) continue;
|
|
739
|
+
|
|
649
740
|
const items = JSON.parse(event.bookingItems || '[]');
|
|
650
741
|
const reservations = JSON.parse(event.reservations || '[]');
|
|
742
|
+
|
|
651
743
|
reservations
|
|
652
|
-
.filter(r => r.uid === uid)
|
|
744
|
+
.filter(r => String(r.uid) === uid)
|
|
653
745
|
.forEach(r => {
|
|
654
746
|
const item = items.find(i => i.id === r.itemId);
|
|
655
747
|
result.push({
|
|
@@ -663,14 +755,15 @@ Plugin.init = async function (params) {
|
|
|
663
755
|
}
|
|
664
756
|
|
|
665
757
|
result.sort((a, b) => new Date(a.dateStart) - new Date(b.dateStart));
|
|
666
|
-
|
|
667
758
|
res.json(result);
|
|
668
759
|
} catch (err) {
|
|
669
760
|
res.status(500).json({ error: err.message });
|
|
670
761
|
}
|
|
671
762
|
});
|
|
672
763
|
|
|
673
|
-
/*
|
|
764
|
+
/* -----------------------------
|
|
765
|
+
* Admin planning API (future reservations)
|
|
766
|
+
* ---------------------------- */
|
|
674
767
|
|
|
675
768
|
router.get('/api/admin/calendar/planning', middleware.admin.checkPrivileges, async (req, res) => {
|
|
676
769
|
try {
|
|
@@ -681,8 +774,10 @@ Plugin.init = async function (params) {
|
|
|
681
774
|
for (const eid of eids) {
|
|
682
775
|
const event = await getEvent(eid);
|
|
683
776
|
if (!event) continue;
|
|
777
|
+
|
|
684
778
|
const items = JSON.parse(event.bookingItems || '[]');
|
|
685
779
|
const reservations = JSON.parse(event.reservations || '[]');
|
|
780
|
+
|
|
686
781
|
reservations
|
|
687
782
|
.filter(r => r.status === 'pending_admin' || r.status === 'awaiting_payment' || r.status === 'paid')
|
|
688
783
|
.forEach(r => {
|
|
@@ -711,20 +806,13 @@ Plugin.init = async function (params) {
|
|
|
711
806
|
});
|
|
712
807
|
};
|
|
713
808
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
function renderMyReservationsPage(req, res) {
|
|
719
|
-
res.render('calendar-my-reservations', { title: 'Mes réservations' });
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
function renderPlanningPage(req, res) {
|
|
723
|
-
res.render('admin/calendar-planning', { title: 'Planning des réservations' });
|
|
724
|
-
}
|
|
809
|
+
/* -----------------------------
|
|
810
|
+
* ACP nav entry
|
|
811
|
+
* ---------------------------- */
|
|
725
812
|
|
|
726
813
|
Plugin.addAdminNavigation = function (header) {
|
|
727
814
|
header.plugins.push({
|
|
815
|
+
// NodeBB ACP will resolve this under /admin
|
|
728
816
|
route: '/plugins/calendar-onekite',
|
|
729
817
|
icon: 'fa fa-calendar',
|
|
730
818
|
name: 'Calendar OneKite',
|
|
@@ -732,6 +820,10 @@ Plugin.addAdminNavigation = function (header) {
|
|
|
732
820
|
return header;
|
|
733
821
|
};
|
|
734
822
|
|
|
823
|
+
/* -----------------------------
|
|
824
|
+
* Widget
|
|
825
|
+
* ---------------------------- */
|
|
826
|
+
|
|
735
827
|
Plugin.defineWidgets = async function (widgets) {
|
|
736
828
|
widgets.push({
|
|
737
829
|
widget: 'calendarUpcoming',
|
|
@@ -744,7 +836,7 @@ Plugin.defineWidgets = async function (widgets) {
|
|
|
744
836
|
|
|
745
837
|
Plugin.renderUpcomingWidget = async function (widget, callback) {
|
|
746
838
|
try {
|
|
747
|
-
const settings = (await Settings.get(
|
|
839
|
+
const settings = (await Settings.get(SETTINGS_KEY)) || {};
|
|
748
840
|
const limit = Number(widget.data.limit || settings.limit || 5);
|
|
749
841
|
const events = await getUpcomingEvents(limit);
|
|
750
842
|
const html = await appRef.renderAsync('widgets/calendar-upcoming', { events });
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
<h2>Planning des réservations de matériel</h2>
|
|
3
3
|
|
|
4
4
|
<p>
|
|
5
|
-
<a href="/admin/plugins/calendar-
|
|
6
|
-
← Retour au plugin Calendar
|
|
5
|
+
<a href="/admin/plugins/calendar-onekite" class="btn btn-default btn-sm">
|
|
6
|
+
← Retour au plugin Calendar Onekite
|
|
7
7
|
</a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
@@ -27,4 +27,4 @@
|
|
|
27
27
|
</table>
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
|
-
<script src="/plugins/nodebb-plugin-calendar-
|
|
30
|
+
<script src="/plugins/nodebb-plugin-calendar-onekite/static/js/admin.js"></script>
|