nodebb-plugin-onekite-calendar 1.0.25 → 1.0.27

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/lib/admin.js CHANGED
@@ -41,11 +41,8 @@ async function sendEmail(template, uid, subject, data) {
41
41
  try {
42
42
  if (typeof emailer.send !== 'function') return;
43
43
  // NodeBB 4.x: send(template, uid, params)
44
- if (emailer.send.length >= 3) {
45
- await emailer.send(template, toUid, params);
46
- } else {
47
- await emailer.send(template, toUid);
48
- }
44
+ // Do NOT branch on function.length (unreliable once wrapped/bound).
45
+ await emailer.send(template, toUid, params);
49
46
  } catch (err) {
50
47
  console.warn('[calendar-onekite] Failed to send email', {
51
48
  template,
@@ -162,11 +159,11 @@ admin.approveReservation = async function (req, res) {
162
159
  // User return/back/error URLs must be real pages; webhook uses the plugin endpoint.
163
160
  callbackUrl: returnUrl,
164
161
  webhookUrl: webhookUrl,
165
- itemName: buildHelloAssoItemName('Réservation matériel Onekite', Array.isArray(r.itemNames) ? r.itemNames : [], r.start, r.end),
162
+ itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
166
163
  containsDonation: false,
167
164
  metadata: {
168
165
  reservationId: String(rid),
169
- items: (Array.isArray(r.itemNames) ? r.itemNames : []).filter(Boolean),
166
+ items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
170
167
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
171
168
  },
172
169
  });
@@ -199,8 +196,8 @@ admin.approveReservation = async function (req, res) {
199
196
  await sendEmail('calendar-onekite_approved', requesterUid, 'Location matériel - Réservation validée', {
200
197
  uid: requesterUid,
201
198
  username: requester && requester.username ? requester.username : '',
202
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
203
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
199
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
200
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
204
201
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
205
202
  paymentUrl: paymentUrl || '',
206
203
  pickupAddress: r.pickupAddress || '',
@@ -235,8 +232,8 @@ admin.refuseReservation = async function (req, res) {
235
232
  await sendEmail('calendar-onekite_refused', requesterUid, 'Location matériel - Réservation refusée', {
236
233
  uid: requesterUid,
237
234
  username: requester && requester.username ? requester.username : '',
238
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
239
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
235
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
236
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
240
237
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
241
238
  start: formatFR(r.start),
242
239
  end: formatFR(r.end),
@@ -419,7 +416,7 @@ admin.getAccounting = async function (req, res) {
419
416
 
420
417
  const itemNames = Array.isArray(r.itemNames) && r.itemNames.length
421
418
  ? r.itemNames
422
- : [];
419
+ : (r.itemName ? [r.itemName] : []);
423
420
 
424
421
  const total = Number(r.total) || 0;
425
422
  const startDate = formatFR(r.start);
package/lib/api.js CHANGED
@@ -123,12 +123,11 @@ async function sendEmail(template, uid, subject, data) {
123
123
 
124
124
  try {
125
125
  if (typeof emailer.send !== 'function') return;
126
- // NodeBB: send(template, uid, params)
127
- if (emailer.send.length >= 3) {
128
- await emailer.send(template, toUid, params);
129
- } else {
130
- await emailer.send(template, toUid);
131
- }
126
+ // NodeBB 4.x: send(template, uid, params)
127
+ // NOTE: Do NOT branch on function.length: it is unreliable once the function
128
+ // is wrapped/bound (common in production builds) and can lead to params being
129
+ // dropped, resulting in empty email bodies and missing subjects.
130
+ await emailer.send(template, toUid, params);
132
131
  } catch (err) {
133
132
  // eslint-disable-next-line no-console
134
133
  console.warn('[calendar-onekite] Failed to send email', { template, uid: toUid, err: String((err && err.message) || err) });
@@ -328,18 +327,15 @@ function eventsFor(resv) {
328
327
  const startIsoDate = new Date(parseInt(resv.start, 10)).toISOString().slice(0, 10);
329
328
  const endIsoDate = new Date(parseInt(resv.end, 10)).toISOString().slice(0, 10);
330
329
 
331
- const itemIds = Array.isArray(resv.itemIds) ? resv.itemIds.filter(Boolean) : [];
332
- const itemNames = Array.isArray(resv.itemNames) ? resv.itemNames.filter(Boolean) : [];
330
+ const itemIds = Array.isArray(resv.itemIds) ? resv.itemIds : (resv.itemId ? [resv.itemId] : []);
331
+ const itemNames = Array.isArray(resv.itemNames) ? resv.itemNames : (resv.itemName ? [resv.itemName] : []);
333
332
 
334
333
  // One line = one material: return one calendar event per item
335
- if (!itemIds.length && !itemNames.length) {
336
- return [];
337
- }
338
334
  const out = [];
339
- const count = Math.max(itemIds.length, itemNames.length);
335
+ const count = Math.max(itemIds.length, itemNames.length, 1);
340
336
  for (let i = 0; i < count; i++) {
341
- const itemId = String(itemIds[i] || itemIds[0] || i);
342
- const itemName = String(itemNames[i] || itemNames[0] || itemId);
337
+ const itemId = String(itemIds[i] || itemIds[0] || resv.itemId || '');
338
+ const itemName = String(itemNames[i] || itemNames[0] || resv.itemName || itemId);
343
339
  out.push({
344
340
  // keep id unique per item for FullCalendar, but keep the real rid in extendedProps.rid
345
341
  id: `${resv.rid}:${itemId || i}`,
@@ -356,14 +352,15 @@ function eventsFor(resv) {
356
352
  uid: resv.uid,
357
353
  approvedBy: resv.approvedBy || 0,
358
354
  approvedByUsername: resv.approvedByUsername || '',
359
- itemIds: itemIds,
360
- itemNames: itemNames,
355
+ itemIds: itemIds.filter(Boolean),
356
+ itemNames: itemNames.filter(Boolean),
361
357
  itemIdLine: itemId,
362
358
  itemNameLine: itemName,
363
359
  },
364
360
  });
365
361
  }
366
362
  return out;
363
+ }
367
364
 
368
365
  function eventsForSpecial(ev) {
369
366
  const start = new Date(parseInt(ev.start, 10));
@@ -533,8 +530,8 @@ api.getReservationDetails = async function (req, res) {
533
530
  status: r.status,
534
531
  uid: r.uid,
535
532
  username: r.username || '',
536
- itemNames: Array.isArray(r.itemNames) ? r.itemNames : [],
537
- itemIds: Array.isArray(r.itemIds) ? r.itemIds : [],
533
+ itemNames: Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : []),
534
+ itemIds: Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : []),
538
535
  start: r.start,
539
536
  end: r.end,
540
537
  approvedByUsername: r.approvedByUsername || '',
@@ -698,9 +695,9 @@ api.createReservation = async function (req, res) {
698
695
  return res.status(400).json({ error: "bad-dates" });
699
696
  }
700
697
 
701
- // NodeBB 4.x only: require modern payload
702
- const itemIds = Array.isArray(req.body.itemIds) ? req.body.itemIds.map(String) : [];
703
- const itemNames = Array.isArray(req.body.itemNames) ? req.body.itemNames.map(String) : [];
698
+ // Support both legacy single itemId and new itemIds[] payload
699
+ const itemIds = Array.isArray(req.body.itemIds) ? req.body.itemIds.map(String) : ((req.body.itemId ? [String(req.body.itemId)] : []));
700
+ const itemNames = Array.isArray(req.body.itemNames) ? req.body.itemNames.map(String) : (req.body.itemName ? [String(req.body.itemName)] : []);
704
701
 
705
702
  const total = typeof req.body.total === 'number' ? req.body.total : parseFloat(String(req.body.total || '0'));
706
703
 
@@ -719,7 +716,7 @@ api.createReservation = async function (req, res) {
719
716
  const exStart = parseInt(existing.start, 10);
720
717
  const exEnd = parseInt(existing.end, 10);
721
718
  if (!(exStart < end && start < exEnd)) continue;
722
- const exItemIds = Array.isArray(existing.itemIds) ? existing.itemIds : [];
719
+ const exItemIds = Array.isArray(existing.itemIds) ? existing.itemIds : (existing.itemId ? [existing.itemId] : []);
723
720
  const shared = exItemIds.filter(x => itemIds.includes(String(x)));
724
721
  if (shared.length) {
725
722
  conflicts.push({ rid: existing.rid, itemIds: shared, status: existing.status });
@@ -744,6 +741,9 @@ api.createReservation = async function (req, res) {
744
741
  username: username || null,
745
742
  itemIds,
746
743
  itemNames: itemNames.length ? itemNames : itemIds,
744
+ // keep legacy fields for backward compatibility
745
+ itemId: itemIds[0],
746
+ itemName: (itemNames[0] || itemIds[0]),
747
747
  start,
748
748
  end,
749
749
  status: 'pending',
@@ -754,35 +754,51 @@ api.createReservation = async function (req, res) {
754
754
  // Save
755
755
  await dbLayer.saveReservation(resv);
756
756
 
757
- // Notify groups by email (NodeBB 4.x, UID-only)
757
+ // Notify groups by email (NodeBB emailer config)
758
758
  try {
759
- const notifyGroups = String(settings.notifyGroups || '')
760
- .split(/[\n,;]+/)
761
- .map(s => s.trim())
762
- .filter(Boolean);
763
-
759
+ const notifyGroups = (settings.notifyGroups || '').split(',').map(s => s.trim()).filter(Boolean);
764
760
  if (notifyGroups.length) {
765
- const requester = await user.getUserFields(uid, ['username']);
766
- const itemsLabel = (itemNames && itemNames.length ? itemNames : itemIds).join(', ');
767
-
761
+ const requester = await user.getUserFields(uid, ['username', 'email']);
762
+ const itemsLabel = (resv.itemNames || []).join(', ');
768
763
  for (const g of notifyGroups) {
769
764
  const members = await getMembersByGroupIdentifier(g);
770
765
  const uids = normalizeUids(members);
771
766
 
772
- for (const memberUid of uids) {
767
+ // Batch fetch user email/username when supported by this NodeBB version.
768
+ let usersData = [];
769
+ try {
770
+ if (typeof user.getUsersFields === 'function') {
771
+ usersData = await user.getUsersFields(uids, ['username', 'email']);
772
+ // Some NodeBB versions omit uid in returned rows; re-attach it from input order.
773
+ if (Array.isArray(usersData)) {
774
+ usersData = usersData.map((row, idx) => (row ? Object.assign({ uid: uids[idx] }, row) : null));
775
+ }
776
+ } else {
777
+ usersData = await Promise.all(uids.map(async (memberUid) => {
778
+ try { return await user.getUserFields(memberUid, ['username', 'email']); }
779
+ catch (e) { return null; }
780
+ }));
781
+ }
782
+ } catch (e) {
783
+ usersData = [];
784
+ }
785
+
786
+ for (const md of (usersData || [])) {
787
+ const memberUid = md && (md.uid || md.userId || md.userid || md.user_id);
773
788
  const u = parseInt(memberUid, 10);
774
- if (!Number.isInteger(u) || u <= 0) continue;
775
-
776
- await sendEmail('calendar-onekite_pending', u, 'Location matériel - Demande de réservation', {
777
- uid: u,
778
- requester: requester && requester.username ? requester.username : '',
779
- itemName: itemsLabel,
780
- itemNames: (itemNames && itemNames.length ? itemNames : itemIds),
781
- dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
782
- start: formatFR(start),
783
- end: formatFR(end),
784
- total: resv.total || 0,
785
- });
789
+ if (Number.isInteger(u) && u > 0) {
790
+ await sendEmail('calendar-onekite_pending', u, 'Location matériel - Demande de réservation', {
791
+ uid: u,
792
+ username: md && md.username ? md.username : '',
793
+ requester: requester.username,
794
+ itemName: itemsLabel,
795
+ itemNames: resv.itemNames || [],
796
+ dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
797
+ start: formatFR(start),
798
+ end: formatFR(end),
799
+ total: resv.total || 0,
800
+ });
801
+ }
786
802
  }
787
803
  }
788
804
  }
@@ -796,8 +812,8 @@ api.createReservation = async function (req, res) {
796
812
  rid: resv.rid,
797
813
  uid: resv.uid,
798
814
  username: resv.username || '',
799
- itemIds: itemIds || [],
800
- itemNames: itemNames || [],
815
+ itemIds: resv.itemIds || [],
816
+ itemNames: resv.itemNames || [],
801
817
  start: resv.start,
802
818
  end: resv.end,
803
819
  status: resv.status,
@@ -821,7 +837,8 @@ api.approveReservation = async function (req, res) {
821
837
  if (r.status !== 'pending') return res.status(400).json({ error: 'bad-status' });
822
838
 
823
839
  r.status = 'awaiting_payment';
824
- r.pickupAddress = String((req.body && req.body.pickupAddress) || '').trim();
840
+ // Backwards compatible: old clients sent `adminNote` to describe the pickup place.
841
+ r.pickupAddress = String((req.body && (req.body.pickupAddress || req.body.adminNote)) || '').trim();
825
842
  r.notes = String((req.body && req.body.notes) || '').trim();
826
843
  r.pickupTime = String((req.body && req.body.pickupTime) || '').trim();
827
844
  r.pickupLat = String((req.body && req.body.pickupLat) || '').trim();
@@ -861,11 +878,11 @@ api.approveReservation = async function (req, res) {
861
878
  // Can be overridden via ACP setting `helloassoCallbackUrl`.
862
879
  callbackUrl: normalizeReturnUrl(meta),
863
880
  webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
864
- itemName: buildHelloAssoItemName('', Array.isArray(r.itemNames) ? r.itemNames : [], r.start, r.end),
881
+ itemName: buildHelloAssoItemName('', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
865
882
  containsDonation: false,
866
883
  metadata: {
867
884
  reservationId: String(rid),
868
- items: (Array.isArray(r.itemNames) ? r.itemNames : []).filter(Boolean),
885
+ items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
869
886
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
870
887
  },
871
888
  });
@@ -897,8 +914,8 @@ api.approveReservation = async function (req, res) {
897
914
  await sendEmail('calendar-onekite_approved', requesterUid, 'Location matériel - Réservation validée', {
898
915
  uid: requesterUid,
899
916
  username: requester && requester.username ? requester.username : '',
900
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
901
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
917
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
918
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
902
919
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
903
920
  start: formatFR(r.start),
904
921
  end: formatFR(r.end),
@@ -938,8 +955,8 @@ api.refuseReservation = async function (req, res) {
938
955
  await sendEmail('calendar-onekite_refused', requesterUid2, 'Location matériel - Demande de réservation', {
939
956
  uid: requesterUid2,
940
957
  username: requester && requester.username ? requester.username : '',
941
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
942
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
958
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
959
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
943
960
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
944
961
  start: formatFR(r.start),
945
962
  end: formatFR(r.end),
@@ -1000,6 +1017,4 @@ api.cancelReservation = async function (req, res) {
1000
1017
  return res.json({ ok: true, status: 'cancelled' });
1001
1018
  };
1002
1019
 
1003
- module.exports = api;
1004
-
1005
- }
1020
+ module.exports = api;
@@ -32,11 +32,9 @@ async function sendEmail(template, uid, subject, data) {
32
32
  if (typeof emailer.send !== 'function') return;
33
33
  const params = Object.assign({}, data || {}, subject ? { subject } : {});
34
34
  // NodeBB 4.x: send(template, uid, params)
35
- if (emailer.send.length >= 3) {
36
- await emailer.send(template, toUid, params);
37
- } else {
38
- await emailer.send(template, toUid);
39
- }
35
+ // Do NOT branch on function.length: it is unreliable once wrapped/bound and
36
+ // can lead to params being dropped (empty email bodies / missing subjects).
37
+ await emailer.send(template, toUid, params);
40
38
  } catch (err) {
41
39
  // eslint-disable-next-line no-console
42
40
  console.warn('[calendar-onekite] Failed to send email (webhook)', { template, uid: toUid, err: String((err && err.message) || err) });
@@ -330,8 +328,8 @@ async function handler(req, res, next) {
330
328
  await sendEmail('calendar-onekite_paid', requesterUid, 'Location matériel - Paiement reçu', {
331
329
  uid: requesterUid,
332
330
  username: requester && requester.username ? requester.username : '',
333
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
334
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
331
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
332
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
335
333
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
336
334
  paymentReceiptUrl: r.paymentReceiptUrl || '',
337
335
  });
@@ -343,8 +341,8 @@ async function handler(req, res, next) {
343
341
  rid: r.rid,
344
342
  uid: r.uid,
345
343
  username: (requester && requester.username) ? requester.username : (r.username || ''),
346
- itemIds: Array.isArray(r.itemIds) ? r.itemIds : [],
347
- itemNames: Array.isArray(r.itemNames) ? r.itemNames : [],
344
+ itemIds: r.itemIds || (r.itemId ? [r.itemId] : []),
345
+ itemNames: r.itemNames || (r.itemName ? [r.itemName] : []),
348
346
  start: r.start,
349
347
  end: r.end,
350
348
  status: r.status,
package/lib/scheduler.js CHANGED
@@ -66,12 +66,9 @@ async function processAwaitingPayment() {
66
66
 
67
67
  try {
68
68
  if (typeof emailer.send !== 'function') return;
69
- // NodeBB: send(template, uid, params)
70
- if (emailer.send.length >= 3) {
71
- await emailer.send(template, toUid, params);
72
- } else {
73
- await emailer.send(template, toUid);
74
- }
69
+ // NodeBB 4.x: send(template, uid, params)
70
+ // Do NOT branch on function.length (unreliable once wrapped/bound).
71
+ await emailer.send(template, toUid, params);
75
72
  } catch (err) {
76
73
  // eslint-disable-next-line no-console
77
74
  console.warn('[calendar-onekite] Failed to send email (scheduler)', {
@@ -118,8 +115,8 @@ async function processAwaitingPayment() {
118
115
  await sendEmail('calendar-onekite_reminder', toUid, 'Location matériel - Rappel', {
119
116
  uid: toUid,
120
117
  username: (u && u.username) ? u.username : '',
121
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
122
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
118
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
119
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
123
120
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
124
121
  paymentUrl: r.paymentUrl || '',
125
122
  delayMinutes: holdMins,
@@ -152,8 +149,8 @@ async function processAwaitingPayment() {
152
149
  await sendEmail('calendar-onekite_expired', toUid, 'Location matériel - Rappel', {
153
150
  uid: toUid,
154
151
  username: (u && u.username) ? u.username : '',
155
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
156
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
152
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
153
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
157
154
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
158
155
  delayMinutes: holdMins,
159
156
  });
package/library.js CHANGED
@@ -20,26 +20,6 @@ const Plugin = {};
20
20
  const isFn = (fn) => typeof fn === 'function';
21
21
  const mw = (...fns) => fns.filter(isFn);
22
22
 
23
- // Express throws during route registration if any callback is undefined.
24
- // To keep NodeBB bootable and surface a clearer error, we wrap handlers and
25
- // provide a deterministic 500 handler when a target function is missing.
26
- function safeHandler(fn, name) {
27
- if (typeof fn !== 'function') {
28
- return function missingHandler(req, res) {
29
- // eslint-disable-next-line no-console
30
- console.error(`[calendar-onekite] Missing route handler: ${name}`);
31
- return res.status(500).json({ error: 'missing-handler', handler: name });
32
- };
33
- }
34
- return async function wrappedHandler(req, res, next) {
35
- try {
36
- return await fn(req, res, next);
37
- } catch (err) {
38
- return next(err);
39
- }
40
- };
41
- }
42
-
43
23
  Plugin.init = async function (params) {
44
24
  const { router, middleware } = params;
45
25
 
@@ -78,43 +58,44 @@ Plugin.init = async function (params) {
78
58
  // Page routes (HTML)
79
59
  // IMPORTANT: pass an ARRAY for middlewares (even if empty), otherwise
80
60
  // setupPageRoute will throw "middlewares is not iterable".
81
- routeHelpers.setupPageRoute(router, '/calendar', mw(), safeHandler(controllers.renderCalendar, 'controllers.renderCalendar'));
82
- routeHelpers.setupAdminPageRoute(router, '/admin/plugins/calendar-onekite', mw(), safeHandler(admin.renderAdmin, 'admin.renderAdmin'));
61
+ routeHelpers.setupPageRoute(router, '/calendar', mw(), controllers.renderCalendar);
62
+ routeHelpers.setupAdminPageRoute(router, '/admin/plugins/calendar-onekite', mw(), admin.renderAdmin);
83
63
 
84
64
  // Public API (JSON) — NodeBB 4.x only (v3 API)
85
- router.get('/api/v3/plugins/calendar-onekite/events', ...publicExpose, safeHandler(api.getEvents, 'api.getEvents'));
86
- router.get('/api/v3/plugins/calendar-onekite/items', ...publicExpose, safeHandler(api.getItems, 'api.getItems'));
87
- router.get('/api/v3/plugins/calendar-onekite/capabilities', ...publicExpose, safeHandler(api.getCapabilities, 'api.getCapabilities'));
65
+ router.get('/api/v3/plugins/calendar-onekite/events', ...publicExpose, api.getEvents);
66
+ router.get('/api/v3/plugins/calendar-onekite/items', ...publicExpose, api.getItems);
67
+ router.get('/api/v3/plugins/calendar-onekite/capabilities', ...publicExpose, api.getCapabilities);
88
68
 
89
- router.post('/api/v3/plugins/calendar-onekite/reservations', ...publicExpose, safeHandler(api.createReservation, 'api.createReservation'));
90
- router.get('/api/v3/plugins/calendar-onekite/reservations/:rid', ...publicExpose, safeHandler(api.getReservationDetails, 'api.getReservationDetails'));
91
- router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/approve', ...publicExpose, safeHandler(api.approveReservation, 'api.approveReservation'));
92
- router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/refuse', ...publicExpose, safeHandler(api.refuseReservation, 'api.refuseReservation'));
93
- router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/cancel', ...publicExpose, safeHandler(api.cancelReservation, 'api.cancelReservation'));
69
+ router.post('/api/v3/plugins/calendar-onekite/reservations', ...publicExpose, api.createReservation);
70
+ router.get('/api/v3/plugins/calendar-onekite/reservations/:rid', ...publicExpose, api.getReservationDetails);
71
+ router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/approve', ...publicExpose, api.approveReservation);
72
+ router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/refuse', ...publicExpose, api.refuseReservation);
73
+ router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/cancel', ...publicExpose, api.cancelReservation);
94
74
 
95
- router.post('/api/v3/plugins/calendar-onekite/special-events', ...publicExpose, safeHandler(api.createSpecialEvent, 'api.createSpecialEvent'));
96
- router.get('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, safeHandler(api.getSpecialEventDetails, 'api.getSpecialEventDetails'));
97
- router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, safeHandler(api.deleteSpecialEvent, 'api.deleteSpecialEvent'));
75
+ router.post('/api/v3/plugins/calendar-onekite/special-events', ...publicExpose, api.createSpecialEvent);
76
+ router.get('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.getSpecialEventDetails);
77
+ router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.deleteSpecialEvent);
98
78
 
99
79
  // Admin API (JSON)
100
80
  const adminBases = ['/api/v3/admin/plugins/calendar-onekite'];
101
81
 
102
82
  adminBases.forEach((base) => {
103
- router.get(`${base}/settings`, ...adminMws, safeHandler(admin.getSettings, 'admin.getSettings'));
104
- router.put(`${base}/settings`, ...adminMws, safeHandler(admin.saveSettings, 'admin.saveSettings'));
83
+ router.get(`${base}/settings`, ...adminMws, admin.getSettings);
84
+ router.put(`${base}/settings`, ...adminMws, admin.saveSettings);
105
85
 
106
- router.get(`${base}/pending`, ...adminMws, safeHandler(admin.listPending, 'admin.listPending'));
107
- router.put(`${base}/reservations/:rid/approve`, ...adminMws, safeHandler(admin.approveReservation, 'admin.approveReservation'));
108
- router.put(`${base}/reservations/:rid/refuse`, ...adminMws, safeHandler(admin.refuseReservation, 'admin.refuseReservation'));
86
+ router.get(`${base}/pending`, ...adminMws, admin.listPending);
87
+ router.put(`${base}/reservations/:rid/approve`, ...adminMws, admin.approveReservation);
88
+ router.put(`${base}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
109
89
 
110
- router.post(`${base}/purge`, ...adminMws, safeHandler(admin.purgeByYear, 'admin.purgeByYear'));
90
+ router.post(`${base}/purge`, ...adminMws, admin.purgeByYear);
91
+ router.get(`${base}/debug`, ...adminMws, admin.debugHelloAsso);
111
92
  // Accounting / exports
112
- router.get(`${base}/accounting`, ...adminMws, safeHandler(admin.getAccounting, 'admin.getAccounting'));
113
- router.get(`${base}/accounting.csv`, ...adminMws, safeHandler(admin.exportAccountingCsv, 'admin.exportAccountingCsv'));
114
- router.post(`${base}/accounting/purge`, ...adminMws, safeHandler(admin.purgeAccounting, 'admin.purgeAccounting'));
93
+ router.get(`${base}/accounting`, ...adminMws, admin.getAccounting);
94
+ router.get(`${base}/accounting.csv`, ...adminMws, admin.exportAccountingCsv);
95
+ router.post(`${base}/accounting/purge`, ...adminMws, admin.purgeAccounting);
115
96
 
116
97
  // Purge special events by year
117
- router.post(`${base}/special-events/purge`, ...adminMws, safeHandler(admin.purgeSpecialEventsByYear, 'admin.purgeSpecialEventsByYear'));
98
+ router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
118
99
  });
119
100
 
120
101
  // HelloAsso callback endpoint (hardened)
@@ -128,8 +109,13 @@ Plugin.init = async function (params) {
128
109
  },
129
110
  type: ['application/json', 'application/*+json'],
130
111
  });
131
- router.post('/plugins/calendar-onekite/helloasso', helloassoJson, safeHandler(helloassoWebhook.handler, 'helloassoWebhook.handler'));
112
+ // Accept webhook on both legacy root path and namespaced plugin path.
113
+ // Some reverse proxies block unknown root paths, so /plugins/... is recommended.
114
+ router.post('/helloasso', helloassoJson, helloassoWebhook.handler);
115
+ router.post('/plugins/calendar-onekite/helloasso', helloassoJson, helloassoWebhook.handler);
132
116
 
117
+ // Optional: health checks
118
+ router.get('/helloasso', (req, res) => res.json({ ok: true }));
133
119
  router.get('/plugins/calendar-onekite/helloasso', (req, res) => res.json({ ok: true }));
134
120
 
135
121
  scheduler.start();
@@ -147,7 +133,8 @@ Plugin.addAdminNavigation = async function (header) {
147
133
 
148
134
 
149
135
  // Ensure our transactional emails always get a subject.
150
- // Ensure our transactional emails always get a subject (NodeBB email hook).
136
+ // NodeBB's Emailer.sendToEmail signature expects (template, email, language, params),
137
+ // so plugins typically inject/modify the subject via this hook.
151
138
  Plugin.emailModify = async function (data) {
152
139
  try {
153
140
  if (!data || !data.template) return data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "1.0.25"
42
+ "version": "1.0.26"
43
43
  }
package/public/admin.js CHANGED
@@ -221,14 +221,14 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
221
221
  for (const r of list) {
222
222
  if (r && r.rid) pendingCache.set(String(r.rid), r);
223
223
  const created = r.createdAt ? fmtFR(r.createdAt) : '';
224
- const itemNames = Array.isArray(r.itemNames) ? r.itemNames : [];
224
+ const itemNames = Array.isArray(r.itemNames) && r.itemNames.length ? r.itemNames : [r.itemName || r.itemId].filter(Boolean);
225
225
  const itemsHtml = `<ul style="margin: 0 0 10px 18px;">${itemNames.map(n => `<li>${escapeHtml(String(n))}</li>`).join('')}</ul>`;
226
226
  const div = document.createElement('div');
227
227
  div.className = 'list-group-item onekite-pending-row';
228
228
  div.innerHTML = `
229
229
  <div class="d-flex justify-content-between align-items-start gap-2">
230
230
  <div style="min-width: 0;">
231
- <div><strong>${itemsHtml}</strong></div>
231
+ <div><strong>${itemsHtml || escapeHtml(r.itemName || '')}</strong></div>
232
232
  <div class="text-muted" style="font-size: 12px;">Créée: ${escapeHtml(created)}</div>
233
233
  <div class="text-muted" style="font-size: 12px;">Période: ${escapeHtml(new Date(parseInt(r.start, 10)).toLocaleDateString('fr-FR'))} → ${escapeHtml(new Date(parseInt(r.end, 10)).toLocaleDateString('fr-FR'))}</div>
234
234
  </div>
@@ -492,7 +492,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
492
492
  ? r.itemNames
493
493
  : (typeof r.itemNames === 'string' && r.itemNames.trim()
494
494
  ? r.itemNames.split(',').map(s => s.trim()).filter(Boolean)
495
- : ([]));
495
+ : ([r.itemName || r.itemId].filter(Boolean)));
496
496
  const itemsListHtml = itemNames.length
497
497
  ? `<div class="mb-2"><strong>Matériel</strong><ul style="margin:0.25rem 0 0 1.1rem; padding:0;">${itemNames.map(n => `<li>${escapeHtml(String(n))}</li>`).join('')}</ul></div>`
498
498
  : '';
package/public/client.js CHANGED
@@ -787,7 +787,7 @@ function toDatetimeLocalValue(date) {
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;
790
- const ids = (ev.extendedProps && ev.extendedProps.itemIds) || [];
790
+ const ids = (ev.extendedProps && ev.extendedProps.itemIds) || (ev.extendedProps && ev.extendedProps.itemId ? [ev.extendedProps.itemId] : []);
791
791
  ids.forEach((id) => blocked.add(String(id)));
792
792
  });
793
793
  } catch (e) {}
@@ -1300,7 +1300,12 @@ function toDatetimeLocalValue(date) {
1300
1300
  p = Object.assign({}, p0, details);
1301
1301
  } else if (p0.type === 'special' && p0.eid) {
1302
1302
  const details = await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(String(p0.eid))}`);
1303
- p = Object.assign({}, p0, details);
1303
+ p = Object.assign({}, p0, details, {
1304
+ // keep backward compat with older field names used by templates below
1305
+ pickupAddress: details.address || details.pickupAddress || p0.pickupAddress,
1306
+ pickupLat: details.lat || details.pickupLat || p0.pickupLat,
1307
+ pickupLon: details.lon || details.pickupLon || p0.pickupLon,
1308
+ });
1304
1309
  }
1305
1310
  } catch (e) {
1306
1311
  // ignore detail fetch errors; fall back to minimal props
@@ -1365,7 +1370,7 @@ function toDatetimeLocalValue(date) {
1365
1370
  ? `<div class="mb-2"><strong>Réservée par</strong><br><a class="onekite-user-link" href="${window.location.origin}/user/${encodeURIComponent(username)}">${escapeHtml(username)}</a></div>`
1366
1371
  : '';
1367
1372
  const itemsHtml = (() => {
1368
- const names = Array.isArray(p.itemNames) ? p.itemNames : (typeof p.itemNames === 'string' && p.itemNames.trim() ? p.itemNames.split(',').map(s=>s.trim()).filter(Boolean) : []);
1373
+ const names = Array.isArray(p.itemNames) ? p.itemNames : (typeof p.itemNames === 'string' && p.itemNames.trim() ? p.itemNames.split(',').map(s=>s.trim()).filter(Boolean) : (p.itemName ? [p.itemName] : []));
1369
1374
  if (names.length) {
1370
1375
  return `<ul style="margin:0 0 0 1.1rem; padding:0;">${names.map(n => `<li>${String(n).replace(/</g,'&lt;').replace(/>/g,'&gt;')}</li>`).join('')}</ul>`;
1371
1376
  }
@@ -1500,7 +1505,7 @@ function toDatetimeLocalValue(date) {
1500
1505
  callback: async () => {
1501
1506
  const itemNames = Array.isArray(p.itemNames) && p.itemNames.length
1502
1507
  ? p.itemNames
1503
- : (typeof p.itemNames === 'string' && p.itemNames.trim() ? p.itemNames.split(',').map(s=>s.trim()).filter(Boolean) : []);
1508
+ : (typeof p.itemNames === 'string' && p.itemNames.trim() ? p.itemNames.split(',').map(s=>s.trim()).filter(Boolean) : (p.itemName ? [p.itemName] : []));
1504
1509
  const itemsListHtml = itemNames.length
1505
1510
  ? `<div class="mb-2"><strong>Matériel</strong><ul style="margin:0.25rem 0 0 1.1rem; padding:0;">${itemNames.map(n => `<li>${String(n).replace(/</g, '&lt;').replace(/>/g, '&gt;')}</li>`).join('')}</ul></div>`
1506
1511
  : '';