nodebb-plugin-onekite-calendar 2.0.20 → 2.0.22

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,17 @@
1
1
  # Changelog – calendar-onekite
2
2
 
3
+ ## 1.3.17
4
+ - Temps réel : les **créations** (locations / évènements), **changements de statut** (validation, refus, annulation, paiement reçu) et **expirations** se répercutent automatiquement sur le calendrier pour **tous les utilisateurs connectés**, sans rechargement de page.
5
+
6
+ ## 1.3.16
7
+ - Calendrier : le **jour actuel** n’est plus affiché comme indisponible (gris + panneau sens interdit). Seules les **dates passées** restent désactivées visuellement.
8
+
9
+ ## 1.3.15
10
+ - Permissions : les **modérateurs globaux / gestionnaires** (groupe NodeBB « Global Moderators », selon le slug) sont désormais considérés comme validateurs pour les actions de modération (dont l’**annulation**).
11
+
12
+ ## 1.3.14
13
+ - Mobile FAB : la modale de sélection de dates autorise désormais la réservation **le jour même** (seules les dates passées sont désactivées).
14
+
3
15
  ## 1.3.13
4
16
  - Réservations : il est désormais possible de réserver pour le jour même (seules les dates passées sont refusées).
5
17
  - Annulation : les validateurs peuvent annuler une réservation déjà payée.
package/lib/admin.js CHANGED
@@ -73,6 +73,7 @@ function normalizeReturnUrl(meta) {
73
73
 
74
74
 
75
75
  const dbLayer = require('./db');
76
+ const realtime = require('./realtime');
76
77
  const helloasso = require('./helloasso');
77
78
 
78
79
  const ADMIN_PRIV = 'admin:settings';
@@ -207,6 +208,12 @@ admin.approveReservation = async function (req, res) {
207
208
 
208
209
  await dbLayer.saveReservation(r);
209
210
 
211
+ // Real-time refresh for all viewers
212
+ realtime.emitCalendarUpdated({ kind: 'reservation', action: 'refused', rid: String(rid), status: r.status });
213
+
214
+ // Real-time refresh for all viewers
215
+ realtime.emitCalendarUpdated({ kind: 'reservation', action: 'approved', rid: String(rid), status: r.status });
216
+
210
217
  // Email requester
211
218
  try {
212
219
  const requesterUid = parseInt(r.uid, 10);
@@ -249,6 +256,9 @@ admin.refuseReservation = async function (req, res) {
249
256
  r.refusedReason = String((req.body && (req.body.reason || req.body.refusedReason || req.body.refuseReason)) || '').trim();
250
257
  await dbLayer.saveReservation(r);
251
258
 
259
+ // Real-time refresh for all viewers
260
+ realtime.emitCalendarUpdated({ kind: 'reservation', action: 'refused', rid: String(rid), status: r.status });
261
+
252
262
  try {
253
263
  const requesterUid = parseInt(r.uid, 10);
254
264
  const requester = await user.getUserFields(requesterUid, ['username']);
package/lib/api.js CHANGED
@@ -112,6 +112,7 @@ function normalizeAllowedGroups(raw) {
112
112
 
113
113
  const helloasso = require('./helloasso');
114
114
  const discord = require('./discord');
115
+ const realtime = require('./realtime');
115
116
 
116
117
  // Email helper (NodeBB 4.x): always send by uid.
117
118
  // Subject must be provided inside params.subject.
@@ -317,6 +318,16 @@ async function canValidate(uid, settings) {
317
318
  try {
318
319
  const isAdmin = await groups.isMember(uid, 'administrators');
319
320
  if (isAdmin) return true;
321
+
322
+ // NodeBB has a built-in "Global Moderators" group (slug can vary by install/version).
323
+ // Allow them as validators as well.
324
+ const globalModeratorCandidates = ['global-moderators', 'Global Moderators', 'global moderators'];
325
+ for (const g of globalModeratorCandidates) {
326
+ try {
327
+ // eslint-disable-next-line no-await-in-loop
328
+ if (await groups.isMember(uid, g)) return true;
329
+ } catch (e) {}
330
+ }
320
331
  } catch (e) {}
321
332
 
322
333
  const allowed = normalizeAllowedGroups(settings.validatorGroups || '');
@@ -762,6 +773,8 @@ api.createSpecialEvent = async function (req, res) {
762
773
  createdAt: String(Date.now()),
763
774
  };
764
775
  await dbLayer.saveSpecialEvent(ev);
776
+ // Real-time refresh for all viewers
777
+ realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'created', eid: ev.eid });
765
778
  res.json({ ok: true, eid });
766
779
  };
767
780
 
@@ -773,6 +786,7 @@ api.deleteSpecialEvent = async function (req, res) {
773
786
  const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
774
787
  if (!eid) return res.status(400).json({ error: 'bad-id' });
775
788
  await dbLayer.removeSpecialEvent(eid);
789
+ realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'deleted', eid });
776
790
  res.json({ ok: true });
777
791
  };
778
792
 
@@ -972,6 +986,9 @@ api.createReservation = async function (req, res) {
972
986
  // Save
973
987
  await dbLayer.saveReservation(resv);
974
988
 
989
+ // Real-time refresh for all viewers
990
+ realtime.emitCalendarUpdated({ kind: 'reservation', action: 'created', rid: resv.rid, status: resv.status });
991
+
975
992
  // Audit
976
993
  await auditLog(isValidatorFree ? 'reservation_self_checked' : 'reservation_requested', uid, {
977
994
  targetType: 'reservation',
@@ -1174,6 +1191,9 @@ api.approveReservation = async function (req, res) {
1174
1191
 
1175
1192
  await dbLayer.saveReservation(r);
1176
1193
 
1194
+ // Real-time refresh for all viewers
1195
+ realtime.emitCalendarUpdated({ kind: 'reservation', action: 'approved', rid: String(rid), status: r.status });
1196
+
1177
1197
  await auditLog('reservation_approved', uid, {
1178
1198
  targetType: 'reservation',
1179
1199
  targetId: String(rid),
@@ -1241,6 +1261,8 @@ api.refuseReservation = async function (req, res) {
1241
1261
  }
1242
1262
  await dbLayer.saveReservation(r);
1243
1263
 
1264
+ realtime.emitCalendarUpdated({ kind: 'reservation', action: 'refused', rid: String(rid), status: r.status });
1265
+
1244
1266
  await auditLog('reservation_refused', uid, {
1245
1267
  targetType: 'reservation',
1246
1268
  targetId: String(rid),
@@ -1310,6 +1332,8 @@ api.cancelReservation = async function (req, res) {
1310
1332
 
1311
1333
  await dbLayer.saveReservation(r);
1312
1334
 
1335
+ realtime.emitCalendarUpdated({ kind: 'reservation', action: 'cancelled', rid: String(rid), status: r.status });
1336
+
1313
1337
  await auditLog('reservation_cancelled', uid, {
1314
1338
  targetType: 'reservation',
1315
1339
  targetId: String(rid),
@@ -1385,6 +1409,8 @@ api.setMaintenance = async function (req, res) {
1385
1409
  targetType: 'item',
1386
1410
  targetId: itemId,
1387
1411
  });
1412
+ // Maintenance impacts availability; ask clients to refetch.
1413
+ realtime.emitCalendarUpdated({ kind: 'maintenance', action: enabled ? 'on' : 'off', itemId });
1388
1414
  return res.json({ ok: true, itemId, enabled });
1389
1415
  };
1390
1416
 
@@ -1427,6 +1453,7 @@ api.setMaintenanceAll = async function (req, res) {
1427
1453
  targetId: enabled ? 'all_on' : 'all_off',
1428
1454
  count: result && typeof result.count === 'number' ? result.count : (enabled ? catalogIds.length : 0),
1429
1455
  });
1456
+ realtime.emitCalendarUpdated({ kind: 'maintenance', action: enabled ? 'all_on' : 'all_off' });
1430
1457
  return res.json(Object.assign({ ok: true, enabled }, result || {}));
1431
1458
  };
1432
1459
 
@@ -5,19 +5,12 @@ const crypto = require('crypto');
5
5
  const db = require.main.require('./src/database');
6
6
  const meta = require.main.require('./src/meta');
7
7
  const user = require.main.require('./src/user');
8
- // Real-time updates
9
- let io;
10
- try {
11
- const socketIO = require.main.require('./src/socket.io');
12
- io = socketIO.server || socketIO;
13
- } catch (e) {
14
- io = null;
15
- }
16
-
17
8
 
18
9
  const dbLayer = require('./db');
19
10
  const helloasso = require('./helloasso');
20
11
  const discord = require('./discord');
12
+ const realtime = require('./realtime');
13
+ const realtime = require('./realtime');
21
14
 
22
15
  async function auditLog(action, actorUid, payload) {
23
16
  try {
@@ -342,12 +335,8 @@ async function handler(req, res, next) {
342
335
  paymentId: r.paymentId || '',
343
336
  });
344
337
 
345
- // Real-time notify: refresh calendars for all viewers (owner + validators/admins)
346
- try {
347
- if (io && io.sockets && typeof io.sockets.emit === 'function') {
348
- io.sockets.emit('event:calendar-onekite.reservationUpdated', { rid: r.rid, status: r.status });
349
- }
350
- } catch (e) {}
338
+ // Real-time notify: refresh calendars for all viewers
339
+ realtime.emitCalendarUpdated({ kind: 'reservation', action: 'paid', rid: String(r.rid), status: r.status });
351
340
 
352
341
  // Notify requester
353
342
  const requesterUid = parseInt(r.uid, 10);
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ // Broadcast real-time calendar updates to all connected clients.
4
+ // NodeBB exposes socket.io via ./src/socket.io (either the server itself or an
5
+ // object with a .server property depending on version).
6
+
7
+ let io;
8
+
9
+ function getIO() {
10
+ if (io) return io;
11
+ try {
12
+ const socketIO = require.main.require('./src/socket.io');
13
+ io = socketIO && socketIO.server ? socketIO.server : socketIO;
14
+ } catch (e) {
15
+ io = null;
16
+ }
17
+ return io;
18
+ }
19
+
20
+ function emitCalendarUpdated(payload) {
21
+ try {
22
+ const server = getIO();
23
+ if (!server || !server.sockets || typeof server.sockets.emit !== 'function') return;
24
+
25
+ // New event name (generic).
26
+ server.sockets.emit('event:calendar-onekite.calendarUpdated', payload || {});
27
+
28
+ // Backwards compatible event name (older clients only listened to this).
29
+ // Keep it emitted for *any* calendar mutation, not just reservation status.
30
+ server.sockets.emit('event:calendar-onekite.reservationUpdated', payload || {});
31
+ } catch (e) {}
32
+ }
33
+
34
+ module.exports = {
35
+ emitCalendarUpdated,
36
+ };
package/lib/scheduler.js CHANGED
@@ -4,6 +4,7 @@ const meta = require.main.require('./src/meta');
4
4
  const db = require.main.require('./src/database');
5
5
  const dbLayer = require('./db');
6
6
  const discord = require('./discord');
7
+ const realtime = require('./realtime');
7
8
  const nconf = require.main.require('nconf');
8
9
  const groups = require.main.require('./src/groups');
9
10
 
@@ -219,6 +220,9 @@ async function expirePending() {
219
220
  }
220
221
 
221
222
  await dbLayer.removeReservation(rid);
223
+
224
+ // Real-time refresh for all viewers
225
+ realtime.emitCalendarUpdated({ kind: 'reservation', action: 'expired', rid: String(rid), status: 'expired' });
222
226
  }
223
227
  }
224
228
  }
@@ -380,6 +384,9 @@ async function processAwaitingPayment() {
380
384
  }
381
385
 
382
386
  await dbLayer.removeReservation(rid);
387
+
388
+ // Real-time refresh for all viewers
389
+ realtime.emitCalendarUpdated({ kind: 'reservation', action: 'expired', rid: String(rid), status: 'expired' });
383
390
  }
384
391
  }
385
392
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.20",
3
+ "version": "2.0.22",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -1,5 +1,35 @@
1
1
  # Changelog – calendar-onekite
2
2
 
3
+ ## 1.3.16
4
+ - Calendrier : le **jour actuel** n’est plus affiché comme indisponible (gris + panneau sens interdit). Seules les **dates passées** restent désactivées visuellement.
5
+
6
+ ## 1.3.15
7
+ - Permissions : les **modérateurs globaux / gestionnaires** (groupe NodeBB « Global Moderators », selon le slug) sont désormais considérés comme validateurs pour les actions de modération (dont l’**annulation**).
8
+
9
+ ## 1.3.14
10
+ - Mobile FAB : la modale de sélection de dates autorise désormais la réservation **le jour même** (seules les dates passées sont désactivées).
11
+
12
+ ## 1.3.13
13
+ - Réservations : il est désormais possible de réserver pour le jour même (seules les dates passées sont refusées).
14
+ - Annulation : les validateurs peuvent annuler une réservation déjà payée.
15
+
16
+ ## 1.3.12
17
+ - Expiration automatique : envoi d’un email au demandeur avec la raison :
18
+ - « Demande non prise en charge dans le temps imparti » (demande en attente)
19
+ - « Paiement non reçu dans le temps imparti » (paiement en attente)
20
+ - ACP : ajout de 2 paramètres pour envoyer un rappel email aux validateurs sur les demandes en attente / paiement en attente.
21
+ - Emails validateurs : nouveaux templates (rappel + expiration) avec lien vers l’ACP.
22
+
23
+ ## 1.3.11
24
+ - ACP (Demandes en attente) : affichage du pseudo du demandeur avec lien vers sa fiche utilisateur.
25
+
26
+ ## 1.3.10
27
+ - Mobile FAB : surlignage de la sélection de dates rendu lisible en light mode (contraste renforcé, start/end en primary).
28
+
29
+ ## 1.3.9
30
+ - Mobile FAB : correction de la sélection de plage (tous les jours sélectionnés sont désormais bien inclus dans la surbrillance).
31
+ - Mobile FAB : amélioration du rendu en mode sombre (bordures/fonds adaptés, contraste renforcé).
32
+
3
33
  ## 1.3.8
4
34
  - ACP Comptabilisation : ajout d’un **grand total** (montant total des locations payées sur la période) + compteurs (payées / sorties gratuites).
5
35
 
@@ -317,6 +317,16 @@ async function canValidate(uid, settings) {
317
317
  try {
318
318
  const isAdmin = await groups.isMember(uid, 'administrators');
319
319
  if (isAdmin) return true;
320
+
321
+ // NodeBB has a built-in "Global Moderators" group (slug can vary by install/version).
322
+ // Allow them as validators as well.
323
+ const globalModeratorCandidates = ['global-moderators', 'Global Moderators', 'global moderators'];
324
+ for (const g of globalModeratorCandidates) {
325
+ try {
326
+ // eslint-disable-next-line no-await-in-loop
327
+ if (await groups.isMember(uid, g)) return true;
328
+ } catch (e) {}
329
+ }
320
330
  } catch (e) {}
321
331
 
322
332
  const allowed = normalizeAllowedGroups(settings.validatorGroups || '');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "1.3.13",
3
+ "version": "1.3.16",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "1.3.13"
42
+ "version": "1.3.16"
43
43
  }
@@ -31,7 +31,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
31
31
  touch-action: manipulation;
32
32
  }
33
33
 
34
- /* Show days that cannot be selected for new reservations (today & past) */
34
+ /* Show days that cannot be selected for new reservations (past only) */
35
35
  .fc .onekite-day-disabled {
36
36
  background: rgba(0, 0, 0, 0.03);
37
37
  }
@@ -1460,14 +1460,14 @@ function toDatetimeLocalValue(date) {
1460
1460
  longPressDelay: 300,
1461
1461
  selectLongPressDelay: 300,
1462
1462
  dayCellDidMount: function (arg) {
1463
- // Visually disable today and past days for reservation creation rules,
1463
+ // Visually disable past days for reservation creation rules,
1464
1464
  // without breaking event clicks.
1465
1465
  try {
1466
1466
  const cellDate = arg && arg.date ? new Date(arg.date) : null;
1467
1467
  if (!cellDate) return;
1468
1468
  const ymd = toLocalYmd(cellDate);
1469
1469
  const today = toLocalYmd(new Date());
1470
- if (ymd <= today) {
1470
+ if (ymd < today) {
1471
1471
  arg.el.classList.add('onekite-day-disabled');
1472
1472
  }
1473
1473
  } catch (e) {}
package/plugin.json CHANGED
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "2.0.20"
42
+ "version": "2.0.22"
43
43
  }
package/public/client.js CHANGED
@@ -31,7 +31,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
31
31
  touch-action: manipulation;
32
32
  }
33
33
 
34
- /* Show days that cannot be selected for new reservations (today & past) */
34
+ /* Show days that cannot be selected for new reservations (past only) */
35
35
  .fc .onekite-day-disabled {
36
36
  background: rgba(0, 0, 0, 0.03);
37
37
  }
@@ -1470,14 +1470,14 @@ function toDatetimeLocalValue(date) {
1470
1470
  longPressDelay: 300,
1471
1471
  selectLongPressDelay: 300,
1472
1472
  dayCellDidMount: function (arg) {
1473
- // Visually disable today and past days for reservation creation rules,
1473
+ // Visually disable past days for reservation creation rules,
1474
1474
  // without breaking event clicks.
1475
1475
  try {
1476
1476
  const cellDate = arg && arg.date ? new Date(arg.date) : null;
1477
1477
  if (!cellDate) return;
1478
1478
  const ymd = toLocalYmd(cellDate);
1479
1479
  const today = toLocalYmd(new Date());
1480
- if (ymd <= today) {
1480
+ if (ymd < today) {
1481
1481
  arg.el.classList.add('onekite-day-disabled');
1482
1482
  }
1483
1483
  } catch (e) {}
@@ -2091,11 +2091,10 @@ function toDatetimeLocalValue(date) {
2091
2091
  async function openFabDatePicker() {
2092
2092
  if (!lockAction('fab-date-picker', 700)) return;
2093
2093
 
2094
- // Cannot book today or past (mobile FAB).
2094
+ // Cannot book past dates (mobile FAB). Same-day booking is allowed.
2095
2095
  const today = new Date();
2096
2096
  today.setHours(0, 0, 0, 0);
2097
2097
  const minStart = new Date(today);
2098
- minStart.setDate(minStart.getDate() + 1);
2099
2098
 
2100
2099
  // Month cursor for the single-calendar range picker
2101
2100
  const cursor = new Date(minStart);
@@ -2120,7 +2119,7 @@ async function openFabDatePicker() {
2120
2119
  </div>
2121
2120
  <div class="onekite-range-grid" id="onekite-range-grid"></div>
2122
2121
  <div class="onekite-range-summary" id="onekite-range-summary"></div>
2123
- <div class="form-text mt-2">Sélectionne une date de début puis une date de fin (incluse). Les dates d'aujourd'hui et passées sont désactivées.</div>
2122
+ <div class="form-text mt-2">Sélectionne une date de début puis une date de fin (incluse). Les dates passées sont désactivées.</div>
2124
2123
  </div>
2125
2124
  `;
2126
2125
 
@@ -2370,10 +2369,17 @@ function autoInit(data) {
2370
2369
 
2371
2370
 
2372
2371
 
2373
- // Live refresh when a reservation changes (e.g., payment confirmed by webhook)
2372
+ // Live refresh when the calendar changes (reservation status, new reservation,
2373
+ // new special event, maintenance toggles, webhook payment, etc.)
2374
2374
  try {
2375
2375
  if (!window.__oneKiteSocketBound && typeof socket !== 'undefined' && socket && typeof socket.on === 'function') {
2376
2376
  window.__oneKiteSocketBound = true;
2377
+ socket.on('event:calendar-onekite.calendarUpdated', function () {
2378
+ try {
2379
+ const cal = window.oneKiteCalendar;
2380
+ scheduleRefetch(cal);
2381
+ } catch (e) {}
2382
+ });
2377
2383
  socket.on('event:calendar-onekite.reservationUpdated', function () {
2378
2384
  try {
2379
2385
  const cal = window.oneKiteCalendar;