nodebb-plugin-calendar-onekite 12.0.4 → 12.0.5
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 +18 -24
- package/lib/api.js +6 -12
- package/package.json +1 -1
- package/public/admin.js +4 -1
- package/public/client.js +22 -118
package/lib/admin.js
CHANGED
|
@@ -202,40 +202,34 @@ 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
|
-
|
|
205
|
+
// IMPORTANT:
|
|
206
|
+
// Purging "locations" (reservations) must NOT wipe accounting.
|
|
207
|
+
// Accounting is computed from paid reservations, so for paid ones we keep the
|
|
208
|
+
// reservation object but mark it as hidden from the calendar.
|
|
209
|
+
// Non-paid reservations can be safely removed.
|
|
210
|
+
const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs, 100000);
|
|
206
211
|
const ts = Date.now();
|
|
207
212
|
let removed = 0;
|
|
208
|
-
let
|
|
213
|
+
let archivedForAccounting = 0;
|
|
209
214
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
}
|
|
215
|
+
for (const rid of ids) {
|
|
216
|
+
const r = await dbLayer.getReservation(rid);
|
|
217
|
+
if (!r) continue;
|
|
224
218
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (String(r.status) === 'paid' || r.paidAt) {
|
|
219
|
+
if (String(r.status) === 'paid') {
|
|
220
|
+
if (!r.calendarPurgedAt) {
|
|
228
221
|
r.calendarPurgedAt = ts;
|
|
229
222
|
await dbLayer.saveReservation(r);
|
|
230
|
-
keptForAccounting++;
|
|
231
|
-
} else {
|
|
232
|
-
await dbLayer.removeReservation(rid);
|
|
233
|
-
removed++;
|
|
234
223
|
}
|
|
224
|
+
archivedForAccounting++;
|
|
225
|
+
continue;
|
|
235
226
|
}
|
|
227
|
+
|
|
228
|
+
await dbLayer.removeReservation(rid);
|
|
229
|
+
removed++;
|
|
236
230
|
}
|
|
237
231
|
|
|
238
|
-
res.json({ ok: true, removed,
|
|
232
|
+
res.json({ ok: true, removed, archivedForAccounting });
|
|
239
233
|
};
|
|
240
234
|
|
|
241
235
|
admin.purgeSpecialEventsByYear = async function (req, res) {
|
package/lib/api.js
CHANGED
|
@@ -153,17 +153,12 @@ 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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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;
|
|
156
|
+
for (const rid of ids) {
|
|
157
|
+
const r = await dbLayer.getReservation(rid);
|
|
158
|
+
if (!r) continue;
|
|
159
|
+
// If the reservation was purged from the calendar (but kept for accounting),
|
|
160
|
+
// do not show it in the calendar UI.
|
|
161
|
+
if (r.calendarPurgedAt) continue;
|
|
167
162
|
// Only show active statuses
|
|
168
163
|
if (!['pending', 'awaiting_payment', 'paid'].includes(r.status)) continue;
|
|
169
164
|
const rStart = parseInt(r.start, 10);
|
|
@@ -200,7 +195,6 @@ api.getEvents = async function (req, res) {
|
|
|
200
195
|
}
|
|
201
196
|
}
|
|
202
197
|
out.push(minimal);
|
|
203
|
-
}
|
|
204
198
|
}
|
|
205
199
|
}
|
|
206
200
|
|
package/package.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -636,7 +636,10 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
636
636
|
if (!ok) return;
|
|
637
637
|
try {
|
|
638
638
|
const r = await purge(year);
|
|
639
|
-
|
|
639
|
+
const removed = r.removed || 0;
|
|
640
|
+
const archived = r.archivedForAccounting || 0;
|
|
641
|
+
const extra = archived ? `, ${archived} archivée(s) (compta conservée)` : '';
|
|
642
|
+
showAlert('success', `Purge OK (${removed} supprimée(s)${extra}).`);
|
|
640
643
|
await refreshPending();
|
|
641
644
|
} catch (e) {
|
|
642
645
|
showAlert('error', 'Purge impossible.');
|
package/public/client.js
CHANGED
|
@@ -36,10 +36,6 @@ 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
|
-
|
|
43
39
|
function escapeHtml(str) {
|
|
44
40
|
return String(str)
|
|
45
41
|
.replace(/&/g, '&')
|
|
@@ -438,73 +434,6 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
438
434
|
return await res.json();
|
|
439
435
|
}
|
|
440
436
|
|
|
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
|
-
|
|
508
437
|
// Simple in-memory cache for JSON endpoints (used for events prefetch + ETag).
|
|
509
438
|
const jsonCache = new Map(); // url -> { etag, data, ts }
|
|
510
439
|
|
|
@@ -958,36 +887,33 @@ function toDatetimeLocalValue(date) {
|
|
|
958
887
|
|
|
959
888
|
async function init(selector) {
|
|
960
889
|
|
|
961
|
-
|
|
962
|
-
|
|
890
|
+
const container = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
891
|
+
if (!container) return;
|
|
963
892
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
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';
|
|
974
900
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
901
|
+
// If a previous instance exists (shouldn't, but happens in some navigation flows), destroy it.
|
|
902
|
+
try {
|
|
903
|
+
if (window.oneKiteCalendar && typeof window.oneKiteCalendar.destroy === 'function') {
|
|
904
|
+
window.oneKiteCalendar.destroy();
|
|
905
|
+
}
|
|
906
|
+
window.oneKiteCalendar = null;
|
|
907
|
+
} catch (e) {}
|
|
908
|
+
const el = document.querySelector(selector);
|
|
909
|
+
if (!el) {
|
|
978
910
|
return;
|
|
979
911
|
}
|
|
980
912
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
986
|
-
window.oneKiteCalendar = null;
|
|
987
|
-
} catch (e) {}
|
|
988
|
-
|
|
989
|
-
const el = typeof selector === 'string' ? document.querySelector(selector) : container;
|
|
990
|
-
if (!el) return;
|
|
913
|
+
if (typeof FullCalendar === 'undefined') {
|
|
914
|
+
showAlert('error', 'FullCalendar non chargé');
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
991
917
|
|
|
992
918
|
const items = await loadItems();
|
|
993
919
|
const caps = await loadCapabilities().catch(() => ({}));
|
|
@@ -1186,15 +1112,10 @@ function toDatetimeLocalValue(date) {
|
|
|
1186
1112
|
}
|
|
1187
1113
|
const abort = new AbortController();
|
|
1188
1114
|
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);
|
|
1193
1115
|
|
|
1194
1116
|
const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
|
|
1195
1117
|
const url = `/api/v3/plugins/calendar-onekite/events?${qs.toString()}`;
|
|
1196
1118
|
const data = await fetchJsonCached(url, { signal: abort.signal });
|
|
1197
|
-
clearTimeout(timeout);
|
|
1198
1119
|
|
|
1199
1120
|
// Prefetch adjacent range (previous/next) for snappier navigation.
|
|
1200
1121
|
try {
|
|
@@ -1238,18 +1159,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1238
1159
|
});
|
|
1239
1160
|
|
|
1240
1161
|
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;
|
|
1244
1162
|
} 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) {}
|
|
1253
1163
|
failureCallback(e);
|
|
1254
1164
|
}
|
|
1255
1165
|
},
|
|
@@ -1779,9 +1689,6 @@ function toDatetimeLocalValue(date) {
|
|
|
1779
1689
|
// Expose for live updates
|
|
1780
1690
|
try { window.oneKiteCalendar = calendar; } catch (e) {}
|
|
1781
1691
|
|
|
1782
|
-
// Mark initialised only after we have a valid FullCalendar instance.
|
|
1783
|
-
try { if (container.dataset) container.dataset.onekiteCalendarInit = '1'; } catch (e) {}
|
|
1784
|
-
|
|
1785
1692
|
calendar.render();
|
|
1786
1693
|
|
|
1787
1694
|
// Keep the custom button label stable even if FullCalendar rerenders the toolbar
|
|
@@ -1917,9 +1824,6 @@ try {
|
|
|
1917
1824
|
socket.on('event:calendar-onekite.reservationUpdated', function () {
|
|
1918
1825
|
try {
|
|
1919
1826
|
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;
|
|
1923
1827
|
scheduleRefetch(cal);
|
|
1924
1828
|
} catch (e) {}
|
|
1925
1829
|
});
|