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 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
- const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs, 200000);
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 keptForAccounting = 0;
213
+ let archivedForAccounting = 0;
209
214
 
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
- }
215
+ for (const rid of ids) {
216
+ const r = await dbLayer.getReservation(rid);
217
+ if (!r) continue;
224
218
 
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) {
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, keptForAccounting });
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
- // 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;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "12.0.4",
3
+ "version": "12.0.5",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
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
- showAlert('success', `Purge OK (${r.removed || 0} supprimées).`);
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, '&amp;')
@@ -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
- const container = typeof selector === 'string' ? document.querySelector(selector) : selector;
962
- if (!container) return;
890
+ const container = typeof selector === 'string' ? document.querySelector(selector) : selector;
891
+ if (!container) return;
963
892
 
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
- }
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
- // Prevent double-init (ajaxify + initial load, or return from payment)
976
- if (container.dataset && container.dataset.onekiteCalendarInit === '1') {
977
- try { refreshDesktopModeButton(); } catch (e) {}
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
- // 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;
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
  });