nodebb-plugin-equipment-calendar 2.2.2 → 4.9.0

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/library.js CHANGED
@@ -17,6 +17,16 @@ const nconf = require.main.require('nconf');
17
17
 
18
18
  const winston = require.main.require('winston');
19
19
 
20
+ function decorateCalendarEvents(events) {
21
+ return (events || []).map((ev) => {
22
+ const status = ev.status || (ev.extendedProps && ev.extendedProps.status) || 'pending';
23
+ return Object.assign({}, ev, {
24
+ classNames: (ev.classNames || []).concat(['ec-status-' + status]),
25
+ extendedProps: Object.assign({}, ev.extendedProps || {}, { status }),
26
+ });
27
+ });
28
+ }
29
+
20
30
  function generateId() {
21
31
  try {
22
32
  // Node 14+ / modern: crypto.randomUUID
@@ -35,6 +45,11 @@ const crypto = require('crypto');
35
45
  const plugin = {};
36
46
  const SETTINGS_KEY = 'equipmentCalendar';
37
47
 
48
+ const EC_KEYS = {
49
+ reservationsZset: 'ec:reservations',
50
+ reservationKey: (id) => `ec:reservation:${id}`,
51
+ };
52
+
38
53
  const DEFAULT_SETTINGS = {
39
54
  creatorGroups: 'registered-users',
40
55
  approverGroup: 'administrators',
@@ -345,9 +360,13 @@ async function getBookingRids(bookingId) {
345
360
  return await db.getSetMembers(`equipmentCalendar:booking:${bookingId}:rids`) || [];
346
361
  }
347
362
 
363
+ const GLOBAL_INDEX_KEY = 'ec:reservations';
364
+
348
365
  async function saveReservation(res) {
349
366
  await db.setObject(resKey(res.id), res);
350
367
  await db.sortedSetAdd(itemIndexKey(res.itemId), res.startMs, res.id);
368
+ // Global index for ACP listing
369
+ await db.sortedSetAdd(GLOBAL_INDEX_KEY, res.startMs, res.id);
351
370
  }
352
371
 
353
372
  async function getReservation(id) {
@@ -681,90 +700,29 @@ plugin.addAdminRoutes = async function (params) {
681
700
  };
682
701
 
683
702
 
684
- async function renderAdminReservationsPage(req, res) {
685
- if (!(await ensureIsAdmin(req, res))) return;
686
-
687
- const settings = await getSettings();
688
- const items = await getActiveItems(settings);
689
- const itemById = {};
690
- items.forEach(it => { itemById[it.id] = it; });
691
-
692
- const status = String(req.query.status || ''); // optional filter
693
- const itemId = String(req.query.itemId || ''); // optional filter
694
- const q = String(req.query.q || '').trim(); // search rid/user/notes
695
-
696
- const page = Math.max(1, parseInt(req.query.page, 10) || 1);
697
- const perPage = Math.min(100, Math.max(10, parseInt(req.query.perPage, 10) || 50));
698
-
699
- const allRids = await db.getSortedSetRevRange('equipmentCalendar:reservations', 0, -1);
700
- const totalAll = allRids.length;
701
-
702
- const rows = [];
703
- for (const rid of allRids) {
704
- // eslint-disable-next-line no-await-in-loop
705
- const r = await db.getObject(`equipmentCalendar:reservation:${rid}`);
706
- if (!r || !r.rid) continue;
707
-
708
- if (status && String(r.status) !== status) continue;
709
- if (itemId && String(r.itemId) !== itemId) continue;
710
-
711
- const notes = String(r.notesUser || '');
712
- const ridStr = String(r.rid || rid);
713
- if (q) {
714
- const hay = (ridStr + ' ' + String(r.uid || '') + ' ' + notes).toLowerCase();
715
- if (!hay.includes(q.toLowerCase())) continue;
716
- }
717
703
 
704
+ async function renderAdminReservationsPage(req, res) {
705
+ const isAdmin = await user.isAdminOrGlobalMod(req.uid);
706
+ if (!isAdmin) return res.status(403).render('403', {});
707
+ const ids = await db.getSortedSetRevRange(GLOBAL_INDEX_KEY, 0, 199);
708
+ const rowsRaw = await db.getObjects(ids.map(id => resKey(id)));
709
+ const rows = (rowsRaw || []).filter(Boolean).map((r) => {
718
710
  const startMs = parseInt(r.startMs, 10) || 0;
719
711
  const endMs = parseInt(r.endMs, 10) || 0;
720
- const createdAt = parseInt(r.createdAt, 10) || 0;
721
-
722
- rows.push({
723
- rid: ridStr,
724
- itemId: String(r.itemId || ''),
725
- itemName: (itemById[String(r.itemId || '')] && itemById[String(r.itemId || '')].name) || String(r.itemId || ''),
726
- uid: String(r.uid || ''),
727
- status: String(r.status || ''),
728
- start: startMs ? new Date(startMs).toISOString() : '',
729
- end: endMs ? new Date(endMs).toISOString() : '',
730
- createdAt: createdAt ? new Date(createdAt).toISOString() : '',
731
- notesUser: notes,
732
- });
733
- }
734
-
735
- const total = rows.length;
736
- const totalPages = Math.max(1, Math.ceil(total / perPage));
737
- const safePage = Math.min(page, totalPages);
738
- const startIndex = (safePage - 1) * perPage;
739
- const pageRows = rows.slice(startIndex, startIndex + perPage);
740
-
741
- const itemOptions = [{ id: '', name: 'Tous' }].concat(items.map(i => ({ id: i.id, name: i.name })));
742
- const statusOptions = [
743
- { id: '', name: 'Tous' },
744
- { id: 'pending', name: 'pending' },
745
- { id: 'approved', name: 'approved' },
746
- { id: 'paid', name: 'paid' },
747
- { id: 'rejected', name: 'rejected' },
748
- { id: 'cancelled', name: 'cancelled' },
749
- ];
750
-
751
- res.render('admin/plugins/equipment-calendar-reservations', {
752
- title: 'Equipment Calendar - Réservations',
753
- settings,
754
- rows: pageRows,
755
- hasRows: pageRows.length > 0,
756
- itemOptions: itemOptions.map(o => ({ ...o, selected: o.id === itemId })),
757
- statusOptions: statusOptions.map(o => ({ ...o, selected: o.id === status })),
758
- q,
759
- page: safePage,
760
- perPage,
761
- total,
762
- totalAll,
763
- totalPages,
764
- prevPage: safePage > 1 ? safePage - 1 : 0,
765
- nextPage: safePage < totalPages ? safePage + 1 : 0,
766
- actionBase: '/admin/plugins/equipment-calendar/reservations',
712
+ return {
713
+ id: r.id,
714
+ bookingId: r.bookingId || '',
715
+ itemName: r.itemName || r.itemId,
716
+ startIso: startMs ? new Date(startMs).toISOString().slice(0,10) : '',
717
+ endIso: endMs ? new Date(endMs - 24*60*60*1000).toISOString().slice(0,10) : '',
718
+ days: Math.max(1, Math.round((endMs - startMs) / (24*60*60*1000))),
719
+ status: r.status || 'pending',
720
+ total: r.total || '0',
721
+ uid: r.uid,
722
+ createdAt: r.createdAt || 0,
723
+ };
767
724
  });
725
+ res.render('admin/plugins/equipment-calendar-reservations', { title: 'Réservations', rows, hasRows: rows.length>0 });
768
726
  }
769
727
 
770
728
  async function handleAdminApprove(req, res) {
@@ -989,6 +947,8 @@ async function renderCalendarPage(req, res) {
989
947
  }));
990
948
 
991
949
  res.render('equipment-calendar/calendar', {
950
+ defaultView: settings.defaultView,
951
+
992
952
  title: 'Réservation de matériel',
993
953
  items,
994
954
  view,
@@ -1203,7 +1163,7 @@ async function handleCreateReservation(req, res) {
1203
1163
  const created = [];
1204
1164
  for (const itemId of itemIds) {
1205
1165
  const rid = await createReservationForItem(req, res, settings, itemId, startMs, endMs, notesUser, bookingId);
1206
- created.push(rid);
1166
+ created.push(rid.id || rid);
1207
1167
  }
1208
1168
 
1209
1169
  // Redirect to calendar with a success flag
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-equipment-calendar",
3
- "version": "2.2.2",
3
+ "version": "4.9.0",
4
4
  "description": "Equipment reservation calendar for NodeBB (FullCalendar, approvals, HelloAsso payments)",
5
5
  "main": "library.js",
6
6
  "scripts": {
package/plugin.json CHANGED
@@ -25,6 +25,6 @@
25
25
  "scripts": [
26
26
  "public/js/client.js"
27
27
  ],
28
- "version": "0.7.4",
28
+ "version": "0.7.7",
29
29
  "minver": "4.7.1"
30
30
  }
@@ -93,10 +93,26 @@ require(['jquery', 'bootstrap'], function ($, bootstrap) {
93
93
  const events = Array.isArray(window.EC_EVENTS) ? window.EC_EVENTS : [];
94
94
 
95
95
  const calendar = new window.FullCalendar.Calendar(calendarEl, {
96
- initialView: 'dayGridMonth',
96
+ headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,dayGridWeek,timeGridWeek,listMonth,listWeek' },
97
+ initialView: (window.EC_DEFAULT_VIEW || 'dayGridMonth'),
97
98
  selectable: canCreate,
98
99
  selectMirror: true,
99
100
  events,
101
+ eventContent: function(arg) {
102
+ const status = (arg.event.extendedProps && arg.event.extendedProps.status) || 'pending';
103
+ const wrap = document.createElement('div');
104
+ wrap.className = 'ec-event';
105
+ const dot = document.createElement('span');
106
+ dot.className = 'ec-dot';
107
+ const title = document.createElement('span');
108
+ title.className = 'ec-title';
109
+ title.textContent = arg.event.title || '';
110
+ wrap.appendChild(dot);
111
+ wrap.appendChild(title);
112
+ return { domNodes: [wrap] };
113
+ },
114
+ eventClassNames:function(arg){return (arg.event.classNames||[]);},
115
+ eventDidMount:function(info){try{const s=info.event.extendedProps&&info.event.extendedProps.status;info.el.title=(s?('Statut: '+s+' — '):'')+info.event.title;}catch(e){}},
100
116
  dateClick: function (info) {
101
117
  if (!canCreate) return;
102
118
  const startMs = Date.UTC(info.date.getUTCFullYear(), info.date.getUTCMonth(), info.date.getUTCDate());
@@ -35,7 +35,22 @@
35
35
  </div>
36
36
  </div>
37
37
 
38
+
38
39
  <div class="card card-body mb-3">
40
+ <h5>Affichage calendrier</h5>
41
+ <div class="mb-0">
42
+ <label class="form-label">Vue par défaut</label>
43
+ <select class="form-select" name="defaultView">
44
+ <option value="dayGridMonth" {{{ if view_dayGridMonth }}}selected{{{ end }}}>Mois</option>
45
+ <option value="dayGridWeek" {{{ if view_dayGridWeek }}}selected{{{ end }}}>Semaine</option>
46
+ <option value="timeGridWeek" {{{ if view_timeGridWeek }}}selected{{{ end }}}>Semaine (grille)</option>
47
+ <option value="listMonth" {{{ if view_listMonth }}}selected{{{ end }}}>Liste (mois)</option>
48
+ <option value="listWeek" {{{ if view_listWeek }}}selected{{{ end }}}>Liste (semaine)</option>
49
+ </select>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="card card-body mb-3">
39
54
  <h5>HelloAsso</h5>
40
55
  <div class="mb-3">
41
56
  <label class="form-label">API Base URL (prod/sandbox)</label>
@@ -1,3 +1,24 @@
1
+
2
+ <style>
3
+ /* Professional status styling */
4
+ .fc .ec-event { display:flex; align-items:center; gap:.4rem; padding:.05rem .35rem; border-radius:.5rem; }
5
+ .fc .ec-dot { width:.55rem; height:.55rem; border-radius:999px; display:inline-block; }
6
+ .fc .ec-title { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
7
+
8
+ .fc-event.ec-status-pending { border-color: rgba(108,117,125,.35); }
9
+ .fc-event.ec-status-approved { border-color: rgba(13,110,253,.35); }
10
+ .fc-event.ec-status-paid { border-color: rgba(25,135,84,.35); }
11
+ .fc-event.ec-status-cancelled { opacity:.55; text-decoration: line-through; }
12
+
13
+ .fc-event.ec-status-pending .ec-dot { background: #6c757d; }
14
+ .fc-event.ec-status-approved .ec-dot { background: #0d6efd; }
15
+ .fc-event.ec-status-paid .ec-dot { background: #198754; }
16
+ .fc-event.ec-status-cancelled .ec-dot { background: #dc3545; }
17
+
18
+ /* list view rows */
19
+ .fc .fc-list-event.ec-status-cancelled { opacity:.6; text-decoration: line-through; }
20
+ </style>
21
+
1
22
  <div class="equipment-calendar-page">
2
23
  <h1>Réservation de matériel</h1>
3
24
 
@@ -10,7 +31,15 @@
10
31
  {{{ end }}}
11
32
 
12
33
  <div class="card card-body mb-3">
13
- <div id="ec-calendar"></div>
34
+
35
+ <div class="d-flex flex-wrap gap-2 align-items-center mb-2" id="ec-legend">
36
+ <span class="badge text-bg-secondary"><i class="fa fa-hourglass-half me-1"></i>En attente</span>
37
+ <span class="badge text-bg-primary"><i class="fa fa-check me-1"></i>Validée</span>
38
+ <span class="badge text-bg-success"><i class="fa fa-check-double me-1"></i>Payée</span>
39
+ <span class="badge text-bg-danger"><i class="fa fa-times me-1"></i>Annulée</span>
40
+ </div>
41
+
42
+ <div id="ec-calendar"></div>
14
43
  </div>
15
44
 
16
45
  <!-- Modal de demande -->
@@ -74,6 +103,7 @@
74
103
  </div>
75
104
 
76
105
  <script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js"></script>
106
+ <script>window.EC_DEFAULT_VIEW = '{defaultView}';</script>
77
107
  <script src="{relative_path}/plugins/nodebb-plugin-equipment-calendar/js/client.js"></script>
78
108
 
79
109
  <script>
@@ -82,3 +112,5 @@
82
112
  window.EC_CAN_CREATE = {canCreateJs};
83
113
  window.EC_TZ = '{tz}';
84
114
  </script>
115
+
116
+ <style>.ec-status-pending{opacity:.85}.ec-status-paid{font-weight:600}.ec-status-cancelled,.ec-status-expired{opacity:.6;text-decoration:line-through}</style>