nodebb-plugin-calendar-onekite 11.1.73 → 11.1.74

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/api.js CHANGED
@@ -15,15 +15,12 @@ const helloasso = require('./helloasso');
15
15
  async function sendEmail(template, toEmail, subject, data) {
16
16
  if (!toEmail) return;
17
17
  const emailer = require.main.require('./src/emailer');
18
+ const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
18
19
  try {
19
- // Newer NodeBB builds expose sendToEmail
20
+ // NodeBB's Emailer API differs across versions; the most reliable approach is to pass `subject` inside `data`.
20
21
  if (typeof emailer.sendToEmail === 'function') {
21
- if (emailer.sendToEmail.length >= 4) {
22
- await emailer.sendToEmail(template, toEmail, subject, data);
23
- return;
24
- }
25
- if (emailer.sendToEmail.length === 3) {
26
- const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
22
+ // Prefer 3-args (template, email, data) when possible
23
+ if (emailer.sendToEmail.length >= 3) {
27
24
  await emailer.sendToEmail(template, toEmail, dataWithSubject);
28
25
  return;
29
26
  }
@@ -32,28 +29,20 @@ async function sendEmail(template, toEmail, subject, data) {
32
29
  return;
33
30
  }
34
31
  if (typeof emailer.send === 'function') {
35
- // Common: (template, email, subject, data)
36
- if (emailer.send.length >= 4) {
37
- await emailer.send(template, toEmail, subject, data);
38
- return;
39
- }
40
- // Some builds: (template, email, data)
41
- // In that case, subject is expected inside `data.subject`.
42
- if (emailer.send.length === 3) {
43
- const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
32
+ if (emailer.send.length >= 3) {
44
33
  await emailer.send(template, toEmail, dataWithSubject);
45
34
  return;
46
35
  }
47
- // Fallback: try 4-args anyway
48
36
  await emailer.send(template, toEmail, subject, data);
49
37
  return;
50
38
  }
51
39
  } catch (err) {
52
40
  // eslint-disable-next-line no-console
53
- console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err && err.message || err) });
41
+ console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String((err && err.message) || err) });
54
42
  }
55
43
  }
56
44
 
45
+
57
46
  function normalizeBaseUrl(meta) {
58
47
  // Prefer meta.config.url, fallback to nconf.get('url')
59
48
  let base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
@@ -507,7 +496,7 @@ api.createReservation = async function (req, res) {
507
496
  await sendEmail(
508
497
  'calendar-onekite_pending',
509
498
  md.email,
510
- 'Location matériel - Demande de réservation',
499
+ 'Location - Demande de réservation',
511
500
  {
512
501
  username: md.username,
513
502
  requester: requester.username,
@@ -605,7 +594,7 @@ api.approveReservation = async function (req, res) {
605
594
  const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
606
595
  ? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
607
596
  : '';
608
- await sendEmail('calendar-onekite_approved', requester.email, 'Location matériel - Réservation validée', {
597
+ await sendEmail('calendar-onekite_approved', requester.email, 'Location - Réservation validée', {
609
598
  username: requester.username,
610
599
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
611
600
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
@@ -641,7 +630,7 @@ api.refuseReservation = async function (req, res) {
641
630
 
642
631
  const requester = await user.getUserFields(r.uid, ['username', 'email']);
643
632
  if (requester && requester.email) {
644
- await sendEmail('calendar-onekite_refused', requester.email, 'Location matériel - Demande de réservation', {
633
+ await sendEmail('calendar-onekite_refused', requester.email, 'Location - Demande de réservation', {
645
634
  username: requester.username,
646
635
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
647
636
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
package/library.js CHANGED
@@ -77,12 +77,12 @@ Plugin.renderMiniWidget = async function (hookData, callback) {
77
77
  // Note: We render client-side to avoid extra server-side queries. The widget fetches
78
78
  // events via the existing public API endpoint.
79
79
  widget.html = `
80
- <div class="calendar-onekite-widget" id="${widgetId}">
80
+ <div class="calendar-onekite-widget" id="${widgetId}" data-show-special="${showSpecial ? '1' : '0'}">
81
81
  <div class="calendar-onekite-widget__header">
82
82
  <strong>${escapeHtml(title)}</strong>
83
83
  <a class="calendar-onekite-widget__link" href="/calendar">Ouvrir</a>
84
84
  </div>
85
- <div class="calendar-onekite-widget__month" data-show-special="${showSpecial ? '1' : '0'}"></div>
85
+ <div class="calendar-onekite-widget__month"></div>
86
86
  <div class="calendar-onekite-widget__legend">
87
87
  <span class="calendar-onekite-dot calendar-onekite-dot--pending"></span> en attente
88
88
  <span class="calendar-onekite-dot calendar-onekite-dot--paid"></span> payée
@@ -108,87 +108,6 @@ Plugin.renderMiniWidget = async function (hookData, callback) {
108
108
  .calendar-onekite-badge--special{background:#fd7e14}
109
109
  .calendar-onekite-widget__legend{margin-top:8px;font-size:12px;opacity:.8}
110
110
  </style>
111
- <script>
112
- (function(){
113
- const widgetEl = document.getElementById(${JSON.stringify(widgetId)});
114
- if (!widgetEl) return;
115
-
116
- const monthEl = widgetEl.querySelector('.calendar-onekite-widget__month');
117
- const showSpecial = monthEl.getAttribute('data-show-special') === '1';
118
-
119
- const now = new Date();
120
- const year = now.getFullYear();
121
- const month = now.getMonth();
122
- const first = new Date(year, month, 1);
123
- const last = new Date(year, month + 1, 0);
124
- const start = new Date(year, month, 1);
125
- start.setDate(start.getDate() - ((start.getDay() + 6) % 7)); // Monday-start
126
- const end = new Date(start);
127
- end.setDate(end.getDate() + 41);
128
-
129
- function iso(d){ return d.toISOString(); }
130
- function ymd(d){ return d.toISOString().slice(0,10); }
131
-
132
- const apiUrl = window.location.origin + '/api/v3/plugins/calendar-onekite/events?from=' + encodeURIComponent(iso(start)) + '&to=' + encodeURIComponent(iso(end));
133
-
134
- fetch(apiUrl, { credentials: 'same-origin' })
135
- .then(r => r.ok ? r.json() : Promise.reject(r))
136
- .then(payload => {
137
- const events = Array.isArray(payload) ? payload : (payload.events || payload.data || []);
138
- const dayMap = new Map();
139
- for (const ev of events) {
140
- if (!ev || !ev.start) continue;
141
- const d = new Date(ev.start);
142
- const key = ymd(d);
143
- const props = ev.extendedProps || {};
144
- const isSpecial = props.type === 'special' || ev.type === 'special' || (ev.classNames || []).includes('onekite-special');
145
- if (isSpecial && !showSpecial) continue;
146
- const status = props.status || ev.status || '';
147
- const bucket = dayMap.get(key) || { pending: 0, paid: 0, special: 0 };
148
- if (isSpecial) bucket.special++;
149
- else if (status === 'paid') bucket.paid++;
150
- else bucket.pending++;
151
- dayMap.set(key, bucket);
152
- }
153
- render(dayMap);
154
- })
155
- .catch(() => render(new Map()));
156
-
157
- function render(dayMap){
158
- const grid = document.createElement('div');
159
- grid.className = 'calendar-onekite-widget__grid';
160
- const labels = ['L','M','M','J','V','S','D'];
161
- for (const l of labels){
162
- const el = document.createElement('div');
163
- el.className = 'calendar-onekite-widget__cell calendar-onekite-widget__cell--muted';
164
- el.textContent = l;
165
- grid.appendChild(el);
166
- }
167
- for (let i=0;i<42;i++){
168
- const d = new Date(start);
169
- d.setDate(start.getDate()+i);
170
- const cell = document.createElement('a');
171
- cell.href = '/calendar';
172
- cell.className = 'calendar-onekite-widget__cell';
173
- if (d.getMonth() !== month) cell.classList.add('calendar-onekite-widget__cell--muted');
174
- if (ymd(d) === ymd(now)) cell.classList.add('calendar-onekite-widget__cell--today');
175
- cell.textContent = d.getDate();
176
- const b = dayMap.get(ymd(d));
177
- if (b && (b.pending || b.paid || b.special)){
178
- const badges = document.createElement('div');
179
- badges.className = 'calendar-onekite-badges';
180
- if (b.pending) { const s=document.createElement('span'); s.className='calendar-onekite-badge calendar-onekite-badge--pending'; badges.appendChild(s); }
181
- if (b.paid) { const s=document.createElement('span'); s.className='calendar-onekite-badge calendar-onekite-badge--paid'; badges.appendChild(s); }
182
- if (showSpecial && b.special) { const s=document.createElement('span'); s.className='calendar-onekite-badge calendar-onekite-badge--special'; badges.appendChild(s); }
183
- cell.appendChild(badges);
184
- }
185
- grid.appendChild(cell);
186
- }
187
- monthEl.innerHTML = '';
188
- monthEl.appendChild(grid);
189
- }
190
- })();
191
- </script>
192
111
  `;
193
112
 
194
113
  hookData.widget = widget;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "11.1.73",
3
+ "version": "11.1.74",
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/plugin.json CHANGED
@@ -37,5 +37,5 @@
37
37
  "acpScripts": [
38
38
  "public/admin.js"
39
39
  ],
40
- "version": "1.0.48"
40
+ "version": "1.0.49"
41
41
  }
package/public/client.js CHANGED
@@ -18,7 +18,15 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
18
18
 
19
19
  async function openSpecialEventDialog(selectionInfo) {
20
20
  const start = selectionInfo.start;
21
- const end = selectionInfo.end;
21
+ let end = selectionInfo.end;
22
+ // FullCalendar all-day selection uses an exclusive end (next day 00:00). For a single-day click,
23
+ // this looks like a 2-day range. Default to a 1-hour duration in that case.
24
+ try {
25
+ if (selectionInfo && selectionInfo.allDay && start && end && (end.getTime() - start.getTime()) <= 86400000 + 1000) {
26
+ end = new Date(start.getTime() + 60 * 60 * 1000);
27
+ }
28
+ } catch (e) {}
29
+
22
30
  const html = `
23
31
  <div class="mb-3">
24
32
  <label class="form-label">Titre</label>
@@ -51,7 +59,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
51
59
  `;
52
60
 
53
61
  return await new Promise((resolve) => {
54
- bootbox.dialog({
62
+ const dlg = bootbox.dialog({
55
63
  title: 'Créer un évènement',
56
64
  message: html,
57
65
  buttons: {
@@ -78,14 +86,18 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
78
86
  },
79
87
  });
80
88
 
81
- // init leaflet
82
- setTimeout(async () => {
89
+ // init leaflet once the modal is visible (Leaflet needs a laid-out container)
90
+ dlg.on('shown.bs.modal', async () => {
83
91
  try {
84
92
  const mapEl = document.getElementById('onekite-se-map');
85
93
  if (!mapEl) return;
86
94
  const L = await loadLeaflet();
87
95
  const map = L.map(mapEl).setView([46.5, 2.5], 5);
88
96
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '&copy; OpenStreetMap' }).addTo(map);
97
+
98
+ setTimeout(() => {
99
+ try { map.invalidateSize(); } catch (e) {}
100
+ }, 50);
89
101
  let marker = null;
90
102
  function setMarker(lat, lon) {
91
103
  if (marker) map.removeLayer(marker);
@@ -114,7 +126,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
114
126
  } catch (e) {
115
127
  // ignore leaflet errors
116
128
  }
117
- }, 0);
129
+ });
118
130
  });
119
131
  }
120
132
 
@@ -125,12 +137,12 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
125
137
  ${safeAddr ? `<div class="mb-2">${escapeHtml(safeAddr)}</div>` : ''}
126
138
  <div id="${mapId}" style="height:260px; border:1px solid #ddd; border-radius:6px;"></div>
127
139
  `;
128
- bootbox.dialog({
140
+ const dlg = bootbox.dialog({
129
141
  title: title || 'Carte',
130
142
  message: html,
131
143
  buttons: { close: { label: 'Fermer', className: 'btn-secondary' } },
132
144
  });
133
- setTimeout(async () => {
145
+ dlg.on('shown.bs.modal', async () => {
134
146
  try {
135
147
  const el = document.getElementById(mapId);
136
148
  if (!el) return;
@@ -156,10 +168,14 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
156
168
  }
157
169
  }
158
170
  map.setView([46.5, 2.5], 5);
171
+
172
+ setTimeout(() => {
173
+ try { map.invalidateSize(); } catch (e) {}
174
+ }, 50);
159
175
  } catch (e) {
160
176
  // ignore leaflet errors
161
177
  }
162
- }, 0);
178
+ });
163
179
  }
164
180
 
165
181
  // Click handler for map links in popups
@@ -536,9 +552,10 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
536
552
  isDialogOpen = false;
537
553
  return;
538
554
  }
539
- // Send date strings (no hours) so reservations are day-based.
540
- const startDate = new Date(info.start).toISOString().slice(0, 10);
541
- const endDate = new Date(info.end).toISOString().slice(0, 10);
555
+ // Send date-only strings from FullCalendar to avoid timezone shifts.
556
+ // For all-day selections, startStr/endStr are YYYY-MM-DD (end is exclusive).
557
+ const startDate = info.startStr;
558
+ const endDate = info.endStr;
542
559
  await requestReservation({
543
560
  start: startDate,
544
561
  end: endDate,
@@ -806,6 +823,12 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
806
823
  // Default view (France-ish)
807
824
  map.setView([46.7, 2.5], 5);
808
825
 
826
+ // Bootbox/Bootstrap modal layouts can report a zero-sized container on first paint.
827
+ // Force Leaflet to recompute sizes once the modal is fully displayed.
828
+ setTimeout(() => {
829
+ try { map.invalidateSize(); } catch (e) {}
830
+ }, 50);
831
+
809
832
  let marker = null;
810
833
  function setMarker(lat, lon, zoom) {
811
834
  const ll = [lat, lon];
@@ -903,6 +926,95 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
903
926
  // call once after current tick.
904
927
  setTimeout(() => autoInit({ template: (ajaxify && ajaxify.data && ajaxify.data.template) || { name: '' } }), 0);
905
928
 
929
+ // Render mini widgets (ACP Widgets) without inline scripts (CSP-friendly).
930
+ async function renderMiniWidgets() {
931
+ const widgets = Array.from(document.querySelectorAll('.calendar-onekite-widget'));
932
+ if (!widgets.length) return;
933
+
934
+ const now = new Date();
935
+ const year = now.getFullYear();
936
+ const month = now.getMonth();
937
+
938
+ const start = new Date(year, month, 1);
939
+ start.setDate(start.getDate() - ((start.getDay() + 6) % 7)); // Monday-start
940
+ const end = new Date(start);
941
+ end.setDate(end.getDate() + 41);
942
+
943
+ function iso(d){ return d.toISOString(); }
944
+ function ymd(d){ return d.toISOString().slice(0, 10); }
945
+
946
+ let events = [];
947
+ try {
948
+ const qs = new URLSearchParams({ start: iso(start), end: iso(end) });
949
+ events = await fetchJson(`/api/v3/plugins/calendar-onekite/events?${qs.toString()}`)
950
+ .catch(() => fetchJson(`/api/plugins/calendar-onekite/events?${qs.toString()}`));
951
+ } catch (e) {
952
+ events = [];
953
+ }
954
+
955
+ const dayMap = new Map();
956
+ for (const ev of (events || [])) {
957
+ if (!ev || !ev.start) continue;
958
+ const d = new Date(ev.start);
959
+ const key = ymd(d);
960
+ const props = ev.extendedProps || {};
961
+ const isSpecial = props.type === 'special' || ev.type === 'special' || (ev.classNames || []).includes('onekite-special');
962
+ const status = props.status || ev.status || '';
963
+ const bucket = dayMap.get(key) || { pending: 0, paid: 0, special: 0 };
964
+ if (isSpecial) bucket.special++;
965
+ else if (status === 'paid') bucket.paid++;
966
+ else bucket.pending++;
967
+ dayMap.set(key, bucket);
968
+ }
969
+
970
+ for (const widgetEl of widgets) {
971
+ const monthEl = widgetEl.querySelector('.calendar-onekite-widget__month');
972
+ if (!monthEl) continue;
973
+ const showSpecial = widgetEl.getAttribute('data-show-special') === '1';
974
+
975
+ const grid = document.createElement('div');
976
+ grid.className = 'calendar-onekite-widget__grid';
977
+ const labels = ['L','M','M','J','V','S','D'];
978
+ for (const l of labels) {
979
+ const el = document.createElement('div');
980
+ el.className = 'calendar-onekite-widget__cell calendar-onekite-widget__cell--muted';
981
+ el.textContent = l;
982
+ grid.appendChild(el);
983
+ }
984
+ for (let i = 0; i < 42; i++) {
985
+ const d = new Date(start);
986
+ d.setDate(start.getDate() + i);
987
+ const cell = document.createElement('a');
988
+ cell.href = '/calendar';
989
+ cell.className = 'calendar-onekite-widget__cell';
990
+ if (d.getMonth() !== month) cell.classList.add('calendar-onekite-widget__cell--muted');
991
+ if (ymd(d) === ymd(now)) cell.classList.add('calendar-onekite-widget__cell--today');
992
+ cell.textContent = d.getDate();
993
+
994
+ const b = dayMap.get(ymd(d));
995
+ if (b && (b.pending || b.paid || (showSpecial && b.special))) {
996
+ const badges = document.createElement('div');
997
+ badges.className = 'calendar-onekite-badges';
998
+ if (b.pending) { const s = document.createElement('span'); s.className = 'calendar-onekite-badge calendar-onekite-badge--pending'; badges.appendChild(s); }
999
+ if (b.paid) { const s = document.createElement('span'); s.className = 'calendar-onekite-badge calendar-onekite-badge--paid'; badges.appendChild(s); }
1000
+ if (showSpecial && b.special) { const s = document.createElement('span'); s.className = 'calendar-onekite-badge calendar-onekite-badge--special'; badges.appendChild(s); }
1001
+ cell.appendChild(badges);
1002
+ }
1003
+
1004
+ grid.appendChild(cell);
1005
+ }
1006
+
1007
+ monthEl.innerHTML = '';
1008
+ monthEl.appendChild(grid);
1009
+ }
1010
+ }
1011
+
1012
+ // Re-render widgets on every page load/end.
1013
+ if (hooks && typeof hooks.on === 'function') {
1014
+ hooks.on('action:ajaxify.end', () => { renderMiniWidgets().catch(() => {}); });
1015
+ }
1016
+ setTimeout(() => { renderMiniWidgets().catch(() => {}); }, 0);
1017
+
906
1018
 
907
1019
 
908
1020
  // Live refresh when a reservation changes (e.g., payment confirmed by webhook)
@@ -6,19 +6,19 @@
6
6
 
7
7
  <ul class="nav nav-tabs mt-3" role="tablist">
8
8
  <li class="nav-item" role="presentation">
9
- <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#onekite-tab-settings" type="button" role="tab">Locations</button>
9
+ <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#onekite-tab-settings" type="button" role="tab" aria-controls="onekite-tab-settings" aria-selected="true">Locations</button>
10
10
  </li>
11
11
  <li class="nav-item" role="presentation">
12
- <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-events" type="button" role="tab">Évènements</button>
12
+ <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-events" type="button" role="tab" aria-controls="onekite-tab-events" aria-selected="false">Évènements</button>
13
13
  </li>
14
14
  <li class="nav-item" role="presentation">
15
- <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-pending" type="button" role="tab">Demandes en attente</button>
15
+ <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-pending" type="button" role="tab" aria-controls="onekite-tab-pending" aria-selected="false">Demandes en attente</button>
16
16
  </li>
17
17
  <li class="nav-item" role="presentation">
18
- <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-debug" type="button" role="tab">Debug HelloAsso</button>
18
+ <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-debug" type="button" role="tab" aria-controls="onekite-tab-debug" aria-selected="false">Debug HelloAsso</button>
19
19
  </li>
20
20
  <li class="nav-item" role="presentation">
21
- <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-accounting" type="button" role="tab">Comptabilisation</button>
21
+ <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-accounting" type="button" role="tab" aria-controls="onekite-tab-accounting" aria-selected="false">Comptabilisation</button>
22
22
  </li>
23
23
  </ul>
24
24
 
@@ -183,4 +183,4 @@
183
183
  });
184
184
  </script>
185
185
 
186
- <!-- IMPORT admin/partials/settings/footer.tpl -->
186
+ <!-- IMPORT admin/partials/settings/footer.tpl -->