nodebb-plugin-onekite-calendar 2.0.41 → 2.0.42

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
@@ -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 || '',
@@ -493,8 +493,8 @@ api.getEvents = async function (req, res) {
493
493
  const settings = await meta.settings.get('calendar-onekite');
494
494
  const canMod = req.uid ? await canValidate(req.uid, settings) : false;
495
495
  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;
496
+ const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings) : false;
497
+ const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings) : false;
498
498
 
499
499
  // Fetch a wider window because an event can start before the query range
500
500
  // and still overlap.
@@ -611,7 +611,7 @@ api.getEvents = async function (req, res) {
611
611
  type: 'special',
612
612
  eid: sev.eid,
613
613
  canCreateSpecial: canSpecialCreate,
614
- canDeleteSpecial: canSpecialDeleteResolved,
614
+ canDeleteSpecial: canSpecialDelete,
615
615
  },
616
616
  };
617
617
  if (sev.username && (canMod || canSpecialDelete || (req.uid && String(req.uid) === String(sev.uid)))) {
@@ -696,19 +696,17 @@ api.getSpecialEventDetails = async function (req, res) {
696
696
 
697
697
  const settings = await meta.settings.get('calendar-onekite');
698
698
  const canMod = await canValidate(uid, settings);
699
+ const canSpecialDelete = await canDeleteSpecial(uid, settings);
699
700
 
700
701
  const eid = String(req.params.eid || '').trim();
701
702
  if (!eid) return res.status(400).json({ error: 'missing-eid' });
702
703
  const ev = await dbLayer.getSpecialEvent(eid);
703
704
  if (!ev) return res.status(404).json({ error: 'not-found' });
704
705
 
705
- const canSpecialDeleteResolved = await canDeleteSpecial(uid, settings, ev && ev.start ? Number(ev.start) : Date.now());
706
-
707
706
  // Anyone who can see the calendar can view special events, but creator username
708
707
  // is only visible to moderators/allowed users or the creator.
709
708
  const out = {
710
709
  eid: ev.eid,
711
- kind: String(ev.kind || 'event'),
712
710
  title: ev.title || '',
713
711
  start: ev.start,
714
712
  end: ev.end,
@@ -716,9 +714,9 @@ api.getSpecialEventDetails = async function (req, res) {
716
714
  lat: ev.lat || '',
717
715
  lon: ev.lon || '',
718
716
  notes: ev.notes || '',
719
- canDeleteSpecial: canSpecialDeleteResolved,
717
+ canDeleteSpecial: canSpecialDelete,
720
718
  };
721
- if (ev.username && (canMod || canSpecialDeleteResolved || (uid && String(uid) === String(ev.uid)))) {
719
+ if (ev.username && (canMod || canSpecialDelete || (uid && String(uid) === String(ev.uid)))) {
722
720
  out.username = String(ev.username);
723
721
  }
724
722
  return res.json(out);
@@ -730,28 +728,23 @@ api.getCapabilities = async function (req, res) {
730
728
  const canMod = uid ? await canValidate(uid, settings) : false;
731
729
  res.json({
732
730
  canModerate: canMod,
733
- canCreateSpecial: uid ? await canCreateSpecial(uid, settings, Date.now()) : false,
734
- canDeleteSpecial: uid ? await canDeleteSpecial(uid, settings, Date.now()) : false,
731
+ canCreateSpecial: uid ? await canCreateSpecial(uid, settings) : false,
732
+ canDeleteSpecial: uid ? await canDeleteSpecial(uid, settings) : false,
735
733
  });
736
734
  };
737
735
 
738
736
  api.createSpecialEvent = async function (req, res) {
739
737
  const settings = await meta.settings.get('calendar-onekite');
740
738
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
739
+ const ok = await canCreateSpecial(req.uid, settings);
740
+ if (!ok) return res.status(403).json({ error: 'not-allowed' });
741
741
 
742
- const kind = String((req.body && req.body.kind) || 'event').trim().toLowerCase();
743
- const safeKind = (kind === 'outing') ? 'outing' : 'event';
744
-
742
+ const title = String((req.body && req.body.title) || '').trim() || 'Évènement';
745
743
  const startTs = toTs(req.body && req.body.start);
746
744
  const endTs = toTs(req.body && req.body.end);
747
745
  if (!Number.isFinite(startTs) || !Number.isFinite(endTs) || !(startTs < endTs)) {
748
746
  return res.status(400).json({ error: 'bad-dates' });
749
747
  }
750
-
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');
755
748
  const address = String((req.body && req.body.address) || '').trim();
756
749
  const notes = String((req.body && req.body.notes) || '').trim();
757
750
  const lat = String((req.body && req.body.lat) || '').trim();
@@ -761,7 +754,6 @@ api.createSpecialEvent = async function (req, res) {
761
754
  const eid = crypto.randomUUID();
762
755
  const ev = {
763
756
  eid,
764
- kind: safeKind,
765
757
  title,
766
758
  start: String(startTs),
767
759
  end: String(endTs),
@@ -773,41 +765,21 @@ api.createSpecialEvent = async function (req, res) {
773
765
  username: u && u.username ? String(u.username) : '',
774
766
  createdAt: String(Date.now()),
775
767
  };
776
-
777
768
  await dbLayer.saveSpecialEvent(ev);
778
-
779
- // Discord notifications (separate webhooks per kind)
780
- try {
781
- await discord.notifySpecialEvent(settings, 'created', ev);
782
- } catch (e) {}
783
-
784
769
  // Real-time refresh for all viewers
785
- realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'created', eid: ev.eid, specialKind: ev.kind });
770
+ realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'created', eid: ev.eid });
786
771
  res.json({ ok: true, eid });
787
772
  };
788
773
 
789
774
  api.deleteSpecialEvent = async function (req, res) {
790
775
  const settings = await meta.settings.get('calendar-onekite');
791
776
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
792
-
777
+ const ok = await canDeleteSpecial(req.uid, settings);
778
+ if (!ok) return res.status(403).json({ error: 'not-allowed' });
793
779
  const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
794
780
  if (!eid) return res.status(400).json({ error: 'bad-id' });
795
-
796
- const ev = await dbLayer.getSpecialEvent(eid);
797
- if (!ev) return res.status(404).json({ error: 'not-found' });
798
-
799
- const canSpecialDeleteResolved = await canDeleteSpecial(uid, settings, ev && ev.start ? Number(ev.start) : Date.now());
800
-
801
- const ok = await canDeleteSpecial(req.uid, settings, ev && ev.start ? Number(ev.start) : Date.now());
802
- if (!ok) return res.status(403).json({ error: 'not-allowed' });
803
-
804
781
  await dbLayer.removeSpecialEvent(eid);
805
-
806
- try {
807
- await discord.notifySpecialEvent(settings, 'deleted', Object.assign({ eid }, ev || {}));
808
- } catch (e) {}
809
-
810
- realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'deleted', eid, specialKind: ev && ev.kind ? String(ev.kind) : 'event' });
782
+ realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'deleted', eid });
811
783
  res.json({ ok: true });
812
784
  };
813
785
 
package/lib/discord.js CHANGED
@@ -183,65 +183,8 @@ async function notifyReservationCancelled(settings, reservation) {
183
183
  }
184
184
  }
185
185
 
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
-
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');
197
-
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) : '';
203
-
204
- const fields = [];
205
- if (username) fields.push({ name: 'Créé par', value: username, inline: true });
206
- if (Number.isFinite(start) && Number.isFinite(end)) {
207
- fields.push({ name: 'Période', value: `Du ${formatFRShort(start)} au ${formatFRShort(end)}`, inline: false });
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 });
211
-
212
- return {
213
- username: webhookUsername,
214
- content: '',
215
- embeds: [
216
- {
217
- title,
218
- url: calUrl,
219
- description: (ev && ev.title) ? String(ev.title) : '',
220
- fields,
221
- footer: { text: 'Onekite • Calendrier' },
222
- timestamp: new Date().toISOString(),
223
- },
224
- ],
225
- };
226
- }
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();
233
- if (!url) return;
234
-
235
- try {
236
- await postWebhook(url, buildSpecialWebhookPayload(action, ev));
237
- } catch (e) {
238
- console.warn('[calendar-onekite] Discord webhook failed (special)', e && e.message ? e.message : String(e));
239
- }
240
- }
241
-
242
186
  module.exports = {
243
187
  notifyReservationRequested,
244
188
  notifyPaymentReceived,
245
189
  notifyReservationCancelled,
246
- notifySpecialEvent,
247
190
  };
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.42",
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.42"
43
43
  }
package/public/client.js CHANGED
@@ -208,12 +208,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
208
208
  return opts.join('');
209
209
  }
210
210
 
211
- async function openSpecialEventDialog(selectionInfo, opts) {
212
- const options = opts || {};
213
- const kind = (String(options.kind || 'event').trim().toLowerCase() === 'outing') ? 'outing' : 'event';
214
- const dialogTitle = String(options.dialogTitle || (kind === 'outing' ? 'Créer une prévision de sortie' : 'Créer un évènement'));
215
- const defaultTitle = String(options.defaultTitle || (kind === 'outing' ? 'Sortie' : 'Évènement'));
216
-
211
+ async function openSpecialEventDialog(selectionInfo) {
217
212
  const start = selectionInfo.start;
218
213
  // FullCalendar can omit `end` for certain interactions. Also, for all-day
219
214
  // selections, `end` is exclusive (next day at 00:00). We normalise below
@@ -267,7 +262,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
267
262
  const html = `
268
263
  <div class="mb-3">
269
264
  <label class="form-label">Titre</label>
270
- <input type="text" class="form-control" id="onekite-se-title" placeholder="Ex: ..." value="${escapeHtml(defaultTitle)}" />
265
+ <input type="text" class="form-control" id="onekite-se-title" placeholder="Ex: ..." />
271
266
  </div>
272
267
  <div class="row g-2">
273
268
  <div class="col-12 col-md-6">
@@ -312,7 +307,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
312
307
  return await new Promise((resolve) => {
313
308
  let resolved = false;
314
309
  const dialog = bootbox.dialog({
315
- title: dialogTitle,
310
+ title: 'Créer un évènement',
316
311
  message: html,
317
312
  buttons: {
318
313
  cancel: {
@@ -361,7 +356,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
361
356
  const lat = (document.getElementById('onekite-se-lat')?.value || '').trim();
362
357
  const lon = (document.getElementById('onekite-se-lon')?.value || '').trim();
363
358
  resolved = true;
364
- resolve({ kind, title, start: startVal, end: endVal, address, notes, lat, lon });
359
+ resolve({ title, start: startVal, end: endVal, address, notes, lat, lon });
365
360
  return true;
366
361
  },
367
362
  },
@@ -1363,55 +1358,6 @@ function toDatetimeLocalValue(date) {
1363
1358
  return;
1364
1359
  }
1365
1360
 
1366
-
1367
-
1368
- // On a day click / selection in reservation mode, propose the user to create either
1369
- // a Location (reservation) or a "Prévision de sortie" (special event).
1370
- try {
1371
- const choice = await new Promise((resolve) => {
1372
- bootbox.dialog({
1373
- title: 'Créer',
1374
- message: '<div>Que veux-tu créer ?</div>',
1375
- buttons: {
1376
- cancel: { label: 'Annuler', className: 'btn-secondary', callback: () => resolve(null) },
1377
- outing: { label: 'Prévision de sortie', className: 'btn-info', callback: () => resolve('outing') },
1378
- reservation: { label: 'Location', className: 'btn-primary', callback: () => resolve('reservation') },
1379
- },
1380
- });
1381
- });
1382
-
1383
- if (!choice) {
1384
- calendar.unselect();
1385
- isDialogOpen = false;
1386
- return;
1387
- }
1388
-
1389
- if (choice === 'outing') {
1390
- const payload = await openSpecialEventDialog(info, { kind: 'outing', dialogTitle: 'Créer une prévision de sortie', defaultTitle: 'Sortie' });
1391
- if (!payload) {
1392
- calendar.unselect();
1393
- isDialogOpen = false;
1394
- return;
1395
- }
1396
- await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1397
- method: 'POST',
1398
- body: JSON.stringify(payload),
1399
- }).catch(async () => {
1400
- return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1401
- method: 'POST',
1402
- body: JSON.stringify(payload),
1403
- });
1404
- });
1405
- showAlert('success', 'Sortie créée.');
1406
- invalidateEventsCache();
1407
- scheduleRefetch(calendar);
1408
- calendar.unselect();
1409
- isDialogOpen = false;
1410
- return;
1411
- }
1412
- } catch (e) {
1413
- // ignore
1414
- }
1415
1361
  // Business rule: reservations cannot start in the past.
1416
1362
  // (We validate again on the server, but this gives immediate feedback.)
1417
1363
  try {
@@ -1598,19 +1544,15 @@ function toDatetimeLocalValue(date) {
1598
1544
  }
1599
1545
  },
1600
1546
  eventDidMount: function (arg) {
1601
- // Keep special event colors consistent (kind-specific).
1547
+ // Keep special event colors consistent.
1602
1548
  try {
1603
1549
  const ev = arg && arg.event;
1604
1550
  if (!ev) return;
1605
1551
  if (ev.extendedProps && ev.extendedProps.type === 'special') {
1606
- const kind = String((ev.extendedProps.kind || 'event'));
1607
- const palette = (kind === 'outing')
1608
- ? { bg: '#0d6efd', border: '#0d6efd' }
1609
- : { bg: '#8e44ad', border: '#8e44ad' };
1610
1552
  const el2 = arg.el;
1611
1553
  if (el2 && el2.style) {
1612
- el2.style.backgroundColor = palette.bg;
1613
- el2.style.borderColor = palette.border;
1554
+ el2.style.backgroundColor = '#8e44ad';
1555
+ el2.style.borderColor = '#8e44ad';
1614
1556
  el2.style.color = '#ffffff';
1615
1557
  }
1616
1558
  }
@@ -1656,8 +1598,6 @@ function toDatetimeLocalValue(date) {
1656
1598
 
1657
1599
  try {
1658
1600
  if (p.type === 'special') {
1659
- const specialKind = String((p.kind || p0.kind || 'event'));
1660
- const specialLabel = (specialKind === 'outing') ? 'Sortie' : 'Évènement';
1661
1601
  const username = String(p.username || '').trim();
1662
1602
  const userLine = username
1663
1603
  ? `<div class="mb-2"><strong>Créé par</strong><br><a class="onekite-user-link" href="${window.location.origin}/user/${encodeURIComponent(username)}">${escapeHtml(username)}</a></div>`
@@ -1681,7 +1621,7 @@ function toDatetimeLocalValue(date) {
1681
1621
  `;
1682
1622
  const canDel = !!(p.canDeleteSpecial || canDeleteSpecial);
1683
1623
  bootbox.dialog({
1684
- title: specialLabel,
1624
+ title: 'Évènement',
1685
1625
  message: html,
1686
1626
  buttons: {
1687
1627
  close: { label: 'Fermer', className: 'btn-secondary' },
@@ -1693,7 +1633,7 @@ function toDatetimeLocalValue(date) {
1693
1633
  try {
1694
1634
  const eid = String(p.eid || ev.id).replace(/^special:/, '');
1695
1635
  await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}`, { method: 'DELETE' });
1696
- showAlert('success', `${specialLabel} supprimé.`);
1636
+ showAlert('success', 'Évènement supprimé.');
1697
1637
  calendar.refetchEvents();
1698
1638
  } catch (e) {
1699
1639
  showAlert('error', 'Suppression impossible.');
@@ -2212,40 +2152,6 @@ async function openFabDatePicker() {
2212
2152
  try {
2213
2153
  if (isDialogOpen) return;
2214
2154
  isDialogOpen = true;
2215
-
2216
- // Let the user pick between a Location or an Outing forecast.
2217
- const choice = await new Promise((resolve) => {
2218
- bootbox.dialog({
2219
- title: 'Créer',
2220
- message: '<div>Que veux-tu créer ?</div>',
2221
- buttons: {
2222
- cancel: { label: 'Annuler', className: 'btn-secondary', callback: () => resolve(null) },
2223
- outing: { label: 'Prévision de sortie', className: 'btn-info', callback: () => resolve('outing') },
2224
- reservation: { label: 'Location', className: 'btn-primary', callback: () => resolve('reservation') },
2225
- },
2226
- });
2227
- });
2228
-
2229
- if (!choice) return;
2230
-
2231
- if (choice === 'outing') {
2232
- const payload = await openSpecialEventDialog({ start: s, end: endExcl, allDay: true }, { kind: 'outing', dialogTitle: 'Créer une prévision de sortie', defaultTitle: 'Sortie' });
2233
- if (!payload) return;
2234
- await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
2235
- method: 'POST',
2236
- body: JSON.stringify(payload),
2237
- }).catch(async () => {
2238
- return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
2239
- method: 'POST',
2240
- body: JSON.stringify(payload),
2241
- });
2242
- });
2243
- showAlert('success', 'Sortie créée.');
2244
- invalidateEventsCache();
2245
- if (currentCalendar) scheduleRefetch(currentCalendar);
2246
- return;
2247
- }
2248
-
2249
2155
  const items = cachedItems || (await loadItems());
2250
2156
  const chosen = await openReservationDialog({ start: s, end: endExcl }, items);
2251
2157
  if (chosen && chosen.itemIds && chosen.itemIds.length) {
@@ -80,19 +80,6 @@
80
80
  <div class="form-text">Si vide, aucune notification Discord n'est envoyée.</div>
81
81
  </div>
82
82
 
83
- <div class="mb-3">
84
- <label class="form-label">Webhook URL (Évènements)</label>
85
- <input class="form-control" name="discordWebhookUrlEvents" placeholder="https://discord.com/api/webhooks/...">
86
- <div class="form-text">Notifications Discord pour les <strong>évènements</strong> (création & annulation). Si vide, aucune notification n'est envoyée.</div>
87
- </div>
88
-
89
- <div class="mb-3">
90
- <label class="form-label">Webhook URL (Sorties)</label>
91
- <input class="form-control" name="discordWebhookUrlOutings" placeholder="https://discord.com/api/webhooks/...">
92
- <div class="form-text">Notifications Discord pour les <strong>sorties</strong> (création & annulation). Si vide, aucune notification n'est envoyée.</div>
93
- </div>
94
-
95
-
96
83
  <div class="mb-3">
97
84
  <label class="form-label">Envoyer une notification à la demande</label>
98
85
  <select class="form-select" name="discordNotifyOnRequest">
@@ -170,6 +157,17 @@
170
157
  <div class="tab-pane fade" id="onekite-tab-events" role="tabpanel">
171
158
  <h4>Évènements (autre couleur)</h4>
172
159
  <div class="form-text mb-3">Permet de créer des évènements horaires (début/fin) avec adresse (Leaflet) et notes.</div>
160
+
161
+ <div class="mb-3">
162
+ <label class="form-label">Groupes autorisés à créer des évènements (csv)</label>
163
+ <input class="form-control" name="specialCreatorGroups" placeholder="ex: staff,instructors">
164
+ </div>
165
+
166
+ <div class="mb-3">
167
+ <label class="form-label">Groupes autorisés à supprimer des évènements (csv)</label>
168
+ <input class="form-control" name="specialDeleterGroups" placeholder="Si vide: même liste que la création">
169
+ </div>
170
+
173
171
  <hr class="my-4" />
174
172
  <h4>Purge des évènements</h4>
175
173
  <div class="d-flex gap-2 align-items-center">