nodebb-plugin-onekite-calendar 2.0.68 → 2.0.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api.js +67 -1
- package/library.js +2 -0
- package/package.json +1 -1
- package/public/client.js +108 -9
package/lib/api.js
CHANGED
|
@@ -647,6 +647,7 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
647
647
|
const settings = await getSettings();
|
|
648
648
|
const canMod = uid ? await canValidate(uid, settings) : false;
|
|
649
649
|
const canSpecialDelete = uid ? await canDeleteSpecial(uid, settings) : false;
|
|
650
|
+
const canSpecialCreate = uid ? await canCreateSpecial(uid, settings) : false;
|
|
650
651
|
|
|
651
652
|
const eid = String(req.params.eid || '').trim();
|
|
652
653
|
if (!eid) return res.status(400).json({ error: 'missing-eid' });
|
|
@@ -680,6 +681,7 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
680
681
|
notes: ev.notes || '',
|
|
681
682
|
participants,
|
|
682
683
|
participantsUsernames: await usernamesByUids(participants),
|
|
684
|
+
canEditSpecial: canSpecialCreate,
|
|
683
685
|
canDeleteSpecial: canSpecialDelete,
|
|
684
686
|
canJoin: uid ? await canJoinSpecial(uid, settings) : false,
|
|
685
687
|
isParticipant: uid ? participants.includes(String(uid)) : false,
|
|
@@ -871,6 +873,37 @@ api.deleteSpecialEvent = async function (req, res) {
|
|
|
871
873
|
res.json({ ok: true });
|
|
872
874
|
};
|
|
873
875
|
|
|
876
|
+
api.updateSpecialEvent = async function (req, res) {
|
|
877
|
+
const settings = await getSettings();
|
|
878
|
+
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
879
|
+
const ok = await canCreateSpecial(req.uid, settings);
|
|
880
|
+
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
881
|
+
|
|
882
|
+
const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
|
|
883
|
+
if (!eid) return res.status(400).json({ error: 'bad-id' });
|
|
884
|
+
|
|
885
|
+
const ev = await dbLayer.getSpecialEvent(eid);
|
|
886
|
+
if (!ev) return res.status(404).json({ error: 'not-found' });
|
|
887
|
+
|
|
888
|
+
const startTs = toTs(req.body && req.body.start);
|
|
889
|
+
const endTs = toTs(req.body && req.body.end);
|
|
890
|
+
if (!Number.isFinite(startTs) || !Number.isFinite(endTs) || !(startTs < endTs)) {
|
|
891
|
+
return res.status(400).json({ error: 'bad-dates' });
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
ev.title = String((req.body && req.body.title) || '').trim() || ev.title || 'Évènement';
|
|
895
|
+
ev.start = String(startTs);
|
|
896
|
+
ev.end = String(endTs);
|
|
897
|
+
ev.address = String((req.body && req.body.address) || '').trim();
|
|
898
|
+
ev.lat = String((req.body && req.body.lat) || '').trim();
|
|
899
|
+
ev.lon = String((req.body && req.body.lon) || '').trim();
|
|
900
|
+
ev.notes = String((req.body && req.body.notes) || '').trim();
|
|
901
|
+
|
|
902
|
+
await dbLayer.saveSpecialEvent(ev);
|
|
903
|
+
realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'updated', eid });
|
|
904
|
+
return res.json({ ok: true });
|
|
905
|
+
};
|
|
906
|
+
|
|
874
907
|
/**
|
|
875
908
|
* Get detailed information about an outing (prévision de sortie).
|
|
876
909
|
*
|
|
@@ -911,6 +944,7 @@ api.getOutingDetails = async function (req, res) {
|
|
|
911
944
|
});
|
|
912
945
|
|
|
913
946
|
const participants = normalizeUidList(o.participants);
|
|
947
|
+
const canEditOuting = uid ? await canRequest(uid, settings, Date.now()) : false;
|
|
914
948
|
const out = {
|
|
915
949
|
oid: o.oid,
|
|
916
950
|
title: o.title || '',
|
|
@@ -922,7 +956,8 @@ api.getOutingDetails = async function (req, res) {
|
|
|
922
956
|
notes: o.notes || '',
|
|
923
957
|
participants,
|
|
924
958
|
participantsUsernames: await usernamesByUids(participants),
|
|
925
|
-
|
|
959
|
+
canEditOuting,
|
|
960
|
+
canJoin: canEditOuting,
|
|
926
961
|
isParticipant: uid ? participants.includes(String(uid)) : false,
|
|
927
962
|
canDeleteOuting: canMod || (uid && String(uid) === String(o.uid)),
|
|
928
963
|
icsUrl: links.icsUrl,
|
|
@@ -1090,6 +1125,37 @@ api.deleteOuting = async function (req, res) {
|
|
|
1090
1125
|
return res.json({ ok: true });
|
|
1091
1126
|
};
|
|
1092
1127
|
|
|
1128
|
+
api.updateOuting = async function (req, res) {
|
|
1129
|
+
const settings = await getSettings();
|
|
1130
|
+
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
1131
|
+
const ok = await canRequest(req.uid, settings, Date.now());
|
|
1132
|
+
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
1133
|
+
|
|
1134
|
+
const oid = String(req.params.oid || '').replace(/^outing:/, '').trim();
|
|
1135
|
+
if (!oid) return res.status(400).json({ error: 'bad-id' });
|
|
1136
|
+
|
|
1137
|
+
const o = await dbLayer.getOuting(oid);
|
|
1138
|
+
if (!o) return res.status(404).json({ error: 'not-found' });
|
|
1139
|
+
|
|
1140
|
+
const startTs = toTs(req.body && req.body.start);
|
|
1141
|
+
const endTs = toTs(req.body && req.body.end);
|
|
1142
|
+
if (!Number.isFinite(startTs) || !Number.isFinite(endTs) || !(startTs < endTs)) {
|
|
1143
|
+
return res.status(400).json({ error: 'bad-dates' });
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
o.title = String((req.body && req.body.title) || '').trim() || o.title || 'Sortie';
|
|
1147
|
+
o.start = String(startTs);
|
|
1148
|
+
o.end = String(endTs);
|
|
1149
|
+
o.address = String((req.body && req.body.address) || '').trim();
|
|
1150
|
+
o.lat = String((req.body && req.body.lat) || '').trim();
|
|
1151
|
+
o.lon = String((req.body && req.body.lon) || '').trim();
|
|
1152
|
+
o.notes = String((req.body && req.body.notes) || '').trim();
|
|
1153
|
+
|
|
1154
|
+
await dbLayer.saveOuting(o);
|
|
1155
|
+
realtime.emitCalendarUpdated({ kind: 'outing', action: 'updated', oid });
|
|
1156
|
+
return res.json({ ok: true });
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1093
1159
|
api.getItems = async function (req, res) {
|
|
1094
1160
|
const settings = await getSettings();
|
|
1095
1161
|
|
package/library.js
CHANGED
|
@@ -82,6 +82,7 @@ Plugin.init = async function (params) {
|
|
|
82
82
|
router.post('/api/v3/plugins/calendar-onekite/special-events', ...publicExpose, api.createSpecialEvent);
|
|
83
83
|
router.get('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.getSpecialEventDetails);
|
|
84
84
|
router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.deleteSpecialEvent);
|
|
85
|
+
router.put('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.updateSpecialEvent);
|
|
85
86
|
// Participants (self-join) for special events
|
|
86
87
|
router.post('/api/v3/plugins/calendar-onekite/special-events/:eid/participants', ...publicExpose, api.joinSpecialEvent);
|
|
87
88
|
router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid/participants', ...publicExpose, api.leaveSpecialEvent);
|
|
@@ -90,6 +91,7 @@ Plugin.init = async function (params) {
|
|
|
90
91
|
router.post('/api/v3/plugins/calendar-onekite/outings', ...publicExpose, api.createOuting);
|
|
91
92
|
router.get('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.getOutingDetails);
|
|
92
93
|
router.delete('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.deleteOuting);
|
|
94
|
+
router.put('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.updateOuting);
|
|
93
95
|
// Participants (self-join) for outings
|
|
94
96
|
router.post('/api/v3/plugins/calendar-onekite/outings/:oid/participants', ...publicExpose, api.joinOuting);
|
|
95
97
|
router.delete('/api/v3/plugins/calendar-onekite/outings/:oid/participants', ...publicExpose, api.leaveOuting);
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -239,8 +239,32 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
239
239
|
return opts.join('');
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
function buildInitialValuesFromEvent(details) {
|
|
243
|
+
function toLocal(ts) {
|
|
244
|
+
const d = new Date(parseInt(ts, 10));
|
|
245
|
+
return {
|
|
246
|
+
ymd: `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`,
|
|
247
|
+
time: `${pad2(d.getHours())}:${pad2(d.getMinutes())}`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const s = toLocal(details.start);
|
|
251
|
+
const e = toLocal(details.end);
|
|
252
|
+
return {
|
|
253
|
+
title: details.title || '',
|
|
254
|
+
startYmd: s.ymd,
|
|
255
|
+
startTime: s.time,
|
|
256
|
+
endYmd: e.ymd,
|
|
257
|
+
endTime: e.time,
|
|
258
|
+
address: details.address || details.pickupAddress || '',
|
|
259
|
+
lat: details.lat || details.pickupLat || '',
|
|
260
|
+
lon: details.lon || details.pickupLon || '',
|
|
261
|
+
notes: details.notes || '',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
242
265
|
async function openSpecialEventDialog(selectionInfo, opts) {
|
|
243
266
|
const kind = (opts && opts.kind) ? String(opts.kind) : 'special';
|
|
267
|
+
const iv = opts && opts.initialValues;
|
|
244
268
|
const start = selectionInfo.start;
|
|
245
269
|
// FullCalendar can omit `end` for certain interactions. Also, for all-day
|
|
246
270
|
// selections, `end` is exclusive (next day at 00:00). We normalise below
|
|
@@ -289,12 +313,20 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
289
313
|
}
|
|
290
314
|
}
|
|
291
315
|
|
|
316
|
+
// Edit mode: override computed dates with existing event values.
|
|
317
|
+
if (iv) {
|
|
318
|
+
if (iv.startYmd && iv.startTime) seStart = new Date(`${iv.startYmd}T${iv.startTime}`);
|
|
319
|
+
if (iv.endYmd && iv.endTime) seEnd = new Date(`${iv.endYmd}T${iv.endTime}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
292
322
|
const seStartTime = timeString(seStart);
|
|
293
323
|
const seEndTime = timeString(seEnd);
|
|
294
|
-
// Same UX for all types: an example placeholder, but never pre-fill the value.
|
|
295
|
-
// If left empty, the server can still apply its own default label.
|
|
296
324
|
const defaultTitlePlaceholder = 'Ex: ...';
|
|
297
|
-
const defaultTitleValue = '';
|
|
325
|
+
const defaultTitleValue = iv ? (iv.title || '') : '';
|
|
326
|
+
const ivAddress = iv ? (iv.address || '') : '';
|
|
327
|
+
const ivLat = iv ? (iv.lat || '') : '';
|
|
328
|
+
const ivLon = iv ? (iv.lon || '') : '';
|
|
329
|
+
const ivNotes = iv ? (iv.notes || '') : '';
|
|
298
330
|
|
|
299
331
|
const html = `
|
|
300
332
|
<div class="mb-3">
|
|
@@ -328,16 +360,16 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
328
360
|
<div class="mt-3">
|
|
329
361
|
<label class="form-label">Adresse</label>
|
|
330
362
|
<div class="input-group">
|
|
331
|
-
<input type="text" class="form-control" id="onekite-se-address" placeholder="Adresse complète" />
|
|
363
|
+
<input type="text" class="form-control" id="onekite-se-address" placeholder="Adresse complète" value="${escapeHtml(ivAddress)}" />
|
|
332
364
|
<button class="btn btn-outline-secondary" type="button" id="onekite-se-geocode">Rechercher</button>
|
|
333
365
|
</div>
|
|
334
366
|
<div id="onekite-se-map" style="height:220px; border:1px solid #ddd; border-radius:6px; margin-top:0.5rem;"></div>
|
|
335
|
-
<input type="hidden" id="onekite-se-lat" />
|
|
336
|
-
<input type="hidden" id="onekite-se-lon" />
|
|
367
|
+
<input type="hidden" id="onekite-se-lat" value="${escapeHtml(ivLat)}" />
|
|
368
|
+
<input type="hidden" id="onekite-se-lon" value="${escapeHtml(ivLon)}" />
|
|
337
369
|
</div>
|
|
338
370
|
<div class="mt-3">
|
|
339
371
|
<label class="form-label">Notes (facultatif)</label>
|
|
340
|
-
<textarea class="form-control" id="onekite-se-notes" rows="3" placeholder="..."
|
|
372
|
+
<textarea class="form-control" id="onekite-se-notes" rows="3" placeholder="...">${escapeHtml(ivNotes)}</textarea>
|
|
341
373
|
</div>
|
|
342
374
|
${(kind !== 'outing' && opts && opts.withContent) ? `
|
|
343
375
|
<div class="mt-3">
|
|
@@ -349,8 +381,11 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
349
381
|
|
|
350
382
|
return await new Promise((resolve) => {
|
|
351
383
|
let resolved = false;
|
|
384
|
+
const isEditMode = !!iv;
|
|
352
385
|
const dialog = bootbox.dialog({
|
|
353
|
-
title:
|
|
386
|
+
title: isEditMode
|
|
387
|
+
? (kind === 'outing' ? 'Modifier la sortie' : "Modifier l'évènement")
|
|
388
|
+
: (kind === 'outing' ? 'Créer une prévision de sortie' : 'Créer un évènement'),
|
|
354
389
|
message: html,
|
|
355
390
|
buttons: {
|
|
356
391
|
cancel: {
|
|
@@ -362,7 +397,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
362
397
|
},
|
|
363
398
|
},
|
|
364
399
|
ok: {
|
|
365
|
-
label: 'Créer',
|
|
400
|
+
label: isEditMode ? 'Enregistrer' : 'Créer',
|
|
366
401
|
className: 'btn-primary',
|
|
367
402
|
callback: () => {
|
|
368
403
|
const title = (document.getElementById('onekite-se-title')?.value || '').trim();
|
|
@@ -446,6 +481,14 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
446
481
|
document.getElementById('onekite-se-lon').value = String(lon);
|
|
447
482
|
map.setView([lat, lon], 14);
|
|
448
483
|
}
|
|
484
|
+
// Pre-fill marker if editing an event with existing coordinates.
|
|
485
|
+
if (ivLat && ivLon) {
|
|
486
|
+
const initLat = Number(ivLat);
|
|
487
|
+
const initLon = Number(ivLon);
|
|
488
|
+
if (Number.isFinite(initLat) && Number.isFinite(initLon)) {
|
|
489
|
+
setMarker(initLat, initLon);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
449
492
|
const geocodeBtn = document.getElementById('onekite-se-geocode');
|
|
450
493
|
const addrInput = document.getElementById('onekite-se-address');
|
|
451
494
|
try {
|
|
@@ -1763,11 +1806,39 @@ function toDatetimeLocalValue(date) {
|
|
|
1763
1806
|
${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
|
|
1764
1807
|
`;
|
|
1765
1808
|
const canDel = !!(p.canDeleteSpecial || canDeleteSpecial);
|
|
1809
|
+
const canEdit = !!p.canEditSpecial;
|
|
1766
1810
|
const dlg = bootbox.dialog({
|
|
1767
1811
|
title: 'Évènement',
|
|
1768
1812
|
message: html,
|
|
1769
1813
|
buttons: {
|
|
1770
1814
|
close: { label: 'Fermer', className: 'btn-secondary' },
|
|
1815
|
+
...(canEdit ? {
|
|
1816
|
+
edit: {
|
|
1817
|
+
label: 'Éditer',
|
|
1818
|
+
className: 'btn-default',
|
|
1819
|
+
callback: () => {
|
|
1820
|
+
(async () => {
|
|
1821
|
+
try { dlg.modal('hide'); } catch (e) { try { bootbox.hideAll(); } catch (e2) {} }
|
|
1822
|
+
const eid = String(p.eid || ev.id).replace(/^special:/, '');
|
|
1823
|
+
const initialValues = buildInitialValuesFromEvent(p);
|
|
1824
|
+
const payload = await openSpecialEventDialog({}, { kind: 'special', initialValues });
|
|
1825
|
+
if (!payload) return;
|
|
1826
|
+
try {
|
|
1827
|
+
await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}`, {
|
|
1828
|
+
method: 'PUT',
|
|
1829
|
+
body: JSON.stringify(payload),
|
|
1830
|
+
});
|
|
1831
|
+
showAlert('success', 'Évènement mis à jour.');
|
|
1832
|
+
invalidateEventsCache();
|
|
1833
|
+
scheduleRefetch(calendar);
|
|
1834
|
+
} catch (e) {
|
|
1835
|
+
showAlert('error', 'Mise à jour impossible.');
|
|
1836
|
+
}
|
|
1837
|
+
})();
|
|
1838
|
+
return false;
|
|
1839
|
+
},
|
|
1840
|
+
},
|
|
1841
|
+
} : {}),
|
|
1771
1842
|
...(canDel ? {
|
|
1772
1843
|
del: {
|
|
1773
1844
|
label: 'Supprimer',
|
|
@@ -1913,11 +1984,39 @@ function toDatetimeLocalValue(date) {
|
|
|
1913
1984
|
${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
|
|
1914
1985
|
`;
|
|
1915
1986
|
const canDel = !!(p.canDeleteOuting);
|
|
1987
|
+
const canEditOuting = !!p.canEditOuting;
|
|
1916
1988
|
const dlg = bootbox.dialog({
|
|
1917
1989
|
title: 'Sortie',
|
|
1918
1990
|
message: html,
|
|
1919
1991
|
buttons: {
|
|
1920
1992
|
close: { label: 'Fermer', className: 'btn-secondary' },
|
|
1993
|
+
...(canEditOuting ? {
|
|
1994
|
+
edit: {
|
|
1995
|
+
label: 'Éditer',
|
|
1996
|
+
className: 'btn-default',
|
|
1997
|
+
callback: () => {
|
|
1998
|
+
(async () => {
|
|
1999
|
+
try { dlg.modal('hide'); } catch (e) { try { bootbox.hideAll(); } catch (e2) {} }
|
|
2000
|
+
const oid = String(p.oid || ev.id).replace(/^outing:/, '');
|
|
2001
|
+
const initialValues = buildInitialValuesFromEvent(p);
|
|
2002
|
+
const payload = await openSpecialEventDialog({}, { kind: 'outing', initialValues });
|
|
2003
|
+
if (!payload) return;
|
|
2004
|
+
try {
|
|
2005
|
+
await fetchJson(`/api/v3/plugins/calendar-onekite/outings/${encodeURIComponent(oid)}`, {
|
|
2006
|
+
method: 'PUT',
|
|
2007
|
+
body: JSON.stringify(payload),
|
|
2008
|
+
});
|
|
2009
|
+
showAlert('success', 'Sortie mise à jour.');
|
|
2010
|
+
invalidateEventsCache();
|
|
2011
|
+
scheduleRefetch(calendar);
|
|
2012
|
+
} catch (e) {
|
|
2013
|
+
showAlert('error', 'Mise à jour impossible.');
|
|
2014
|
+
}
|
|
2015
|
+
})();
|
|
2016
|
+
return false;
|
|
2017
|
+
},
|
|
2018
|
+
},
|
|
2019
|
+
} : {}),
|
|
1921
2020
|
...(canDel ? {
|
|
1922
2021
|
del: {
|
|
1923
2022
|
label: 'Supprimer',
|