nodebb-plugin-calendar-onekite 11.1.89 → 11.1.90

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
@@ -116,6 +116,15 @@ admin.approveReservation = async function (req, res) {
116
116
  r.pickupLon = String((req.body && req.body.pickupLon) || '').trim();
117
117
  r.approvedAt = Date.now();
118
118
 
119
+ try {
120
+ const approver = await user.getUserFields(req.uid, ['username']);
121
+ r.approvedBy = req.uid;
122
+ r.approvedByUsername = approver && approver.username ? approver.username : '';
123
+ } catch (e) {
124
+ r.approvedBy = req.uid;
125
+ r.approvedByUsername = '';
126
+ }
127
+
119
128
  // Create HelloAsso payment link if configured
120
129
  const settings = await meta.settings.get('calendar-onekite');
121
130
  const env = settings.helloassoEnv || 'prod';
@@ -184,6 +193,8 @@ admin.approveReservation = async function (req, res) {
184
193
  pickupLat: r.pickupLat || '',
185
194
  pickupLon: r.pickupLon || '',
186
195
  mapUrl,
196
+ validatedBy: r.approvedByUsername || '',
197
+ validatedByUrl: (r.approvedByUsername ? `https://www.onekite.com/users/${encodeURIComponent(String(r.approvedByUsername))}` : ''),
187
198
  });
188
199
  }
189
200
  } catch (e) {}
package/lib/api.js CHANGED
@@ -210,6 +210,8 @@ function eventsFor(resv) {
210
210
  rid: resv.rid,
211
211
  status,
212
212
  uid: resv.uid,
213
+ approvedBy: resv.approvedBy || 0,
214
+ approvedByUsername: resv.approvedByUsername || '',
213
215
  itemIds: itemIds.filter(Boolean),
214
216
  itemNames: itemNames.filter(Boolean),
215
217
  itemIdLine: itemId,
@@ -545,6 +547,14 @@ api.approveReservation = async function (req, res) {
545
547
  r.pickupLat = String((req.body && req.body.pickupLat) || '').trim();
546
548
  r.pickupLon = String((req.body && req.body.pickupLon) || '').trim();
547
549
  r.approvedAt = Date.now();
550
+ try {
551
+ const approver = await user.getUserFields(uid, ['username']);
552
+ r.approvedBy = uid;
553
+ r.approvedByUsername = approver && approver.username ? approver.username : '';
554
+ } catch (e) {
555
+ r.approvedBy = uid;
556
+ r.approvedByUsername = '';
557
+ }
548
558
  // Create HelloAsso payment link on validation
549
559
  try {
550
560
  const settings2 = await meta.settings.get('calendar-onekite');
@@ -613,6 +623,8 @@ api.approveReservation = async function (req, res) {
613
623
  pickupLon: r.pickupLon || '',
614
624
  mapUrl,
615
625
  paymentUrl: r.paymentUrl || '',
626
+ validatedBy: r.approvedByUsername || '',
627
+ validatedByUrl: (r.approvedByUsername ? `https://www.onekite.com/users/${encodeURIComponent(String(r.approvedByUsername))}` : ''),
616
628
  });
617
629
  }
618
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.89",
3
+ "version": "11.1.90",
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
@@ -105,6 +105,13 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
105
105
  }
106
106
  const geocodeBtn = document.getElementById('onekite-se-geocode');
107
107
  const addrInput = document.getElementById('onekite-se-address');
108
+ try {
109
+ attachAddressAutocomplete(addrInput, (h) => {
110
+ if (h && Number.isFinite(h.lat) && Number.isFinite(h.lon)) {
111
+ setMarker(h.lat, h.lon);
112
+ }
113
+ });
114
+ } catch (e) {}
108
115
  geocodeBtn?.addEventListener('click', async () => {
109
116
  const q = (addrInput?.value || '').trim();
110
117
  if (!q) return;
@@ -298,6 +305,145 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
298
305
  return { lat, lon, displayName: hit.display_name || q };
299
306
  }
300
307
 
308
+ async function searchAddresses(query, limit) {
309
+ const q = String(query || '').trim();
310
+ const lim = Math.min(10, Math.max(1, Number(limit) || 5));
311
+ if (q.length < 3) return [];
312
+ const url = `https://nominatim.openstreetmap.org/search?format=jsonv2&addressdetails=1&limit=${lim}&q=${encodeURIComponent(q)}`;
313
+ const res = await fetch(url, {
314
+ method: 'GET',
315
+ headers: {
316
+ 'Accept': 'application/json',
317
+ 'Accept-Language': 'fr',
318
+ },
319
+ });
320
+ if (!res.ok) return [];
321
+ const arr = await res.json();
322
+ if (!Array.isArray(arr)) return [];
323
+ return arr.map((hit) => {
324
+ const lat = Number(hit.lat);
325
+ const lon = Number(hit.lon);
326
+ return {
327
+ displayName: hit.display_name || q,
328
+ lat: Number.isFinite(lat) ? lat : null,
329
+ lon: Number.isFinite(lon) ? lon : null,
330
+ };
331
+ }).filter(h => h && h.displayName);
332
+ }
333
+
334
+ function attachAddressAutocomplete(inputEl, onPick) {
335
+ if (!inputEl) return;
336
+ // Avoid double attach
337
+ if (inputEl.getAttribute('data-onekite-autocomplete') === '1') return;
338
+ inputEl.setAttribute('data-onekite-autocomplete', '1');
339
+
340
+ const wrapper = document.createElement('div');
341
+ wrapper.style.position = 'relative';
342
+ inputEl.parentNode && inputEl.parentNode.insertBefore(wrapper, inputEl);
343
+ wrapper.appendChild(inputEl);
344
+
345
+ const menu = document.createElement('div');
346
+ menu.className = 'onekite-autocomplete-menu';
347
+ menu.style.position = 'absolute';
348
+ menu.style.left = '0';
349
+ menu.style.right = '0';
350
+ menu.style.top = '100%';
351
+ menu.style.zIndex = '2000';
352
+ menu.style.background = '#fff';
353
+ menu.style.border = '1px solid rgba(0,0,0,.15)';
354
+ menu.style.borderTop = '0';
355
+ menu.style.maxHeight = '220px';
356
+ menu.style.overflowY = 'auto';
357
+ menu.style.display = 'none';
358
+ menu.style.borderRadius = '0 0 .375rem .375rem';
359
+ wrapper.appendChild(menu);
360
+
361
+ let timer = null;
362
+ let lastQuery = '';
363
+ let busy = false;
364
+
365
+ function hide() {
366
+ menu.style.display = 'none';
367
+ menu.innerHTML = '';
368
+ }
369
+
370
+ function show(hits) {
371
+ if (!hits || !hits.length) {
372
+ hide();
373
+ return;
374
+ }
375
+ menu.innerHTML = '';
376
+ hits.forEach((h) => {
377
+ const btn = document.createElement('button');
378
+ btn.type = 'button';
379
+ btn.className = 'onekite-autocomplete-item';
380
+ btn.textContent = h.displayName;
381
+ btn.style.display = 'block';
382
+ btn.style.width = '100%';
383
+ btn.style.textAlign = 'left';
384
+ btn.style.padding = '.35rem .5rem';
385
+ btn.style.border = '0';
386
+ btn.style.background = 'transparent';
387
+ btn.style.cursor = 'pointer';
388
+ btn.addEventListener('click', () => {
389
+ inputEl.value = h.displayName;
390
+ hide();
391
+ try { onPick && onPick(h); } catch (e) {}
392
+ });
393
+ btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(0,0,0,.05)'; });
394
+ btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent'; });
395
+ menu.appendChild(btn);
396
+ });
397
+ menu.style.display = 'block';
398
+ }
399
+
400
+ async function run(q) {
401
+ if (busy) return;
402
+ busy = true;
403
+ try {
404
+ const hits = await searchAddresses(q, 6);
405
+ // Don't show stale results
406
+ if (String(inputEl.value || '').trim() !== q) return;
407
+ show(hits);
408
+ } catch (e) {
409
+ hide();
410
+ } finally {
411
+ busy = false;
412
+ }
413
+ }
414
+
415
+ inputEl.addEventListener('input', () => {
416
+ const q = String(inputEl.value || '').trim();
417
+ lastQuery = q;
418
+ if (timer) clearTimeout(timer);
419
+ if (q.length < 3) {
420
+ hide();
421
+ return;
422
+ }
423
+ timer = setTimeout(() => run(lastQuery), 250);
424
+ });
425
+
426
+ inputEl.addEventListener('focus', () => {
427
+ const q = String(inputEl.value || '').trim();
428
+ if (q.length >= 3) {
429
+ if (timer) clearTimeout(timer);
430
+ timer = setTimeout(() => run(q), 150);
431
+ }
432
+ });
433
+
434
+ // Close when clicking outside
435
+ document.addEventListener('click', (e) => {
436
+ try {
437
+ if (!wrapper.contains(e.target)) hide();
438
+ } catch (err) {}
439
+ });
440
+
441
+ inputEl.addEventListener('keydown', (e) => {
442
+ if (e.key === 'Escape') hide();
443
+ });
444
+ }
445
+
446
+
301
447
  async function loadItems() {
302
448
  try {
303
449
  return await fetchJson('/api/v3/plugins/calendar-onekite/items');
@@ -721,6 +867,11 @@ function toDatetimeLocalValue(date) {
721
867
  })();
722
868
  const period = `${formatDt(ev.start)} → ${formatDt(ev.end)}`;
723
869
 
870
+ const approvedBy = String(p.approvedByUsername || '').trim();
871
+ 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>`
873
+ : '';
874
+
724
875
  // Pickup details (address / time / notes) shown once validated
725
876
  const pickupAddress = String(p.pickupAddress || '').trim();
726
877
  const pickupTime = String(p.pickupTime || '').trim();
@@ -750,6 +901,7 @@ function toDatetimeLocalValue(date) {
750
901
  ${userLine}
751
902
  <div class="mb-2"><strong>Matériel</strong><br>${itemsHtml}</div>
752
903
  <div class="mb-2"><strong>Période</strong><br>${period}</div>
904
+ ${validatedByHtml}
753
905
  ${pickupHtml}
754
906
  <div class="text-muted" style="font-size: 12px;">Statut: ${statusLabel(status)}</div>
755
907
  `;
@@ -919,6 +1071,14 @@ function toDatetimeLocalValue(date) {
919
1071
  const geocodeBtn = document.getElementById('onekite-geocode');
920
1072
  const addrInput = document.getElementById('onekite-pickup-address');
921
1073
 
1074
+ try {
1075
+ attachAddressAutocomplete(addrInput, (h) => {
1076
+ if (h && Number.isFinite(h.lat) && Number.isFinite(h.lon)) {
1077
+ setMarker(h.lat, h.lon, 16);
1078
+ }
1079
+ });
1080
+ } catch (e) {}
1081
+
922
1082
  async function runGeocode() {
923
1083
  try {
924
1084
  const addr = (addrInput?.value || '').trim();
@@ -1013,11 +1173,14 @@ function toDatetimeLocalValue(date) {
1013
1173
  if (canCreateSpecial) {
1014
1174
  const modeBtn = document.createElement('button');
1015
1175
  modeBtn.type = 'button';
1016
- modeBtn.className = 'btn btn-sm btn-outline-primary';
1176
+ modeBtn.className = 'btn btn-sm btn-outline-warning onekite-mode-btn';
1017
1177
  function refreshModeBtn() {
1018
1178
  const isSpecial = mode === 'special';
1019
1179
  modeBtn.textContent = isSpecial ? 'Mode évènement ✓' : 'Mode évènement';
1020
1180
  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);
1021
1184
  }
1022
1185
  modeBtn.addEventListener('click', () => {
1023
1186
  mode = (mode === 'special') ? 'reservation' : 'special';
@@ -18,6 +18,16 @@
18
18
 
19
19
 
20
20
  <style>
21
+ /* Make the custom "Évènement" button distinct from view buttons */
22
+ .fc .fc-newSpecial-button {
23
+ background: #8e44ad;
24
+ border-color: #8e44ad;
25
+ color: #fff;
26
+ }
27
+ .fc .fc-newSpecial-button:hover {
28
+ filter: brightness(0.95);
29
+ }
30
+
21
31
  /* Mobile tweaks for FullCalendar */
22
32
  @media (max-width: 576px) {
23
33
  #onekite-calendar { margin-top: .5rem !important; }
@@ -1,6 +1,10 @@
1
1
  <p>Bonjour {username},</p>
2
2
  <p>Votre réservation a été validée.</p>
3
3
 
4
+ <!-- IF validatedBy -->
5
+ <p><strong>Validée par :</strong> <a href="{validatedByUrl}">{validatedBy}</a></p>
6
+ <!-- ENDIF validatedBy -->
7
+
4
8
  <p><strong>Matériel</strong></p>
5
9
  <ul>
6
10
  <!-- BEGIN itemNames -->