nodebb-plugin-onekite-calendar 2.0.3 → 2.0.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Changelog – calendar-onekite
2
2
 
3
+ ## 1.2.11
4
+ - ACP : ajout de la recherche automatique d’adresse (autocomplete Nominatim) dans la modale de validation (unitaire + batch), comme sur le calendrier
5
+
3
6
  ## 1.2.10
4
7
  - HelloAsso : calcul des jours 100% fiable (différence en jours calendaires Y/M/J, sans dépendance aux heures/au fuseau/DST)
5
8
  - FullCalendar : endDate traitée comme exclusive partout (UI + checkout)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
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
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "2.0.3"
42
+ "version": "2.0.4"
43
43
  }
package/public/admin.js CHANGED
@@ -187,6 +187,148 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
187
187
  return { lat, lon, displayName: hit.display_name || q };
188
188
  }
189
189
 
190
+ // Lightweight address autocomplete (OpenStreetMap Nominatim)
191
+ // Designed to work inside Bootstrap "input-group" without moving DOM nodes.
192
+ async function searchAddresses(query, limit) {
193
+ const q = String(query || '').trim();
194
+ const lim = Math.min(10, Math.max(1, Number(limit) || 6));
195
+ if (q.length < 3) return [];
196
+ const url = `https://nominatim.openstreetmap.org/search?format=jsonv2&addressdetails=1&limit=${lim}&q=${encodeURIComponent(q)}`;
197
+ const res = await fetch(url, {
198
+ method: 'GET',
199
+ headers: {
200
+ 'Accept': 'application/json',
201
+ 'Accept-Language': 'fr',
202
+ },
203
+ });
204
+ if (!res.ok) return [];
205
+ const arr = await res.json();
206
+ if (!Array.isArray(arr)) return [];
207
+ return arr.map((hit) => {
208
+ const lat = Number(hit.lat);
209
+ const lon = Number(hit.lon);
210
+ return {
211
+ displayName: hit.display_name || q,
212
+ lat: Number.isFinite(lat) ? lat : null,
213
+ lon: Number.isFinite(lon) ? lon : null,
214
+ };
215
+ }).filter(h => h && h.displayName);
216
+ }
217
+
218
+ function attachAddressAutocomplete(inputEl, onPick) {
219
+ if (!inputEl) return;
220
+ if (inputEl.getAttribute('data-onekite-autocomplete') === '1') return;
221
+ inputEl.setAttribute('data-onekite-autocomplete', '1');
222
+
223
+ const anchor = inputEl.closest('.input-group') || inputEl.parentNode;
224
+ if (!anchor) return;
225
+
226
+ // Ensure positioning context
227
+ if (getComputedStyle(anchor).position === 'static') {
228
+ anchor.style.position = 'relative';
229
+ }
230
+
231
+ const menu = document.createElement('div');
232
+ menu.className = 'onekite-autocomplete-menu';
233
+ menu.style.position = 'absolute';
234
+ menu.style.left = '0';
235
+ menu.style.right = '0';
236
+ menu.style.top = '100%';
237
+ menu.style.zIndex = '2000';
238
+ menu.style.background = '#fff';
239
+ menu.style.border = '1px solid rgba(0,0,0,.15)';
240
+ menu.style.borderTop = '0';
241
+ menu.style.maxHeight = '220px';
242
+ menu.style.overflowY = 'auto';
243
+ menu.style.display = 'none';
244
+ menu.style.borderRadius = '0 0 .375rem .375rem';
245
+ menu.style.boxShadow = '0 .25rem .75rem rgba(0,0,0,.08)';
246
+ anchor.appendChild(menu);
247
+
248
+ let timer = null;
249
+ let lastQuery = '';
250
+ let busy = false;
251
+
252
+ function hide() {
253
+ menu.style.display = 'none';
254
+ menu.innerHTML = '';
255
+ }
256
+
257
+ function show(hits) {
258
+ if (!hits || !hits.length) {
259
+ hide();
260
+ return;
261
+ }
262
+ menu.innerHTML = '';
263
+ hits.forEach((h) => {
264
+ const btn = document.createElement('button');
265
+ btn.type = 'button';
266
+ btn.className = 'onekite-autocomplete-item';
267
+ btn.textContent = h.displayName;
268
+ btn.style.display = 'block';
269
+ btn.style.width = '100%';
270
+ btn.style.textAlign = 'left';
271
+ btn.style.padding = '.35rem .5rem';
272
+ btn.style.border = '0';
273
+ btn.style.background = 'transparent';
274
+ btn.style.cursor = 'pointer';
275
+ btn.addEventListener('click', () => {
276
+ inputEl.value = h.displayName;
277
+ hide();
278
+ try { onPick && onPick(h); } catch (e) {}
279
+ });
280
+ btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(0,0,0,.05)'; });
281
+ btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent'; });
282
+ menu.appendChild(btn);
283
+ });
284
+ menu.style.display = 'block';
285
+ }
286
+
287
+ async function run(q) {
288
+ if (busy) return;
289
+ busy = true;
290
+ try {
291
+ const hits = await searchAddresses(q, 6);
292
+ if (String(inputEl.value || '').trim() !== q) return; // ignore stale
293
+ show(hits);
294
+ } catch (e) {
295
+ hide();
296
+ } finally {
297
+ busy = false;
298
+ }
299
+ }
300
+
301
+ inputEl.addEventListener('input', () => {
302
+ const q = String(inputEl.value || '').trim();
303
+ lastQuery = q;
304
+ if (timer) clearTimeout(timer);
305
+ if (q.length < 3) {
306
+ hide();
307
+ return;
308
+ }
309
+ timer = setTimeout(() => run(lastQuery), 250);
310
+ });
311
+
312
+ inputEl.addEventListener('focus', () => {
313
+ const q = String(inputEl.value || '').trim();
314
+ if (q.length >= 3) {
315
+ if (timer) clearTimeout(timer);
316
+ timer = setTimeout(() => run(q), 150);
317
+ }
318
+ });
319
+
320
+ inputEl.addEventListener('keydown', (e) => {
321
+ if (e.key === 'Escape') hide();
322
+ });
323
+
324
+ // Close when clicking outside (once per menu)
325
+ document.addEventListener('click', (e) => {
326
+ try {
327
+ if (!anchor.contains(e.target)) hide();
328
+ } catch (err) {}
329
+ });
330
+ }
331
+
190
332
  function formToObject(form) {
191
333
  const out = {};
192
334
  new FormData(form).forEach((v, k) => {
@@ -698,6 +840,19 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
698
840
  } catch (e) {}
699
841
  });
700
842
  }
843
+
844
+ // Autocomplete like on the calendar validation modal
845
+ const addrInput = document.getElementById('onekite-pickup-address');
846
+ attachAddressAutocomplete(addrInput, (h) => {
847
+ try {
848
+ if (!h || !Number.isFinite(Number(h.lat)) || !Number.isFinite(Number(h.lon))) return;
849
+ const lat = Number(h.lat);
850
+ const lon = Number(h.lon);
851
+ marker.setLatLng([lat, lon]);
852
+ map.setView([lat, lon], 16);
853
+ setLatLon(lat, lon);
854
+ } catch (e) {}
855
+ });
701
856
  setTimeout(() => { try { map.invalidateSize(); } catch (e) {} }, 250);
702
857
  } catch (e) {}
703
858
  });
@@ -887,6 +1042,14 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
887
1042
  }
888
1043
  });
889
1044
  }
1045
+
1046
+ // Autocomplete like on the calendar validation modal
1047
+ attachAddressAutocomplete(addrInput, (h) => {
1048
+ try {
1049
+ if (!h || !Number.isFinite(Number(h.lat)) || !Number.isFinite(Number(h.lon))) return;
1050
+ setMarker(Number(h.lat), Number(h.lon), 16);
1051
+ } catch (e) {}
1052
+ });
890
1053
  } catch (e) {
891
1054
  // ignore
892
1055
  }