nodebb-plugin-equipment-calendar 2.0.6 → 3.0.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
|
@@ -16,6 +16,30 @@ const helpers = require.main.require('./src/controllers/helpers');
|
|
|
16
16
|
const nconf = require.main.require('nconf');
|
|
17
17
|
|
|
18
18
|
const winston = require.main.require('winston');
|
|
19
|
+
|
|
20
|
+
function decorateCalendarEvents(events) {
|
|
21
|
+
return (events || []).map((ev) => {
|
|
22
|
+
const status = ev.status || (ev.extendedProps && ev.extendedProps.status) || 'pending';
|
|
23
|
+
const icon = status === 'paid' ? '✅' : (status === 'approved' ? '✔️' : (status === 'pending' ? '⏳' : (status === 'cancelled' ? '❌' : '⚠️')));
|
|
24
|
+
const title = ev.title || '';
|
|
25
|
+
return Object.assign({}, ev, {
|
|
26
|
+
title: `${icon} ${title}`,
|
|
27
|
+
classNames: ['ec-status-' + status],
|
|
28
|
+
extendedProps: Object.assign({}, ev.extendedProps || {}, { status }),
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function generateId() {
|
|
34
|
+
try {
|
|
35
|
+
// Node 14+ / modern: crypto.randomUUID
|
|
36
|
+
const crypto = require('crypto');
|
|
37
|
+
if (crypto.randomUUID) return crypto.randomUUID();
|
|
38
|
+
return crypto.randomBytes(16).toString('hex');
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return String(Date.now()) + '-' + Math.random().toString(16).slice(2);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
19
43
|
const axios = require('axios');
|
|
20
44
|
const { DateTime } = require('luxon');
|
|
21
45
|
const { v4: uuidv4 } = require('uuid');
|
|
@@ -24,6 +48,11 @@ const crypto = require('crypto');
|
|
|
24
48
|
const plugin = {};
|
|
25
49
|
const SETTINGS_KEY = 'equipmentCalendar';
|
|
26
50
|
|
|
51
|
+
const EC_KEYS = {
|
|
52
|
+
reservationsZset: 'ec:reservations',
|
|
53
|
+
reservationKey: (id) => `ec:reservation:${id}`,
|
|
54
|
+
};
|
|
55
|
+
|
|
27
56
|
const DEFAULT_SETTINGS = {
|
|
28
57
|
creatorGroups: 'registered-users',
|
|
29
58
|
approverGroup: 'administrators',
|
|
@@ -287,7 +316,7 @@ async function getActiveItems() {
|
|
|
287
316
|
const items = (Array.isArray(rawItems) ? rawItems : []).map((it) => ({
|
|
288
317
|
id: String(it.id || '').trim(),
|
|
289
318
|
name: String(it.name || '').trim(),
|
|
290
|
-
price:
|
|
319
|
+
price: Number(it.price || 0) || 0,
|
|
291
320
|
active: true,
|
|
292
321
|
})).filter(it => it.id && it.name);
|
|
293
322
|
return items;
|
|
@@ -670,90 +699,26 @@ plugin.addAdminRoutes = async function (params) {
|
|
|
670
699
|
};
|
|
671
700
|
|
|
672
701
|
|
|
673
|
-
async function renderAdminReservationsPage(req, res) {
|
|
674
|
-
if (!(await ensureIsAdmin(req, res))) return;
|
|
675
|
-
|
|
676
|
-
const settings = await getSettings();
|
|
677
|
-
const items = await getActiveItems(settings);
|
|
678
|
-
const itemById = {};
|
|
679
|
-
items.forEach(it => { itemById[it.id] = it; });
|
|
680
|
-
|
|
681
|
-
const status = String(req.query.status || ''); // optional filter
|
|
682
|
-
const itemId = String(req.query.itemId || ''); // optional filter
|
|
683
|
-
const q = String(req.query.q || '').trim(); // search rid/user/notes
|
|
684
|
-
|
|
685
|
-
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
|
|
686
|
-
const perPage = Math.min(100, Math.max(10, parseInt(req.query.perPage, 10) || 50));
|
|
687
|
-
|
|
688
|
-
const allRids = await db.getSortedSetRevRange('equipmentCalendar:reservations', 0, -1);
|
|
689
|
-
const totalAll = allRids.length;
|
|
690
|
-
|
|
691
|
-
const rows = [];
|
|
692
|
-
for (const rid of allRids) {
|
|
693
|
-
// eslint-disable-next-line no-await-in-loop
|
|
694
|
-
const r = await db.getObject(`equipmentCalendar:reservation:${rid}`);
|
|
695
|
-
if (!r || !r.rid) continue;
|
|
696
|
-
|
|
697
|
-
if (status && String(r.status) !== status) continue;
|
|
698
|
-
if (itemId && String(r.itemId) !== itemId) continue;
|
|
699
|
-
|
|
700
|
-
const notes = String(r.notesUser || '');
|
|
701
|
-
const ridStr = String(r.rid || rid);
|
|
702
|
-
if (q) {
|
|
703
|
-
const hay = (ridStr + ' ' + String(r.uid || '') + ' ' + notes).toLowerCase();
|
|
704
|
-
if (!hay.includes(q.toLowerCase())) continue;
|
|
705
|
-
}
|
|
706
702
|
|
|
703
|
+
async function renderAdminReservationsPage(req, res) {
|
|
704
|
+
const isAdmin = await user.isAdminOrGlobalMod(req.uid);
|
|
705
|
+
if (!isAdmin) return res.status(403).render('403', {});
|
|
706
|
+
const ids = await db.getSortedSetRevRange(EC_KEYS.reservationsZset, 0, 199);
|
|
707
|
+
const rowsRaw = await db.getObjects(ids.map(id => EC_KEYS.reservationKey(id)));
|
|
708
|
+
const rows = (rowsRaw || []).filter(Boolean).map((r) => {
|
|
707
709
|
const startMs = parseInt(r.startMs, 10) || 0;
|
|
708
710
|
const endMs = parseInt(r.endMs, 10) || 0;
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
end: endMs ? new Date(endMs).toISOString() : '',
|
|
719
|
-
createdAt: createdAt ? new Date(createdAt).toISOString() : '',
|
|
720
|
-
notesUser: notes,
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const total = rows.length;
|
|
725
|
-
const totalPages = Math.max(1, Math.ceil(total / perPage));
|
|
726
|
-
const safePage = Math.min(page, totalPages);
|
|
727
|
-
const startIndex = (safePage - 1) * perPage;
|
|
728
|
-
const pageRows = rows.slice(startIndex, startIndex + perPage);
|
|
729
|
-
|
|
730
|
-
const itemOptions = [{ id: '', name: 'Tous' }].concat(items.map(i => ({ id: i.id, name: i.name })));
|
|
731
|
-
const statusOptions = [
|
|
732
|
-
{ id: '', name: 'Tous' },
|
|
733
|
-
{ id: 'pending', name: 'pending' },
|
|
734
|
-
{ id: 'approved', name: 'approved' },
|
|
735
|
-
{ id: 'paid', name: 'paid' },
|
|
736
|
-
{ id: 'rejected', name: 'rejected' },
|
|
737
|
-
{ id: 'cancelled', name: 'cancelled' },
|
|
738
|
-
];
|
|
739
|
-
|
|
740
|
-
res.render('admin/plugins/equipment-calendar-reservations', {
|
|
741
|
-
title: 'Equipment Calendar - Réservations',
|
|
742
|
-
settings,
|
|
743
|
-
rows: pageRows,
|
|
744
|
-
hasRows: pageRows.length > 0,
|
|
745
|
-
itemOptions: itemOptions.map(o => ({ ...o, selected: o.id === itemId })),
|
|
746
|
-
statusOptions: statusOptions.map(o => ({ ...o, selected: o.id === status })),
|
|
747
|
-
q,
|
|
748
|
-
page: safePage,
|
|
749
|
-
perPage,
|
|
750
|
-
total,
|
|
751
|
-
totalAll,
|
|
752
|
-
totalPages,
|
|
753
|
-
prevPage: safePage > 1 ? safePage - 1 : 0,
|
|
754
|
-
nextPage: safePage < totalPages ? safePage + 1 : 0,
|
|
755
|
-
actionBase: '/admin/plugins/equipment-calendar/reservations',
|
|
711
|
+
return {
|
|
712
|
+
id: r.id,
|
|
713
|
+
itemName: r.itemName || r.itemId,
|
|
714
|
+
startIso: startMs ? new Date(startMs).toISOString().slice(0,10) : '',
|
|
715
|
+
endIso: endMs ? new Date(endMs - 24*60*60*1000).toISOString().slice(0,10) : '',
|
|
716
|
+
days: parseInt(r.days, 10) || 1,
|
|
717
|
+
status: r.status || 'pending',
|
|
718
|
+
total: r.total || '0',
|
|
719
|
+
};
|
|
756
720
|
});
|
|
721
|
+
res.render('admin/plugins/equipment-calendar-reservations', { title: 'Réservations', rows, hasRows: rows.length>0 });
|
|
757
722
|
}
|
|
758
723
|
|
|
759
724
|
async function handleAdminApprove(req, res) {
|
|
@@ -978,6 +943,8 @@ async function renderCalendarPage(req, res) {
|
|
|
978
943
|
}));
|
|
979
944
|
|
|
980
945
|
res.render('equipment-calendar/calendar', {
|
|
946
|
+
defaultView: settings.defaultView,
|
|
947
|
+
|
|
981
948
|
title: 'Réservation de matériel',
|
|
982
949
|
items,
|
|
983
950
|
view,
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/js/client.js
CHANGED
|
@@ -93,10 +93,12 @@ 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,listMonth,listWeek'},
|
|
97
|
+
initialView: (window.EC_DEFAULT_VIEW || 'dayGridMonth'),
|
|
97
98
|
selectable: canCreate,
|
|
98
99
|
selectMirror: true,
|
|
99
100
|
events,
|
|
101
|
+
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
102
|
dateClick: function (info) {
|
|
101
103
|
if (!canCreate) return;
|
|
102
104
|
const startMs = Date.UTC(info.date.getUTCFullYear(), info.date.getUTCMonth(), info.date.getUTCDate());
|
|
@@ -35,7 +35,20 @@
|
|
|
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="listMonth" {{{ if view_listMonth }}}selected{{{ end }}}>Liste (mois)</option>
|
|
46
|
+
<option value="listWeek" {{{ if view_listWeek }}}selected{{{ end }}}>Liste (semaine)</option>
|
|
47
|
+
</select>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="card card-body mb-3">
|
|
39
52
|
<h5>HelloAsso</h5>
|
|
40
53
|
<div class="mb-3">
|
|
41
54
|
<label class="form-label">API Base URL (prod/sandbox)</label>
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
</div>
|
|
75
75
|
|
|
76
76
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js"></script>
|
|
77
|
+
<script>window.EC_DEFAULT_VIEW = '{defaultView}';</script>
|
|
77
78
|
<script src="{relative_path}/plugins/nodebb-plugin-equipment-calendar/js/client.js"></script>
|
|
78
79
|
|
|
79
80
|
<script>
|
|
@@ -82,3 +83,5 @@
|
|
|
82
83
|
window.EC_CAN_CREATE = {canCreateJs};
|
|
83
84
|
window.EC_TZ = '{tz}';
|
|
84
85
|
</script>
|
|
86
|
+
|
|
87
|
+
<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>
|