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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
status:
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
createdAt: createdAt
|
|
731
|
-
|
|
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
package/plugin.json
CHANGED
package/public/js/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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>
|