nodebb-plugin-onekite-calendar 2.0.52 → 2.0.53

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
@@ -4,6 +4,7 @@ const meta = require.main.require('./src/meta');
4
4
  const user = require.main.require('./src/user');
5
5
  const emailer = require.main.require('./src/emailer');
6
6
  const { forumBaseUrl, formatFR } = require('./utils');
7
+ const { getSettings, invalidateSettings } = require('./settings');
7
8
  const realtime = require('./realtime');
8
9
  const crypto = require('crypto');
9
10
  const nconf = require.main.require('nconf');
@@ -108,12 +109,13 @@ admin.renderAdmin = async function (req, res) {
108
109
  };
109
110
 
110
111
  admin.getSettings = async function (req, res) {
111
- const settings = await meta.settings.get('calendar-onekite');
112
+ const settings = await getSettings();
112
113
  res.json(settings || {});
113
114
  };
114
115
 
115
116
  admin.saveSettings = async function (req, res) {
116
117
  await meta.settings.set('calendar-onekite', req.body || {});
118
+ invalidateSettings();
117
119
  res.json({ ok: true });
118
120
  };
119
121
 
@@ -173,7 +175,7 @@ admin.approveReservation = async function (req, res) {
173
175
  }
174
176
 
175
177
  // Create HelloAsso payment link if configured
176
- const settings = await meta.settings.get('calendar-onekite');
178
+ const settings = await getSettings();
177
179
  const env = settings.helloassoEnv || 'prod';
178
180
  const token = await helloasso.getAccessToken({
179
181
  env,
@@ -378,7 +380,7 @@ admin.purgeOutingsByYear = async function (req, res) {
378
380
 
379
381
 
380
382
  admin.debugHelloAsso = async function (req, res) {
381
- const settings = await meta.settings.get('calendar-onekite');
383
+ const settings = await getSettings();
382
384
  const env = (settings && settings.helloassoEnv) || 'prod';
383
385
 
384
386
  // Never expose secrets in debug output
@@ -531,7 +533,7 @@ admin.getAccounting = async function (req, res) {
531
533
  if (!Number.isFinite(y)) return null;
532
534
  if (catalogByYear.has(y)) return catalogByYear.get(y);
533
535
  try {
534
- if (!settingsCache) settingsCache = await meta.settings.get('calendar-onekite');
536
+ if (!settingsCache) settingsCache = await getSettings();
535
537
  if (!settingsCache || !settingsCache.helloassoClientId || !settingsCache.helloassoClientSecret || !settingsCache.helloassoOrganizationSlug || !settingsCache.helloassoFormType) {
536
538
  catalogByYear.set(y, null);
537
539
  return null;
package/lib/api.js CHANGED
@@ -11,7 +11,7 @@ const db = require.main.require('./src/database');
11
11
  const logger = require.main.require('./src/logger');
12
12
 
13
13
  const dbLayer = require('./db');
14
- const { formatFR } = require('./utils');
14
+ const { getSettings } = require('./settings');
15
15
 
16
16
  // Resolve group identifiers from ACP.
17
17
  // Admins may enter a group *name* ("Test") or a *slug* ("onekite-ffvl-2026").
@@ -331,7 +331,7 @@ async function canValidate(uid, settings) {
331
331
  }
332
332
 
333
333
  async function canModerate(uid) {
334
- const settings = await meta.settings.get('calendar-onekite');
334
+ const settings = await getSettings();
335
335
  return await canValidate(uid, settings);
336
336
  }
337
337
 
@@ -634,7 +634,7 @@ api.getEvents = async function (req, res) {
634
634
  const startTs = toTs(qStartRaw) || 0;
635
635
  const endTs = toTs(qEndRaw) || (Date.now() + 365 * 24 * 3600 * 1000);
636
636
 
637
- const settings = await meta.settings.get('calendar-onekite');
637
+ const settings = await getSettings();
638
638
  const canMod = req.uid ? await canValidate(req.uid, settings) : false;
639
639
  const widgetMode = String((req.query && req.query.widget) || '') === '1';
640
640
  const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings) : false;
@@ -826,7 +826,7 @@ api.getReservationDetails = async function (req, res) {
826
826
  const uid = req.uid;
827
827
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
828
828
 
829
- const settings = await meta.settings.get('calendar-onekite');
829
+ const settings = await getSettings();
830
830
  const canMod = await canValidate(uid, settings);
831
831
 
832
832
  const rid = String(req.params.rid || '').trim();
@@ -873,7 +873,7 @@ api.getSpecialEventDetails = async function (req, res) {
873
873
  const uid = req.uid;
874
874
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
875
875
 
876
- const settings = await meta.settings.get('calendar-onekite');
876
+ const settings = await getSettings();
877
877
  const canMod = await canValidate(uid, settings);
878
878
  const canSpecialDelete = await canDeleteSpecial(uid, settings);
879
879
 
@@ -917,7 +917,7 @@ api.getSpecialEventDetails = async function (req, res) {
917
917
  };
918
918
 
919
919
  api.getCapabilities = async function (req, res) {
920
- const settings = await meta.settings.get('calendar-onekite');
920
+ const settings = await getSettings();
921
921
  const uid = req.uid || 0;
922
922
  const canMod = uid ? await canValidate(uid, settings) : false;
923
923
  res.json({
@@ -931,7 +931,7 @@ api.getCapabilities = async function (req, res) {
931
931
  };
932
932
 
933
933
  api.createSpecialEvent = async function (req, res) {
934
- const settings = await meta.settings.get('calendar-onekite');
934
+ const settings = await getSettings();
935
935
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
936
936
  const ok = await canCreateSpecial(req.uid, settings);
937
937
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
@@ -987,7 +987,7 @@ api.createSpecialEvent = async function (req, res) {
987
987
  };
988
988
 
989
989
  api.deleteSpecialEvent = async function (req, res) {
990
- const settings = await meta.settings.get('calendar-onekite');
990
+ const settings = await getSettings();
991
991
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
992
992
  const ok = await canDeleteSpecial(req.uid, settings);
993
993
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
@@ -1007,7 +1007,7 @@ api.getOutingDetails = async function (req, res) {
1007
1007
  const uid = req.uid;
1008
1008
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
1009
1009
 
1010
- const settings = await meta.settings.get('calendar-onekite');
1010
+ const settings = await getSettings();
1011
1011
  const canMod = await canValidate(uid, settings);
1012
1012
 
1013
1013
  const oid = String(req.params.oid || '').trim();
@@ -1048,7 +1048,7 @@ api.getOutingDetails = async function (req, res) {
1048
1048
  };
1049
1049
 
1050
1050
  api.createOuting = async function (req, res) {
1051
- const settings = await meta.settings.get('calendar-onekite');
1051
+ const settings = await getSettings();
1052
1052
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
1053
1053
 
1054
1054
  const startTs = toTs(req.body && req.body.start);
@@ -1115,7 +1115,7 @@ api.createOuting = async function (req, res) {
1115
1115
  };
1116
1116
 
1117
1117
  api.deleteOuting = async function (req, res) {
1118
- const settings = await meta.settings.get('calendar-onekite');
1118
+ const settings = await getSettings();
1119
1119
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
1120
1120
 
1121
1121
  const oid = String(req.params.oid || '').replace(/^outing:/, '').trim();
@@ -1139,7 +1139,7 @@ api.deleteOuting = async function (req, res) {
1139
1139
  };
1140
1140
 
1141
1141
  api.getItems = async function (req, res) {
1142
- const settings = await meta.settings.get('calendar-onekite');
1142
+ const settings = await getSettings();
1143
1143
 
1144
1144
  const env = settings.helloassoEnv || 'prod';
1145
1145
  const token = await helloasso.getAccessToken({
@@ -1191,7 +1191,7 @@ api.createReservation = async function (req, res) {
1191
1191
  const uid = req.uid;
1192
1192
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
1193
1193
 
1194
- const settings = await meta.settings.get('calendar-onekite');
1194
+ const settings = await getSettings();
1195
1195
  const startPreview = toTs(req.body.start);
1196
1196
  const ok = await canRequest(uid, settings, startPreview);
1197
1197
  if (!ok) {
@@ -1438,7 +1438,7 @@ api.createReservation = async function (req, res) {
1438
1438
  api.approveReservation = async function (req, res) {
1439
1439
  const uid = req.uid;
1440
1440
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
1441
- const settings = await meta.settings.get('calendar-onekite');
1441
+ const settings = await getSettings();
1442
1442
  const ok = await canValidate(uid, settings);
1443
1443
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
1444
1444
 
@@ -1471,7 +1471,7 @@ api.approveReservation = async function (req, res) {
1471
1471
  }
1472
1472
  // Create HelloAsso payment link on validation
1473
1473
  try {
1474
- const settings2 = await meta.settings.get('calendar-onekite');
1474
+ const settings2 = await getSettings();
1475
1475
  const token = await helloasso.getAccessToken({ env: settings2.helloassoEnv || 'prod', clientId: settings2.helloassoClientId, clientSecret: settings2.helloassoClientSecret });
1476
1476
  const payer = await user.getUserFields(r.uid, ['email']);
1477
1477
  const year = yearFromTs(r.start);
@@ -1619,7 +1619,7 @@ api.approveReservation = async function (req, res) {
1619
1619
  api.refuseReservation = async function (req, res) {
1620
1620
  const uid = req.uid;
1621
1621
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
1622
- const settings = await meta.settings.get('calendar-onekite');
1622
+ const settings = await getSettings();
1623
1623
  const ok = await canValidate(uid, settings);
1624
1624
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
1625
1625
 
@@ -1679,7 +1679,7 @@ api.cancelReservation = async function (req, res) {
1679
1679
  const uid = req.uid;
1680
1680
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
1681
1681
 
1682
- const settings = await meta.settings.get('calendar-onekite');
1682
+ const settings = await getSettings();
1683
1683
  const rid = String(req.params.rid || '').trim();
1684
1684
  if (!rid) return res.status(400).json({ error: 'missing-rid' });
1685
1685
 
@@ -1806,7 +1806,7 @@ api.setMaintenanceAll = async function (req, res) {
1806
1806
  // When enabling, we need the current catalog IDs (HelloAsso shop)
1807
1807
  let catalogIds = [];
1808
1808
  if (enabled) {
1809
- const settings = await meta.settings.get('calendar-onekite');
1809
+ const settings = await getSettings();
1810
1810
  const env = settings.helloassoEnv || 'prod';
1811
1811
  const token = await helloasso.getAccessToken({
1812
1812
  env,
package/lib/scheduler.js CHANGED
@@ -8,6 +8,7 @@ const realtime = require('./realtime');
8
8
  const nconf = require.main.require('nconf');
9
9
  const groups = require.main.require('./src/groups');
10
10
  const utils = require('./utils');
11
+ const { getSettings } = require('./settings');
11
12
 
12
13
  let timer = null;
13
14
 
@@ -138,7 +139,7 @@ async function getNotifiedValidatorUids(settings) {
138
139
 
139
140
  // Pending holds: short lock after a user creates a request (defaults to 5 minutes)
140
141
  async function expirePending() {
141
- const settings = await meta.settings.get('calendar-onekite');
142
+ const settings = await getSettings();
142
143
  const holdMins = parseInt(getSetting(settings, 'pendingHoldMinutes', '5'), 10) || 5;
143
144
  const validatorReminderMins = parseInt(getSetting(settings, 'validatorReminderMinutesPending', '0'), 10) || 0;
144
145
  const now = Date.now();
@@ -257,7 +258,7 @@ async function expirePending() {
257
258
  // - We send a reminder after `paymentHoldMinutes` (default 60)
258
259
  // - We expire (and remove) after `2 * paymentHoldMinutes`
259
260
  async function processAwaitingPayment() {
260
- const settings = await meta.settings.get('calendar-onekite');
261
+ const settings = await getSettings();
261
262
  const holdMins = parseInt(
262
263
  getSetting(settings, 'paymentHoldMinutes', getSetting(settings, 'holdMinutes', '60')),
263
264
  10
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const meta = require.main.require('./src/meta');
4
+
5
+ const SETTINGS_KEY = 'calendar-onekite';
6
+
7
+ // Simple in-memory cache to reduce repeated meta.settings.get calls.
8
+ // Per-process only (NodeBB is often multi-process), but still beneficial.
9
+ const CACHE_TTL_MS = 5000;
10
+ let cachedSettings = null;
11
+ let cachedAtMs = 0;
12
+
13
+ async function getSettings(options) {
14
+ const opts = options || {};
15
+ const force = !!opts.force;
16
+ const now = Date.now();
17
+
18
+ if (!force && cachedSettings && (now - cachedAtMs) < CACHE_TTL_MS) {
19
+ return cachedSettings;
20
+ }
21
+
22
+ cachedSettings = await meta.settings.get(SETTINGS_KEY);
23
+ cachedAtMs = now;
24
+ return cachedSettings || {};
25
+ }
26
+
27
+ function invalidateSettings() {
28
+ cachedSettings = null;
29
+ cachedAtMs = 0;
30
+ }
31
+
32
+ module.exports = {
33
+ SETTINGS_KEY,
34
+ getSettings,
35
+ invalidateSettings,
36
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.52",
3
+ "version": "2.0.53",
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.52"
42
+ "version": "2.0.53"
43
43
  }
package/public/client.js CHANGED
@@ -1045,10 +1045,6 @@ function toDatetimeLocalValue(date) {
1045
1045
  const endDisplay = endInclusiveForDisplay(start, end);
1046
1046
 
1047
1047
  const messageHtml = `
1048
- <div class="mb-2">
1049
- <label class="form-label">Titre (facultatif)</label>
1050
- <input type="text" class="form-control" id="onekite-res-title" placeholder="Ex: ..." />
1051
- </div>
1052
1048
  <div class="mb-2" id="onekite-period"><strong>Période</strong><br>${formatDt(start)} → ${formatDt(endDisplay)} <span class="text-muted" id="onekite-days">(${days} jour${days > 1 ? 's' : ''})</span></div>
1053
1049
  ${shortcutsHtml}
1054
1050
  <div class="mb-2"><strong>Matériel</strong></div>
@@ -1084,7 +1080,6 @@ function toDatetimeLocalValue(date) {
1084
1080
  label: 'Envoyer',
1085
1081
  className: 'btn-primary',
1086
1082
  callback: function () {
1087
- const title = (document.getElementById('onekite-res-title')?.value || '').trim();
1088
1083
  const cbs = Array.from(document.querySelectorAll('.onekite-item-cb')).filter(cb => cb.checked);
1089
1084
  if (!cbs.length) {
1090
1085
  showAlert('error', 'Choisis au moins un matériel.');
@@ -1096,7 +1091,7 @@ function toDatetimeLocalValue(date) {
1096
1091
  const total = (sum / 100) * days;
1097
1092
  // Return the effective end date (exclusive) because duration shortcuts can
1098
1093
  // change the range without updating the original FullCalendar selection.
1099
- resolve({ title, itemIds, itemNames, total, days, endDate: toLocalYmd(end) });
1094
+ resolve({ itemIds, itemNames, total, days, endDate: toLocalYmd(end) });
1100
1095
  },
1101
1096
  },
1102
1097
  },
@@ -1561,8 +1556,8 @@ function toDatetimeLocalValue(date) {
1561
1556
 
1562
1557
  // IMPORTANT: align "special" events and "outings" display exactly like
1563
1558
  // reservation icons.
1564
- // We inject the clock + time range directly into the event title so FC
1565
- // doesn't reserve a separate time column (which creates a leading gap).
1559
+ // We inject the time range directly into the event title so FC doesn't
1560
+ // reserve a separate time column (which creates a leading gap).
1566
1561
  const mapped = (Array.isArray(data) ? data : []).map((ev) => {
1567
1562
  try {
1568
1563
  if (ev && ev.extendedProps && (ev.extendedProps.type === 'special' || ev.extendedProps.type === 'outing') && ev.start && ev.end) {
@@ -1575,10 +1570,18 @@ function toDatetimeLocalValue(date) {
1575
1570
  const ts = `${pad2(s.getHours())}:${pad2(s.getMinutes())}`;
1576
1571
  const te = `${pad2(e.getHours())}:${pad2(e.getMinutes())}`;
1577
1572
  const baseTitle = (ev.title || '').trim();
1578
- // Avoid double prefix on rerenders
1579
- const prefix = `🕒 ${ts}-${te} `;
1580
- if (!baseTitle.startsWith('🕒 ')) {
1581
- ev.title = `${prefix}${baseTitle}`.trim();
1573
+ // Avoid double prefix on rerenders.
1574
+ // UX request: no clock icon for "prévision de sortie" (outing).
1575
+ const isOuting = ev.extendedProps.type === 'outing';
1576
+ const prefix = isOuting ? `${ts}-${te} ` : `🕒 ${ts}-${te} `;
1577
+ if (isOuting) {
1578
+ if (!/^\d{2}:\d{2}-\d{2}:\d{2}\s/.test(baseTitle)) {
1579
+ ev.title = `${prefix}${baseTitle}`.trim();
1580
+ }
1581
+ } else {
1582
+ if (!baseTitle.startsWith('🕒 ')) {
1583
+ ev.title = `${prefix}${baseTitle}`.trim();
1584
+ }
1582
1585
  }
1583
1586
  }
1584
1587
  } catch (e2) {}
@@ -2,6 +2,13 @@
2
2
  <div class="row">
3
3
  <div class="col-12">
4
4
  <h1>Calendrier</h1>
5
+ <div class="onekite-calendar-legend" aria-label="Légende des couleurs" style="margin:.25rem 0 .75rem;">
6
+ <span class="onekite-legend-item"><span class="onekite-legend-dot" style="background:#f39c12"></span>En attente</span>
7
+ <span class="onekite-legend-item"><span class="onekite-legend-dot" style="background:#d35400"></span>Paiement en attente</span>
8
+ <span class="onekite-legend-item"><span class="onekite-legend-dot" style="background:#27ae60"></span>Payée</span>
9
+ <span class="onekite-legend-item"><span class="onekite-legend-dot" style="background:#8e44ad"></span>Évènement</span>
10
+ <span class="onekite-legend-item"><span class="onekite-legend-dot" style="background:#2980b9"></span>Prévision de sortie</span>
11
+ </div>
5
12
  <div id="onekite-calendar" style="margin-top: 1rem;"></div>
6
13
  </div>
7
14
  </div>
@@ -23,6 +30,29 @@
23
30
 
24
31
 
25
32
  <style>
33
+ .onekite-calendar-legend {
34
+ display: flex;
35
+ flex-wrap: wrap;
36
+ gap: .5rem .75rem;
37
+ align-items: center;
38
+ font-size: .9rem;
39
+ }
40
+ .onekite-legend-item {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ gap: .35rem;
44
+ padding: .15rem .4rem;
45
+ border-radius: 999px;
46
+ background: rgba(0,0,0,.04);
47
+ }
48
+ .onekite-legend-dot {
49
+ width: .7rem;
50
+ height: .7rem;
51
+ border-radius: 999px;
52
+ display: inline-block;
53
+ border: 1px solid rgba(0,0,0,.15);
54
+ }
55
+
26
56
  /* Make the custom "Évènement" button distinct from view buttons */
27
57
  .fc .fc-newSpecial-button {
28
58
  background: #8e44ad;
@@ -36,6 +66,7 @@
36
66
  /* Mobile tweaks for FullCalendar */
37
67
  @media (max-width: 576px) {
38
68
  #onekite-calendar { margin-top: .5rem !important; }
69
+ .onekite-calendar-legend { font-size: .82rem; }
39
70
  .fc .fc-toolbar { flex-wrap: wrap; gap: .25rem; }
40
71
  .fc .fc-toolbar-title { font-size: 1.05rem; line-height: 1.2; }
41
72
  .fc .fc-button { padding: .25rem .45rem; font-size: .75rem; }