nodebb-plugin-onekite-calendar 2.0.84 → 2.0.85

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
@@ -362,6 +362,20 @@ admin.purgeSpecialEventsByYear = async function (req, res) {
362
362
  return res.json({ ok: true, removed: count });
363
363
  };
364
364
 
365
+ admin.listGroups = async function (req, res) {
366
+ try {
367
+ const Groups = require.main.require('./src/groups');
368
+ const q = String((req.query && req.query.q) || '').trim().toLowerCase();
369
+ const names = await Groups.getGroups('groups:createtime', 0, 500);
370
+ const filtered = q
371
+ ? (names || []).filter(n => n && String(n).toLowerCase().includes(q))
372
+ : (names || []).filter(Boolean);
373
+ res.json(filtered.slice(0, 100));
374
+ } catch (e) {
375
+ res.json([]);
376
+ }
377
+ };
378
+
365
379
  admin.purgeOutingsByYear = async function (req, res) {
366
380
  const year = (req.body && req.body.year ? String(req.body.year) : '').trim();
367
381
  if (!/^\d{4}$/.test(year)) {
@@ -707,12 +721,6 @@ admin.getAccounting = async function (req, res) {
707
721
  };
708
722
 
709
723
  admin.exportAccountingCsv = async function (req, res) {
710
- // Reuse the same logic and emit a CSV.
711
- const fakeRes = { json: (x) => x };
712
- const data = await admin.getAccounting(req, fakeRes);
713
- // If getAccounting returned via res.json, data is undefined; rebuild by calling logic directly.
714
- // Easiest: call getAccounting's internals by fetching the endpoint logic via HTTP is not possible here.
715
- // So we re-run getAccounting but capture output by monkeypatching.
716
724
  let payload;
717
725
  await admin.getAccounting(req, { json: (x) => { payload = x; return x; } });
718
726
  if (!payload || !payload.ok) {
package/library.js CHANGED
@@ -107,6 +107,7 @@ Plugin.init = async function (params) {
107
107
  router.get(`${adminBase}/settings`, ...adminMws, admin.getSettings);
108
108
  router.put(`${adminBase}/settings`, ...adminMws, admin.saveSettings);
109
109
 
110
+ router.get(`${adminBase}/groups`, ...adminMws, admin.listGroups);
110
111
  router.get(`${adminBase}/pending`, ...adminMws, admin.listPending);
111
112
  router.put(`${adminBase}/reservations/:rid/approve`, ...adminMws, admin.approveReservation);
112
113
  router.put(`${adminBase}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.84",
3
+ "version": "2.0.85",
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/admin.js CHANGED
@@ -345,6 +345,129 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
345
345
  });
346
346
  }
347
347
 
348
+ // Cached list of NodeBB groups (fetched once per ACP session)
349
+ let groupsCache = null;
350
+
351
+ async function loadGroups() {
352
+ if (groupsCache) return groupsCache;
353
+ try {
354
+ const data = await fetchJson('/api/v3/admin/plugins/calendar-onekite/groups');
355
+ groupsCache = Array.isArray(data) ? data : [];
356
+ } catch (e) {
357
+ groupsCache = [];
358
+ }
359
+ return groupsCache;
360
+ }
361
+
362
+ // Autocomplete for CSV group inputs (creatorGroups, validatorGroups, etc.)
363
+ // Reuses the same .onekite-autocomplete-* CSS as address autocomplete.
364
+ function attachGroupCsvAutocomplete(inputEl) {
365
+ if (!inputEl || inputEl.getAttribute('data-onekite-group-ac') === '1') return;
366
+ inputEl.setAttribute('data-onekite-group-ac', '1');
367
+
368
+ const anchor = (inputEl.closest && inputEl.closest('.input-group'))
369
+ ? inputEl.closest('.input-group')
370
+ : (inputEl.parentNode || document.body);
371
+
372
+ try {
373
+ const cs = window.getComputedStyle(anchor);
374
+ if (!cs || cs.position === 'static') anchor.style.position = 'relative';
375
+ } catch (e) {
376
+ anchor.style.position = 'relative';
377
+ }
378
+
379
+ const menu = document.createElement('div');
380
+ menu.className = 'onekite-autocomplete-menu';
381
+ menu.style.position = 'absolute';
382
+ menu.style.left = '0';
383
+ menu.style.right = '0';
384
+ menu.style.top = '100%';
385
+ menu.style.zIndex = '2000';
386
+ menu.style.borderTop = '0';
387
+ menu.style.maxHeight = '220px';
388
+ menu.style.overflowY = 'auto';
389
+ menu.style.display = 'none';
390
+ menu.style.borderRadius = '0 0 .375rem .375rem';
391
+ anchor.appendChild(menu);
392
+
393
+ function hide() {
394
+ menu.style.display = 'none';
395
+ menu.innerHTML = '';
396
+ }
397
+
398
+ function getCurrentToken() {
399
+ const val = inputEl.value;
400
+ const cursor = typeof inputEl.selectionStart === 'number' ? inputEl.selectionStart : val.length;
401
+ const before = val.slice(0, cursor);
402
+ const commaIdx = before.lastIndexOf(',');
403
+ return before.slice(commaIdx + 1).trimStart();
404
+ }
405
+
406
+ function replaceCurrentToken(replacement) {
407
+ const val = inputEl.value;
408
+ const cursor = typeof inputEl.selectionStart === 'number' ? inputEl.selectionStart : val.length;
409
+ const before = val.slice(0, cursor);
410
+ const after = val.slice(cursor);
411
+ const commaIdx = before.lastIndexOf(',');
412
+ const prefix = commaIdx >= 0 ? before.slice(0, commaIdx + 1) + ' ' : '';
413
+ const rest = after.replace(/^[\s,]+/, '');
414
+ inputEl.value = prefix + replacement + (rest ? ', ' + rest : '');
415
+ const newPos = prefix.length + replacement.length;
416
+ try { inputEl.setSelectionRange(newPos, newPos); } catch (e) {}
417
+ }
418
+
419
+ async function showSuggestions() {
420
+ const token = getCurrentToken();
421
+ if (token.length < 1) { hide(); return; }
422
+ const groups = await loadGroups();
423
+ const q = token.toLowerCase();
424
+ const existing = new Set(
425
+ inputEl.value.split(',').map(s => s.trim().toLowerCase()).filter(Boolean)
426
+ );
427
+ const matches = groups
428
+ .filter(g => String(g).toLowerCase().includes(q) && !existing.has(String(g).toLowerCase()))
429
+ .slice(0, 8);
430
+ if (!matches.length) { hide(); return; }
431
+ menu.innerHTML = '';
432
+ matches.forEach(g => {
433
+ const btn = document.createElement('button');
434
+ btn.type = 'button';
435
+ btn.className = 'onekite-autocomplete-item';
436
+ btn.textContent = g;
437
+ btn.style.display = 'block';
438
+ btn.style.width = '100%';
439
+ btn.style.textAlign = 'left';
440
+ btn.style.padding = '.35rem .5rem';
441
+ btn.style.border = '0';
442
+ btn.style.cursor = 'pointer';
443
+ btn.addEventListener('mousedown', (e) => {
444
+ e.preventDefault();
445
+ replaceCurrentToken(g);
446
+ hide();
447
+ inputEl.focus();
448
+ });
449
+ menu.appendChild(btn);
450
+ });
451
+ menu.style.display = 'block';
452
+ }
453
+
454
+ let timer = null;
455
+ inputEl.addEventListener('input', () => {
456
+ if (timer) clearTimeout(timer);
457
+ timer = setTimeout(showSuggestions, 150);
458
+ });
459
+ inputEl.addEventListener('click', () => {
460
+ if (timer) clearTimeout(timer);
461
+ timer = setTimeout(showSuggestions, 100);
462
+ });
463
+ inputEl.addEventListener('keydown', (e) => {
464
+ if (e.key === 'Escape') hide();
465
+ });
466
+ document.addEventListener('click', (e) => {
467
+ try { if (!anchor.contains(e.target)) hide(); } catch (err) {}
468
+ });
469
+ }
470
+
348
471
  function formToObject(form) {
349
472
  const out = {};
350
473
  new FormData(form).forEach((v, k) => {
@@ -546,31 +669,17 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
546
669
  }
547
670
 
548
671
  async function purgeSpecialEvents(year) {
549
- try {
550
- return await fetchJson('/api/v3/admin/plugins/calendar-onekite/special-events/purge', {
551
- method: 'POST',
552
- body: JSON.stringify({ year }),
553
- });
554
- } catch (e) {
555
- return await fetchJson('/api/v3/admin/plugins/calendar-onekite/special-events/purge', {
556
- method: 'POST',
557
- body: JSON.stringify({ year }),
558
- });
559
- }
672
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/special-events/purge', {
673
+ method: 'POST',
674
+ body: JSON.stringify({ year }),
675
+ });
560
676
  }
561
677
 
562
678
  async function purgeOutings(year) {
563
- try {
564
- return await fetchJson('/api/v3/admin/plugins/calendar-onekite/outings/purge', {
565
- method: 'POST',
566
- body: JSON.stringify({ year }),
567
- });
568
- } catch (e) {
569
- return await fetchJson('/api/v3/admin/plugins/calendar-onekite/outings/purge', {
570
- method: 'POST',
571
- body: JSON.stringify({ year }),
572
- });
573
- }
679
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/outings/purge', {
680
+ method: 'POST',
681
+ body: JSON.stringify({ year }),
682
+ });
574
683
  }
575
684
 
576
685
  async function markPaid(rid, payload) {
@@ -581,11 +690,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
581
690
  }
582
691
 
583
692
  async function debugHelloAsso() {
584
- try {
585
- return await fetchJson('/api/v3/admin/plugins/calendar-onekite/debug');
586
- } catch (e) {
587
- return await fetchJson('/api/v3/admin/plugins/calendar-onekite/debug');
588
- }
693
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/debug');
589
694
  }
590
695
 
591
696
  async function loadAccounting(from, to) {
@@ -617,6 +722,12 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
617
722
  document.head.appendChild(style);
618
723
  })();
619
724
 
725
+ // Attach group autocomplete to all CSV group fields (loads groups list lazily on first keystroke)
726
+ ['creatorGroups', 'validatorGroups', 'notifyGroups', 'specialCreatorGroups', 'specialDeleterGroups'].forEach(name => {
727
+ const el = form.querySelector(`[name="${name}"]`);
728
+ if (el) attachGroupCsvAutocomplete(el);
729
+ });
730
+
620
731
  // Load settings
621
732
  try {
622
733
  const s = await loadSettings();