nodebb-plugin-onekite-calendar 2.0.68 → 2.0.70

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/api.js CHANGED
@@ -647,6 +647,7 @@ api.getSpecialEventDetails = async function (req, res) {
647
647
  const settings = await getSettings();
648
648
  const canMod = uid ? await canValidate(uid, settings) : false;
649
649
  const canSpecialDelete = uid ? await canDeleteSpecial(uid, settings) : false;
650
+ const canSpecialCreate = uid ? await canCreateSpecial(uid, settings) : false;
650
651
 
651
652
  const eid = String(req.params.eid || '').trim();
652
653
  if (!eid) return res.status(400).json({ error: 'missing-eid' });
@@ -680,6 +681,7 @@ api.getSpecialEventDetails = async function (req, res) {
680
681
  notes: ev.notes || '',
681
682
  participants,
682
683
  participantsUsernames: await usernamesByUids(participants),
684
+ canEditSpecial: canSpecialCreate,
683
685
  canDeleteSpecial: canSpecialDelete,
684
686
  canJoin: uid ? await canJoinSpecial(uid, settings) : false,
685
687
  isParticipant: uid ? participants.includes(String(uid)) : false,
@@ -871,6 +873,37 @@ api.deleteSpecialEvent = async function (req, res) {
871
873
  res.json({ ok: true });
872
874
  };
873
875
 
876
+ api.updateSpecialEvent = async function (req, res) {
877
+ const settings = await getSettings();
878
+ if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
879
+ const ok = await canCreateSpecial(req.uid, settings);
880
+ if (!ok) return res.status(403).json({ error: 'not-allowed' });
881
+
882
+ const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
883
+ if (!eid) return res.status(400).json({ error: 'bad-id' });
884
+
885
+ const ev = await dbLayer.getSpecialEvent(eid);
886
+ if (!ev) return res.status(404).json({ error: 'not-found' });
887
+
888
+ const startTs = toTs(req.body && req.body.start);
889
+ const endTs = toTs(req.body && req.body.end);
890
+ if (!Number.isFinite(startTs) || !Number.isFinite(endTs) || !(startTs < endTs)) {
891
+ return res.status(400).json({ error: 'bad-dates' });
892
+ }
893
+
894
+ ev.title = String((req.body && req.body.title) || '').trim() || ev.title || 'Évènement';
895
+ ev.start = String(startTs);
896
+ ev.end = String(endTs);
897
+ ev.address = String((req.body && req.body.address) || '').trim();
898
+ ev.lat = String((req.body && req.body.lat) || '').trim();
899
+ ev.lon = String((req.body && req.body.lon) || '').trim();
900
+ ev.notes = String((req.body && req.body.notes) || '').trim();
901
+
902
+ await dbLayer.saveSpecialEvent(ev);
903
+ realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'updated', eid });
904
+ return res.json({ ok: true });
905
+ };
906
+
874
907
  /**
875
908
  * Get detailed information about an outing (prévision de sortie).
876
909
  *
@@ -911,6 +944,7 @@ api.getOutingDetails = async function (req, res) {
911
944
  });
912
945
 
913
946
  const participants = normalizeUidList(o.participants);
947
+ const canEditOuting = uid ? await canRequest(uid, settings, Date.now()) : false;
914
948
  const out = {
915
949
  oid: o.oid,
916
950
  title: o.title || '',
@@ -922,7 +956,8 @@ api.getOutingDetails = async function (req, res) {
922
956
  notes: o.notes || '',
923
957
  participants,
924
958
  participantsUsernames: await usernamesByUids(participants),
925
- canJoin: uid ? await canRequest(uid, settings, Date.now()) : false,
959
+ canEditOuting,
960
+ canJoin: canEditOuting,
926
961
  isParticipant: uid ? participants.includes(String(uid)) : false,
927
962
  canDeleteOuting: canMod || (uid && String(uid) === String(o.uid)),
928
963
  icsUrl: links.icsUrl,
@@ -1090,6 +1125,37 @@ api.deleteOuting = async function (req, res) {
1090
1125
  return res.json({ ok: true });
1091
1126
  };
1092
1127
 
1128
+ api.updateOuting = async function (req, res) {
1129
+ const settings = await getSettings();
1130
+ if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
1131
+ const ok = await canRequest(req.uid, settings, Date.now());
1132
+ if (!ok) return res.status(403).json({ error: 'not-allowed' });
1133
+
1134
+ const oid = String(req.params.oid || '').replace(/^outing:/, '').trim();
1135
+ if (!oid) return res.status(400).json({ error: 'bad-id' });
1136
+
1137
+ const o = await dbLayer.getOuting(oid);
1138
+ if (!o) return res.status(404).json({ error: 'not-found' });
1139
+
1140
+ const startTs = toTs(req.body && req.body.start);
1141
+ const endTs = toTs(req.body && req.body.end);
1142
+ if (!Number.isFinite(startTs) || !Number.isFinite(endTs) || !(startTs < endTs)) {
1143
+ return res.status(400).json({ error: 'bad-dates' });
1144
+ }
1145
+
1146
+ o.title = String((req.body && req.body.title) || '').trim() || o.title || 'Sortie';
1147
+ o.start = String(startTs);
1148
+ o.end = String(endTs);
1149
+ o.address = String((req.body && req.body.address) || '').trim();
1150
+ o.lat = String((req.body && req.body.lat) || '').trim();
1151
+ o.lon = String((req.body && req.body.lon) || '').trim();
1152
+ o.notes = String((req.body && req.body.notes) || '').trim();
1153
+
1154
+ await dbLayer.saveOuting(o);
1155
+ realtime.emitCalendarUpdated({ kind: 'outing', action: 'updated', oid });
1156
+ return res.json({ ok: true });
1157
+ };
1158
+
1093
1159
  api.getItems = async function (req, res) {
1094
1160
  const settings = await getSettings();
1095
1161
 
package/lib/discord.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const https = require('https');
4
4
  const { URL } = require('url');
5
- const { formatFRShort } = require('./shared');
5
+ const { formatFRShort, forumBaseUrl } = require('./shared');
6
6
 
7
7
  function isEnabled(v, defaultValue) {
8
8
  if (v === undefined || v === null || v === '') return defaultValue !== false;
@@ -53,7 +53,7 @@ function postWebhook(webhookUrl, payload) {
53
53
  }
54
54
 
55
55
  function buildReservationMessage(kind, reservation) {
56
- const calUrl = 'https://www.onekite.com/calendar';
56
+ const calUrl = forumBaseUrl() + '/calendar';
57
57
  const username = reservation && reservation.username ? String(reservation.username) : '';
58
58
  const items = (reservation && Array.isArray(reservation.itemNames) && reservation.itemNames.length)
59
59
  ? reservation.itemNames.map(String)
@@ -87,7 +87,7 @@ function buildWebhookPayload(kind, reservation) {
87
87
  ? 'Onekite • Paiement'
88
88
  : (kind === 'cancelled' ? 'Onekite • Annulation' : 'Onekite • Réservation');
89
89
 
90
- const calUrl = 'https://www.onekite.com/calendar';
90
+ const calUrl = forumBaseUrl() + '/calendar';
91
91
  const username = reservation && reservation.username ? String(reservation.username) : '';
92
92
  const items = (reservation && Array.isArray(reservation.itemNames) && reservation.itemNames.length)
93
93
  ? reservation.itemNames.map(String)
@@ -186,7 +186,7 @@ async function notifyReservationCancelled(settings, reservation) {
186
186
 
187
187
  function buildSimpleCalendarPayload(kind, label, entity, opts) {
188
188
  const options = opts || {};
189
- const calUrl = options.calUrl || 'https://www.onekite.com/calendar';
189
+ const calUrl = options.calUrl || forumBaseUrl() + '/calendar';
190
190
  const webhookUsername = options.webhookUsername || `Onekite • ${label}`;
191
191
 
192
192
  const title = kind === 'deleted' ? '❌ ' + label + ' annulé(e)' : label + ' créé(e)';
package/lib/shared.js CHANGED
@@ -44,6 +44,7 @@ function hmacSecret() {
44
44
  const s = String(nconf.get('secret') || '').trim();
45
45
  if (s) return s;
46
46
  } catch (_) { /* ignore */ }
47
+ console.warn('[calendar-onekite] nconf.secret not set — ICS URLs use weak fallback key');
47
48
  return 'calendar-onekite';
48
49
  }
49
50
 
package/lib/widgets.js CHANGED
@@ -17,8 +17,7 @@ function makeDomId() {
17
17
  }
18
18
 
19
19
  function widgetCalendarUrl() {
20
- // Per request, keep the public URL fixed (even if forum base differs)
21
- return 'https://www.onekite.com/calendar';
20
+ return forumBaseUrl() + '/calendar';
22
21
  }
23
22
 
24
23
  const widgets = {};
package/library.js CHANGED
@@ -82,6 +82,7 @@ Plugin.init = async function (params) {
82
82
  router.post('/api/v3/plugins/calendar-onekite/special-events', ...publicExpose, api.createSpecialEvent);
83
83
  router.get('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.getSpecialEventDetails);
84
84
  router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.deleteSpecialEvent);
85
+ router.put('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.updateSpecialEvent);
85
86
  // Participants (self-join) for special events
86
87
  router.post('/api/v3/plugins/calendar-onekite/special-events/:eid/participants', ...publicExpose, api.joinSpecialEvent);
87
88
  router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid/participants', ...publicExpose, api.leaveSpecialEvent);
@@ -90,6 +91,7 @@ Plugin.init = async function (params) {
90
91
  router.post('/api/v3/plugins/calendar-onekite/outings', ...publicExpose, api.createOuting);
91
92
  router.get('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.getOutingDetails);
92
93
  router.delete('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.deleteOuting);
94
+ router.put('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.updateOuting);
93
95
  // Participants (self-join) for outings
94
96
  router.post('/api/v3/plugins/calendar-onekite/outings/:oid/participants', ...publicExpose, api.joinOuting);
95
97
  router.delete('/api/v3/plugins/calendar-onekite/outings/:oid/participants', ...publicExpose, api.leaveOuting);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.68",
3
+ "version": "2.0.70",
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": "2.0.66"
42
+ "version": "2.0.70"
43
43
  }
package/public/client.js CHANGED
@@ -239,8 +239,32 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
239
239
  return opts.join('');
240
240
  }
241
241
 
242
+ function buildInitialValuesFromEvent(details) {
243
+ function toLocal(ts) {
244
+ const d = new Date(parseInt(ts, 10));
245
+ return {
246
+ ymd: `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`,
247
+ time: `${pad2(d.getHours())}:${pad2(d.getMinutes())}`,
248
+ };
249
+ }
250
+ const s = toLocal(details.start);
251
+ const e = toLocal(details.end);
252
+ return {
253
+ title: details.title || '',
254
+ startYmd: s.ymd,
255
+ startTime: s.time,
256
+ endYmd: e.ymd,
257
+ endTime: e.time,
258
+ address: details.address || details.pickupAddress || '',
259
+ lat: details.lat || details.pickupLat || '',
260
+ lon: details.lon || details.pickupLon || '',
261
+ notes: details.notes || '',
262
+ };
263
+ }
264
+
242
265
  async function openSpecialEventDialog(selectionInfo, opts) {
243
266
  const kind = (opts && opts.kind) ? String(opts.kind) : 'special';
267
+ const iv = opts && opts.initialValues;
244
268
  const start = selectionInfo.start;
245
269
  // FullCalendar can omit `end` for certain interactions. Also, for all-day
246
270
  // selections, `end` is exclusive (next day at 00:00). We normalise below
@@ -289,12 +313,20 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
289
313
  }
290
314
  }
291
315
 
316
+ // Edit mode: override computed dates with existing event values.
317
+ if (iv) {
318
+ if (iv.startYmd && iv.startTime) seStart = new Date(`${iv.startYmd}T${iv.startTime}`);
319
+ if (iv.endYmd && iv.endTime) seEnd = new Date(`${iv.endYmd}T${iv.endTime}`);
320
+ }
321
+
292
322
  const seStartTime = timeString(seStart);
293
323
  const seEndTime = timeString(seEnd);
294
- // Same UX for all types: an example placeholder, but never pre-fill the value.
295
- // If left empty, the server can still apply its own default label.
296
324
  const defaultTitlePlaceholder = 'Ex: ...';
297
- const defaultTitleValue = '';
325
+ const defaultTitleValue = iv ? (iv.title || '') : '';
326
+ const ivAddress = iv ? (iv.address || '') : '';
327
+ const ivLat = iv ? (iv.lat || '') : '';
328
+ const ivLon = iv ? (iv.lon || '') : '';
329
+ const ivNotes = iv ? (iv.notes || '') : '';
298
330
 
299
331
  const html = `
300
332
  <div class="mb-3">
@@ -328,16 +360,16 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
328
360
  <div class="mt-3">
329
361
  <label class="form-label">Adresse</label>
330
362
  <div class="input-group">
331
- <input type="text" class="form-control" id="onekite-se-address" placeholder="Adresse complète" />
363
+ <input type="text" class="form-control" id="onekite-se-address" placeholder="Adresse complète" value="${escapeHtml(ivAddress)}" />
332
364
  <button class="btn btn-outline-secondary" type="button" id="onekite-se-geocode">Rechercher</button>
333
365
  </div>
334
366
  <div id="onekite-se-map" style="height:220px; border:1px solid #ddd; border-radius:6px; margin-top:0.5rem;"></div>
335
- <input type="hidden" id="onekite-se-lat" />
336
- <input type="hidden" id="onekite-se-lon" />
367
+ <input type="hidden" id="onekite-se-lat" value="${escapeHtml(ivLat)}" />
368
+ <input type="hidden" id="onekite-se-lon" value="${escapeHtml(ivLon)}" />
337
369
  </div>
338
370
  <div class="mt-3">
339
371
  <label class="form-label">Notes (facultatif)</label>
340
- <textarea class="form-control" id="onekite-se-notes" rows="3" placeholder="..."></textarea>
372
+ <textarea class="form-control" id="onekite-se-notes" rows="3" placeholder="...">${escapeHtml(ivNotes)}</textarea>
341
373
  </div>
342
374
  ${(kind !== 'outing' && opts && opts.withContent) ? `
343
375
  <div class="mt-3">
@@ -349,8 +381,11 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
349
381
 
350
382
  return await new Promise((resolve) => {
351
383
  let resolved = false;
384
+ const isEditMode = !!iv;
352
385
  const dialog = bootbox.dialog({
353
- title: kind === 'outing' ? 'Créer une prévision de sortie' : 'Créer un évènement',
386
+ title: isEditMode
387
+ ? (kind === 'outing' ? 'Modifier la sortie' : "Modifier l'évènement")
388
+ : (kind === 'outing' ? 'Créer une prévision de sortie' : 'Créer un évènement'),
354
389
  message: html,
355
390
  buttons: {
356
391
  cancel: {
@@ -362,7 +397,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
362
397
  },
363
398
  },
364
399
  ok: {
365
- label: 'Créer',
400
+ label: isEditMode ? 'Enregistrer' : 'Créer',
366
401
  className: 'btn-primary',
367
402
  callback: () => {
368
403
  const title = (document.getElementById('onekite-se-title')?.value || '').trim();
@@ -446,6 +481,14 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
446
481
  document.getElementById('onekite-se-lon').value = String(lon);
447
482
  map.setView([lat, lon], 14);
448
483
  }
484
+ // Pre-fill marker if editing an event with existing coordinates.
485
+ if (ivLat && ivLon) {
486
+ const initLat = Number(ivLat);
487
+ const initLon = Number(ivLon);
488
+ if (Number.isFinite(initLat) && Number.isFinite(initLon)) {
489
+ setMarker(initLat, initLon);
490
+ }
491
+ }
449
492
  const geocodeBtn = document.getElementById('onekite-se-geocode');
450
493
  const addrInput = document.getElementById('onekite-se-address');
451
494
  try {
@@ -1223,8 +1266,14 @@ function toDatetimeLocalValue(date) {
1223
1266
  }
1224
1267
 
1225
1268
  if (typeof FullCalendar === 'undefined') {
1226
- showAlert('error', 'FullCalendar non chargé');
1227
- return;
1269
+ const loaded = await new Promise((resolve) => {
1270
+ const start = Date.now();
1271
+ const iv = setInterval(() => {
1272
+ if (typeof FullCalendar !== 'undefined') { clearInterval(iv); resolve(true); }
1273
+ else if (Date.now() - start > 5000) { clearInterval(iv); resolve(false); }
1274
+ }, 50);
1275
+ });
1276
+ if (!loaded) { showAlert('error', 'FullCalendar non chargé'); return; }
1228
1277
  }
1229
1278
 
1230
1279
  const items = await loadItems();
@@ -1763,11 +1812,39 @@ function toDatetimeLocalValue(date) {
1763
1812
  ${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
1764
1813
  `;
1765
1814
  const canDel = !!(p.canDeleteSpecial || canDeleteSpecial);
1815
+ const canEdit = !!p.canEditSpecial;
1766
1816
  const dlg = bootbox.dialog({
1767
1817
  title: 'Évènement',
1768
1818
  message: html,
1769
1819
  buttons: {
1770
1820
  close: { label: 'Fermer', className: 'btn-secondary' },
1821
+ ...(canEdit ? {
1822
+ edit: {
1823
+ label: 'Éditer',
1824
+ className: 'btn-default',
1825
+ callback: () => {
1826
+ (async () => {
1827
+ try { dlg.modal('hide'); } catch (e) { try { bootbox.hideAll(); } catch (e2) {} }
1828
+ const eid = String(p.eid || ev.id).replace(/^special:/, '');
1829
+ const initialValues = buildInitialValuesFromEvent(p);
1830
+ const payload = await openSpecialEventDialog({}, { kind: 'special', initialValues });
1831
+ if (!payload) return;
1832
+ try {
1833
+ await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}`, {
1834
+ method: 'PUT',
1835
+ body: JSON.stringify(payload),
1836
+ });
1837
+ showAlert('success', 'Évènement mis à jour.');
1838
+ invalidateEventsCache();
1839
+ scheduleRefetch(calendar);
1840
+ } catch (e) {
1841
+ showAlert('error', 'Mise à jour impossible.');
1842
+ }
1843
+ })();
1844
+ return false;
1845
+ },
1846
+ },
1847
+ } : {}),
1771
1848
  ...(canDel ? {
1772
1849
  del: {
1773
1850
  label: 'Supprimer',
@@ -1913,11 +1990,39 @@ function toDatetimeLocalValue(date) {
1913
1990
  ${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
1914
1991
  `;
1915
1992
  const canDel = !!(p.canDeleteOuting);
1993
+ const canEditOuting = !!p.canEditOuting;
1916
1994
  const dlg = bootbox.dialog({
1917
1995
  title: 'Sortie',
1918
1996
  message: html,
1919
1997
  buttons: {
1920
1998
  close: { label: 'Fermer', className: 'btn-secondary' },
1999
+ ...(canEditOuting ? {
2000
+ edit: {
2001
+ label: 'Éditer',
2002
+ className: 'btn-default',
2003
+ callback: () => {
2004
+ (async () => {
2005
+ try { dlg.modal('hide'); } catch (e) { try { bootbox.hideAll(); } catch (e2) {} }
2006
+ const oid = String(p.oid || ev.id).replace(/^outing:/, '');
2007
+ const initialValues = buildInitialValuesFromEvent(p);
2008
+ const payload = await openSpecialEventDialog({}, { kind: 'outing', initialValues });
2009
+ if (!payload) return;
2010
+ try {
2011
+ await fetchJson(`/api/v3/plugins/calendar-onekite/outings/${encodeURIComponent(oid)}`, {
2012
+ method: 'PUT',
2013
+ body: JSON.stringify(payload),
2014
+ });
2015
+ showAlert('success', 'Sortie mise à jour.');
2016
+ invalidateEventsCache();
2017
+ scheduleRefetch(calendar);
2018
+ } catch (e) {
2019
+ showAlert('error', 'Mise à jour impossible.');
2020
+ }
2021
+ })();
2022
+ return false;
2023
+ },
2024
+ },
2025
+ } : {}),
1921
2026
  ...(canDel ? {
1922
2027
  del: {
1923
2028
  label: 'Supprimer',