nodebb-plugin-onekite-calendar 2.0.68 → 2.0.69

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/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.69",
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/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 {
@@ -1763,11 +1806,39 @@ function toDatetimeLocalValue(date) {
1763
1806
  ${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
1764
1807
  `;
1765
1808
  const canDel = !!(p.canDeleteSpecial || canDeleteSpecial);
1809
+ const canEdit = !!p.canEditSpecial;
1766
1810
  const dlg = bootbox.dialog({
1767
1811
  title: 'Évènement',
1768
1812
  message: html,
1769
1813
  buttons: {
1770
1814
  close: { label: 'Fermer', className: 'btn-secondary' },
1815
+ ...(canEdit ? {
1816
+ edit: {
1817
+ label: 'Éditer',
1818
+ className: 'btn-default',
1819
+ callback: () => {
1820
+ (async () => {
1821
+ try { dlg.modal('hide'); } catch (e) { try { bootbox.hideAll(); } catch (e2) {} }
1822
+ const eid = String(p.eid || ev.id).replace(/^special:/, '');
1823
+ const initialValues = buildInitialValuesFromEvent(p);
1824
+ const payload = await openSpecialEventDialog({}, { kind: 'special', initialValues });
1825
+ if (!payload) return;
1826
+ try {
1827
+ await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}`, {
1828
+ method: 'PUT',
1829
+ body: JSON.stringify(payload),
1830
+ });
1831
+ showAlert('success', 'Évènement mis à jour.');
1832
+ invalidateEventsCache();
1833
+ scheduleRefetch(calendar);
1834
+ } catch (e) {
1835
+ showAlert('error', 'Mise à jour impossible.');
1836
+ }
1837
+ })();
1838
+ return false;
1839
+ },
1840
+ },
1841
+ } : {}),
1771
1842
  ...(canDel ? {
1772
1843
  del: {
1773
1844
  label: 'Supprimer',
@@ -1913,11 +1984,39 @@ function toDatetimeLocalValue(date) {
1913
1984
  ${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
1914
1985
  `;
1915
1986
  const canDel = !!(p.canDeleteOuting);
1987
+ const canEditOuting = !!p.canEditOuting;
1916
1988
  const dlg = bootbox.dialog({
1917
1989
  title: 'Sortie',
1918
1990
  message: html,
1919
1991
  buttons: {
1920
1992
  close: { label: 'Fermer', className: 'btn-secondary' },
1993
+ ...(canEditOuting ? {
1994
+ edit: {
1995
+ label: 'Éditer',
1996
+ className: 'btn-default',
1997
+ callback: () => {
1998
+ (async () => {
1999
+ try { dlg.modal('hide'); } catch (e) { try { bootbox.hideAll(); } catch (e2) {} }
2000
+ const oid = String(p.oid || ev.id).replace(/^outing:/, '');
2001
+ const initialValues = buildInitialValuesFromEvent(p);
2002
+ const payload = await openSpecialEventDialog({}, { kind: 'outing', initialValues });
2003
+ if (!payload) return;
2004
+ try {
2005
+ await fetchJson(`/api/v3/plugins/calendar-onekite/outings/${encodeURIComponent(oid)}`, {
2006
+ method: 'PUT',
2007
+ body: JSON.stringify(payload),
2008
+ });
2009
+ showAlert('success', 'Sortie mise à jour.');
2010
+ invalidateEventsCache();
2011
+ scheduleRefetch(calendar);
2012
+ } catch (e) {
2013
+ showAlert('error', 'Mise à jour impossible.');
2014
+ }
2015
+ })();
2016
+ return false;
2017
+ },
2018
+ },
2019
+ } : {}),
1921
2020
  ...(canDel ? {
1922
2021
  del: {
1923
2022
  label: 'Supprimer',