nodebb-plugin-onekite-calendar 1.0.24 → 1.0.26

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
@@ -162,11 +162,11 @@ admin.approveReservation = async function (req, res) {
162
162
  // User return/back/error URLs must be real pages; webhook uses the plugin endpoint.
163
163
  callbackUrl: returnUrl,
164
164
  webhookUrl: webhookUrl,
165
- itemName: buildHelloAssoItemName('Réservation matériel Onekite', Array.isArray(r.itemNames) ? r.itemNames : [], r.start, r.end),
165
+ itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
166
166
  containsDonation: false,
167
167
  metadata: {
168
168
  reservationId: String(rid),
169
- items: (Array.isArray(r.itemNames) ? r.itemNames : []).filter(Boolean),
169
+ items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
170
170
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
171
171
  },
172
172
  });
@@ -199,8 +199,8 @@ admin.approveReservation = async function (req, res) {
199
199
  await sendEmail('calendar-onekite_approved', requesterUid, 'Location matériel - Réservation validée', {
200
200
  uid: requesterUid,
201
201
  username: requester && requester.username ? requester.username : '',
202
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
203
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
202
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
203
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
204
204
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
205
205
  paymentUrl: paymentUrl || '',
206
206
  pickupAddress: r.pickupAddress || '',
@@ -235,8 +235,8 @@ admin.refuseReservation = async function (req, res) {
235
235
  await sendEmail('calendar-onekite_refused', requesterUid, 'Location matériel - Réservation refusée', {
236
236
  uid: requesterUid,
237
237
  username: requester && requester.username ? requester.username : '',
238
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
239
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
238
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
239
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
240
240
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
241
241
  start: formatFR(r.start),
242
242
  end: formatFR(r.end),
@@ -419,7 +419,7 @@ admin.getAccounting = async function (req, res) {
419
419
 
420
420
  const itemNames = Array.isArray(r.itemNames) && r.itemNames.length
421
421
  ? r.itemNames
422
- : [];
422
+ : (r.itemName ? [r.itemName] : []);
423
423
 
424
424
  const total = Number(r.total) || 0;
425
425
  const startDate = formatFR(r.start);
package/lib/api.js CHANGED
@@ -328,18 +328,15 @@ function eventsFor(resv) {
328
328
  const startIsoDate = new Date(parseInt(resv.start, 10)).toISOString().slice(0, 10);
329
329
  const endIsoDate = new Date(parseInt(resv.end, 10)).toISOString().slice(0, 10);
330
330
 
331
- const itemIds = Array.isArray(resv.itemIds) ? resv.itemIds.filter(Boolean) : [];
332
- const itemNames = Array.isArray(resv.itemNames) ? resv.itemNames.filter(Boolean) : [];
331
+ const itemIds = Array.isArray(resv.itemIds) ? resv.itemIds : (resv.itemId ? [resv.itemId] : []);
332
+ const itemNames = Array.isArray(resv.itemNames) ? resv.itemNames : (resv.itemName ? [resv.itemName] : []);
333
333
 
334
334
  // One line = one material: return one calendar event per item
335
- if (!itemIds.length && !itemNames.length) {
336
- return [];
337
- }
338
335
  const out = [];
339
- const count = Math.max(itemIds.length, itemNames.length);
336
+ const count = Math.max(itemIds.length, itemNames.length, 1);
340
337
  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);
338
+ const itemId = String(itemIds[i] || itemIds[0] || resv.itemId || '');
339
+ const itemName = String(itemNames[i] || itemNames[0] || resv.itemName || itemId);
343
340
  out.push({
344
341
  // keep id unique per item for FullCalendar, but keep the real rid in extendedProps.rid
345
342
  id: `${resv.rid}:${itemId || i}`,
@@ -356,14 +353,15 @@ function eventsFor(resv) {
356
353
  uid: resv.uid,
357
354
  approvedBy: resv.approvedBy || 0,
358
355
  approvedByUsername: resv.approvedByUsername || '',
359
- itemIds: itemIds,
360
- itemNames: itemNames,
356
+ itemIds: itemIds.filter(Boolean),
357
+ itemNames: itemNames.filter(Boolean),
361
358
  itemIdLine: itemId,
362
359
  itemNameLine: itemName,
363
360
  },
364
361
  });
365
362
  }
366
363
  return out;
364
+ }
367
365
 
368
366
  function eventsForSpecial(ev) {
369
367
  const start = new Date(parseInt(ev.start, 10));
@@ -533,8 +531,8 @@ api.getReservationDetails = async function (req, res) {
533
531
  status: r.status,
534
532
  uid: r.uid,
535
533
  username: r.username || '',
536
- itemNames: Array.isArray(r.itemNames) ? r.itemNames : [],
537
- itemIds: Array.isArray(r.itemIds) ? r.itemIds : [],
534
+ itemNames: Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : []),
535
+ itemIds: Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : []),
538
536
  start: r.start,
539
537
  end: r.end,
540
538
  approvedByUsername: r.approvedByUsername || '',
@@ -698,9 +696,9 @@ api.createReservation = async function (req, res) {
698
696
  return res.status(400).json({ error: "bad-dates" });
699
697
  }
700
698
 
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) : [];
699
+ // Support both legacy single itemId and new itemIds[] payload
700
+ const itemIds = Array.isArray(req.body.itemIds) ? req.body.itemIds.map(String) : ((req.body.itemId ? [String(req.body.itemId)] : []));
701
+ const itemNames = Array.isArray(req.body.itemNames) ? req.body.itemNames.map(String) : (req.body.itemName ? [String(req.body.itemName)] : []);
704
702
 
705
703
  const total = typeof req.body.total === 'number' ? req.body.total : parseFloat(String(req.body.total || '0'));
706
704
 
@@ -719,7 +717,7 @@ api.createReservation = async function (req, res) {
719
717
  const exStart = parseInt(existing.start, 10);
720
718
  const exEnd = parseInt(existing.end, 10);
721
719
  if (!(exStart < end && start < exEnd)) continue;
722
- const exItemIds = Array.isArray(existing.itemIds) ? existing.itemIds : [];
720
+ const exItemIds = Array.isArray(existing.itemIds) ? existing.itemIds : (existing.itemId ? [existing.itemId] : []);
723
721
  const shared = exItemIds.filter(x => itemIds.includes(String(x)));
724
722
  if (shared.length) {
725
723
  conflicts.push({ rid: existing.rid, itemIds: shared, status: existing.status });
@@ -744,6 +742,9 @@ api.createReservation = async function (req, res) {
744
742
  username: username || null,
745
743
  itemIds,
746
744
  itemNames: itemNames.length ? itemNames : itemIds,
745
+ // keep legacy fields for backward compatibility
746
+ itemId: itemIds[0],
747
+ itemName: (itemNames[0] || itemIds[0]),
747
748
  start,
748
749
  end,
749
750
  status: 'pending',
@@ -754,35 +755,51 @@ api.createReservation = async function (req, res) {
754
755
  // Save
755
756
  await dbLayer.saveReservation(resv);
756
757
 
757
- // Notify groups by email (NodeBB 4.x, UID-only)
758
+ // Notify groups by email (NodeBB emailer config)
758
759
  try {
759
- const notifyGroups = String(settings.notifyGroups || '')
760
- .split(/[\n,;]+/)
761
- .map(s => s.trim())
762
- .filter(Boolean);
763
-
760
+ const notifyGroups = (settings.notifyGroups || '').split(',').map(s => s.trim()).filter(Boolean);
764
761
  if (notifyGroups.length) {
765
- const requester = await user.getUserFields(uid, ['username']);
766
- const itemsLabel = (itemNames && itemNames.length ? itemNames : itemIds).join(', ');
767
-
762
+ const requester = await user.getUserFields(uid, ['username', 'email']);
763
+ const itemsLabel = (resv.itemNames || []).join(', ');
768
764
  for (const g of notifyGroups) {
769
765
  const members = await getMembersByGroupIdentifier(g);
770
766
  const uids = normalizeUids(members);
771
767
 
772
- for (const memberUid of uids) {
768
+ // Batch fetch user email/username when supported by this NodeBB version.
769
+ let usersData = [];
770
+ try {
771
+ if (typeof user.getUsersFields === 'function') {
772
+ usersData = await user.getUsersFields(uids, ['username', 'email']);
773
+ // Some NodeBB versions omit uid in returned rows; re-attach it from input order.
774
+ if (Array.isArray(usersData)) {
775
+ usersData = usersData.map((row, idx) => (row ? Object.assign({ uid: uids[idx] }, row) : null));
776
+ }
777
+ } else {
778
+ usersData = await Promise.all(uids.map(async (memberUid) => {
779
+ try { return await user.getUserFields(memberUid, ['username', 'email']); }
780
+ catch (e) { return null; }
781
+ }));
782
+ }
783
+ } catch (e) {
784
+ usersData = [];
785
+ }
786
+
787
+ for (const md of (usersData || [])) {
788
+ const memberUid = md && (md.uid || md.userId || md.userid || md.user_id);
773
789
  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
- });
790
+ if (Number.isInteger(u) && u > 0) {
791
+ await sendEmail('calendar-onekite_pending', u, 'Location matériel - Demande de réservation', {
792
+ uid: u,
793
+ username: md && md.username ? md.username : '',
794
+ requester: requester.username,
795
+ itemName: itemsLabel,
796
+ itemNames: resv.itemNames || [],
797
+ dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
798
+ start: formatFR(start),
799
+ end: formatFR(end),
800
+ total: resv.total || 0,
801
+ });
802
+ }
786
803
  }
787
804
  }
788
805
  }
@@ -796,8 +813,8 @@ api.createReservation = async function (req, res) {
796
813
  rid: resv.rid,
797
814
  uid: resv.uid,
798
815
  username: resv.username || '',
799
- itemIds: itemIds || [],
800
- itemNames: itemNames || [],
816
+ itemIds: resv.itemIds || [],
817
+ itemNames: resv.itemNames || [],
801
818
  start: resv.start,
802
819
  end: resv.end,
803
820
  status: resv.status,
@@ -821,7 +838,8 @@ api.approveReservation = async function (req, res) {
821
838
  if (r.status !== 'pending') return res.status(400).json({ error: 'bad-status' });
822
839
 
823
840
  r.status = 'awaiting_payment';
824
- r.pickupAddress = String((req.body && req.body.pickupAddress) || '').trim();
841
+ // Backwards compatible: old clients sent `adminNote` to describe the pickup place.
842
+ r.pickupAddress = String((req.body && (req.body.pickupAddress || req.body.adminNote)) || '').trim();
825
843
  r.notes = String((req.body && req.body.notes) || '').trim();
826
844
  r.pickupTime = String((req.body && req.body.pickupTime) || '').trim();
827
845
  r.pickupLat = String((req.body && req.body.pickupLat) || '').trim();
@@ -861,11 +879,11 @@ api.approveReservation = async function (req, res) {
861
879
  // Can be overridden via ACP setting `helloassoCallbackUrl`.
862
880
  callbackUrl: normalizeReturnUrl(meta),
863
881
  webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
864
- itemName: buildHelloAssoItemName('', Array.isArray(r.itemNames) ? r.itemNames : [], r.start, r.end),
882
+ itemName: buildHelloAssoItemName('', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
865
883
  containsDonation: false,
866
884
  metadata: {
867
885
  reservationId: String(rid),
868
- items: (Array.isArray(r.itemNames) ? r.itemNames : []).filter(Boolean),
886
+ items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
869
887
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
870
888
  },
871
889
  });
@@ -897,8 +915,8 @@ api.approveReservation = async function (req, res) {
897
915
  await sendEmail('calendar-onekite_approved', requesterUid, 'Location matériel - Réservation validée', {
898
916
  uid: requesterUid,
899
917
  username: requester && requester.username ? requester.username : '',
900
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
901
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
918
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
919
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
902
920
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
903
921
  start: formatFR(r.start),
904
922
  end: formatFR(r.end),
@@ -938,8 +956,8 @@ api.refuseReservation = async function (req, res) {
938
956
  await sendEmail('calendar-onekite_refused', requesterUid2, 'Location matériel - Demande de réservation', {
939
957
  uid: requesterUid2,
940
958
  username: requester && requester.username ? requester.username : '',
941
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
942
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
959
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
960
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
943
961
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
944
962
  start: formatFR(r.start),
945
963
  end: formatFR(r.end),
@@ -1000,6 +1018,4 @@ api.cancelReservation = async function (req, res) {
1000
1018
  return res.json({ ok: true, status: 'cancelled' });
1001
1019
  };
1002
1020
 
1003
- module.exports = api;
1004
-
1005
- }
1021
+ module.exports = api;
@@ -330,8 +330,8 @@ async function handler(req, res, next) {
330
330
  await sendEmail('calendar-onekite_paid', requesterUid, 'Location matériel - Paiement reçu', {
331
331
  uid: requesterUid,
332
332
  username: requester && requester.username ? requester.username : '',
333
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
334
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
333
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
334
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
335
335
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
336
336
  paymentReceiptUrl: r.paymentReceiptUrl || '',
337
337
  });
@@ -343,8 +343,8 @@ async function handler(req, res, next) {
343
343
  rid: r.rid,
344
344
  uid: r.uid,
345
345
  username: (requester && requester.username) ? requester.username : (r.username || ''),
346
- itemIds: Array.isArray(r.itemIds) ? r.itemIds : [],
347
- itemNames: Array.isArray(r.itemNames) ? r.itemNames : [],
346
+ itemIds: r.itemIds || (r.itemId ? [r.itemId] : []),
347
+ itemNames: r.itemNames || (r.itemName ? [r.itemName] : []),
348
348
  start: r.start,
349
349
  end: r.end,
350
350
  status: r.status,
package/lib/scheduler.js CHANGED
@@ -118,8 +118,8 @@ async function processAwaitingPayment() {
118
118
  await sendEmail('calendar-onekite_reminder', toUid, 'Location matériel - Rappel', {
119
119
  uid: toUid,
120
120
  username: (u && u.username) ? u.username : '',
121
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
122
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
121
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
122
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
123
123
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
124
124
  paymentUrl: r.paymentUrl || '',
125
125
  delayMinutes: holdMins,
@@ -152,8 +152,8 @@ async function processAwaitingPayment() {
152
152
  await sendEmail('calendar-onekite_expired', toUid, 'Location matériel - Rappel', {
153
153
  uid: toUid,
154
154
  username: (u && u.username) ? u.username : '',
155
- itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
156
- itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
155
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
156
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
157
157
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
158
158
  delayMinutes: holdMins,
159
159
  });
package/library.js CHANGED
@@ -109,8 +109,13 @@ Plugin.init = async function (params) {
109
109
  },
110
110
  type: ['application/json', 'application/*+json'],
111
111
  });
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);
112
115
  router.post('/plugins/calendar-onekite/helloasso', helloassoJson, helloassoWebhook.handler);
113
116
 
117
+ // Optional: health checks
118
+ router.get('/helloasso', (req, res) => res.json({ ok: true }));
114
119
  router.get('/plugins/calendar-onekite/helloasso', (req, res) => res.json({ ok: true }));
115
120
 
116
121
  scheduler.start();
@@ -128,7 +133,8 @@ Plugin.addAdminNavigation = async function (header) {
128
133
 
129
134
 
130
135
  // Ensure our transactional emails always get a subject.
131
- // 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.
132
138
  Plugin.emailModify = async function (data) {
133
139
  try {
134
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.24",
3
+ "version": "1.0.26",
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.24"
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
  : '';