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 +11 -0
- package/lib/api.js +12 -0
- package/package.json +1 -1
- package/public/client.js +164 -1
- package/templates/calendar-onekite.tpl +10 -0
- package/templates/emails/calendar-onekite_approved.tpl +4 -0
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
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-
|
|
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 -->
|