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 +41 -69
- package/lib/discord.js +0 -57
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/client.js +9 -103
- package/templates/admin/plugins/calendar-onekite.tpl +11 -13
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
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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 ||
|
|
451
|
+
title: `${ev.title || 'Évènement'}`.trim(),
|
|
450
452
|
allDay: false,
|
|
451
453
|
start: startIso,
|
|
452
454
|
end: endIso,
|
|
453
|
-
backgroundColor:
|
|
454
|
-
borderColor:
|
|
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
|
-
|
|
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
|
|
497
|
-
const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings
|
|
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:
|
|
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:
|
|
717
|
+
canDeleteSpecial: canSpecialDelete,
|
|
720
718
|
};
|
|
721
|
-
if (ev.username && (canMod ||
|
|
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
|
|
734
|
-
canDeleteSpecial: uid ? await canDeleteSpecial(uid, settings
|
|
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
|
|
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
|
|
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
package/plugin.json
CHANGED
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
|
|
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: ..."
|
|
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:
|
|
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({
|
|
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
|
|
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 =
|
|
1613
|
-
el2.style.borderColor =
|
|
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:
|
|
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',
|
|
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">
|