nodebb-plugin-onekite-calendar 2.0.73 → 2.0.75

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
@@ -778,7 +778,7 @@ api.getCapabilities = async function (req, res) {
778
778
  isValidator: canMod,
779
779
  canCreateSpecial: canSpecialC,
780
780
  canDeleteSpecial: canSpecialD,
781
- canCreateOuting: canReq,
781
+ canCreateOuting: canMod || canReq,
782
782
  canCreateReservation: canReq,
783
783
  specialEventCategoryCid: parseInt(settings && settings.specialEventCategoryId, 10) || 0,
784
784
  });
@@ -938,7 +938,7 @@ api.getOutingDetails = async function (req, res) {
938
938
  });
939
939
 
940
940
  const participants = normalizeUidList(o.participants);
941
- const canEditOuting = uid ? await canRequest(uid, settings, Date.now()) : false;
941
+ const canEditOuting = uid ? (canMod || await canRequest(uid, settings, Date.now())) : false;
942
942
  const out = {
943
943
  oid: o.oid,
944
944
  title: o.title || '',
@@ -967,8 +967,7 @@ api.joinOuting = async function (req, res) {
967
967
  const settings = await getSettings();
968
968
  const uid = req.uid;
969
969
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
970
- // Outings share the same rights as reservations/locations.
971
- const ok = await canRequest(uid, settings, Date.now());
970
+ const ok = (await canValidate(uid, settings)) || (await canRequest(uid, settings, Date.now()));
972
971
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
973
972
 
974
973
  const oid = String(req.params.oid || '').replace(/^outing:/, '').trim();
@@ -999,8 +998,7 @@ api.leaveOuting = async function (req, res) {
999
998
  const settings = await getSettings();
1000
999
  const uid = req.uid;
1001
1000
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
1002
- // Outings share the same rights as reservations/locations.
1003
- const ok = await canRequest(uid, settings, Date.now());
1001
+ const ok = (await canValidate(uid, settings)) || (await canRequest(uid, settings, Date.now()));
1004
1002
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
1005
1003
 
1006
1004
  const oid = String(req.params.oid || '').replace(/^outing:/, '').trim();
@@ -1032,12 +1030,9 @@ api.createOuting = async function (req, res) {
1032
1030
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
1033
1031
 
1034
1032
  const startTs = toTs(req.body && req.body.start);
1035
- // Permissions for outings must match reservations/locations rights.
1036
- // We intentionally base the "auto" yearly group on the *current* year,
1037
- // not on the outing date, so members can plan future outings without
1038
- // requiring next-year group membership.
1039
- const ok = await canRequest(req.uid, settings, Date.now());
1040
- if (!ok) {
1033
+ const isValidatorForOuting = await canValidate(req.uid, settings);
1034
+ const canMakeOuting = isValidatorForOuting || (await canRequest(req.uid, settings, Date.now()));
1035
+ if (!canMakeOuting) {
1041
1036
  return res.status(403).json({
1042
1037
  error: 'not-allowed',
1043
1038
  code: 'NOT_MEMBER',
@@ -1051,18 +1046,20 @@ api.createOuting = async function (req, res) {
1051
1046
  return res.status(400).json({ error: 'bad-dates' });
1052
1047
  }
1053
1048
 
1054
- // Business rule: nothing can be created in the past.
1055
- try {
1056
- const today0 = new Date();
1057
- today0.setHours(0, 0, 0, 0);
1058
- const today0ts = today0.getTime();
1059
- if (startTs < today0ts) {
1060
- return res.status(400).json({
1061
- error: 'date-too-soon',
1062
- message: "Impossible de créer pour une date passée.",
1063
- });
1064
- }
1065
- } catch (e) {}
1049
+ // Validators can create outings in the past (regularization); others cannot.
1050
+ if (!isValidatorForOuting) {
1051
+ try {
1052
+ const today0 = new Date();
1053
+ today0.setHours(0, 0, 0, 0);
1054
+ const today0ts = today0.getTime();
1055
+ if (startTs < today0ts) {
1056
+ return res.status(400).json({
1057
+ error: 'date-too-soon',
1058
+ message: "Impossible de créer pour une date passée.",
1059
+ });
1060
+ }
1061
+ } catch (e) {}
1062
+ }
1066
1063
 
1067
1064
  const address = String((req.body && req.body.address) || '').trim();
1068
1065
  const notes = String((req.body && req.body.notes) || '').trim();
@@ -1122,7 +1119,7 @@ api.deleteOuting = async function (req, res) {
1122
1119
  api.updateOuting = async function (req, res) {
1123
1120
  const settings = await getSettings();
1124
1121
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
1125
- const ok = await canRequest(req.uid, settings, Date.now());
1122
+ const ok = (await canValidate(req.uid, settings)) || (await canRequest(req.uid, settings, Date.now()));
1126
1123
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
1127
1124
 
1128
1125
  const oid = String(req.params.oid || '').replace(/^outing:/, '').trim();
@@ -1465,6 +1462,22 @@ api.createReservation = async function (req, res) {
1465
1462
  res.json({ ok: true, rid, status: resv.status, autoPaid: !!(isValidatorFree || isRegularization) });
1466
1463
  };
1467
1464
 
1465
+ api.searchUsers = async function (req, res) {
1466
+ const uid = req.uid;
1467
+ if (!uid) return res.status(401).json({ error: 'not-logged-in' });
1468
+ const settings = await getSettings();
1469
+ if (!(await canValidate(uid, settings))) return res.status(403).json({ error: 'not-allowed' });
1470
+ const q = String(req.query.q || '').trim();
1471
+ if (q.length < 2) return res.json([]);
1472
+ try {
1473
+ const result = await user.search({ query: q, searchBy: 'username', resultsPerPage: 8, uid });
1474
+ const list = (result && result.users) ? result.users : [];
1475
+ return res.json(list.map(u => ({ uid: u.uid, username: u.username })));
1476
+ } catch (e) {
1477
+ return res.json([]);
1478
+ }
1479
+ };
1480
+
1468
1481
  // Validator actions (from calendar popup)
1469
1482
  api.approveReservation = async function (req, res) {
1470
1483
  const uid = req.uid;
package/library.js CHANGED
@@ -65,6 +65,7 @@ Plugin.init = async function (params) {
65
65
  router.get('/api/v3/plugins/calendar-onekite/events', ...publicExpose, api.getEvents);
66
66
  router.get('/api/v3/plugins/calendar-onekite/items', ...publicExpose, api.getItems);
67
67
  router.get('/api/v3/plugins/calendar-onekite/capabilities', ...publicExpose, api.getCapabilities);
68
+ router.get('/api/v3/plugins/calendar-onekite/users/search', ...publicExpose, api.searchUsers);
68
69
 
69
70
  // Maintenance / audit (restricted to validator groups)
70
71
  router.get('/api/v3/plugins/calendar-onekite/maintenance', ...publicExpose, api.getMaintenance);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.73",
3
+ "version": "2.0.75",
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
@@ -821,6 +821,107 @@ async function searchAddresses(query, limit) {
821
821
  }).filter(h => h && h.displayName);
822
822
  }
823
823
 
824
+ async function searchUsers(q, limit) {
825
+ try {
826
+ const qs = new URLSearchParams({ q: String(q || ''), limit: String(limit || 8) });
827
+ const arr = await fetchJson(`/api/v3/plugins/calendar-onekite/users/search?${qs}`);
828
+ if (!Array.isArray(arr)) return [];
829
+ return arr;
830
+ } catch (e) {
831
+ return [];
832
+ }
833
+ }
834
+
835
+ function attachUserAutocomplete(inputEl) {
836
+ if (!inputEl) return;
837
+ if (inputEl.getAttribute('data-onekite-user-ac') === '1') return;
838
+ inputEl.setAttribute('data-onekite-user-ac', '1');
839
+
840
+ const host = inputEl.parentNode || document.body;
841
+ try {
842
+ const cs = window.getComputedStyle(host);
843
+ if (!cs || cs.position === 'static') host.style.position = 'relative';
844
+ } catch (e) {
845
+ host.style.position = 'relative';
846
+ }
847
+
848
+ const menu = document.createElement('div');
849
+ menu.className = 'onekite-autocomplete-menu';
850
+ menu.style.position = 'absolute';
851
+ menu.style.left = '0';
852
+ menu.style.right = '0';
853
+ menu.style.top = '100%';
854
+ menu.style.zIndex = '2000';
855
+ menu.style.background = '#fff';
856
+ menu.style.border = '1px solid rgba(0,0,0,.15)';
857
+ menu.style.borderTop = '0';
858
+ menu.style.maxHeight = '220px';
859
+ menu.style.overflowY = 'auto';
860
+ menu.style.display = 'none';
861
+ menu.style.borderRadius = '0 0 .375rem .375rem';
862
+ host.appendChild(menu);
863
+
864
+ let timer = null;
865
+ let busy = false;
866
+
867
+ function hide() { menu.style.display = 'none'; menu.innerHTML = ''; }
868
+
869
+ function show(hits) {
870
+ if (!hits || !hits.length) { hide(); return; }
871
+ menu.innerHTML = '';
872
+ hits.forEach((h) => {
873
+ const btn = document.createElement('button');
874
+ btn.type = 'button';
875
+ btn.className = 'onekite-autocomplete-item';
876
+ btn.textContent = h.username;
877
+ btn.style.display = 'block';
878
+ btn.style.width = '100%';
879
+ btn.style.textAlign = 'left';
880
+ btn.style.padding = '.35rem .5rem';
881
+ btn.style.border = '0';
882
+ btn.style.background = 'transparent';
883
+ btn.style.cursor = 'pointer';
884
+ btn.addEventListener('click', () => { inputEl.value = h.username; hide(); });
885
+ btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(0,0,0,.05)'; });
886
+ btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent'; });
887
+ menu.appendChild(btn);
888
+ });
889
+ menu.style.display = 'block';
890
+ }
891
+
892
+ async function run(q) {
893
+ if (busy) return;
894
+ busy = true;
895
+ try {
896
+ const hits = await searchUsers(q, 8);
897
+ if (String(inputEl.value || '').trim() !== q) return;
898
+ show(hits);
899
+ } catch (e) {
900
+ hide();
901
+ } finally {
902
+ busy = false;
903
+ }
904
+ }
905
+
906
+ inputEl.addEventListener('input', () => {
907
+ const q = String(inputEl.value || '').trim();
908
+ if (timer) clearTimeout(timer);
909
+ if (q.length < 2) { hide(); return; }
910
+ timer = setTimeout(() => run(q), 250);
911
+ });
912
+
913
+ inputEl.addEventListener('focus', () => {
914
+ const q = String(inputEl.value || '').trim();
915
+ if (q.length >= 2) { if (timer) clearTimeout(timer); timer = setTimeout(() => run(q), 150); }
916
+ });
917
+
918
+ document.addEventListener('click', (e) => {
919
+ try { if (!host.contains(e.target)) hide(); } catch (err) {}
920
+ });
921
+
922
+ inputEl.addEventListener('keydown', (e) => { if (e.key === 'Escape') hide(); });
923
+ }
924
+
824
925
  function attachAddressAutocomplete(inputEl, onPick) {
825
926
  if (!inputEl) return;
826
927
  // Avoid double attach
@@ -1187,6 +1288,10 @@ function toDatetimeLocalValue(date) {
1187
1288
 
1188
1289
  // live total update
1189
1290
  setTimeout(() => {
1291
+ if (isValidatorMode) {
1292
+ attachUserAutocomplete(document.getElementById('onekite-target-username'));
1293
+ }
1294
+
1190
1295
  const totalEl = document.getElementById('onekite-total');
1191
1296
  const periodEl = document.getElementById('onekite-period');
1192
1297
  const daysEl = document.getElementById('onekite-days');