nodebb-plugin-calendar-onekite 11.2.32 → 12.0.0
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/CHANGELOG.md +7 -4
- package/lib/api.js +141 -17
- package/library.js +2 -0
- package/package.json +1 -1
- package/plugin.json +2 -1
- package/public/admin.js +1 -22
- package/public/client.js +119 -13
- package/templates/admin/plugins/calendar-onekite.tpl +5 -8
- package/templates/calendar-onekite.tpl +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
## 1.1.0
|
|
2
|
+
### Perf / prod (NodeBB v4)
|
|
3
|
+
- FullCalendar : passage au CDN **@latest** et utilisation de `main.min.css` (supprime l’erreur 404 `index.global.min.css`).
|
|
4
|
+
- API `events` : payload allégé (les détails sont chargés à la demande), tri stable.
|
|
5
|
+
- Cache intelligent : support **ETag** côté serveur + requêtes conditionnelles côté client (réduit les transferts lors des refetch).
|
|
6
|
+
- Prefetch : préchargement du mois précédent/suivant (si l’utilisateur navigue, la vue est instantanée).
|
|
7
|
+
- Robustesse : anti double-refetch (debounce sur les updates socket) et annulation des fetch concurrents.
|
|
1
8
|
|
|
2
|
-
## 1.0.2
|
|
3
|
-
- Front : empêche la double initialisation du calendrier (évite le clignotement au chargement / après actions).
|
|
4
|
-
- ACP (NodeBB v4) : init robuste via `hooks.on('action:ajaxify.end')` + garde-fou anti double-bind (évite les multi-popups).
|
|
5
|
-
- Maintenance : suppression du mapping module ACP legacy (`../admin/plugins/...`).
|
|
6
9
|
|
|
7
10
|
## 1.0.1.1
|
|
8
11
|
- ACP (NodeBB v4) : empêche l’affichage multiple des popups de succès lors de l’enregistrement (déduplication des alerts).
|
package/lib/api.js
CHANGED
|
@@ -252,6 +252,12 @@ function eventsForSpecial(ev) {
|
|
|
252
252
|
|
|
253
253
|
const api = {};
|
|
254
254
|
|
|
255
|
+
function computeEtag(payload) {
|
|
256
|
+
// Weak ETag is fine here: it is only used to skip identical JSON payloads.
|
|
257
|
+
const hash = crypto.createHash('sha1').update(JSON.stringify(payload)).digest('hex');
|
|
258
|
+
return `W/"${hash}"`;
|
|
259
|
+
}
|
|
260
|
+
|
|
255
261
|
api.getEvents = async function (req, res) {
|
|
256
262
|
const startTs = toTs(req.query.start) || 0;
|
|
257
263
|
const endTs = toTs(req.query.end) || (Date.now() + 365 * 24 * 3600 * 1000);
|
|
@@ -276,22 +282,35 @@ api.getEvents = async function (req, res) {
|
|
|
276
282
|
if (!(rStart < endTs && startTs < rEnd)) continue; // overlap check
|
|
277
283
|
const evs = eventsFor(r);
|
|
278
284
|
for (const ev of evs) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
285
|
+
const p = ev.extendedProps || {};
|
|
286
|
+
const minimal = {
|
|
287
|
+
id: ev.id,
|
|
288
|
+
title: ev.title,
|
|
289
|
+
backgroundColor: ev.backgroundColor,
|
|
290
|
+
borderColor: ev.borderColor,
|
|
291
|
+
textColor: ev.textColor,
|
|
292
|
+
allDay: ev.allDay,
|
|
293
|
+
start: ev.start,
|
|
294
|
+
end: ev.end,
|
|
295
|
+
extendedProps: {
|
|
296
|
+
type: 'reservation',
|
|
297
|
+
rid: p.rid,
|
|
298
|
+
status: p.status,
|
|
299
|
+
uid: p.uid,
|
|
300
|
+
canModerate: canMod,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
// Only expose username on the event list to owner/moderators.
|
|
283
304
|
if (r.username && ((req.uid && String(req.uid) === String(r.uid)) || canMod)) {
|
|
284
|
-
|
|
305
|
+
minimal.extendedProps.username = String(r.username);
|
|
285
306
|
}
|
|
286
|
-
|
|
287
|
-
ev.extendedProps.createdAt = r.createdAt || null;
|
|
288
|
-
// Expose payment URL only to the requester (or moderators) when awaiting payment
|
|
307
|
+
// Let the UI decide if a "Payer" button might exist, without exposing the URL in list.
|
|
289
308
|
if (r.status === 'awaiting_payment' && r.paymentUrl && (/^https?:\/\//i).test(String(r.paymentUrl))) {
|
|
290
309
|
if ((req.uid && String(req.uid) === String(r.uid)) || canMod) {
|
|
291
|
-
|
|
310
|
+
minimal.extendedProps.hasPayment = true;
|
|
292
311
|
}
|
|
293
312
|
}
|
|
294
|
-
out.push(
|
|
313
|
+
out.push(minimal);
|
|
295
314
|
}
|
|
296
315
|
}
|
|
297
316
|
|
|
@@ -304,19 +323,124 @@ api.getEvents = async function (req, res) {
|
|
|
304
323
|
const sStart = parseInt(sev.start, 10);
|
|
305
324
|
const sEnd = parseInt(sev.end, 10);
|
|
306
325
|
if (!(sStart < endTs && startTs < sEnd)) continue;
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
326
|
+
const full = eventsForSpecial(sev);
|
|
327
|
+
const minimal = {
|
|
328
|
+
id: full.id,
|
|
329
|
+
title: full.title,
|
|
330
|
+
allDay: full.allDay,
|
|
331
|
+
start: full.start,
|
|
332
|
+
end: full.end,
|
|
333
|
+
backgroundColor: full.backgroundColor,
|
|
334
|
+
borderColor: full.borderColor,
|
|
335
|
+
textColor: full.textColor,
|
|
336
|
+
extendedProps: {
|
|
337
|
+
type: 'special',
|
|
338
|
+
eid: sev.eid,
|
|
339
|
+
canCreateSpecial: canSpecialCreate,
|
|
340
|
+
canDeleteSpecial: canSpecialDelete,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
311
343
|
if (sev.username && (canMod || canSpecialDelete || (req.uid && String(req.uid) === String(sev.uid)))) {
|
|
312
|
-
|
|
344
|
+
minimal.extendedProps.username = String(sev.username);
|
|
313
345
|
}
|
|
314
|
-
out.push(
|
|
346
|
+
out.push(minimal);
|
|
315
347
|
}
|
|
316
348
|
} catch (e) {
|
|
317
349
|
// ignore
|
|
318
350
|
}
|
|
319
|
-
|
|
351
|
+
|
|
352
|
+
// Stable ordering -> stable ETag
|
|
353
|
+
out.sort((a, b) => {
|
|
354
|
+
const as = String(a.start || '');
|
|
355
|
+
const bs = String(b.start || '');
|
|
356
|
+
if (as !== bs) return as < bs ? -1 : 1;
|
|
357
|
+
const ai = String(a.id || '');
|
|
358
|
+
const bi = String(b.id || '');
|
|
359
|
+
return ai < bi ? -1 : ai > bi ? 1 : 0;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const etag = computeEtag(out);
|
|
363
|
+
res.setHeader('ETag', etag);
|
|
364
|
+
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
|
|
365
|
+
if (String(req.headers['if-none-match'] || '') === etag) {
|
|
366
|
+
return res.status(304).end();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return res.json(out);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
api.getReservationDetails = async function (req, res) {
|
|
373
|
+
const uid = req.uid;
|
|
374
|
+
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
375
|
+
|
|
376
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
377
|
+
const canMod = await canValidate(uid, settings);
|
|
378
|
+
|
|
379
|
+
const rid = String(req.params.rid || '').trim();
|
|
380
|
+
if (!rid) return res.status(400).json({ error: 'missing-rid' });
|
|
381
|
+
const r = await dbLayer.getReservation(rid);
|
|
382
|
+
if (!r) return res.status(404).json({ error: 'not-found' });
|
|
383
|
+
|
|
384
|
+
const isOwner = String(r.uid) === String(uid);
|
|
385
|
+
if (!isOwner && !canMod) return res.status(403).json({ error: 'not-allowed' });
|
|
386
|
+
|
|
387
|
+
const out = {
|
|
388
|
+
rid: r.rid,
|
|
389
|
+
status: r.status,
|
|
390
|
+
uid: r.uid,
|
|
391
|
+
username: r.username || '',
|
|
392
|
+
itemNames: Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : []),
|
|
393
|
+
itemIds: Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : []),
|
|
394
|
+
start: r.start,
|
|
395
|
+
end: r.end,
|
|
396
|
+
approvedByUsername: r.approvedByUsername || '',
|
|
397
|
+
pickupAddress: r.pickupAddress || '',
|
|
398
|
+
pickupTime: r.pickupTime || '',
|
|
399
|
+
pickupLat: r.pickupLat || '',
|
|
400
|
+
pickupLon: r.pickupLon || '',
|
|
401
|
+
notes: r.notes || '',
|
|
402
|
+
refusedReason: r.refusedReason || '',
|
|
403
|
+
total: r.total || 0,
|
|
404
|
+
canModerate: canMod,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
if (r.status === 'awaiting_payment' && r.paymentUrl && (/^https?:\/\//i).test(String(r.paymentUrl))) {
|
|
408
|
+
out.paymentUrl = String(r.paymentUrl);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return res.json(out);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
api.getSpecialEventDetails = async function (req, res) {
|
|
415
|
+
const uid = req.uid;
|
|
416
|
+
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
417
|
+
|
|
418
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
419
|
+
const canMod = await canValidate(uid, settings);
|
|
420
|
+
const canSpecialDelete = await canDeleteSpecial(uid, settings);
|
|
421
|
+
|
|
422
|
+
const eid = String(req.params.eid || '').trim();
|
|
423
|
+
if (!eid) return res.status(400).json({ error: 'missing-eid' });
|
|
424
|
+
const ev = await dbLayer.getSpecialEvent(eid);
|
|
425
|
+
if (!ev) return res.status(404).json({ error: 'not-found' });
|
|
426
|
+
|
|
427
|
+
// Anyone who can see the calendar can view special events, but creator username
|
|
428
|
+
// is only visible to moderators/allowed users or the creator.
|
|
429
|
+
const out = {
|
|
430
|
+
eid: ev.eid,
|
|
431
|
+
title: ev.title || '',
|
|
432
|
+
start: ev.start,
|
|
433
|
+
end: ev.end,
|
|
434
|
+
address: ev.address || '',
|
|
435
|
+
lat: ev.lat || '',
|
|
436
|
+
lon: ev.lon || '',
|
|
437
|
+
notes: ev.notes || '',
|
|
438
|
+
canDeleteSpecial: canSpecialDelete,
|
|
439
|
+
};
|
|
440
|
+
if (ev.username && (canMod || canSpecialDelete || (uid && String(uid) === String(ev.uid)))) {
|
|
441
|
+
out.username = String(ev.username);
|
|
442
|
+
}
|
|
443
|
+
return res.json(out);
|
|
320
444
|
};
|
|
321
445
|
|
|
322
446
|
api.getCapabilities = async function (req, res) {
|
package/library.js
CHANGED
|
@@ -66,11 +66,13 @@ Plugin.init = async function (params) {
|
|
|
66
66
|
router.get('/api/v3/plugins/calendar-onekite/capabilities', ...publicExpose, api.getCapabilities);
|
|
67
67
|
|
|
68
68
|
router.post('/api/v3/plugins/calendar-onekite/reservations', ...publicExpose, api.createReservation);
|
|
69
|
+
router.get('/api/v3/plugins/calendar-onekite/reservations/:rid', ...publicExpose, api.getReservationDetails);
|
|
69
70
|
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/approve', ...publicExpose, api.approveReservation);
|
|
70
71
|
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/refuse', ...publicExpose, api.refuseReservation);
|
|
71
72
|
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/cancel', ...publicExpose, api.cancelReservation);
|
|
72
73
|
|
|
73
74
|
router.post('/api/v3/plugins/calendar-onekite/special-events', ...publicExpose, api.createSpecialEvent);
|
|
75
|
+
router.get('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.getSpecialEventDetails);
|
|
74
76
|
router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.deleteSpecialEvent);
|
|
75
77
|
|
|
76
78
|
// Admin API (JSON)
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"templates": "./templates",
|
|
24
24
|
"modules": {
|
|
25
|
+
"../admin/plugins/calendar-onekite.js": "./public/admin.js",
|
|
25
26
|
"admin/plugins/calendar-onekite": "./public/admin.js"
|
|
26
27
|
},
|
|
27
28
|
"scripts": [
|
|
@@ -30,5 +31,5 @@
|
|
|
30
31
|
"acpScripts": [
|
|
31
32
|
"public/admin.js"
|
|
32
33
|
],
|
|
33
|
-
"version": "1.0.
|
|
34
|
+
"version": "1.0.1.1"
|
|
34
35
|
}
|
package/public/admin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'
|
|
2
|
+
define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts, bootbox) {
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
// Cache of pending reservations keyed by rid so delegated click handlers
|
|
@@ -335,12 +335,6 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox', 'hooks'], functio
|
|
|
335
335
|
const form = document.getElementById('onekite-settings-form');
|
|
336
336
|
if (!form) return;
|
|
337
337
|
|
|
338
|
-
// Avoid duplicate listeners/toasts when revisiting the ACP page via ajaxify.
|
|
339
|
-
if (form.getAttribute('data-onekite-bound') === '1') {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
form.setAttribute('data-onekite-bound', '1');
|
|
343
|
-
|
|
344
338
|
// Make the HelloAsso debug output readable in both light and dark ACP themes.
|
|
345
339
|
// NodeBB 4.x uses Bootstrap variables, so we can rely on CSS variables here.
|
|
346
340
|
(function injectAdminCss() {
|
|
@@ -807,20 +801,5 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox', 'hooks'], functio
|
|
|
807
801
|
}
|
|
808
802
|
}
|
|
809
803
|
|
|
810
|
-
// Auto-init when navigating in ACP via ajaxify.
|
|
811
|
-
try {
|
|
812
|
-
const autoInit = function (data) {
|
|
813
|
-
const tpl = data && data.template ? data.template.name : (window.ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '');
|
|
814
|
-
if (tpl === 'admin/plugins/calendar-onekite') {
|
|
815
|
-
init();
|
|
816
|
-
}
|
|
817
|
-
};
|
|
818
|
-
if (hooks && typeof hooks.on === 'function') {
|
|
819
|
-
hooks.on('action:ajaxify.end', autoInit);
|
|
820
|
-
}
|
|
821
|
-
// Also try once for the initial render.
|
|
822
|
-
setTimeout(() => autoInit({ template: (window.ajaxify && ajaxify.data && ajaxify.data.template) || { name: '' } }), 0);
|
|
823
|
-
} catch (e) {}
|
|
824
|
-
|
|
825
804
|
return { init };
|
|
826
805
|
});
|
package/public/client.js
CHANGED
|
@@ -434,6 +434,69 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
434
434
|
return await res.json();
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
+
// Simple in-memory cache for JSON endpoints (used for events prefetch + ETag).
|
|
438
|
+
const jsonCache = new Map(); // url -> { etag, data, ts }
|
|
439
|
+
|
|
440
|
+
function scheduleRefetch(cal) {
|
|
441
|
+
try {
|
|
442
|
+
if (!cal || typeof cal.refetchEvents !== 'function') return;
|
|
443
|
+
clearTimeout(window.__onekiteRefetchTimer);
|
|
444
|
+
window.__onekiteRefetchTimer = setTimeout(() => {
|
|
445
|
+
try { cal.refetchEvents(); } catch (e) {}
|
|
446
|
+
}, 150);
|
|
447
|
+
} catch (e) {}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function fetchJsonCached(url, opts) {
|
|
451
|
+
const cached = jsonCache.get(url);
|
|
452
|
+
const headers = Object.assign({}, (opts && opts.headers) || {});
|
|
453
|
+
if (cached && cached.etag) {
|
|
454
|
+
headers['If-None-Match'] = cached.etag;
|
|
455
|
+
}
|
|
456
|
+
let res;
|
|
457
|
+
try {
|
|
458
|
+
res = await fetch(url, {
|
|
459
|
+
credentials: 'same-origin',
|
|
460
|
+
headers: (() => {
|
|
461
|
+
// reuse csrf header builder (fetchJson) by calling it indirectly
|
|
462
|
+
const base = { 'Content-Type': 'application/json' };
|
|
463
|
+
const token =
|
|
464
|
+
(window.config && (window.config.csrf_token || window.config.csrfToken)) ||
|
|
465
|
+
(window.ajaxify && window.ajaxify.data && window.ajaxify.data.csrf_token) ||
|
|
466
|
+
(document.querySelector('meta[name="csrf-token"]') && document.querySelector('meta[name="csrf-token"]').getAttribute('content')) ||
|
|
467
|
+
(document.querySelector('meta[name="csrf_token"]') && document.querySelector('meta[name="csrf_token"]').getAttribute('content')) ||
|
|
468
|
+
(typeof app !== 'undefined' && app && app.csrfToken) ||
|
|
469
|
+
null;
|
|
470
|
+
if (token) base['x-csrf-token'] = token;
|
|
471
|
+
return Object.assign(base, headers);
|
|
472
|
+
})(),
|
|
473
|
+
...opts,
|
|
474
|
+
});
|
|
475
|
+
} catch (e) {
|
|
476
|
+
// If offline and we have cache, use it.
|
|
477
|
+
if (cached && cached.data) return cached.data;
|
|
478
|
+
throw e;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (res.status === 304 && cached && cached.data) {
|
|
482
|
+
return cached.data;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!res.ok) {
|
|
486
|
+
let payload = null;
|
|
487
|
+
try { payload = await res.json(); } catch (e) {}
|
|
488
|
+
const err = new Error(`${res.status}`);
|
|
489
|
+
err.status = res.status;
|
|
490
|
+
err.payload = payload;
|
|
491
|
+
throw err;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const data = await res.json();
|
|
495
|
+
const etag = res.headers.get('ETag') || '';
|
|
496
|
+
jsonCache.set(url, { etag, data, ts: Date.now() });
|
|
497
|
+
return data;
|
|
498
|
+
}
|
|
499
|
+
|
|
437
500
|
async function loadCapabilities() {
|
|
438
501
|
return await fetchJson('/api/v3/plugins/calendar-onekite/capabilities');
|
|
439
502
|
}
|
|
@@ -828,12 +891,6 @@ function toDatetimeLocalValue(date) {
|
|
|
828
891
|
return;
|
|
829
892
|
}
|
|
830
893
|
|
|
831
|
-
// Avoid double init (ajaxify + initial load tick).
|
|
832
|
-
if (el.getAttribute('data-onekite-initialised') === '1') {
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
el.setAttribute('data-onekite-initialised', '1');
|
|
836
|
-
|
|
837
894
|
if (typeof FullCalendar === 'undefined') {
|
|
838
895
|
showAlert('error', 'FullCalendar non chargé');
|
|
839
896
|
return;
|
|
@@ -1015,8 +1072,32 @@ function toDatetimeLocalValue(date) {
|
|
|
1015
1072
|
selectMirror: true,
|
|
1016
1073
|
events: async function (info, successCallback, failureCallback) {
|
|
1017
1074
|
try {
|
|
1075
|
+
// Abort previous in-flight events fetch to avoid "double refresh" effects.
|
|
1076
|
+
if (window.__onekiteEventsAbort) {
|
|
1077
|
+
try { window.__onekiteEventsAbort.abort(); } catch (e) {}
|
|
1078
|
+
}
|
|
1079
|
+
const abort = new AbortController();
|
|
1080
|
+
window.__onekiteEventsAbort = abort;
|
|
1081
|
+
|
|
1018
1082
|
const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
|
|
1019
|
-
const
|
|
1083
|
+
const url = `/api/v3/plugins/calendar-onekite/events?${qs.toString()}`;
|
|
1084
|
+
const data = await fetchJsonCached(url, { signal: abort.signal });
|
|
1085
|
+
|
|
1086
|
+
// Prefetch adjacent range (previous/next) for snappier navigation.
|
|
1087
|
+
try {
|
|
1088
|
+
const spanMs = (info.end && info.start) ? (info.end.getTime() - info.start.getTime()) : 0;
|
|
1089
|
+
if (spanMs > 0 && spanMs < 1000 * 3600 * 24 * 120) {
|
|
1090
|
+
const prevStart = new Date(info.start.getTime() - spanMs);
|
|
1091
|
+
const prevEnd = new Date(info.start.getTime());
|
|
1092
|
+
const nextStart = new Date(info.end.getTime());
|
|
1093
|
+
const nextEnd = new Date(info.end.getTime() + spanMs);
|
|
1094
|
+
const toStr = (d) => new Date(d.getTime()).toISOString();
|
|
1095
|
+
const qPrev = new URLSearchParams({ start: toStr(prevStart), end: toStr(prevEnd) });
|
|
1096
|
+
const qNext = new URLSearchParams({ start: toStr(nextStart), end: toStr(nextEnd) });
|
|
1097
|
+
fetchJsonCached(`/api/v3/plugins/calendar-onekite/events?${qPrev.toString()}`).catch(() => {});
|
|
1098
|
+
fetchJsonCached(`/api/v3/plugins/calendar-onekite/events?${qNext.toString()}`).catch(() => {});
|
|
1099
|
+
}
|
|
1100
|
+
} catch (e) {}
|
|
1020
1101
|
|
|
1021
1102
|
// IMPORTANT: align "special" event display exactly like reservation icons.
|
|
1022
1103
|
// We inject the clock + time range directly into the event title so FC
|
|
@@ -1192,9 +1273,33 @@ function toDatetimeLocalValue(date) {
|
|
|
1192
1273
|
},
|
|
1193
1274
|
|
|
1194
1275
|
eventClick: async function (info) {
|
|
1276
|
+
if (isDialogOpen) return;
|
|
1277
|
+
isDialogOpen = true;
|
|
1195
1278
|
const ev = info.event;
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1279
|
+
const p0 = ev.extendedProps || {};
|
|
1280
|
+
|
|
1281
|
+
// Load full details lazily (events list is lightweight for perf).
|
|
1282
|
+
let p = p0;
|
|
1283
|
+
try {
|
|
1284
|
+
if (p0.type === 'reservation' && p0.rid) {
|
|
1285
|
+
const details = await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${encodeURIComponent(String(p0.rid))}`);
|
|
1286
|
+
p = Object.assign({}, p0, details);
|
|
1287
|
+
} else if (p0.type === 'special' && p0.eid) {
|
|
1288
|
+
const details = await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(String(p0.eid))}`);
|
|
1289
|
+
p = Object.assign({}, p0, details, {
|
|
1290
|
+
// keep backward compat with older field names used by templates below
|
|
1291
|
+
pickupAddress: details.address || details.pickupAddress || p0.pickupAddress,
|
|
1292
|
+
pickupLat: details.lat || details.pickupLat || p0.pickupLat,
|
|
1293
|
+
pickupLon: details.lon || details.pickupLon || p0.pickupLon,
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
} catch (e) {
|
|
1297
|
+
// ignore detail fetch errors; fall back to minimal props
|
|
1298
|
+
p = p0;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
try {
|
|
1302
|
+
if (p.type === 'special') {
|
|
1198
1303
|
const username = String(p.username || '').trim();
|
|
1199
1304
|
const userLine = username
|
|
1200
1305
|
? `<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>`
|
|
@@ -1541,6 +1646,9 @@ function toDatetimeLocalValue(date) {
|
|
|
1541
1646
|
message: baseHtml,
|
|
1542
1647
|
buttons,
|
|
1543
1648
|
});
|
|
1649
|
+
} finally {
|
|
1650
|
+
isDialogOpen = false;
|
|
1651
|
+
}
|
|
1544
1652
|
},
|
|
1545
1653
|
});
|
|
1546
1654
|
|
|
@@ -1662,12 +1770,10 @@ function toDatetimeLocalValue(date) {
|
|
|
1662
1770
|
try {
|
|
1663
1771
|
if (!window.__oneKiteSocketBound && typeof socket !== 'undefined' && socket && typeof socket.on === 'function') {
|
|
1664
1772
|
window.__oneKiteSocketBound = true;
|
|
1665
|
-
socket.on('event:calendar-onekite.reservationUpdated', function (
|
|
1773
|
+
socket.on('event:calendar-onekite.reservationUpdated', function () {
|
|
1666
1774
|
try {
|
|
1667
1775
|
const cal = window.oneKiteCalendar;
|
|
1668
|
-
|
|
1669
|
-
cal.refetchEvents();
|
|
1670
|
-
}
|
|
1776
|
+
scheduleRefetch(cal);
|
|
1671
1777
|
} catch (e) {}
|
|
1672
1778
|
});
|
|
1673
1779
|
}
|
|
@@ -180,14 +180,11 @@
|
|
|
180
180
|
</div>
|
|
181
181
|
|
|
182
182
|
<script>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
}
|
|
183
|
+
require(['admin/plugins/calendar-onekite'], function (mod) {
|
|
184
|
+
if (mod && mod.init) {
|
|
185
|
+
mod.init();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
191
188
|
</script>
|
|
192
189
|
|
|
193
190
|
<!-- IMPORT admin/partials/settings/footer.tpl -->
|
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
</div>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
<!--
|
|
11
|
+
FullCalendar
|
|
12
|
+
- Use CDN "latest" so NodeBB v4 stays simple to maintain.
|
|
13
|
+
- Use main.min.css (the global bundle does not ship index.global.min.css).
|
|
14
|
+
-->
|
|
15
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@latest/main.min.css" />
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@latest/index.global.min.js"></script>
|
|
17
|
+
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@latest/locales-all.global.min.js"></script>
|
|
13
18
|
|
|
14
19
|
<!--
|
|
15
20
|
No inline require() here.
|