nodebb-plugin-onekite-calendar 2.0.40 → 2.0.41
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 +69 -41
- package/lib/discord.js +57 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/client.js +103 -9
- package/templates/admin/plugins/calendar-onekite.tpl +13 -11
package/lib/api.js
CHANGED
|
@@ -359,30 +359,17 @@ async function auditLog(action, actorUid, payload) {
|
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
async function canCreateSpecial(uid, settings) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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;
|
|
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);
|
|
373
367
|
}
|
|
374
368
|
|
|
375
|
-
async function canDeleteSpecial(uid, settings) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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;
|
|
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);
|
|
386
373
|
}
|
|
387
374
|
|
|
388
375
|
function eventsFor(resv) {
|
|
@@ -442,23 +429,36 @@ function eventsFor(resv) {
|
|
|
442
429
|
}
|
|
443
430
|
|
|
444
431
|
function eventsForSpecial(ev) {
|
|
432
|
+
const kind = String(ev.kind || 'event');
|
|
445
433
|
const start = new Date(parseInt(ev.start, 10));
|
|
446
434
|
const end = new Date(parseInt(ev.end, 10));
|
|
447
435
|
const startIso = start.toISOString();
|
|
448
436
|
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
|
+
|
|
449
447
|
return {
|
|
450
448
|
id: `special:${ev.eid}`,
|
|
451
|
-
title: `${ev.title ||
|
|
449
|
+
title: `${ev.title || defaultTitle}`.trim(),
|
|
452
450
|
allDay: false,
|
|
453
451
|
start: startIso,
|
|
454
452
|
end: endIso,
|
|
455
|
-
backgroundColor:
|
|
456
|
-
borderColor:
|
|
453
|
+
backgroundColor: palette.bg,
|
|
454
|
+
borderColor: palette.border,
|
|
457
455
|
textColor: '#ffffff',
|
|
458
456
|
extendedProps: {
|
|
459
457
|
type: 'special',
|
|
458
|
+
kind,
|
|
460
459
|
eid: ev.eid,
|
|
461
|
-
|
|
460
|
+
kind: String(ev.kind || 'event'),
|
|
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) : false;
|
|
497
|
-
const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings) : false;
|
|
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;
|
|
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:
|
|
614
|
+
canDeleteSpecial: canSpecialDeleteResolved,
|
|
615
615
|
},
|
|
616
616
|
};
|
|
617
617
|
if (sev.username && (canMod || canSpecialDelete || (req.uid && String(req.uid) === String(sev.uid)))) {
|
|
@@ -696,17 +696,19 @@ 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);
|
|
700
699
|
|
|
701
700
|
const eid = String(req.params.eid || '').trim();
|
|
702
701
|
if (!eid) return res.status(400).json({ error: 'missing-eid' });
|
|
703
702
|
const ev = await dbLayer.getSpecialEvent(eid);
|
|
704
703
|
if (!ev) return res.status(404).json({ error: 'not-found' });
|
|
705
704
|
|
|
705
|
+
const canSpecialDeleteResolved = await canDeleteSpecial(uid, settings, ev && ev.start ? Number(ev.start) : Date.now());
|
|
706
|
+
|
|
706
707
|
// Anyone who can see the calendar can view special events, but creator username
|
|
707
708
|
// is only visible to moderators/allowed users or the creator.
|
|
708
709
|
const out = {
|
|
709
710
|
eid: ev.eid,
|
|
711
|
+
kind: String(ev.kind || 'event'),
|
|
710
712
|
title: ev.title || '',
|
|
711
713
|
start: ev.start,
|
|
712
714
|
end: ev.end,
|
|
@@ -714,9 +716,9 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
714
716
|
lat: ev.lat || '',
|
|
715
717
|
lon: ev.lon || '',
|
|
716
718
|
notes: ev.notes || '',
|
|
717
|
-
canDeleteSpecial:
|
|
719
|
+
canDeleteSpecial: canSpecialDeleteResolved,
|
|
718
720
|
};
|
|
719
|
-
if (ev.username && (canMod ||
|
|
721
|
+
if (ev.username && (canMod || canSpecialDeleteResolved || (uid && String(uid) === String(ev.uid)))) {
|
|
720
722
|
out.username = String(ev.username);
|
|
721
723
|
}
|
|
722
724
|
return res.json(out);
|
|
@@ -728,23 +730,28 @@ api.getCapabilities = async function (req, res) {
|
|
|
728
730
|
const canMod = uid ? await canValidate(uid, settings) : false;
|
|
729
731
|
res.json({
|
|
730
732
|
canModerate: canMod,
|
|
731
|
-
canCreateSpecial: uid ? await canCreateSpecial(uid, settings) : false,
|
|
732
|
-
canDeleteSpecial: uid ? await canDeleteSpecial(uid, settings) : false,
|
|
733
|
+
canCreateSpecial: uid ? await canCreateSpecial(uid, settings, Date.now()) : false,
|
|
734
|
+
canDeleteSpecial: uid ? await canDeleteSpecial(uid, settings, Date.now()) : false,
|
|
733
735
|
});
|
|
734
736
|
};
|
|
735
737
|
|
|
736
738
|
api.createSpecialEvent = async function (req, res) {
|
|
737
739
|
const settings = await meta.settings.get('calendar-onekite');
|
|
738
740
|
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
|
|
742
|
+
const kind = String((req.body && req.body.kind) || 'event').trim().toLowerCase();
|
|
743
|
+
const safeKind = (kind === 'outing') ? 'outing' : 'event';
|
|
744
|
+
|
|
743
745
|
const startTs = toTs(req.body && req.body.start);
|
|
744
746
|
const endTs = toTs(req.body && req.body.end);
|
|
745
747
|
if (!Number.isFinite(startTs) || !Number.isFinite(endTs) || !(startTs < endTs)) {
|
|
746
748
|
return res.status(400).json({ error: 'bad-dates' });
|
|
747
749
|
}
|
|
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');
|
|
748
755
|
const address = String((req.body && req.body.address) || '').trim();
|
|
749
756
|
const notes = String((req.body && req.body.notes) || '').trim();
|
|
750
757
|
const lat = String((req.body && req.body.lat) || '').trim();
|
|
@@ -754,6 +761,7 @@ api.createSpecialEvent = async function (req, res) {
|
|
|
754
761
|
const eid = crypto.randomUUID();
|
|
755
762
|
const ev = {
|
|
756
763
|
eid,
|
|
764
|
+
kind: safeKind,
|
|
757
765
|
title,
|
|
758
766
|
start: String(startTs),
|
|
759
767
|
end: String(endTs),
|
|
@@ -765,21 +773,41 @@ api.createSpecialEvent = async function (req, res) {
|
|
|
765
773
|
username: u && u.username ? String(u.username) : '',
|
|
766
774
|
createdAt: String(Date.now()),
|
|
767
775
|
};
|
|
776
|
+
|
|
768
777
|
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
|
+
|
|
769
784
|
// Real-time refresh for all viewers
|
|
770
|
-
realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'created', eid: ev.eid });
|
|
785
|
+
realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'created', eid: ev.eid, specialKind: ev.kind });
|
|
771
786
|
res.json({ ok: true, eid });
|
|
772
787
|
};
|
|
773
788
|
|
|
774
789
|
api.deleteSpecialEvent = async function (req, res) {
|
|
775
790
|
const settings = await meta.settings.get('calendar-onekite');
|
|
776
791
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
777
|
-
|
|
778
|
-
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
792
|
+
|
|
779
793
|
const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
|
|
780
794
|
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
|
+
|
|
781
804
|
await dbLayer.removeSpecialEvent(eid);
|
|
782
|
-
|
|
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' });
|
|
783
811
|
res.json({ ok: true });
|
|
784
812
|
};
|
|
785
813
|
|
package/lib/discord.js
CHANGED
|
@@ -183,8 +183,65 @@ 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
|
+
|
|
186
242
|
module.exports = {
|
|
187
243
|
notifyReservationRequested,
|
|
188
244
|
notifyPaymentReceived,
|
|
189
245
|
notifyReservationCancelled,
|
|
246
|
+
notifySpecialEvent,
|
|
190
247
|
};
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/client.js
CHANGED
|
@@ -208,7 +208,12 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
208
208
|
return opts.join('');
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
async function openSpecialEventDialog(selectionInfo) {
|
|
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
|
+
|
|
212
217
|
const start = selectionInfo.start;
|
|
213
218
|
// FullCalendar can omit `end` for certain interactions. Also, for all-day
|
|
214
219
|
// selections, `end` is exclusive (next day at 00:00). We normalise below
|
|
@@ -262,7 +267,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
262
267
|
const html = `
|
|
263
268
|
<div class="mb-3">
|
|
264
269
|
<label class="form-label">Titre</label>
|
|
265
|
-
<input type="text" class="form-control" id="onekite-se-title" placeholder="Ex: ..." />
|
|
270
|
+
<input type="text" class="form-control" id="onekite-se-title" placeholder="Ex: ..." value="${escapeHtml(defaultTitle)}" />
|
|
266
271
|
</div>
|
|
267
272
|
<div class="row g-2">
|
|
268
273
|
<div class="col-12 col-md-6">
|
|
@@ -307,7 +312,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
307
312
|
return await new Promise((resolve) => {
|
|
308
313
|
let resolved = false;
|
|
309
314
|
const dialog = bootbox.dialog({
|
|
310
|
-
title:
|
|
315
|
+
title: dialogTitle,
|
|
311
316
|
message: html,
|
|
312
317
|
buttons: {
|
|
313
318
|
cancel: {
|
|
@@ -356,7 +361,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
356
361
|
const lat = (document.getElementById('onekite-se-lat')?.value || '').trim();
|
|
357
362
|
const lon = (document.getElementById('onekite-se-lon')?.value || '').trim();
|
|
358
363
|
resolved = true;
|
|
359
|
-
resolve({ title, start: startVal, end: endVal, address, notes, lat, lon });
|
|
364
|
+
resolve({ kind, title, start: startVal, end: endVal, address, notes, lat, lon });
|
|
360
365
|
return true;
|
|
361
366
|
},
|
|
362
367
|
},
|
|
@@ -1358,6 +1363,55 @@ function toDatetimeLocalValue(date) {
|
|
|
1358
1363
|
return;
|
|
1359
1364
|
}
|
|
1360
1365
|
|
|
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
|
+
}
|
|
1361
1415
|
// Business rule: reservations cannot start in the past.
|
|
1362
1416
|
// (We validate again on the server, but this gives immediate feedback.)
|
|
1363
1417
|
try {
|
|
@@ -1544,15 +1598,19 @@ function toDatetimeLocalValue(date) {
|
|
|
1544
1598
|
}
|
|
1545
1599
|
},
|
|
1546
1600
|
eventDidMount: function (arg) {
|
|
1547
|
-
// Keep special event colors consistent.
|
|
1601
|
+
// Keep special event colors consistent (kind-specific).
|
|
1548
1602
|
try {
|
|
1549
1603
|
const ev = arg && arg.event;
|
|
1550
1604
|
if (!ev) return;
|
|
1551
1605
|
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' };
|
|
1552
1610
|
const el2 = arg.el;
|
|
1553
1611
|
if (el2 && el2.style) {
|
|
1554
|
-
el2.style.backgroundColor =
|
|
1555
|
-
el2.style.borderColor =
|
|
1612
|
+
el2.style.backgroundColor = palette.bg;
|
|
1613
|
+
el2.style.borderColor = palette.border;
|
|
1556
1614
|
el2.style.color = '#ffffff';
|
|
1557
1615
|
}
|
|
1558
1616
|
}
|
|
@@ -1598,6 +1656,8 @@ function toDatetimeLocalValue(date) {
|
|
|
1598
1656
|
|
|
1599
1657
|
try {
|
|
1600
1658
|
if (p.type === 'special') {
|
|
1659
|
+
const specialKind = String((p.kind || p0.kind || 'event'));
|
|
1660
|
+
const specialLabel = (specialKind === 'outing') ? 'Sortie' : 'Évènement';
|
|
1601
1661
|
const username = String(p.username || '').trim();
|
|
1602
1662
|
const userLine = username
|
|
1603
1663
|
? `<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>`
|
|
@@ -1621,7 +1681,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1621
1681
|
`;
|
|
1622
1682
|
const canDel = !!(p.canDeleteSpecial || canDeleteSpecial);
|
|
1623
1683
|
bootbox.dialog({
|
|
1624
|
-
title:
|
|
1684
|
+
title: specialLabel,
|
|
1625
1685
|
message: html,
|
|
1626
1686
|
buttons: {
|
|
1627
1687
|
close: { label: 'Fermer', className: 'btn-secondary' },
|
|
@@ -1633,7 +1693,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1633
1693
|
try {
|
|
1634
1694
|
const eid = String(p.eid || ev.id).replace(/^special:/, '');
|
|
1635
1695
|
await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}`, { method: 'DELETE' });
|
|
1636
|
-
showAlert('success',
|
|
1696
|
+
showAlert('success', `${specialLabel} supprimé.`);
|
|
1637
1697
|
calendar.refetchEvents();
|
|
1638
1698
|
} catch (e) {
|
|
1639
1699
|
showAlert('error', 'Suppression impossible.');
|
|
@@ -2152,6 +2212,40 @@ async function openFabDatePicker() {
|
|
|
2152
2212
|
try {
|
|
2153
2213
|
if (isDialogOpen) return;
|
|
2154
2214
|
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
|
+
|
|
2155
2249
|
const items = cachedItems || (await loadItems());
|
|
2156
2250
|
const chosen = await openReservationDialog({ start: s, end: endExcl }, items);
|
|
2157
2251
|
if (chosen && chosen.itemIds && chosen.itemIds.length) {
|
|
@@ -80,6 +80,19 @@
|
|
|
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
|
+
|
|
83
96
|
<div class="mb-3">
|
|
84
97
|
<label class="form-label">Envoyer une notification à la demande</label>
|
|
85
98
|
<select class="form-select" name="discordNotifyOnRequest">
|
|
@@ -157,17 +170,6 @@
|
|
|
157
170
|
<div class="tab-pane fade" id="onekite-tab-events" role="tabpanel">
|
|
158
171
|
<h4>Évènements (autre couleur)</h4>
|
|
159
172
|
<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
|
-
|
|
171
173
|
<hr class="my-4" />
|
|
172
174
|
<h4>Purge des évènements</h4>
|
|
173
175
|
<div class="d-flex gap-2 align-items-center">
|