nodebb-plugin-onekite-calendar 2.0.41 → 2.0.43

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
@@ -305,6 +305,24 @@ admin.purgeSpecialEventsByYear = async function (req, res) {
305
305
  return res.json({ ok: true, removed: count });
306
306
  };
307
307
 
308
+ admin.purgeOutingsByYear = async function (req, res) {
309
+ const year = (req.body && req.body.year ? String(req.body.year) : '').trim();
310
+ if (!/^\d{4}$/.test(year)) {
311
+ return res.status(400).json({ error: 'invalid-year' });
312
+ }
313
+ const y = parseInt(year, 10);
314
+ const startTs = new Date(Date.UTC(y, 0, 1)).getTime();
315
+ const endTs = new Date(Date.UTC(y + 1, 0, 1)).getTime() - 1;
316
+
317
+ const ids = await dbLayer.listOutingIdsByStartRange(startTs, endTs, 200000);
318
+ let count = 0;
319
+ for (const oid of ids) {
320
+ await dbLayer.removeOuting(oid);
321
+ count++;
322
+ }
323
+ return res.json({ ok: true, removed: count });
324
+ };
325
+
308
326
  // Debug endpoint to validate HelloAsso connectivity and item loading
309
327
 
310
328
 
package/lib/api.js CHANGED
@@ -359,17 +359,30 @@ async function auditLog(action, actorUid, payload) {
359
359
  }
360
360
  }
361
361
 
362
- async function canCreateSpecial(uid, settings, startTs) {
363
- // Special events ("Évènements" / "Sorties") use the same rights as creating a reservation.
364
- // startTs is used to resolve the auto yearly group (onekite-ffvl-YYYY).
365
- const ts = Number(startTs) || Date.now();
366
- return await canRequest(uid, settings, ts);
362
+ async function canCreateSpecial(uid, settings) {
363
+ if (!uid) return false;
364
+ try {
365
+ const isAdmin = await groups.isMember(uid, 'administrators');
366
+ if (isAdmin) return true;
367
+ } catch (e) {}
368
+ const allowed = normalizeAllowedGroups(settings.specialCreatorGroups || '');
369
+ if (!allowed.length) return false;
370
+ if (await userInAnyGroup(uid, allowed)) return true;
371
+
372
+ return false;
367
373
  }
368
374
 
369
- async function canDeleteSpecial(uid, settings, startTs) {
370
- // Deletion rights are the same as creation rights (creatorGroups).
371
- const ts = Number(startTs) || Date.now();
372
- return await canRequest(uid, settings, ts);
375
+ async function canDeleteSpecial(uid, settings) {
376
+ if (!uid) return false;
377
+ try {
378
+ const isAdmin = await groups.isMember(uid, 'administrators');
379
+ if (isAdmin) return true;
380
+ } catch (e) {}
381
+ const allowed = normalizeAllowedGroups(settings.specialDeleterGroups || settings.specialCreatorGroups || '');
382
+ if (!allowed.length) return false;
383
+ if (await userInAnyGroup(uid, allowed)) return true;
384
+
385
+ return false;
373
386
  }
374
387
 
375
388
  function eventsFor(resv) {
@@ -429,36 +442,23 @@ function eventsFor(resv) {
429
442
  }
430
443
 
431
444
  function eventsForSpecial(ev) {
432
- const kind = String(ev.kind || 'event');
433
445
  const start = new Date(parseInt(ev.start, 10));
434
446
  const end = new Date(parseInt(ev.end, 10));
435
447
  const startIso = start.toISOString();
436
448
  const endIso = end.toISOString();
437
-
438
- // Color coding:
439
- // - event: purple
440
- // - outing: blue
441
- const palette = (kind === 'outing')
442
- ? { bg: '#0d6efd', border: '#0d6efd' }
443
- : { bg: '#8e44ad', border: '#8e44ad' };
444
-
445
- const defaultTitle = (kind === 'outing') ? 'Sortie' : 'Évènement';
446
-
447
449
  return {
448
450
  id: `special:${ev.eid}`,
449
- title: `${ev.title || defaultTitle}`.trim(),
451
+ title: `${ev.title || 'Évènement'}`.trim(),
450
452
  allDay: false,
451
453
  start: startIso,
452
454
  end: endIso,
453
- backgroundColor: palette.bg,
454
- borderColor: palette.border,
455
+ backgroundColor: '#8e44ad',
456
+ borderColor: '#8e44ad',
455
457
  textColor: '#ffffff',
456
458
  extendedProps: {
457
459
  type: 'special',
458
- kind,
459
460
  eid: ev.eid,
460
- kind: String(ev.kind || 'event'),
461
- title: ev.title || '',
461
+ title: ev.title || '',
462
462
  notes: ev.notes || '',
463
463
  pickupAddress: ev.address || '',
464
464
  pickupLat: ev.lat || '',
@@ -469,6 +469,35 @@ function eventsForSpecial(ev) {
469
469
  };
470
470
  }
471
471
 
472
+
473
+ function eventsForOuting(o) {
474
+ const start = new Date(parseInt(o.start, 10));
475
+ const end = new Date(parseInt(o.end, 10));
476
+ const startIso = start.toISOString();
477
+ const endIso = end.toISOString();
478
+ return {
479
+ id: `outing:${o.oid}`,
480
+ title: `${o.title || 'Prévision de sortie'}`.trim(),
481
+ allDay: false,
482
+ start: startIso,
483
+ end: endIso,
484
+ backgroundColor: '#2980b9',
485
+ borderColor: '#2980b9',
486
+ textColor: '#ffffff',
487
+ extendedProps: {
488
+ type: 'outing',
489
+ oid: o.oid,
490
+ title: o.title || '',
491
+ notes: o.notes || '',
492
+ pickupAddress: o.address || '',
493
+ pickupLat: o.lat || '',
494
+ pickupLon: o.lon || '',
495
+ createdBy: o.uid || 0,
496
+ username: o.username || '',
497
+ },
498
+ };
499
+ }
500
+
472
501
  const api = {};
473
502
 
474
503
  function computeEtag(payload) {
@@ -493,8 +522,8 @@ api.getEvents = async function (req, res) {
493
522
  const settings = await meta.settings.get('calendar-onekite');
494
523
  const canMod = req.uid ? await canValidate(req.uid, settings) : false;
495
524
  const widgetMode = String((req.query && req.query.widget) || '') === '1';
496
- const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings, Date.now()) : false;
497
- const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings, Date.now()) : false;
525
+ const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings) : false;
526
+ const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings) : false;
498
527
 
499
528
  // Fetch a wider window because an event can start before the query range
500
529
  // and still overlap.
@@ -611,7 +640,7 @@ api.getEvents = async function (req, res) {
611
640
  type: 'special',
612
641
  eid: sev.eid,
613
642
  canCreateSpecial: canSpecialCreate,
614
- canDeleteSpecial: canSpecialDeleteResolved,
643
+ canDeleteSpecial: canSpecialDelete,
615
644
  },
616
645
  };
617
646
  if (sev.username && (canMod || canSpecialDelete || (req.uid && String(req.uid) === String(sev.uid)))) {
@@ -623,6 +652,41 @@ api.getEvents = async function (req, res) {
623
652
  // ignore
624
653
  }
625
654
 
655
+
656
+ // Outings (prévisions de sortie)
657
+ try {
658
+ const outingIds = await dbLayer.listOutingIdsByStartRange(wideStart, endTs, 8000);
659
+ const outings = await dbLayer.getOutings(outingIds);
660
+ for (const o of (outings || [])) {
661
+ if (!o) continue;
662
+ const oStart = parseInt(o.start, 10);
663
+ const oEnd = parseInt(o.end, 10);
664
+ if (!(oStart < endTs && startTs < oEnd)) continue;
665
+ const full = eventsForOuting(o);
666
+ const minimal = {
667
+ id: full.id,
668
+ title: full.title,
669
+ allDay: full.allDay,
670
+ start: full.start,
671
+ end: full.end,
672
+ backgroundColor: full.backgroundColor,
673
+ borderColor: full.borderColor,
674
+ textColor: full.textColor,
675
+ extendedProps: {
676
+ type: 'outing',
677
+ oid: o.oid,
678
+ canDeleteOuting: canMod || (req.uid && String(req.uid) === String(o.uid)),
679
+ },
680
+ };
681
+ if (o.username && (canMod || (req.uid && String(req.uid) === String(o.uid)))) {
682
+ minimal.extendedProps.username = String(o.username);
683
+ }
684
+ out.push(minimal);
685
+ }
686
+ } catch (e) {
687
+ // ignore
688
+ }
689
+
626
690
  // Stable ordering -> stable ETag
627
691
  out.sort((a, b) => {
628
692
  const as = String(a.start || '');
@@ -696,19 +760,17 @@ api.getSpecialEventDetails = async function (req, res) {
696
760
 
697
761
  const settings = await meta.settings.get('calendar-onekite');
698
762
  const canMod = await canValidate(uid, settings);
763
+ const canSpecialDelete = await canDeleteSpecial(uid, settings);
699
764
 
700
765
  const eid = String(req.params.eid || '').trim();
701
766
  if (!eid) return res.status(400).json({ error: 'missing-eid' });
702
767
  const ev = await dbLayer.getSpecialEvent(eid);
703
768
  if (!ev) return res.status(404).json({ error: 'not-found' });
704
769
 
705
- const canSpecialDeleteResolved = await canDeleteSpecial(uid, settings, ev && ev.start ? Number(ev.start) : Date.now());
706
-
707
770
  // Anyone who can see the calendar can view special events, but creator username
708
771
  // is only visible to moderators/allowed users or the creator.
709
772
  const out = {
710
773
  eid: ev.eid,
711
- kind: String(ev.kind || 'event'),
712
774
  title: ev.title || '',
713
775
  start: ev.start,
714
776
  end: ev.end,
@@ -716,9 +778,9 @@ api.getSpecialEventDetails = async function (req, res) {
716
778
  lat: ev.lat || '',
717
779
  lon: ev.lon || '',
718
780
  notes: ev.notes || '',
719
- canDeleteSpecial: canSpecialDeleteResolved,
781
+ canDeleteSpecial: canSpecialDelete,
720
782
  };
721
- if (ev.username && (canMod || canSpecialDeleteResolved || (uid && String(uid) === String(ev.uid)))) {
783
+ if (ev.username && (canMod || canSpecialDelete || (uid && String(uid) === String(ev.uid)))) {
722
784
  out.username = String(ev.username);
723
785
  }
724
786
  return res.json(out);
@@ -730,28 +792,37 @@ api.getCapabilities = async function (req, res) {
730
792
  const canMod = uid ? await canValidate(uid, settings) : false;
731
793
  res.json({
732
794
  canModerate: canMod,
733
- canCreateSpecial: uid ? await canCreateSpecial(uid, settings, Date.now()) : false,
734
- canDeleteSpecial: uid ? await canDeleteSpecial(uid, settings, Date.now()) : false,
795
+ canCreateSpecial: uid ? await canCreateSpecial(uid, settings) : false,
796
+ canDeleteSpecial: uid ? await canDeleteSpecial(uid, settings) : false,
797
+ canCreateOuting: uid ? await canRequest(uid, settings, Date.now()) : false,
735
798
  });
736
799
  };
737
800
 
738
801
  api.createSpecialEvent = async function (req, res) {
739
802
  const settings = await meta.settings.get('calendar-onekite');
740
803
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
804
+ const ok = await canCreateSpecial(req.uid, settings);
805
+ if (!ok) return res.status(403).json({ error: 'not-allowed' });
741
806
 
742
- const kind = String((req.body && req.body.kind) || 'event').trim().toLowerCase();
743
- const safeKind = (kind === 'outing') ? 'outing' : 'event';
744
-
807
+ const title = String((req.body && req.body.title) || '').trim() || 'Évènement';
745
808
  const startTs = toTs(req.body && req.body.start);
746
809
  const endTs = toTs(req.body && req.body.end);
747
810
  if (!Number.isFinite(startTs) || !Number.isFinite(endTs) || !(startTs < endTs)) {
748
811
  return res.status(400).json({ error: 'bad-dates' });
749
812
  }
750
813
 
751
- const ok = await canCreateSpecial(req.uid, settings, startTs);
752
- if (!ok) return res.status(403).json({ error: 'not-allowed' });
753
-
754
- const title = String((req.body && req.body.title) || '').trim() || (safeKind === 'outing' ? 'Sortie' : 'Évènement');
814
+ // Business rule: nothing can be created in the past.
815
+ try {
816
+ const today0 = new Date();
817
+ today0.setHours(0, 0, 0, 0);
818
+ const today0ts = today0.getTime();
819
+ if (startTs < today0ts) {
820
+ return res.status(400).json({
821
+ error: 'date-too-soon',
822
+ message: "Impossible de créer pour une date passée.",
823
+ });
824
+ }
825
+ } catch (e) {}
755
826
  const address = String((req.body && req.body.address) || '').trim();
756
827
  const notes = String((req.body && req.body.notes) || '').trim();
757
828
  const lat = String((req.body && req.body.lat) || '').trim();
@@ -761,7 +832,6 @@ api.createSpecialEvent = async function (req, res) {
761
832
  const eid = crypto.randomUUID();
762
833
  const ev = {
763
834
  eid,
764
- kind: safeKind,
765
835
  title,
766
836
  start: String(startTs),
767
837
  end: String(endTs),
@@ -773,42 +843,142 @@ api.createSpecialEvent = async function (req, res) {
773
843
  username: u && u.username ? String(u.username) : '',
774
844
  createdAt: String(Date.now()),
775
845
  };
776
-
777
846
  await dbLayer.saveSpecialEvent(ev);
778
847
 
779
- // Discord notifications (separate webhooks per kind)
780
848
  try {
781
- await discord.notifySpecialEvent(settings, 'created', ev);
849
+ await discord.notifySpecialEventCreated(settings, ev);
782
850
  } catch (e) {}
783
851
 
784
852
  // Real-time refresh for all viewers
785
- realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'created', eid: ev.eid, specialKind: ev.kind });
853
+ realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'created', eid: ev.eid });
786
854
  res.json({ ok: true, eid });
787
855
  };
788
856
 
789
857
  api.deleteSpecialEvent = async function (req, res) {
790
858
  const settings = await meta.settings.get('calendar-onekite');
791
859
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
792
-
860
+ const ok = await canDeleteSpecial(req.uid, settings);
861
+ if (!ok) return res.status(403).json({ error: 'not-allowed' });
793
862
  const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
794
863
  if (!eid) return res.status(400).json({ error: 'bad-id' });
864
+ await dbLayer.removeSpecialEvent(eid);
795
865
 
796
- const ev = await dbLayer.getSpecialEvent(eid);
797
- if (!ev) return res.status(404).json({ error: 'not-found' });
866
+ try {
867
+ await discord.notifySpecialEventDeleted(settings, Object.assign({}, { eid }, { deletedBy: String(req.uid || '') }));
868
+ } catch (e) {}
869
+
870
+ realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'deleted', eid });
871
+ res.json({ ok: true });
872
+ };
873
+
874
+ api.getOutingDetails = async function (req, res) {
875
+ const uid = req.uid;
876
+ if (!uid) return res.status(401).json({ error: 'not-logged-in' });
877
+
878
+ const settings = await meta.settings.get('calendar-onekite');
879
+ const canMod = await canValidate(uid, settings);
880
+
881
+ const oid = String(req.params.oid || '').trim();
882
+ if (!oid) return res.status(400).json({ error: 'missing-oid' });
883
+ const o = await dbLayer.getOuting(oid);
884
+ if (!o) return res.status(404).json({ error: 'not-found' });
885
+
886
+ const out = {
887
+ oid: o.oid,
888
+ title: o.title || '',
889
+ start: o.start,
890
+ end: o.end,
891
+ address: o.address || '',
892
+ lat: o.lat || '',
893
+ lon: o.lon || '',
894
+ notes: o.notes || '',
895
+ canDeleteOuting: canMod || (uid && String(uid) === String(o.uid)),
896
+ };
897
+ if (o.username && (canMod || (uid && String(uid) === String(o.uid)))) {
898
+ out.username = String(o.username);
899
+ }
900
+ return res.json(out);
901
+ };
798
902
 
799
- const canSpecialDeleteResolved = await canDeleteSpecial(uid, settings, ev && ev.start ? Number(ev.start) : Date.now());
903
+ api.createOuting = async function (req, res) {
904
+ const settings = await meta.settings.get('calendar-onekite');
905
+ if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
800
906
 
801
- const ok = await canDeleteSpecial(req.uid, settings, ev && ev.start ? Number(ev.start) : Date.now());
907
+ const startTs = toTs(req.body && req.body.start);
908
+ const ok = await canRequest(req.uid, settings, startTs);
802
909
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
803
910
 
804
- await dbLayer.removeSpecialEvent(eid);
911
+ const title = String((req.body && req.body.title) || '').trim() || 'Prévision de sortie';
912
+ const endTs = toTs(req.body && req.body.end);
913
+ if (!Number.isFinite(startTs) || !Number.isFinite(endTs) || !(startTs < endTs)) {
914
+ return res.status(400).json({ error: 'bad-dates' });
915
+ }
916
+
917
+ // Business rule: nothing can be created in the past.
918
+ try {
919
+ const today0 = new Date();
920
+ today0.setHours(0, 0, 0, 0);
921
+ const today0ts = today0.getTime();
922
+ if (startTs < today0ts) {
923
+ return res.status(400).json({
924
+ error: 'date-too-soon',
925
+ message: "Impossible de créer pour une date passée.",
926
+ });
927
+ }
928
+ } catch (e) {}
929
+
930
+ const address = String((req.body && req.body.address) || '').trim();
931
+ const notes = String((req.body && req.body.notes) || '').trim();
932
+ const lat = String((req.body && req.body.lat) || '').trim();
933
+ const lon = String((req.body && req.body.lon) || '').trim();
934
+
935
+ const u = await user.getUserFields(req.uid, ['username']);
936
+ const oid = crypto.randomUUID();
937
+ const o = {
938
+ oid,
939
+ title,
940
+ start: String(startTs),
941
+ end: String(endTs),
942
+ address,
943
+ notes,
944
+ lat,
945
+ lon,
946
+ uid: String(req.uid),
947
+ username: u && u.username ? String(u.username) : '',
948
+ createdAt: String(Date.now()),
949
+ };
950
+ await dbLayer.saveOuting(o);
805
951
 
806
952
  try {
807
- await discord.notifySpecialEvent(settings, 'deleted', Object.assign({ eid }, ev || {}));
953
+ await discord.notifyOutingCreated(settings, o);
808
954
  } catch (e) {}
809
955
 
810
- realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'deleted', eid, specialKind: ev && ev.kind ? String(ev.kind) : 'event' });
811
- res.json({ ok: true });
956
+ realtime.emitCalendarUpdated({ kind: 'outing', action: 'created', oid: o.oid });
957
+ return res.json({ ok: true, oid });
958
+ };
959
+
960
+ api.deleteOuting = async function (req, res) {
961
+ const settings = await meta.settings.get('calendar-onekite');
962
+ if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
963
+
964
+ const oid = String(req.params.oid || '').replace(/^outing:/, '').trim();
965
+ if (!oid) return res.status(400).json({ error: 'bad-id' });
966
+
967
+ const o = await dbLayer.getOuting(oid);
968
+ if (!o) return res.status(404).json({ error: 'not-found' });
969
+
970
+ const canMod = await canValidate(req.uid, settings);
971
+ const isOwner = String(o.uid) === String(req.uid);
972
+ if (!canMod && !isOwner) return res.status(403).json({ error: 'not-allowed' });
973
+
974
+ await dbLayer.removeOuting(oid);
975
+
976
+ try {
977
+ await discord.notifyOutingDeleted(settings, Object.assign({}, o, { deletedBy: String(req.uid || '') }));
978
+ } catch (e) {}
979
+
980
+ realtime.emitCalendarUpdated({ kind: 'outing', action: 'deleted', oid });
981
+ return res.json({ ok: true });
812
982
  };
813
983
 
814
984
  api.getItems = async function (req, res) {
package/lib/db.js CHANGED
@@ -10,6 +10,10 @@ const KEY_CHECKOUT_INTENT_TO_RID = 'calendar-onekite:helloasso:checkoutIntentToR
10
10
  const KEY_SPECIAL_ZSET = 'calendar-onekite:special';
11
11
  const KEY_SPECIAL_OBJ = (eid) => `calendar-onekite:special:${eid}`;
12
12
 
13
+ // Outings (previsions de sortie)
14
+ const KEY_OUTING_ZSET = 'calendar-onekite:outings';
15
+ const KEY_OUTING_OBJ = (oid) => `calendar-onekite:outings:${oid}`;
16
+
13
17
  // Maintenance (simple ON/OFF per item, no dates)
14
18
  const KEY_MAINTENANCE_ZSET = 'calendar-onekite:maintenance:itemIds';
15
19
 
@@ -26,6 +30,10 @@ function specialKey(eid) {
26
30
  return KEY_SPECIAL_OBJ(eid);
27
31
  }
28
32
 
33
+ function outingKey(oid) {
34
+ return KEY_OUTING_OBJ(oid);
35
+ }
36
+
29
37
  async function getReservation(rid) {
30
38
  return await db.getObject(KEY_OBJ(rid));
31
39
  }
@@ -208,6 +216,34 @@ module.exports = {
208
216
  const stop = Math.max(0, (parseInt(limit, 10) || 2000) - 1);
209
217
  return await db.getSortedSetRangeByScore(KEY_SPECIAL_ZSET, start, stop, startTs, endTs);
210
218
  },
219
+
220
+ // Outings
221
+ getOuting: async (oid) => await db.getObject(KEY_OUTING_OBJ(oid)),
222
+ getOutings: async (oids) => {
223
+ const ids = Array.isArray(oids) ? oids.filter(Boolean) : [];
224
+ if (!ids.length) return [];
225
+ const keys = ids.map(outingKey);
226
+ const rows = await db.getObjects(keys);
227
+ return (rows || []).map((row, idx) => {
228
+ if (!row) return null;
229
+ if (!row.oid) row.oid = String(ids[idx]);
230
+ return row;
231
+ });
232
+ },
233
+ saveOuting: async (o) => {
234
+ await db.setObject(KEY_OUTING_OBJ(o.oid), o);
235
+ await db.sortedSetAdd(KEY_OUTING_ZSET, o.start, o.oid);
236
+ },
237
+ removeOuting: async (oid) => {
238
+ await db.sortedSetRemove(KEY_OUTING_ZSET, oid);
239
+ await db.delete(KEY_OUTING_OBJ(oid));
240
+ },
241
+ listOutingIdsByStartRange: async (startTs, endTs, limit = 5000) => {
242
+ const start = 0;
243
+ const stop = Math.max(0, (parseInt(limit, 10) || 5000) - 1);
244
+ return await db.getSortedSetRangeByScore(KEY_OUTING_ZSET, start, stop, startTs, endTs);
245
+ },
246
+
211
247
  listReservationIdsByStartRange,
212
248
  listAllReservationIds,
213
249
 
package/lib/discord.js CHANGED
@@ -184,39 +184,39 @@ async function notifyReservationCancelled(settings, reservation) {
184
184
  }
185
185
 
186
186
 
187
- function buildSpecialWebhookPayload(action, ev) {
188
- const kind = String((ev && ev.kind) || 'event');
189
- const webhookUsername = kind === 'outing'
190
- ? (action === 'deleted' ? 'Onekite • Sortie • Annulation' : 'Onekite • Sortie')
191
- : (action === 'deleted' ? 'Onekite • Évènement • Annulation' : 'Onekite • Évènement');
192
187
 
193
- const calUrl = 'https://www.onekite.com/calendar';
194
- const title = kind === 'outing'
195
- ? (action === 'deleted' ? '❌ Sortie annulée' : '🗓️ Nouvelle sortie')
196
- : (action === 'deleted' ? '❌ Évènement annulé' : '📣 Nouvel évènement');
188
+ // --------------------------
189
+ // Special events & outings
190
+ // --------------------------
191
+
192
+ function buildSimpleCalendarPayload(kind, label, entity, opts) {
193
+ const options = opts || {};
194
+ const calUrl = options.calUrl || 'https://www.onekite.com/calendar';
195
+ const webhookUsername = options.webhookUsername || `Onekite • ${label}`;
197
196
 
198
- const username = ev && ev.username ? String(ev.username) : '';
199
- const start = ev && ev.start ? Number(ev.start) : NaN;
200
- const end = ev && ev.end ? Number(ev.end) : NaN;
201
- const address = ev && ev.address ? String(ev.address) : '';
202
- const notes = ev && ev.notes ? String(ev.notes) : '';
197
+ const title = kind === 'deleted' ? '❌ ' + label + ' annulé(e)' : label + ' créé(e)';
198
+ const showIcon = kind === 'deleted';
199
+ const embedTitle = showIcon ? title : title.replace(/^❌\s*/, '');
203
200
 
201
+ const start = entity && entity.start ? Number(entity.start) : NaN;
202
+ const end = entity && entity.end ? Number(entity.end) : NaN;
204
203
  const fields = [];
205
- if (username) fields.push({ name: 'Créé par', value: username, inline: true });
204
+ const name = entity && entity.title ? String(entity.title) : '';
205
+ if (name) fields.push({ name: 'Titre', value: name, inline: false });
206
206
  if (Number.isFinite(start) && Number.isFinite(end)) {
207
207
  fields.push({ name: 'Période', value: `Du ${formatFRShort(start)} au ${formatFRShort(end)}`, inline: false });
208
208
  }
209
- if (address) fields.push({ name: 'Adresse', value: address, inline: false });
210
- if (notes) fields.push({ name: 'Notes', value: notes.length > 900 ? (notes.slice(0, 897) + '...') : notes, inline: false });
209
+ const username = entity && entity.username ? String(entity.username) : '';
210
+ if (username) fields.push({ name: 'Membre', value: username, inline: true });
211
211
 
212
212
  return {
213
213
  username: webhookUsername,
214
214
  content: '',
215
215
  embeds: [
216
216
  {
217
- title,
217
+ title: embedTitle,
218
218
  url: calUrl,
219
- description: (ev && ev.title) ? String(ev.title) : '',
219
+ description: kind === 'deleted' ? 'Une entrée a été annulée.' : 'Une nouvelle entrée a été créée.',
220
220
  fields,
221
221
  footer: { text: 'Onekite • Calendrier' },
222
222
  timestamp: new Date().toISOString(),
@@ -225,23 +225,51 @@ function buildSpecialWebhookPayload(action, ev) {
225
225
  };
226
226
  }
227
227
 
228
- async function notifySpecialEvent(settings, action, ev) {
229
- const kind = String((ev && ev.kind) || 'event');
230
- const url = kind === 'outing'
231
- ? String((settings && settings.discordWebhookUrlOutings) || '').trim()
232
- : String((settings && settings.discordWebhookUrlEvents) || '').trim();
228
+ async function notifySpecialEventCreated(settings, ev) {
229
+ const url = settings && settings.discordWebhookUrlEvents ? String(settings.discordWebhookUrlEvents).trim() : '';
233
230
  if (!url) return;
231
+ try {
232
+ await postWebhook(url, buildSimpleCalendarPayload('created', 'Évènement', ev, { webhookUsername: 'Onekite • Évènement' }));
233
+ } catch (e) {
234
+ console.warn('[calendar-onekite] Discord webhook failed (event created)', e && e.message ? e.message : String(e));
235
+ }
236
+ }
234
237
 
238
+ async function notifySpecialEventDeleted(settings, ev) {
239
+ const url = settings && settings.discordWebhookUrlEvents ? String(settings.discordWebhookUrlEvents).trim() : '';
240
+ if (!url) return;
235
241
  try {
236
- await postWebhook(url, buildSpecialWebhookPayload(action, ev));
242
+ await postWebhook(url, buildSimpleCalendarPayload('deleted', 'Évènement', ev, { webhookUsername: 'Onekite • Annulation' }));
237
243
  } catch (e) {
238
- console.warn('[calendar-onekite] Discord webhook failed (special)', e && e.message ? e.message : String(e));
244
+ console.warn('[calendar-onekite] Discord webhook failed (event deleted)', e && e.message ? e.message : String(e));
239
245
  }
240
246
  }
241
247
 
248
+ async function notifyOutingCreated(settings, o) {
249
+ const url = settings && settings.discordWebhookUrlOutings ? String(settings.discordWebhookUrlOutings).trim() : '';
250
+ if (!url) return;
251
+ try {
252
+ await postWebhook(url, buildSimpleCalendarPayload('created', 'Sortie', o, { webhookUsername: 'Onekite • Sortie' }));
253
+ } catch (e) {
254
+ console.warn('[calendar-onekite] Discord webhook failed (outing created)', e && e.message ? e.message : String(e));
255
+ }
256
+ }
257
+
258
+ async function notifyOutingDeleted(settings, o) {
259
+ const url = settings && settings.discordWebhookUrlOutings ? String(settings.discordWebhookUrlOutings).trim() : '';
260
+ if (!url) return;
261
+ try {
262
+ await postWebhook(url, buildSimpleCalendarPayload('deleted', 'Sortie', o, { webhookUsername: 'Onekite • Annulation' }));
263
+ } catch (e) {
264
+ console.warn('[calendar-onekite] Discord webhook failed (outing deleted)', e && e.message ? e.message : String(e));
265
+ }
266
+ }
242
267
  module.exports = {
243
268
  notifyReservationRequested,
244
269
  notifyPaymentReceived,
245
270
  notifyReservationCancelled,
246
- notifySpecialEvent,
271
+ notifySpecialEventCreated,
272
+ notifySpecialEventDeleted,
273
+ notifyOutingCreated,
274
+ notifyOutingDeleted,
247
275
  };
package/library.js CHANGED
@@ -83,6 +83,11 @@ Plugin.init = async function (params) {
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
85
 
86
+ // Outings (prévisions de sortie)
87
+ router.post('/api/v3/plugins/calendar-onekite/outings', ...publicExpose, api.createOuting);
88
+ router.get('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.getOutingDetails);
89
+ router.delete('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.deleteOuting);
90
+
86
91
  // Admin API (JSON)
87
92
  const adminBases = ['/api/v3/admin/plugins/calendar-onekite'];
88
93
 
@@ -103,6 +108,8 @@ Plugin.init = async function (params) {
103
108
 
104
109
  // Purge special events by year
105
110
  router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
111
+ // Purge outings by year
112
+ router.post(`${base}/outings/purge`, ...adminMws, admin.purgeOutingsByYear);
106
113
  });
107
114
 
108
115
  // HelloAsso callback endpoint (hardened)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.41",
3
+ "version": "2.0.43",
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.41"
42
+ "version": "2.0.43"
43
43
  }