nodebb-plugin-calendar-onekite 11.1.94 → 11.1.95

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
@@ -20,39 +20,33 @@ function formatFR(tsOrIso) {
20
20
 
21
21
  async function sendEmail(template, toEmail, subject, data) {
22
22
  if (!toEmail) return;
23
- const params = Object.assign({}, data || {});
24
- if (subject) {
25
- params.subject = subject;
26
- }
27
- // NodeBB emailer expects: (template, email, language, params)
28
- const lang = params.lang || (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
29
-
30
23
  try {
31
24
  if (typeof emailer.sendToEmail === 'function') {
32
25
  if (emailer.sendToEmail.length >= 4) {
33
- await emailer.sendToEmail(template, toEmail, lang, params);
26
+ await emailer.sendToEmail(template, toEmail, subject, data);
34
27
  } else {
35
- await emailer.sendToEmail(template, toEmail, params);
28
+ const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
29
+ await emailer.sendToEmail(template, toEmail, dataWithSubject);
36
30
  }
37
31
  return;
38
32
  }
39
33
  if (typeof emailer.send === 'function') {
40
34
  if (emailer.send.length >= 4) {
41
- await emailer.send(template, toEmail, lang, params);
35
+ await emailer.send(template, toEmail, subject, data);
42
36
  return;
43
37
  }
44
38
  if (emailer.send.length === 3) {
45
- await emailer.send(template, toEmail, params);
39
+ const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
40
+ await emailer.send(template, toEmail, dataWithSubject);
46
41
  return;
47
42
  }
48
- await emailer.send(template, toEmail, lang, params);
43
+ await emailer.send(template, toEmail, subject, data);
49
44
  }
50
45
  } catch (err) {
51
- console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String((err && err.message) || err) });
46
+ console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err && err.message || err) });
52
47
  }
53
48
  }
54
49
 
55
-
56
50
  function normalizeCallbackUrl(configured, meta) {
57
51
  const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
58
52
  let url = (configured || '').trim();
@@ -523,4 +517,3 @@ admin.purgeAccounting = async function (req, res) {
523
517
 
524
518
 
525
519
  module.exports = admin;
526
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "11.1.94",
3
+ "version": "11.1.95",
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/client.js CHANGED
@@ -58,7 +58,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
58
58
  cancel: {
59
59
  label: 'Annuler',
60
60
  className: 'btn-secondary',
61
- callback: () => __settle(null),
61
+ callback: () => resolve(null),
62
62
  },
63
63
  ok: {
64
64
  label: 'Créer',
@@ -71,7 +71,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
71
71
  const notes = (document.getElementById('onekite-se-notes')?.value || '').trim();
72
72
  const lat = (document.getElementById('onekite-se-lat')?.value || '').trim();
73
73
  const lon = (document.getElementById('onekite-se-lon')?.value || '').trim();
74
- __settle({ title, start: startVal, end: endVal, address, notes, lat, lon });
74
+ resolve({ title, start: startVal, end: endVal, address, notes, lat, lon });
75
75
  return true;
76
76
  },
77
77
  },
@@ -233,15 +233,6 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
233
233
  return await res.json();
234
234
  }
235
235
 
236
-
237
- async function apiJson(v3Url, legacyUrl, opts) {
238
- try {
239
- return await fetchJson(v3Url, opts);
240
- } catch (e) {
241
- return await fetchJson(legacyUrl, opts);
242
- }
243
- }
244
-
245
236
  async function loadCapabilities() {
246
237
  try {
247
238
  return await fetchJson('/api/v3/plugins/calendar-onekite/capabilities');
@@ -455,34 +446,34 @@ function attachAddressAutocomplete(inputEl, onPick) {
455
446
 
456
447
  async function loadItems() {
457
448
  try {
458
- return await apiJson('/api/v3/plugins/calendar-onekite/items', '/api/plugins/calendar-onekite/items');
449
+ return await fetchJson('/api/v3/plugins/calendar-onekite/items');
459
450
  } catch (e) {
460
451
  return [];
461
452
  }
462
453
  }
463
454
 
464
455
  async function requestReservation(payload) {
465
- return await apiJson('/api/v3/plugins/calendar-onekite/reservations', '/api/plugins/calendar-onekite/reservations', {
456
+ return await fetchJson('/api/v3/plugins/calendar-onekite/reservations', {
466
457
  method: 'POST',
467
458
  body: JSON.stringify(payload),
468
459
  });
469
460
  }
470
461
 
471
462
  async function approveReservation(rid, payload) {
472
- return await apiJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/approve`, `/api/plugins/calendar-onekite/reservations/${rid}/approve`, {
463
+ return await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/approve`, {
473
464
  method: 'PUT',
474
465
  body: JSON.stringify(payload || {}),
475
466
  });
476
467
  }
477
468
 
478
469
  async function cancelReservation(rid) {
479
- return await apiJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/cancel`, `/api/plugins/calendar-onekite/reservations/${rid}/cancel`, {
470
+ return await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/cancel`, {
480
471
  method: 'PUT',
481
472
  });
482
473
  }
483
474
 
484
475
  async function refuseReservation(rid) {
485
- return await apiJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/refuse`, `/api/plugins/calendar-onekite/reservations/${rid}/refuse`, {
476
+ return await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/refuse`, {
486
477
  method: 'PUT',
487
478
  });
488
479
  }
@@ -506,26 +497,6 @@ function attachAddressAutocomplete(inputEl, onPick) {
506
497
  }
507
498
 
508
499
 
509
- function formatPeriodWithTime(start, end) {
510
- const s = formatDtWithTime(start);
511
- if (!end) {
512
- return s;
513
- }
514
- try {
515
- const st = new Date(start).getTime();
516
- const en = new Date(end).getTime();
517
- if (Number.isFinite(st) && Number.isFinite(en) && en <= st) {
518
- return s;
519
- }
520
- } catch (e) {}
521
- const eTxt = formatDtWithTime(end);
522
- if (!eTxt || eTxt === 'Invalid Date') {
523
- return s;
524
- }
525
- return `${s} → ${eTxt}`;
526
- }
527
-
528
-
529
500
 
530
501
  function toLocalYmd(date) {
531
502
  const d = new Date(date);
@@ -551,7 +522,7 @@ function toDatetimeLocalValue(date) {
551
522
  let blocked = new Set();
552
523
  try {
553
524
  const qs = new URLSearchParams({ start: selectionInfo.startStr, end: selectionInfo.endStr });
554
- const evs = await apiJson(`/api/v3/plugins/calendar-onekite/events?${qs.toString()}`, `/api/plugins/calendar-onekite/events?${qs.toString()}`);
525
+ const evs = await fetchJson(`/api/v3/plugins/calendar-onekite/events?${qs.toString()}`);
555
526
  (evs || []).forEach((ev) => {
556
527
  const st = (ev.extendedProps && ev.extendedProps.status) || '';
557
528
  if (!['pending', 'awaiting_payment', 'approved', 'paid'].includes(st)) return;
@@ -600,8 +571,6 @@ function toDatetimeLocalValue(date) {
600
571
  `;
601
572
 
602
573
  return new Promise((resolve) => {
603
- let __settled = false;
604
- const __settle = (v) => { if (__settled) return; __settled = true; __settle(v); };
605
574
  const dlg = bootbox.dialog({
606
575
  title: 'Demander une réservation',
607
576
  message: messageHtml,
@@ -610,7 +579,7 @@ function toDatetimeLocalValue(date) {
610
579
  label: 'Annuler',
611
580
  className: 'btn-secondary',
612
581
  callback: function () {
613
- __settle(null);
582
+ resolve(null);
614
583
  },
615
584
  },
616
585
  ok: {
@@ -626,16 +595,13 @@ function toDatetimeLocalValue(date) {
626
595
  const itemNames = cbs.map(cb => cb.getAttribute('data-name'));
627
596
  const sum = cbs.reduce((acc, cb) => acc + (parseFloat(cb.getAttribute('data-price') || '0') || 0), 0);
628
597
  const total = (sum / 100) * days;
629
- __settle({ itemIds, itemNames, total, days });
598
+ resolve({ itemIds, itemNames, total, days });
630
599
  },
631
600
  },
632
601
  },
633
602
  });
634
603
 
635
-
636
- // Ensure the promise resolves even if the modal is dismissed externally (e.g. mobile rotate)
637
- dlg.on('hidden.bs.modal', () => __settle(null));
638
- // live total update
604
+ // live total update
639
605
  setTimeout(() => {
640
606
  const totalEl = document.getElementById('onekite-total');
641
607
  function refreshTotal() {
@@ -668,7 +634,30 @@ function toDatetimeLocalValue(date) {
668
634
  const canCreateSpecial = !!caps.canCreateSpecial;
669
635
  const canDeleteSpecial = !!caps.canDeleteSpecial;
670
636
 
671
- let mode = 'reservation'; // or 'special'
637
+ let setMode('reservation');
638
+ function refreshDesktopModeButton() {
639
+ try {
640
+ const btn = document.querySelector('#onekite-calendar .fc-newSpecial-button');
641
+ if (!btn) return;
642
+ const isSpecial = mode === 'special';
643
+ btn.textContent = isSpecial ? 'Évènement ✓' : 'Évènement';
644
+ btn.classList.toggle('onekite-active', isSpecial);
645
+ } catch (e) {}
646
+ }
647
+
648
+ function setMode(next) {
649
+ mode = next;
650
+ refreshDesktopModeButton();
651
+ try {
652
+ const mb = document.querySelector('#onekite-mobile-controls .onekite-mode-btn');
653
+ if (mb) {
654
+ const isSpecial = mode === 'special';
655
+ mb.textContent = isSpecial ? 'Mode évènement ✓' : 'Mode évènement';
656
+ mb.classList.toggle('onekite-active', isSpecial);
657
+ }
658
+ } catch (e) {}
659
+ }
660
+ // or 'special'
672
661
 
673
662
  // Inject lightweight responsive CSS once.
674
663
  try {
@@ -694,7 +683,21 @@ function toDatetimeLocalValue(date) {
694
683
  #onekite-calendar .fc .fc-toolbar-title { font-size: 1rem; }
695
684
  #onekite-calendar .fc .fc-button { padding: .2rem .4rem; font-size: .8rem; }
696
685
  }
697
- `;
686
+
687
+ /* Violet action button (events mode) */
688
+ #onekite-calendar .fc .fc-newSpecial-button,
689
+ .onekite-btn-violet {
690
+ background: #6f42c1 !important;
691
+ border-color: #6f42c1 !important;
692
+ color: #fff !important;
693
+ }
694
+ /* Active state */
695
+ #onekite-calendar .fc .fc-newSpecial-button.onekite-active,
696
+ .onekite-btn-violet.onekite-active {
697
+ filter: brightness(0.95);
698
+ box-shadow: 0 0 0 0.15rem rgba(111,66,193,.25);
699
+ }
700
+ `;
698
701
  document.head.appendChild(st);
699
702
  }
700
703
  } catch (e) {}
@@ -724,31 +727,6 @@ function toDatetimeLocalValue(date) {
724
727
  right: (canCreateSpecial ? 'newSpecial ' : '') + 'dayGridMonth,timeGridWeek',
725
728
  };
726
729
 
727
- // Mode UI helpers (desktop FullCalendar button + mobile toggle)
728
- let mobileModeBtn = null;
729
- function refreshModeIndicators() {
730
- const isSpecial = mode === 'special';
731
-
732
- // Desktop "Évènement" button in the FullCalendar header
733
- try {
734
- const desktopBtn = document.querySelector('#onekite-calendar .fc-newSpecial-button');
735
- if (desktopBtn) {
736
- desktopBtn.textContent = isSpecial ? 'Évènement ✓' : 'Évènement';
737
- desktopBtn.classList.toggle('onekite-mode-active', isSpecial);
738
- }
739
- } catch (e) {}
740
-
741
- // Mobile mode toggle (under the calendar)
742
- try {
743
- if (mobileModeBtn) {
744
- mobileModeBtn.textContent = isSpecial ? 'Mode évènement ✓' : 'Mode évènement';
745
- mobileModeBtn.classList.toggle('active', isSpecial);
746
- mobileModeBtn.classList.toggle('onekite-btn-violet', isSpecial);
747
- mobileModeBtn.classList.toggle('onekite-btn-violet-outline', !isSpecial);
748
- }
749
- } catch (e) {}
750
- }
751
-
752
730
  const calendar = new FullCalendar.Calendar(el, {
753
731
  initialView: 'dayGridMonth',
754
732
  height: 'auto',
@@ -763,8 +741,7 @@ function toDatetimeLocalValue(date) {
763
741
  newSpecial: {
764
742
  text: 'Évènement',
765
743
  click: () => {
766
- mode = (mode === 'special') ? 'reservation' : 'special';
767
- refreshModeIndicators();
744
+ setMode((mode === 'special') ? 'reservation' : 'special');
768
745
  if (mode === 'special') {
769
746
  showAlert('success', 'Mode évènement : sélectionne une plage (date/heure) sur le calendrier.');
770
747
  }
@@ -778,7 +755,7 @@ function toDatetimeLocalValue(date) {
778
755
  events: async function (info, successCallback, failureCallback) {
779
756
  try {
780
757
  const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
781
- const data = await apiJson(`/api/v3/plugins/calendar-onekite/events?${qs.toString()}`, `/api/plugins/calendar-onekite/events?${qs.toString()}`);
758
+ const data = await fetchJson(`/api/v3/plugins/calendar-onekite/events?${qs.toString()}`);
782
759
  successCallback(data);
783
760
  } catch (e) {
784
761
  failureCallback(e);
@@ -792,7 +769,7 @@ function toDatetimeLocalValue(date) {
792
769
  try {
793
770
  if (mode === 'special' && canCreateSpecial) {
794
771
  const payload = await openSpecialEventDialog(info);
795
- mode = 'reservation';
772
+ setMode('reservation');
796
773
  if (!payload) {
797
774
  calendar.unselect();
798
775
  isDialogOpen = false;
@@ -854,35 +831,8 @@ function toDatetimeLocalValue(date) {
854
831
  }
855
832
  },
856
833
  dateClick: async function (info) {
857
- // One-day selection convenience.
858
- // For reservations, selecting a day makes a 1-day range.
859
- // For special events ("mode évènement"), we want a single date (no implicit J+1 end).
834
+ // One-day selection convenience
860
835
  const start = info.date;
861
-
862
- if (mode === 'special' && canCreateSpecial) {
863
- if (isDialogOpen) {
864
- return;
865
- }
866
- isDialogOpen = true;
867
- try {
868
- const payload = await openSpecialEventDialog({ start, end: start });
869
- mode = 'reservation';
870
- if (!payload) {
871
- isDialogOpen = false;
872
- return;
873
- }
874
- await createSpecialEvent(payload);
875
- showAlert('success', 'Évènement créé.');
876
- calendar.refetchEvents();
877
- isDialogOpen = false;
878
- return;
879
- } catch (e) {
880
- showAlert('error', 'Impossible de créer l\'évènement.');
881
- isDialogOpen = false;
882
- return;
883
- }
884
- }
885
-
886
836
  const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
887
837
  calendar.select(start, end);
888
838
  },
@@ -908,7 +858,7 @@ function toDatetimeLocalValue(date) {
908
858
  const html = `
909
859
  <div class="mb-2"><strong>Titre</strong><br>${escapeHtml(p.title || ev.title || '')}</div>
910
860
  ${userLine}
911
- <div class="mb-2"><strong>Période</strong><br>${escapeHtml(formatPeriodWithTime(ev.start, ev.end))}</div>
861
+ <div class="mb-2"><strong>Période</strong><br>${escapeHtml(formatDtWithTime(ev.start))} → ${escapeHtml(formatDtWithTime(ev.end))}</div>
912
862
  ${addr ? `<div class="mb-2"><strong>Adresse</strong><br>${addrHtml}</div>` : ''}
913
863
  ${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
914
864
  `;
@@ -1113,10 +1063,7 @@ function toDatetimeLocalValue(date) {
1113
1063
  },
1114
1064
  });
1115
1065
 
1116
-
1117
- // Ensure the promise resolves even if the modal is dismissed externally (e.g. mobile rotate)
1118
- dlg.on('hidden.bs.modal', () => __settle(null));
1119
- // Init Leaflet map once the modal is visible.
1066
+ // Init Leaflet map once the modal is visible.
1120
1067
  dlg.on('shown.bs.modal', async () => {
1121
1068
  try {
1122
1069
  const L = await loadLeaflet();
@@ -1217,8 +1164,9 @@ function toDatetimeLocalValue(date) {
1217
1164
  try { window.oneKiteCalendar = calendar; } catch (e) {}
1218
1165
 
1219
1166
  calendar.render();
1220
- // Sync mode UI after the header is rendered
1221
- try { refreshModeIndicators(); } catch (e) {}
1167
+
1168
+ refreshDesktopModeButton();
1169
+
1222
1170
 
1223
1171
  // Mobile controls: view (month/week) + mode (reservation/event) without bloating the header.
1224
1172
  try {
@@ -1267,17 +1215,22 @@ function toDatetimeLocalValue(date) {
1267
1215
  if (canCreateSpecial) {
1268
1216
  const modeBtn = document.createElement('button');
1269
1217
  modeBtn.type = 'button';
1270
- modeBtn.className = 'btn btn-sm onekite-btn-violet-outline onekite-mode-btn';
1271
- // Expose to the shared mode indicator refresher
1272
- mobileModeBtn = modeBtn;
1218
+ modeBtn.className = 'btn btn-sm onekite-btn-violet onekite-mode-btn';
1219
+ function refreshModeBtn() {
1220
+ const isSpecial = mode === 'special';
1221
+ modeBtn.textContent = isSpecial ? 'Mode évènement ✓' : 'Mode évènement';
1222
+ modeBtn.classList.toggle('active', isSpecial);
1223
+ // Keep violet, only toggle active class
1224
+ modeBtn.classList.toggle('onekite-active', isSpecial);
1225
+ }
1273
1226
  modeBtn.addEventListener('click', () => {
1274
- mode = (mode === 'special') ? 'reservation' : 'special';
1275
- refreshModeIndicators();
1227
+ setMode((mode === 'special') ? 'reservation' : 'special');
1228
+ refreshModeBtn();
1276
1229
  if (mode === 'special') {
1277
1230
  showAlert('success', 'Mode évènement : sélectionne une plage (date/heure) sur le calendrier.');
1278
1231
  }
1279
1232
  });
1280
- refreshModeIndicators();
1233
+ refreshModeBtn();
1281
1234
  controls.appendChild(modeBtn);
1282
1235
  }
1283
1236
 
@@ -27,31 +27,6 @@
27
27
  .fc .fc-newSpecial-button:hover {
28
28
  filter: brightness(0.95);
29
29
  }
30
- /* Indicate active "mode évènement" (desktop) */
31
- .fc .fc-newSpecial-button.onekite-mode-active {
32
- box-shadow: inset 0 0 0 2px rgba(255,255,255,0.35);
33
- }
34
-
35
- /* Violet button variants for mobile mode toggle */
36
- .onekite-btn-violet {
37
- background: #8e44ad !important;
38
- border-color: #8e44ad !important;
39
- color: #fff !important;
40
- }
41
- .onekite-btn-violet:hover {
42
- filter: brightness(0.95);
43
- color: #fff !important;
44
- }
45
- .onekite-btn-violet-outline {
46
- background: transparent !important;
47
- border-color: #8e44ad !important;
48
- color: #8e44ad !important;
49
- }
50
- .onekite-btn-violet-outline:hover {
51
- background: #8e44ad !important;
52
- border-color: #8e44ad !important;
53
- color: #fff !important;
54
- }
55
30
 
56
31
  /* Mobile tweaks for FullCalendar */
57
32
  @media (max-width: 576px) {