nodebb-plugin-calendar-onekite 11.1.96 → 11.1.98

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,30 +20,32 @@ function formatFR(tsOrIso) {
20
20
 
21
21
  async function sendEmail(template, toEmail, subject, data) {
22
22
  if (!toEmail) return;
23
+
24
+ const settings = await meta.settings.get('calendar-onekite').catch(() => ({}));
25
+ const lang = (settings && settings.defaultLang) || (meta && meta.config && meta.config.defaultLang) || 'fr';
26
+ const params = Object.assign({}, data || {}, subject ? { subject } : {});
27
+
23
28
  try {
24
29
  if (typeof emailer.sendToEmail === 'function') {
30
+ // NodeBB: sendToEmail(template, email, language, params)
25
31
  if (emailer.sendToEmail.length >= 4) {
26
- await emailer.sendToEmail(template, toEmail, subject, data);
32
+ await emailer.sendToEmail(template, toEmail, lang, params);
27
33
  } else {
28
- const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
29
- await emailer.sendToEmail(template, toEmail, dataWithSubject);
34
+ // Older signature: sendToEmail(template, email, params)
35
+ await emailer.sendToEmail(template, toEmail, params);
30
36
  }
31
37
  return;
32
38
  }
33
39
  if (typeof emailer.send === 'function') {
34
- if (emailer.send.length >= 4) {
35
- await emailer.send(template, toEmail, subject, data);
36
- return;
37
- }
38
- if (emailer.send.length === 3) {
39
- const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
40
- await emailer.send(template, toEmail, dataWithSubject);
41
- return;
42
- }
43
- await emailer.send(template, toEmail, subject, data);
40
+ // Fallback: send(template, uid, params) - not usable without uid
41
+ return;
44
42
  }
45
43
  } catch (err) {
46
- console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err && err.message || err) });
44
+ console.warn('[calendar-onekite] Failed to send email', {
45
+ template,
46
+ toEmail,
47
+ err: err && err.message ? err.message : String(err),
48
+ });
47
49
  }
48
50
  }
49
51
 
@@ -194,7 +196,7 @@ admin.approveReservation = async function (req, res) {
194
196
  pickupLon: r.pickupLon || '',
195
197
  mapUrl,
196
198
  validatedBy: r.approvedByUsername || '',
197
- validatedByUrl: (r.approvedByUsername ? `https://www.onekite.com/users/${encodeURIComponent(String(r.approvedByUsername))}` : ''),
199
+ validatedByUrl: (r.approvedByUsername ? `https://www.onekite.com/user/${encodeURIComponent(String(r.approvedByUsername))}` : ''),
198
200
  });
199
201
  }
200
202
  } catch (e) {}
package/lib/api.js CHANGED
@@ -624,7 +624,7 @@ api.approveReservation = async function (req, res) {
624
624
  mapUrl,
625
625
  paymentUrl: r.paymentUrl || '',
626
626
  validatedBy: r.approvedByUsername || '',
627
- validatedByUrl: (r.approvedByUsername ? `https://www.onekite.com/users/${encodeURIComponent(String(r.approvedByUsername))}` : ''),
627
+ validatedByUrl: (r.approvedByUsername ? `https://www.onekite.com/user/${encodeURIComponent(String(r.approvedByUsername))}` : ''),
628
628
  });
629
629
  }
630
630
  return res.json({ ok: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "11.1.96",
3
+ "version": "11.1.98",
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
@@ -188,6 +188,20 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
188
188
  const lon = parseFloat(a.getAttribute('data-lon'));
189
189
  openMapViewer('Adresse', addr, lat, lon);
190
190
  });
191
+ // Close any open Bootbox modal when navigating to a user profile from within a modal
192
+ document.addEventListener('click', (ev) => {
193
+ const a = ev.target && ev.target.closest ? ev.target.closest('a.onekite-user-link') : null;
194
+ if (!a) return;
195
+ const inModal = !!(a.closest && a.closest('.modal, .bootbox'));
196
+ if (!inModal) return;
197
+ const href = a.getAttribute('href');
198
+ if (!href) return;
199
+ ev.preventDefault();
200
+ try { bootbox.hideAll(); } catch (e) {}
201
+ setTimeout(() => { window.location.href = href; }, 50);
202
+ });
203
+
204
+
191
205
 
192
206
  function statusLabel(s) {
193
207
  const map = {
@@ -228,7 +242,12 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
228
242
  ...opts,
229
243
  });
230
244
  if (!res.ok) {
231
- throw new Error(`${res.status}`);
245
+ let payload = null;
246
+ try { payload = await res.json(); } catch (e) {}
247
+ const err = new Error(`${res.status}`);
248
+ err.status = res.status;
249
+ err.payload = payload;
250
+ throw err;
232
251
  }
233
252
  return await res.json();
234
253
  }
@@ -634,7 +653,31 @@ function toDatetimeLocalValue(date) {
634
653
  const canCreateSpecial = !!caps.canCreateSpecial;
635
654
  const canDeleteSpecial = !!caps.canDeleteSpecial;
636
655
 
637
- let mode = 'reservation'; // or 'special'
656
+ // Current creation mode: reservation (location) or special (event)
657
+ let mode = 'reservation';
658
+ function refreshDesktopModeButton() {
659
+ try {
660
+ const btn = document.querySelector('#onekite-calendar .fc-newSpecial-button');
661
+ if (!btn) return;
662
+ const isSpecial = mode === 'special';
663
+ btn.textContent = isSpecial ? 'Évènement ✓' : 'Évènement';
664
+ btn.classList.toggle('onekite-active', isSpecial);
665
+ } catch (e) {}
666
+ }
667
+
668
+ function setMode(next) {
669
+ mode = next;
670
+ refreshDesktopModeButton();
671
+ try {
672
+ const mb = document.querySelector('#onekite-mobile-controls .onekite-mode-btn');
673
+ if (mb) {
674
+ const isSpecial = mode === 'special';
675
+ mb.textContent = isSpecial ? 'Mode évènement ✓' : 'Mode évènement';
676
+ mb.classList.toggle('onekite-active', isSpecial);
677
+ }
678
+ } catch (e) {}
679
+ }
680
+ // or 'special'
638
681
 
639
682
  // Inject lightweight responsive CSS once.
640
683
  try {
@@ -660,7 +703,21 @@ function toDatetimeLocalValue(date) {
660
703
  #onekite-calendar .fc .fc-toolbar-title { font-size: 1rem; }
661
704
  #onekite-calendar .fc .fc-button { padding: .2rem .4rem; font-size: .8rem; }
662
705
  }
663
- `;
706
+
707
+ /* Violet action button (events mode) */
708
+ #onekite-calendar .fc .fc-newSpecial-button,
709
+ .onekite-btn-violet {
710
+ background: #6f42c1 !important;
711
+ border-color: #6f42c1 !important;
712
+ color: #fff !important;
713
+ }
714
+ /* Active state */
715
+ #onekite-calendar .fc .fc-newSpecial-button.onekite-active,
716
+ .onekite-btn-violet.onekite-active {
717
+ filter: brightness(0.95);
718
+ box-shadow: 0 0 0 0.15rem rgba(111,66,193,.25);
719
+ }
720
+ `;
664
721
  document.head.appendChild(st);
665
722
  }
666
723
  } catch (e) {}
@@ -704,8 +761,10 @@ function toDatetimeLocalValue(date) {
704
761
  newSpecial: {
705
762
  text: 'Évènement',
706
763
  click: () => {
707
- mode = 'special';
708
- showAlert('success', 'Mode évènement : sélectionne une plage (date/heure) sur le calendrier.');
764
+ setMode((mode === 'special') ? 'reservation' : 'special');
765
+ if (mode === 'special') {
766
+ showAlert('success', 'Mode évènement : sélectionne une plage (date/heure) sur le calendrier.');
767
+ }
709
768
  },
710
769
  },
711
770
  } : {},
@@ -730,7 +789,7 @@ function toDatetimeLocalValue(date) {
730
789
  try {
731
790
  if (mode === 'special' && canCreateSpecial) {
732
791
  const payload = await openSpecialEventDialog(info);
733
- mode = 'reservation';
792
+ setMode('reservation');
734
793
  if (!payload) {
735
794
  calendar.unselect();
736
795
  isDialogOpen = false;
@@ -779,13 +838,22 @@ function toDatetimeLocalValue(date) {
779
838
  calendar.unselect();
780
839
  isDialogOpen = false;
781
840
  } catch (e) {
782
- const code = String(e && e.message || '');
841
+ const code = String((e && (e.status || e.message)) || '');
842
+ const payload = e && e.payload ? e.payload : null;
843
+
783
844
  if (code === '403') {
784
- showAlert('error', 'Impossible de créer la demande : droits insuffisants (groupe).');
845
+ const msg = payload && (payload.message || payload.error || payload.msg) ? String(payload.message || payload.error || payload.msg) : '';
846
+ const c = payload && payload.code ? String(payload.code) : '';
847
+ if (c === 'NOT_MEMBER' || /adh(é|e)rent/i.test(msg) || /membership/i.test(msg)) {
848
+ showAlert('error', msg || 'Vous devez être adhérent pour pouvoir effectuer une réservation.');
849
+ } else {
850
+ showAlert('error', msg || 'Impossible de créer la demande : droits insuffisants (groupe).');
851
+ }
785
852
  } else if (code === '409') {
786
853
  showAlert('error', 'Impossible : au moins un matériel est déjà réservé ou en attente sur cette période.');
787
854
  } else {
788
- showAlert('error', 'Erreur lors de la création de la demande.');
855
+ const msg = payload && (payload.message || payload.error || payload.msg) ? String(payload.message || payload.error || payload.msg) : '';
856
+ showAlert('error', msg || 'Erreur lors de la création de la demande.');
789
857
  }
790
858
  calendar.unselect();
791
859
  isDialogOpen = false;
@@ -794,6 +862,39 @@ function toDatetimeLocalValue(date) {
794
862
  dateClick: async function (info) {
795
863
  // One-day selection convenience
796
864
  const start = info.date;
865
+
866
+ // In "special event" mode, a simple click should propose a one-day event (not two days in the modal)
867
+ if (mode === 'special' && canCreateSpecial) {
868
+ if (isDialogOpen) {
869
+ return;
870
+ }
871
+ isDialogOpen = true;
872
+ try {
873
+ // Default to a same-day event: 00:00 -> 23:59
874
+ const end = new Date(start.getTime() + (23 * 60 + 59) * 60 * 1000);
875
+ const payload = await openSpecialEventDialog({ start, end, allDay: true });
876
+ setMode('reservation');
877
+ if (!payload) {
878
+ isDialogOpen = false;
879
+ return;
880
+ }
881
+ await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
882
+ method: 'POST',
883
+ body: JSON.stringify(payload),
884
+ }).catch(async () => {
885
+ return await fetchJson('/api/plugins/calendar-onekite/special-events', {
886
+ method: 'POST',
887
+ body: JSON.stringify(payload),
888
+ });
889
+ });
890
+ showAlert('success', 'Évènement créé.');
891
+ calendar.refetchEvents();
892
+ } finally {
893
+ isDialogOpen = false;
894
+ }
895
+ return;
896
+ }
897
+
797
898
  const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
798
899
  calendar.select(start, end);
799
900
  },
@@ -804,7 +905,7 @@ function toDatetimeLocalValue(date) {
804
905
  if (p.type === 'special') {
805
906
  const username = String(p.username || '').trim();
806
907
  const userLine = username
807
- ? `<div class="mb-2"><strong>Créé par</strong><br><a href="${window.location.origin}/user/${encodeURIComponent(username)}">${escapeHtml(username)}</a></div>`
908
+ ? `<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>`
808
909
  : '';
809
910
  const addr = String(p.pickupAddress || '').trim();
810
911
  const lat = Number(p.pickupLat);
@@ -856,7 +957,7 @@ function toDatetimeLocalValue(date) {
856
957
  // Reserved-by line (user profile link)
857
958
  const username = String(p.username || p.user || p.reservedBy || p.ownerUsername || '').trim();
858
959
  const userLine = username
859
- ? `<div class="mb-2"><strong>Réservé par</strong><br><a href="${window.location.origin}/user/${encodeURIComponent(username)}">${escapeHtml(username)}</a></div>`
960
+ ? `<div class="mb-2"><strong>Réservée par</strong><br><a class="onekite-user-link" href="${window.location.origin}/user/${encodeURIComponent(username)}">${escapeHtml(username)}</a></div>`
860
961
  : '';
861
962
  const itemsHtml = (() => {
862
963
  const names = Array.isArray(p.itemNames) ? p.itemNames : (typeof p.itemNames === 'string' && p.itemNames.trim() ? p.itemNames.split(',').map(s=>s.trim()).filter(Boolean) : (p.itemName ? [p.itemName] : []));
@@ -869,7 +970,7 @@ function toDatetimeLocalValue(date) {
869
970
 
870
971
  const approvedBy = String(p.approvedByUsername || '').trim();
871
972
  const validatedByHtml = approvedBy
872
- ? `<div class=\"mb-2\"><strong>Validée par</strong><br><a href=\"https://www.onekite.com/users/${encodeURIComponent(approvedBy)}\">${escapeHtml(approvedBy)}</a></div>`
973
+ ? `<div class=\"mb-2\"><strong>Validée par</strong><br><a href=\"https://www.onekite.com/user/${encodeURIComponent(approvedBy)}\">${escapeHtml(approvedBy)}</a></div>`
873
974
  : '';
874
975
 
875
976
  // Pickup details (address / time / notes) shown once validated
@@ -1126,6 +1227,9 @@ function toDatetimeLocalValue(date) {
1126
1227
 
1127
1228
  calendar.render();
1128
1229
 
1230
+ refreshDesktopModeButton();
1231
+
1232
+
1129
1233
  // Mobile controls: view (month/week) + mode (reservation/event) without bloating the header.
1130
1234
  try {
1131
1235
  const controlsId = 'onekite-mobile-controls';
@@ -1173,17 +1277,16 @@ function toDatetimeLocalValue(date) {
1173
1277
  if (canCreateSpecial) {
1174
1278
  const modeBtn = document.createElement('button');
1175
1279
  modeBtn.type = 'button';
1176
- modeBtn.className = 'btn btn-sm btn-outline-warning onekite-mode-btn';
1280
+ modeBtn.className = 'btn btn-sm onekite-btn-violet onekite-mode-btn';
1177
1281
  function refreshModeBtn() {
1178
1282
  const isSpecial = mode === 'special';
1179
1283
  modeBtn.textContent = isSpecial ? 'Mode évènement ✓' : 'Mode évènement';
1180
1284
  modeBtn.classList.toggle('active', isSpecial);
1181
- // Make the button visually distinct from the view buttons
1182
- modeBtn.classList.toggle('btn-warning', isSpecial);
1183
- modeBtn.classList.toggle('btn-outline-warning', !isSpecial);
1285
+ // Keep violet, only toggle active class
1286
+ modeBtn.classList.toggle('onekite-active', isSpecial);
1184
1287
  }
1185
1288
  modeBtn.addEventListener('click', () => {
1186
- mode = (mode === 'special') ? 'reservation' : 'special';
1289
+ setMode((mode === 'special') ? 'reservation' : 'special');
1187
1290
  refreshModeBtn();
1188
1291
  if (mode === 'special') {
1189
1292
  showAlert('success', 'Mode évènement : sélectionne une plage (date/heure) sur le calendrier.');