nodebb-plugin-onekite-calendar 2.0.67 → 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,
@@ -768,6 +770,7 @@ api.getCapabilities = async function (req, res) {
768
770
  canDeleteSpecial: false,
769
771
  canCreateOuting: false,
770
772
  canCreateReservation: false,
773
+ specialEventCategoryCid: 0,
771
774
  });
772
775
  }
773
776
  const [canMod, canSpecialC, canSpecialD, canOuting, canRes] = await Promise.all([
@@ -783,6 +786,7 @@ api.getCapabilities = async function (req, res) {
783
786
  canDeleteSpecial: canSpecialD,
784
787
  canCreateOuting: canOuting,
785
788
  canCreateReservation: canRes,
789
+ specialEventCategoryCid: parseInt(settings && settings.specialEventCategoryId, 10) || 0,
786
790
  });
787
791
  };
788
792
 
@@ -815,6 +819,7 @@ api.createSpecialEvent = async function (req, res) {
815
819
  const notes = String((req.body && req.body.notes) || '').trim();
816
820
  const lat = String((req.body && req.body.lat) || '').trim();
817
821
  const lon = String((req.body && req.body.lon) || '').trim();
822
+ const content = String((req.body && req.body.content) || '').trim();
818
823
 
819
824
  const u = await user.getUserFields(req.uid, ['username']);
820
825
  const eid = crypto.randomUUID();
@@ -827,6 +832,7 @@ api.createSpecialEvent = async function (req, res) {
827
832
  notes,
828
833
  lat,
829
834
  lon,
835
+ content,
830
836
  uid: String(req.uid),
831
837
  username: u && u.username ? String(u.username) : '',
832
838
  createdAt: String(Date.now()),
@@ -841,7 +847,8 @@ api.createSpecialEvent = async function (req, res) {
841
847
 
842
848
  // Real-time refresh for all viewers
843
849
  realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'created', eid: ev.eid });
844
- res.json({ ok: true, eid });
850
+ const categoryCid = parseInt(settings && settings.specialEventCategoryId, 10) || 0;
851
+ res.json({ ok: true, eid, categoryCid });
845
852
  };
846
853
 
847
854
  api.deleteSpecialEvent = async function (req, res) {
@@ -866,6 +873,37 @@ api.deleteSpecialEvent = async function (req, res) {
866
873
  res.json({ ok: true });
867
874
  };
868
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
+
869
907
  /**
870
908
  * Get detailed information about an outing (prévision de sortie).
871
909
  *
@@ -906,6 +944,7 @@ api.getOutingDetails = async function (req, res) {
906
944
  });
907
945
 
908
946
  const participants = normalizeUidList(o.participants);
947
+ const canEditOuting = uid ? await canRequest(uid, settings, Date.now()) : false;
909
948
  const out = {
910
949
  oid: o.oid,
911
950
  title: o.title || '',
@@ -917,7 +956,8 @@ api.getOutingDetails = async function (req, res) {
917
956
  notes: o.notes || '',
918
957
  participants,
919
958
  participantsUsernames: await usernamesByUids(participants),
920
- canJoin: uid ? await canRequest(uid, settings, Date.now()) : false,
959
+ canEditOuting,
960
+ canJoin: canEditOuting,
921
961
  isParticipant: uid ? participants.includes(String(uid)) : false,
922
962
  canDeleteOuting: canMod || (uid && String(uid) === String(o.uid)),
923
963
  icsUrl: links.icsUrl,
@@ -1085,6 +1125,37 @@ api.deleteOuting = async function (req, res) {
1085
1125
  return res.json({ ok: true });
1086
1126
  };
1087
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
+
1088
1159
  api.getItems = async function (req, res) {
1089
1160
  const settings = await getSettings();
1090
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.67",
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/admin.js CHANGED
@@ -615,6 +615,32 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
615
615
  const s = await loadSettings();
616
616
  fillForm(form, s || {});
617
617
 
618
+ // Populate special event category selector
619
+ const catSelect = document.getElementById('onekite-special-category-select');
620
+ if (catSelect) {
621
+ try {
622
+ const cats = await fetchJson('/api/categories');
623
+ const list = cats && (cats.categories || cats.categoryList || cats.data) || [];
624
+ function flatCats(arr) {
625
+ const out = [];
626
+ for (const c of (arr || [])) {
627
+ if (!c || !c.cid) continue;
628
+ out.push(c);
629
+ if (c.children && c.children.length) out.push(...flatCats(c.children));
630
+ }
631
+ return out;
632
+ }
633
+ for (const c of flatCats(list)) {
634
+ const opt = document.createElement('option');
635
+ opt.value = String(c.cid);
636
+ opt.textContent = String(c.name || c.cid);
637
+ catSelect.appendChild(opt);
638
+ }
639
+ // Re-apply saved value (fillForm ran before options were added)
640
+ if (s && s.specialEventCategoryId) catSelect.value = String(s.specialEventCategoryId);
641
+ } catch (e) { /* non-blocking */ }
642
+ }
643
+
618
644
  // Ensure default creator group prefix appears in the ACP field
619
645
  const y = new Date().getFullYear();
620
646
  const defaultGroup = `onekite-ffvl-${y}`;
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,23 +360,32 @@ 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>
373
+ </div>
374
+ ${(kind !== 'outing' && opts && opts.withContent) ? `
375
+ <div class="mt-3">
376
+ <label class="form-label">Message (facultatif)</label>
377
+ <textarea class="form-control" id="onekite-se-content" rows="4" placeholder="Corps du sujet qui sera publié dans le forum (brouillon)..."></textarea>
341
378
  </div>
379
+ ` : ''}
342
380
  `;
343
381
 
344
382
  return await new Promise((resolve) => {
345
383
  let resolved = false;
384
+ const isEditMode = !!iv;
346
385
  const dialog = bootbox.dialog({
347
- 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'),
348
389
  message: html,
349
390
  buttons: {
350
391
  cancel: {
@@ -356,7 +397,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
356
397
  },
357
398
  },
358
399
  ok: {
359
- label: 'Créer',
400
+ label: isEditMode ? 'Enregistrer' : 'Créer',
360
401
  className: 'btn-primary',
361
402
  callback: () => {
362
403
  const title = (document.getElementById('onekite-se-title')?.value || '').trim();
@@ -392,8 +433,11 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
392
433
  const notes = (document.getElementById('onekite-se-notes')?.value || '').trim();
393
434
  const lat = (document.getElementById('onekite-se-lat')?.value || '').trim();
394
435
  const lon = (document.getElementById('onekite-se-lon')?.value || '').trim();
436
+ const content = (kind !== 'outing' && opts && opts.withContent)
437
+ ? (document.getElementById('onekite-se-content')?.value || '').trim()
438
+ : '';
395
439
  resolved = true;
396
- resolve({ title, start: startVal, end: endVal, address, notes, lat, lon });
440
+ resolve({ title, start: startVal, end: endVal, address, notes, lat, lon, content });
397
441
  return true;
398
442
  },
399
443
  },
@@ -437,6 +481,14 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
437
481
  document.getElementById('onekite-se-lon').value = String(lon);
438
482
  map.setView([lat, lon], 14);
439
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
+ }
440
492
  const geocodeBtn = document.getElementById('onekite-se-geocode');
441
493
  const addrInput = document.getElementById('onekite-se-address');
442
494
  try {
@@ -1225,6 +1277,7 @@ function toDatetimeLocalValue(date) {
1225
1277
  const canDeleteSpecial = !!caps.canDeleteSpecial;
1226
1278
  const canCreateOuting = !!caps.canCreateOuting;
1227
1279
  const canCreateReservation = !!caps.canCreateReservation;
1280
+ const specialEventCategoryCid = parseInt(caps.specialEventCategoryCid, 10) || 0;
1228
1281
 
1229
1282
  // Creation chooser: Location / Prévision de sortie / Évènement (si autorisé).
1230
1283
 
@@ -1415,15 +1468,23 @@ function toDatetimeLocalValue(date) {
1415
1468
  },
1416
1469
  onCreateSpecial: async (sel) => {
1417
1470
  try {
1418
- const payload = await openSpecialEventDialog(sel);
1471
+ const payload = await openSpecialEventDialog(sel, { withContent: specialEventCategoryCid > 0 });
1419
1472
  if (!payload) return;
1420
- await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1473
+ const resp = await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1421
1474
  method: 'POST',
1422
1475
  body: JSON.stringify(payload),
1423
1476
  });
1424
1477
  showAlert('success', 'Évènement créé.');
1425
1478
  invalidateEventsCache();
1426
1479
  scheduleRefetch(calendar);
1480
+ const cid = (resp && resp.categoryCid) ? parseInt(resp.categoryCid, 10) : specialEventCategoryCid;
1481
+ if (payload.content && cid > 0) {
1482
+ try {
1483
+ require(['composer'], (composer) => {
1484
+ composer.newTopic({ cid, title: payload.title || 'Évènement', body: payload.content });
1485
+ });
1486
+ } catch (e) {}
1487
+ }
1427
1488
  } catch (e) {
1428
1489
  handleCreateError(e);
1429
1490
  } finally {
@@ -1495,15 +1556,23 @@ function toDatetimeLocalValue(date) {
1495
1556
  },
1496
1557
  onCreateSpecial: async (sel) => {
1497
1558
  try {
1498
- const payload = await openSpecialEventDialog(sel);
1559
+ const payload = await openSpecialEventDialog(sel, { withContent: specialEventCategoryCid > 0 });
1499
1560
  if (!payload) return;
1500
- await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1561
+ const resp = await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1501
1562
  method: 'POST',
1502
1563
  body: JSON.stringify(payload),
1503
1564
  });
1504
1565
  showAlert('success', 'Évènement créé.');
1505
1566
  invalidateEventsCache();
1506
1567
  scheduleRefetch(calendar);
1568
+ const cid = (resp && resp.categoryCid) ? parseInt(resp.categoryCid, 10) : specialEventCategoryCid;
1569
+ if (payload.content && cid > 0) {
1570
+ try {
1571
+ require(['composer'], (composer) => {
1572
+ composer.newTopic({ cid, title: payload.title || 'Évènement', body: payload.content });
1573
+ });
1574
+ } catch (e) {}
1575
+ }
1507
1576
  } catch (e) {
1508
1577
  const msg = (e && e.payload && (e.payload.message || e.payload.error || e.payload.msg)) ? String(e.payload.message || e.payload.error || e.payload.msg) : '';
1509
1578
  showAlert('error', msg || 'Erreur lors de la création.');
@@ -1737,11 +1806,39 @@ function toDatetimeLocalValue(date) {
1737
1806
  ${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
1738
1807
  `;
1739
1808
  const canDel = !!(p.canDeleteSpecial || canDeleteSpecial);
1809
+ const canEdit = !!p.canEditSpecial;
1740
1810
  const dlg = bootbox.dialog({
1741
1811
  title: 'Évènement',
1742
1812
  message: html,
1743
1813
  buttons: {
1744
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
+ } : {}),
1745
1842
  ...(canDel ? {
1746
1843
  del: {
1747
1844
  label: 'Supprimer',
@@ -1887,11 +1984,39 @@ function toDatetimeLocalValue(date) {
1887
1984
  ${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
1888
1985
  `;
1889
1986
  const canDel = !!(p.canDeleteOuting);
1987
+ const canEditOuting = !!p.canEditOuting;
1890
1988
  const dlg = bootbox.dialog({
1891
1989
  title: 'Sortie',
1892
1990
  message: html,
1893
1991
  buttons: {
1894
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
+ } : {}),
1895
2020
  ...(canDel ? {
1896
2021
  del: {
1897
2022
  label: 'Supprimer',
@@ -161,11 +161,13 @@
161
161
  <h4>Évènements (autre couleur)</h4>
162
162
  <div class="form-text mb-3">Permet de créer des évènements horaires (début/fin) avec adresse (Leaflet) et notes.</div>
163
163
 
164
- <h4 class="mt-3">Discord</h4>
164
+ <h4 class="mt-3">Publication forum</h4>
165
165
  <div class="mb-3">
166
- <label class="form-label">Webhook URL Évènements</label>
167
- <input class="form-control" name="discordWebhookUrlEvents" placeholder="https://discord.com/api/webhooks/...">
168
- <div class="form-text">Canal Discord dédié aux notifications d'évènements (création / annulation).</div>
166
+ <label class="form-label">Catégorie de publication des évènements</label>
167
+ <select class="form-select" name="specialEventCategoryId" id="onekite-special-category-select">
168
+ <option value="">— Désactivé —</option>
169
+ </select>
170
+ <div class="form-text">Si une catégorie est sélectionnée, un champ "Message" apparaît dans la modale de création. À la validation, le compositeur NodeBB s'ouvre pré-rempli pour que vous puissiez relire et publier.</div>
169
171
  </div>
170
172
 
171
173
  <div class="mb-3">