nodebb-plugin-calendar-onekite 12.0.2 → 12.0.4
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/admin.js +33 -6
- package/lib/api.js +12 -3
- package/package.json +1 -1
- package/public/client.js +118 -22
package/lib/admin.js
CHANGED
|
@@ -202,13 +202,40 @@ admin.purgeByYear = async function (req, res) {
|
|
|
202
202
|
const startTs = new Date(Date.UTC(y, 0, 1)).getTime();
|
|
203
203
|
const endTs = new Date(Date.UTC(y + 1, 0, 1)).getTime() - 1;
|
|
204
204
|
|
|
205
|
-
const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs, 200000);
|
|
206
|
+
const ts = Date.now();
|
|
207
|
+
let removed = 0;
|
|
208
|
+
let keptForAccounting = 0;
|
|
209
|
+
|
|
210
|
+
// Bulk fetch to avoid one-by-one DB reads.
|
|
211
|
+
const chunkSize = 500;
|
|
212
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
213
|
+
const chunk = ids.slice(i, i + chunkSize);
|
|
214
|
+
const objs = await dbLayer.getReservations(chunk);
|
|
215
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
216
|
+
const rid = chunk[j];
|
|
217
|
+
const r = objs[j];
|
|
218
|
+
if (!r) {
|
|
219
|
+
// Ensure index doesn't keep dangling ids
|
|
220
|
+
try { await dbLayer.removeReservation(rid); } catch (e) {}
|
|
221
|
+
removed++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// IMPORTANT: Do not purge accounting. If the reservation was paid,
|
|
226
|
+
// keep the object for exports and mark it as hidden from the calendar.
|
|
227
|
+
if (String(r.status) === 'paid' || r.paidAt) {
|
|
228
|
+
r.calendarPurgedAt = ts;
|
|
229
|
+
await dbLayer.saveReservation(r);
|
|
230
|
+
keptForAccounting++;
|
|
231
|
+
} else {
|
|
232
|
+
await dbLayer.removeReservation(rid);
|
|
233
|
+
removed++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
210
236
|
}
|
|
211
|
-
|
|
237
|
+
|
|
238
|
+
res.json({ ok: true, removed, keptForAccounting });
|
|
212
239
|
};
|
|
213
240
|
|
|
214
241
|
admin.purgeSpecialEventsByYear = async function (req, res) {
|
package/lib/api.js
CHANGED
|
@@ -153,9 +153,17 @@ api.getEvents = async function (req, res) {
|
|
|
153
153
|
const wideStart = Math.max(0, startTs - 366 * 24 * 3600 * 1000);
|
|
154
154
|
const ids = await dbLayer.listReservationIdsByStartRange(wideStart, endTs, 5000);
|
|
155
155
|
const out = [];
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
|
|
157
|
+
// Bulk fetch to avoid one-by-one DB reads.
|
|
158
|
+
const chunkSize = 500;
|
|
159
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
160
|
+
const chunk = ids.slice(i, i + chunkSize);
|
|
161
|
+
const objs = await dbLayer.getReservations(chunk);
|
|
162
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
163
|
+
const r = objs[j];
|
|
164
|
+
if (!r) continue;
|
|
165
|
+
// Hidden by year purge (kept for accounting, not shown on calendar)
|
|
166
|
+
if (r.calendarPurgedAt) continue;
|
|
159
167
|
// Only show active statuses
|
|
160
168
|
if (!['pending', 'awaiting_payment', 'paid'].includes(r.status)) continue;
|
|
161
169
|
const rStart = parseInt(r.start, 10);
|
|
@@ -192,6 +200,7 @@ api.getEvents = async function (req, res) {
|
|
|
192
200
|
}
|
|
193
201
|
}
|
|
194
202
|
out.push(minimal);
|
|
203
|
+
}
|
|
195
204
|
}
|
|
196
205
|
}
|
|
197
206
|
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -36,6 +36,10 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
36
36
|
// interactions or quick re-renders.
|
|
37
37
|
let isDialogOpen = false;
|
|
38
38
|
|
|
39
|
+
// Used to avoid a visible "flash" of events when a live update triggers
|
|
40
|
+
// a refetch during the very first load.
|
|
41
|
+
let hasLoadedEventsOnce = false;
|
|
42
|
+
|
|
39
43
|
function escapeHtml(str) {
|
|
40
44
|
return String(str)
|
|
41
45
|
.replace(/&/g, '&')
|
|
@@ -434,6 +438,73 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
434
438
|
return await res.json();
|
|
435
439
|
}
|
|
436
440
|
|
|
441
|
+
// Load FullCalendar from CDN when templates are rendered via ajaxify.
|
|
442
|
+
// NodeBB does not reliably execute <script> tags from ajaxified templates,
|
|
443
|
+
// so we must ensure the scripts are present before initialising.
|
|
444
|
+
const FULLCALENDAR_ASSETS = {
|
|
445
|
+
css: 'https://cdn.jsdelivr.net/npm/fullcalendar@latest/main.min.css',
|
|
446
|
+
jsMain: 'https://cdn.jsdelivr.net/npm/fullcalendar@latest/index.global.min.js',
|
|
447
|
+
jsLocales: 'https://cdn.jsdelivr.net/npm/@fullcalendar/core@latest/locales-all.global.min.js',
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
function loadCssOnce(href, id) {
|
|
451
|
+
return new Promise((resolve) => {
|
|
452
|
+
try {
|
|
453
|
+
if (id && document.getElementById(id)) return resolve();
|
|
454
|
+
const existing = [...document.querySelectorAll('link[rel="stylesheet"]')].find(l => l.href === href);
|
|
455
|
+
if (existing) return resolve();
|
|
456
|
+
const link = document.createElement('link');
|
|
457
|
+
link.rel = 'stylesheet';
|
|
458
|
+
link.href = href;
|
|
459
|
+
if (id) link.id = id;
|
|
460
|
+
link.onload = () => resolve();
|
|
461
|
+
link.onerror = () => resolve();
|
|
462
|
+
document.head.appendChild(link);
|
|
463
|
+
// resolve even if browser doesn't fire onload for cached assets
|
|
464
|
+
setTimeout(resolve, 300);
|
|
465
|
+
} catch (e) {
|
|
466
|
+
resolve();
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function loadScriptOnce(src, id) {
|
|
472
|
+
return new Promise((resolve) => {
|
|
473
|
+
try {
|
|
474
|
+
if (id && document.getElementById(id)) return resolve();
|
|
475
|
+
const existing = [...document.querySelectorAll('script')].find(s => s.src === src);
|
|
476
|
+
if (existing) return resolve();
|
|
477
|
+
const s = document.createElement('script');
|
|
478
|
+
if (id) s.id = id;
|
|
479
|
+
s.src = src;
|
|
480
|
+
s.async = true;
|
|
481
|
+
s.onload = () => resolve();
|
|
482
|
+
s.onerror = () => resolve();
|
|
483
|
+
document.head.appendChild(s);
|
|
484
|
+
} catch (e) {
|
|
485
|
+
resolve();
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function ensureFullCalendarLoaded(timeoutMs) {
|
|
491
|
+
const started = Date.now();
|
|
492
|
+
if (typeof FullCalendar !== 'undefined') return true;
|
|
493
|
+
|
|
494
|
+
// Inject assets (safe to call multiple times).
|
|
495
|
+
await loadCssOnce(FULLCALENDAR_ASSETS.css, 'onekite-fc-css');
|
|
496
|
+
await loadScriptOnce(FULLCALENDAR_ASSETS.jsMain, 'onekite-fc-js');
|
|
497
|
+
await loadScriptOnce(FULLCALENDAR_ASSETS.jsLocales, 'onekite-fc-locales');
|
|
498
|
+
|
|
499
|
+
// Wait until FullCalendar becomes available.
|
|
500
|
+
const limit = typeof timeoutMs === 'number' ? timeoutMs : 8000;
|
|
501
|
+
while (Date.now() - started < limit) {
|
|
502
|
+
if (typeof FullCalendar !== 'undefined') return true;
|
|
503
|
+
await new Promise(r => setTimeout(r, 50));
|
|
504
|
+
}
|
|
505
|
+
return typeof FullCalendar !== 'undefined';
|
|
506
|
+
}
|
|
507
|
+
|
|
437
508
|
// Simple in-memory cache for JSON endpoints (used for events prefetch + ETag).
|
|
438
509
|
const jsonCache = new Map(); // url -> { etag, data, ts }
|
|
439
510
|
|
|
@@ -887,34 +958,37 @@ function toDatetimeLocalValue(date) {
|
|
|
887
958
|
|
|
888
959
|
async function init(selector) {
|
|
889
960
|
|
|
890
|
-
const container = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
891
|
-
if (!container) return;
|
|
892
|
-
|
|
893
|
-
// Prevent double-init (ajaxify + initial load, or return from payment)
|
|
894
|
-
if (container.dataset && container.dataset.onekiteCalendarInit === '1') {
|
|
895
|
-
// Still refresh the custom button label in case FC rerendered
|
|
896
|
-
try { refreshDesktopModeButton(); } catch (e) {}
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
if (container.dataset) container.dataset.onekiteCalendarInit = '1';
|
|
961
|
+
const container = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
962
|
+
if (!container) return;
|
|
900
963
|
|
|
901
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
return;
|
|
964
|
+
// FullCalendar must exist before we can render. When the page is loaded via ajaxify,
|
|
965
|
+
// NodeBB does not consistently execute <script> tags in templates.
|
|
966
|
+
// We therefore load the CDN assets here as a fallback.
|
|
967
|
+
if (typeof FullCalendar === 'undefined') {
|
|
968
|
+
const ok = await ensureFullCalendarLoaded(9000);
|
|
969
|
+
if (!ok) {
|
|
970
|
+
showAlert('error', 'Le calendrier ne peut pas se charger (FullCalendar indisponible)');
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
911
973
|
}
|
|
912
974
|
|
|
913
|
-
|
|
914
|
-
|
|
975
|
+
// Prevent double-init (ajaxify + initial load, or return from payment)
|
|
976
|
+
if (container.dataset && container.dataset.onekiteCalendarInit === '1') {
|
|
977
|
+
try { refreshDesktopModeButton(); } catch (e) {}
|
|
915
978
|
return;
|
|
916
979
|
}
|
|
917
980
|
|
|
981
|
+
// If a previous instance exists (shouldn't, but happens in some navigation flows), destroy it.
|
|
982
|
+
try {
|
|
983
|
+
if (window.oneKiteCalendar && typeof window.oneKiteCalendar.destroy === 'function') {
|
|
984
|
+
window.oneKiteCalendar.destroy();
|
|
985
|
+
}
|
|
986
|
+
window.oneKiteCalendar = null;
|
|
987
|
+
} catch (e) {}
|
|
988
|
+
|
|
989
|
+
const el = typeof selector === 'string' ? document.querySelector(selector) : container;
|
|
990
|
+
if (!el) return;
|
|
991
|
+
|
|
918
992
|
const items = await loadItems();
|
|
919
993
|
const caps = await loadCapabilities().catch(() => ({}));
|
|
920
994
|
const canCreateSpecial = !!caps.canCreateSpecial;
|
|
@@ -1112,10 +1186,15 @@ try {
|
|
|
1112
1186
|
}
|
|
1113
1187
|
const abort = new AbortController();
|
|
1114
1188
|
window.__onekiteEventsAbort = abort;
|
|
1189
|
+
// Hard timeout so FullCalendar doesn't appear to load forever if the API hangs
|
|
1190
|
+
const timeout = setTimeout(() => {
|
|
1191
|
+
try { abort.abort(); } catch (e) {}
|
|
1192
|
+
}, 15000);
|
|
1115
1193
|
|
|
1116
1194
|
const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
|
|
1117
1195
|
const url = `/api/v3/plugins/calendar-onekite/events?${qs.toString()}`;
|
|
1118
1196
|
const data = await fetchJsonCached(url, { signal: abort.signal });
|
|
1197
|
+
clearTimeout(timeout);
|
|
1119
1198
|
|
|
1120
1199
|
// Prefetch adjacent range (previous/next) for snappier navigation.
|
|
1121
1200
|
try {
|
|
@@ -1159,7 +1238,18 @@ try {
|
|
|
1159
1238
|
});
|
|
1160
1239
|
|
|
1161
1240
|
successCallback(mapped);
|
|
1241
|
+
// Mark first successful events load. We use this to prevent a
|
|
1242
|
+
// refetch-on-socket-update from flashing the UI during initial load.
|
|
1243
|
+
hasLoadedEventsOnce = true;
|
|
1162
1244
|
} catch (e) {
|
|
1245
|
+
try {
|
|
1246
|
+
// Stop the spinner and give the user feedback.
|
|
1247
|
+
successCallback([]);
|
|
1248
|
+
} catch (e2) {}
|
|
1249
|
+
try {
|
|
1250
|
+
const status = (e && (e.status || e.name)) ? String(e.status || e.name) : '';
|
|
1251
|
+
showAlert('error', status === 'AbortError' ? 'Chargement du calendrier trop long (timeout)' : 'Erreur de chargement du calendrier');
|
|
1252
|
+
} catch (e3) {}
|
|
1163
1253
|
failureCallback(e);
|
|
1164
1254
|
}
|
|
1165
1255
|
},
|
|
@@ -1689,6 +1779,9 @@ try {
|
|
|
1689
1779
|
// Expose for live updates
|
|
1690
1780
|
try { window.oneKiteCalendar = calendar; } catch (e) {}
|
|
1691
1781
|
|
|
1782
|
+
// Mark initialised only after we have a valid FullCalendar instance.
|
|
1783
|
+
try { if (container.dataset) container.dataset.onekiteCalendarInit = '1'; } catch (e) {}
|
|
1784
|
+
|
|
1692
1785
|
calendar.render();
|
|
1693
1786
|
|
|
1694
1787
|
// Keep the custom button label stable even if FullCalendar rerenders the toolbar
|
|
@@ -1824,6 +1917,9 @@ try {
|
|
|
1824
1917
|
socket.on('event:calendar-onekite.reservationUpdated', function () {
|
|
1825
1918
|
try {
|
|
1826
1919
|
const cal = window.oneKiteCalendar;
|
|
1920
|
+
// Avoid a visible "flash" on first load (e.g. returning from payment
|
|
1921
|
+
// while a live update is emitted right away).
|
|
1922
|
+
if (!hasLoadedEventsOnce) return;
|
|
1827
1923
|
scheduleRefetch(cal);
|
|
1828
1924
|
} catch (e) {}
|
|
1829
1925
|
});
|