nodebb-plugin-equipment-calendar 3.0.0 → 5.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 +142 -156
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/js/client.js +2 -3
- package/public/templates/admin/plugins/equipment-calendar-reservations.tpl +26 -81
- package/public/templates/admin/plugins/equipment-calendar.tpl +4 -4
- package/public/templates/equipment-calendar/calendar.tpl +9 -4
package/library.js
CHANGED
|
@@ -17,19 +17,6 @@ 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
|
-
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
20
|
function generateId() {
|
|
34
21
|
try {
|
|
35
22
|
// Node 14+ / modern: crypto.randomUUID
|
|
@@ -51,6 +38,7 @@ const SETTINGS_KEY = 'equipmentCalendar';
|
|
|
51
38
|
const EC_KEYS = {
|
|
52
39
|
reservationsZset: 'ec:reservations',
|
|
53
40
|
reservationKey: (id) => `ec:reservation:${id}`,
|
|
41
|
+
itemZset: (itemId) => `ec:item:${itemId}`,
|
|
54
42
|
};
|
|
55
43
|
|
|
56
44
|
const DEFAULT_SETTINGS = {
|
|
@@ -665,6 +653,9 @@ plugin.init = async function (params) {
|
|
|
665
653
|
|
|
666
654
|
// Page routes are attached via filter:router.page as well, but we also add directly to be safe:
|
|
667
655
|
router.get('/equipment/calendar', middleware.buildHeader, renderCalendarPage);
|
|
656
|
+
router.get('/calendar', middleware.buildHeader, renderCalendarPage);
|
|
657
|
+
router.get('/api/calendar', middleware.buildHeader, renderCalendarPage);
|
|
658
|
+
|
|
668
659
|
router.get('/equipment/approvals', middleware.buildHeader, renderApprovalsPage);
|
|
669
660
|
|
|
670
661
|
router.post('/equipment/reservations/create', middleware.applyCSRF, handleCreateReservation);
|
|
@@ -700,27 +691,6 @@ plugin.addAdminRoutes = async function (params) {
|
|
|
700
691
|
|
|
701
692
|
|
|
702
693
|
|
|
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) => {
|
|
709
|
-
const startMs = parseInt(r.startMs, 10) || 0;
|
|
710
|
-
const endMs = parseInt(r.endMs, 10) || 0;
|
|
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
|
-
};
|
|
720
|
-
});
|
|
721
|
-
res.render('admin/plugins/equipment-calendar-reservations', { title: 'Réservations', rows, hasRows: rows.length>0 });
|
|
722
|
-
}
|
|
723
|
-
|
|
724
694
|
async function handleAdminApprove(req, res) {
|
|
725
695
|
if (!(await ensureIsAdmin(req, res))) return;
|
|
726
696
|
const rid = String(req.params.rid || '').trim();
|
|
@@ -879,88 +849,6 @@ async function handleHelloAssoCallback(req, res) {
|
|
|
879
849
|
});
|
|
880
850
|
}
|
|
881
851
|
|
|
882
|
-
async function renderCalendarPage(req, res) {
|
|
883
|
-
const settings = await getSettings();
|
|
884
|
-
const items = await getActiveItems(settings);
|
|
885
|
-
|
|
886
|
-
const tz = settings.timezone || 'Europe/Paris';
|
|
887
|
-
|
|
888
|
-
// Determine range to render
|
|
889
|
-
const now = DateTime.now().setZone(tz);
|
|
890
|
-
const view = String(req.query.view || settings.defaultView || 'dayGridMonth');
|
|
891
|
-
const startQ = req.query.start;
|
|
892
|
-
const endQ = req.query.end;
|
|
893
|
-
|
|
894
|
-
let start, end;
|
|
895
|
-
if (startQ && endQ) {
|
|
896
|
-
const r = clampRange(String(startQ), String(endQ), tz);
|
|
897
|
-
start = r.start;
|
|
898
|
-
end = r.end;
|
|
899
|
-
} else {
|
|
900
|
-
// Default to current month range
|
|
901
|
-
start = now.startOf('month');
|
|
902
|
-
end = now.endOf('month').plus({ days: 1 });
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// Load reservations for ALL items within range (so we can build availability client-side without extra requests)
|
|
906
|
-
const allReservations = [];
|
|
907
|
-
for (const it of items) {
|
|
908
|
-
// eslint-disable-next-line no-await-in-loop
|
|
909
|
-
const resForItem = await listReservationsForRange(it.id, start.toMillis(), end.toMillis());
|
|
910
|
-
for (const r of resForItem) allReservations.push(r);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
const showRequesterToAll = String(settings.showRequesterToAll) === '1';
|
|
914
|
-
const isApprover = req.uid ? await canApprove(req.uid, settings) : false;
|
|
915
|
-
|
|
916
|
-
const requesterUids = Array.from(new Set(allReservations.map(r => r.uid))).filter(Boolean);
|
|
917
|
-
const users = requesterUids.length ? await user.getUsersData(requesterUids) : [];
|
|
918
|
-
const nameByUid = {};
|
|
919
|
-
(users || []).forEach(u => { nameByUid[u.uid] = u.username || ''; });
|
|
920
|
-
|
|
921
|
-
const itemById = {};
|
|
922
|
-
items.forEach(it => { itemById[it.id] = it; });
|
|
923
|
-
|
|
924
|
-
const events = allReservations
|
|
925
|
-
.filter(r => r.status !== 'rejected' && r.status !== 'cancelled')
|
|
926
|
-
.map(r => {
|
|
927
|
-
const item = itemById[r.itemId];
|
|
928
|
-
// Include item name in title so "all items" view is readable
|
|
929
|
-
const requesterName = nameByUid[r.uid] || '';
|
|
930
|
-
return toEvent(r, item, requesterName, isApprover || showRequesterToAll);
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
const canUserCreate = req.uid ? await canCreate(req.uid, settings) : false;
|
|
934
|
-
|
|
935
|
-
// We expose minimal reservation data for availability checks in the modal
|
|
936
|
-
const blocks = allReservations
|
|
937
|
-
.filter(r => statusBlocksItem(r.status))
|
|
938
|
-
.map(r => ({
|
|
939
|
-
itemId: r.itemId,
|
|
940
|
-
startMs: r.startMs,
|
|
941
|
-
endMs: r.endMs,
|
|
942
|
-
status: r.status,
|
|
943
|
-
}));
|
|
944
|
-
|
|
945
|
-
res.render('equipment-calendar/calendar', {
|
|
946
|
-
defaultView: settings.defaultView,
|
|
947
|
-
|
|
948
|
-
title: 'Réservation de matériel',
|
|
949
|
-
items,
|
|
950
|
-
view,
|
|
951
|
-
tz,
|
|
952
|
-
startISO: start.toISO(),
|
|
953
|
-
endISO: end.toISO(),
|
|
954
|
-
initialDateISO: start.toISODate(),
|
|
955
|
-
canCreate: canUserCreate,
|
|
956
|
-
canCreateJs: canUserCreate ? 'true' : 'false',
|
|
957
|
-
isApprover,
|
|
958
|
-
// events are base64 encoded to avoid template escaping issues
|
|
959
|
-
eventsB64: Buffer.from(JSON.stringify(events), 'utf8').toString('base64'),
|
|
960
|
-
blocksB64: Buffer.from(JSON.stringify(blocks), 'utf8').toString('base64'),
|
|
961
|
-
itemsB64: Buffer.from(JSON.stringify(items.map(i => ({ id: i.id, name: i.name, location: i.location }))), 'utf8').toString('base64'),
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
852
|
|
|
965
853
|
|
|
966
854
|
// --- Approvals page ---
|
|
@@ -1121,52 +1009,70 @@ async function createReservationForItem(req, res, settings, itemId, startMs, end
|
|
|
1121
1009
|
|
|
1122
1010
|
async function handleCreateReservation(req, res) {
|
|
1123
1011
|
try {
|
|
1124
|
-
|
|
1125
|
-
if (!req.uid || !(await canCreate(req.uid, settings))) {
|
|
1126
|
-
return helpers.notAllowed(req, res);
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
const itemIdsRaw = req.body.itemIds || req.body.itemId || '';
|
|
1130
|
-
const itemIds = (Array.isArray(itemIdsRaw) ? itemIdsRaw : String(itemIdsRaw))
|
|
1131
|
-
.split(',')
|
|
1132
|
-
.map(s => String(s).trim())
|
|
1133
|
-
.filter(Boolean);
|
|
1134
|
-
|
|
1135
|
-
if (!itemIds.length) {
|
|
1136
|
-
return res.status(400).send('itemIds required');
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
const tz = settings.timezone || 'Europe/Paris';
|
|
1140
|
-
const start = DateTime.fromISO(String(req.body.start || ''), { zone: tz });
|
|
1141
|
-
const end = DateTime.fromISO(String(req.body.end || ''), { zone: tz });
|
|
1142
|
-
if (!start.isValid || !end.isValid) return res.status(400).send('Invalid dates');
|
|
1143
|
-
|
|
1144
|
-
// Normalize to whole days (no hours). End is exclusive.
|
|
1145
|
-
const startDay = start.startOf('day');
|
|
1146
|
-
let endDay = end.startOf('day');
|
|
1147
|
-
if (endDay <= startDay) {
|
|
1148
|
-
endDay = startDay.plus({ days: 1 });
|
|
1149
|
-
}
|
|
1012
|
+
if (!req.uid) return res.status(403).json({ error: 'not-logged-in' });
|
|
1150
1013
|
|
|
1151
|
-
const
|
|
1152
|
-
const
|
|
1014
|
+
const settings = await getSettings();
|
|
1015
|
+
const canCreate = await isUserInAnyGroups(req.uid, settings.creatorGroups);
|
|
1016
|
+
if (!canCreate) return res.status(403).json({ error: 'forbidden' });
|
|
1153
1017
|
|
|
1154
1018
|
const notesUser = String(req.body.notesUser || '').trim();
|
|
1155
|
-
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1019
|
+
const itemIdsRaw = req.body.itemIds || req.body.itemId || [];
|
|
1020
|
+
const itemIds = Array.isArray(itemIdsRaw) ? itemIdsRaw : String(itemIdsRaw || '').split(',').filter(Boolean);
|
|
1021
|
+
|
|
1022
|
+
const startMsRaw = parseInt(req.body.startMs, 10) || 0;
|
|
1023
|
+
const endMsRaw = parseInt(req.body.endMs, 10) || 0;
|
|
1024
|
+
|
|
1025
|
+
const startDate = startMsRaw ? new Date(startMsRaw) : null;
|
|
1026
|
+
const endDate = endMsRaw ? new Date(endMsRaw) : null;
|
|
1027
|
+
const startMs = startDate ? Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate()) : 0;
|
|
1028
|
+
let endMs = 0;
|
|
1029
|
+
if (endDate) {
|
|
1030
|
+
endMs = Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate());
|
|
1031
|
+
if (endMs <= startMs) endMs = startMs + 24 * 60 * 60 * 1000;
|
|
1032
|
+
}
|
|
1033
|
+
if (!startMs || !endMs) return res.status(400).json({ error: 'invalid-dates' });
|
|
1034
|
+
|
|
1035
|
+
const days = Math.max(1, Math.round((endMs - startMs) / (24 * 60 * 60 * 1000)));
|
|
1036
|
+
|
|
1037
|
+
const items = await getActiveItems();
|
|
1038
|
+
const byId = {};
|
|
1039
|
+
items.forEach((it) => { byId[String(it.id)] = it; });
|
|
1040
|
+
|
|
1041
|
+
const valid = itemIds.map(String).filter((id) => byId[id]);
|
|
1042
|
+
if (!valid.length) return res.status(400).json({ error: 'unknown-item' });
|
|
1043
|
+
|
|
1044
|
+
const bookingId = generateId();
|
|
1045
|
+
for (const itemId of valid) {
|
|
1046
|
+
const it = byId[itemId];
|
|
1047
|
+
const id = generateId();
|
|
1048
|
+
const unitPrice = Number(it.price || 0) || 0;
|
|
1049
|
+
const total = unitPrice * days;
|
|
1050
|
+
|
|
1051
|
+
const reservation = {
|
|
1052
|
+
id,
|
|
1053
|
+
bookingId,
|
|
1054
|
+
uid: req.uid,
|
|
1055
|
+
itemId,
|
|
1056
|
+
itemName: it.name,
|
|
1057
|
+
unitPrice,
|
|
1058
|
+
days,
|
|
1059
|
+
total,
|
|
1060
|
+
notesUser,
|
|
1061
|
+
startMs,
|
|
1062
|
+
endMs,
|
|
1063
|
+
status: 'pending',
|
|
1064
|
+
createdAt: Date.now(),
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
await db.setObject(EC_KEYS.reservationKey(id), reservation);
|
|
1068
|
+
await db.sortedSetAdd(EC_KEYS.reservationsZset, startMs, id);
|
|
1069
|
+
await db.sortedSetAdd(EC_KEYS.itemZset(itemId), startMs, id);
|
|
1163
1070
|
}
|
|
1164
1071
|
|
|
1165
|
-
|
|
1166
|
-
return res.redirect('/equipment/calendar?requested=1');
|
|
1072
|
+
res.redirect(nconf.get('relative_path') + '/calendar?created=1&bookingId=' + bookingId);
|
|
1167
1073
|
} catch (err) {
|
|
1168
|
-
winston.error(err);
|
|
1169
|
-
|
|
1074
|
+
winston.error('[equipment-calendar] create error', err);
|
|
1075
|
+
res.status(500).json({ error: 'internal-error' });
|
|
1170
1076
|
}
|
|
1171
1077
|
}
|
|
1172
1078
|
|
|
@@ -1415,3 +1321,83 @@ function startPaymentTimeoutScheduler() {
|
|
|
1415
1321
|
}
|
|
1416
1322
|
|
|
1417
1323
|
module.exports = plugin;
|
|
1324
|
+
|
|
1325
|
+
async function handleAdminReservationAction(req, res, action) {
|
|
1326
|
+
const isAdmin = await user.isAdminOrGlobalMod(req.uid);
|
|
1327
|
+
if (!isAdmin) return res.status(403).json({ error: 'forbidden' });
|
|
1328
|
+
|
|
1329
|
+
const id = String(req.params.id || '');
|
|
1330
|
+
const key = EC_KEYS.reservationKey(id);
|
|
1331
|
+
const r = await db.getObject(key);
|
|
1332
|
+
if (!r || !r.id) return res.status(404).json({ error: 'not-found' });
|
|
1333
|
+
|
|
1334
|
+
if (action === 'delete') {
|
|
1335
|
+
await db.delete(key);
|
|
1336
|
+
await db.sortedSetRemove(EC_KEYS.reservationsZset, id);
|
|
1337
|
+
if (r.itemId) await db.sortedSetRemove(EC_KEYS.itemZset(r.itemId), id);
|
|
1338
|
+
return res.redirect(nconf.get('relative_path') + '/admin/plugins/equipment-calendar/reservations?done=1');
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const status = action === 'approve' ? 'approved' : 'cancelled';
|
|
1342
|
+
await db.setObjectField(key, 'status', status);
|
|
1343
|
+
return res.redirect(nconf.get('relative_path') + '/admin/plugins/equipment-calendar/reservations?done=1');
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
async function renderAdminReservationsPage(req, res) {
|
|
1347
|
+
const isAdmin = await user.isAdminOrGlobalMod(req.uid);
|
|
1348
|
+
if (!isAdmin) return res.status(403).render('403', {});
|
|
1349
|
+
const ids = await db.getSortedSetRevRange(EC_KEYS.reservationsZset, 0, 199);
|
|
1350
|
+
const rowsRaw = await db.getObjects(ids.map(id => EC_KEYS.reservationKey(id)));
|
|
1351
|
+
const rows = (rowsRaw || []).filter(Boolean).map((r) => {
|
|
1352
|
+
const startMs = parseInt(r.startMs, 10) || 0;
|
|
1353
|
+
const endMs = parseInt(r.endMs, 10) || 0;
|
|
1354
|
+
return {
|
|
1355
|
+
id: r.id,
|
|
1356
|
+
bookingId: r.bookingId || '',
|
|
1357
|
+
itemName: r.itemName || r.itemId,
|
|
1358
|
+
startIso: startMs ? new Date(startMs).toISOString().slice(0,10) : '',
|
|
1359
|
+
endIso: endMs ? new Date(endMs - 24*60*60*1000).toISOString().slice(0,10) : '',
|
|
1360
|
+
days: parseInt(r.days, 10) || 1,
|
|
1361
|
+
status: r.status || 'pending',
|
|
1362
|
+
total: (Number(r.total || 0) || 0),
|
|
1363
|
+
};
|
|
1364
|
+
});
|
|
1365
|
+
res.render('admin/plugins/equipment-calendar-reservations', { title: 'Réservations', rows, hasRows: rows.length>0, csrf: req.csrfToken(), done: String(req.query.done||'')==='1' });
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
async function getCalendarEvents() {
|
|
1369
|
+
const ids = await db.getSortedSetRange(EC_KEYS.reservationsZset, 0, -1);
|
|
1370
|
+
const rows = await db.getObjects(ids.map(id => EC_KEYS.reservationKey(id)));
|
|
1371
|
+
return (rows || []).filter(Boolean).map((r) => {
|
|
1372
|
+
const startMs = parseInt(r.startMs, 10) || 0;
|
|
1373
|
+
const endMs = parseInt(r.endMs, 10) || 0;
|
|
1374
|
+
const status = r.status || 'pending';
|
|
1375
|
+
return {
|
|
1376
|
+
id: r.id,
|
|
1377
|
+
title: (r.itemName || 'Réservation'),
|
|
1378
|
+
start: startMs ? new Date(startMs).toISOString().slice(0,10) : null,
|
|
1379
|
+
end: endMs ? new Date(endMs).toISOString().slice(0,10) : null,
|
|
1380
|
+
allDay: true,
|
|
1381
|
+
classNames: ['ec-status-' + status],
|
|
1382
|
+
extendedProps: { status, bookingId: r.bookingId || '' },
|
|
1383
|
+
};
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
async function renderCalendarPage(req, res) {
|
|
1388
|
+
const settings = await getSettings();
|
|
1389
|
+
const canCreate = req.uid ? await isUserInAnyGroups(req.uid, settings.creatorGroups) : false;
|
|
1390
|
+
const isApprover = req.uid ? await isUserInAnyGroups(req.uid, settings.approverGroup) : false;
|
|
1391
|
+
const items = await getActiveItems();
|
|
1392
|
+
const events = await getCalendarEvents();
|
|
1393
|
+
|
|
1394
|
+
res.render('equipment-calendar/calendar', {
|
|
1395
|
+
title: 'Calendrier',
|
|
1396
|
+
canCreate,
|
|
1397
|
+
isApprover,
|
|
1398
|
+
items,
|
|
1399
|
+
eventsJson: JSON.stringify(events),
|
|
1400
|
+
defaultView: settings.defaultView || 'dayGridMonth',
|
|
1401
|
+
created: String(req.query.created || '') === '1',
|
|
1402
|
+
});
|
|
1403
|
+
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/js/client.js
CHANGED
|
@@ -93,12 +93,11 @@ 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
|
-
|
|
97
|
-
|
|
96
|
+
locale: 'fr',
|
|
97
|
+
headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,dayGridWeek,listMonth,listWeek' },
|
|
98
98
|
selectable: canCreate,
|
|
99
99
|
selectMirror: true,
|
|
100
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){}},
|
|
102
101
|
dateClick: function (info) {
|
|
103
102
|
if (!canCreate) return;
|
|
104
103
|
const startMs = Date.UTC(info.date.getUTCFullYear(), info.date.getUTCMonth(), info.date.getUTCDate());
|
|
@@ -1,104 +1,49 @@
|
|
|
1
|
+
|
|
1
2
|
<div class="acp-page-container">
|
|
2
3
|
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
|
3
|
-
<h1 class="mb-0">
|
|
4
|
-
<
|
|
5
|
-
<a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar">Paramètres</a>
|
|
6
|
-
<a class="btn btn-secondary active" href="/admin/plugins/equipment-calendar/reservations">Réservations</a>
|
|
7
|
-
</div>
|
|
4
|
+
<h1 class="mb-0">Réservations</h1>
|
|
5
|
+
<a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar">Paramètres</a>
|
|
8
6
|
</div>
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{{{ each statusOptions }}}
|
|
16
|
-
<option value="{statusOptions.id}" {{{ if statusOptions.selected }}}selected{{{ end }}}>{statusOptions.name}</option>
|
|
17
|
-
{{{ end }}}
|
|
18
|
-
</select>
|
|
19
|
-
</div>
|
|
20
|
-
<div class="col-md-4">
|
|
21
|
-
<label class="form-label">Matériel</label>
|
|
22
|
-
<select class="form-select" name="itemId">
|
|
23
|
-
{{{ each itemOptions }}}
|
|
24
|
-
<option value="{itemOptions.id}" {{{ if itemOptions.selected }}}selected{{{ end }}}>{itemOptions.name}</option>
|
|
25
|
-
{{{ end }}}
|
|
26
|
-
</select>
|
|
27
|
-
</div>
|
|
28
|
-
<div class="col-md-3">
|
|
29
|
-
<label class="form-label">Recherche</label>
|
|
30
|
-
<input class="form-control" name="q" value="{q}" placeholder="rid, uid, note">
|
|
31
|
-
</div>
|
|
32
|
-
<div class="col-md-2">
|
|
33
|
-
<button class="btn btn-primary w-100" type="submit">Filtrer</button>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</form>
|
|
37
|
-
|
|
38
|
-
<div class="text-muted small mb-2">
|
|
39
|
-
Total (filtré) : <strong>{total}</strong> — Total (global) : <strong>{totalAll}</strong>
|
|
40
|
-
</div>
|
|
8
|
+
{{{ if done }}}
|
|
9
|
+
<script>
|
|
10
|
+
if (window.app && window.app.alertSuccess) window.app.alertSuccess('Action effectuée');
|
|
11
|
+
</script>
|
|
12
|
+
{{{ end }}}
|
|
41
13
|
|
|
42
14
|
{{{ if hasRows }}}
|
|
43
|
-
<div class="table-responsive">
|
|
15
|
+
<div class="table-responsive mt-3">
|
|
44
16
|
<table class="table table-striped align-middle">
|
|
45
|
-
<thead>
|
|
46
|
-
<tr>
|
|
47
|
-
<th>RID</th>
|
|
48
|
-
<th>Matériel</th>
|
|
49
|
-
<th>UID</th>
|
|
50
|
-
<th>Début</th>
|
|
51
|
-
<th>Fin</th>
|
|
52
|
-
<th>Statut</th>
|
|
53
|
-
<th>Créé</th>
|
|
54
|
-
<th>Note</th>
|
|
55
|
-
<th>Actions</th>
|
|
56
|
-
</tr>
|
|
57
|
-
</thead>
|
|
17
|
+
<thead><tr><th>ID</th><th>Matériel</th><th>Période</th><th>Jours</th><th>Statut</th><th>Total</th><th class="text-end">Actions</th></tr></thead>
|
|
58
18
|
<tbody>
|
|
59
|
-
|
|
19
|
+
<!-- BEGIN rows -->
|
|
60
20
|
<tr>
|
|
61
|
-
<td><code>{rows.
|
|
21
|
+
<td><code>{rows.id}</code><br><small class="text-muted">{rows.bookingId}</small></td>
|
|
62
22
|
<td>{rows.itemName}</td>
|
|
63
|
-
<td>{rows.
|
|
64
|
-
<td
|
|
65
|
-
<td><
|
|
66
|
-
<td
|
|
67
|
-
<td
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<input type="hidden" name="_csrf" value="{config.csrf_token}">
|
|
72
|
-
<button class="btn btn-sm btn-success" type="submit">Approve</button>
|
|
23
|
+
<td>{rows.startIso} → {rows.endIso}</td>
|
|
24
|
+
<td>{rows.days}</td>
|
|
25
|
+
<td><span class="badge text-bg-secondary">{rows.status}</span></td>
|
|
26
|
+
<td>{rows.total} €</td>
|
|
27
|
+
<td class="text-end">
|
|
28
|
+
<form class="d-inline" method="post" action="/admin/plugins/equipment-calendar/reservations/{rows.id}/approve">
|
|
29
|
+
<input type="hidden" name="_csrf" value="{csrf}">
|
|
30
|
+
<button class="btn btn-sm btn-primary" type="submit">Approve</button>
|
|
73
31
|
</form>
|
|
74
|
-
<form method="post" action="/admin/plugins/equipment-calendar/reservations/{rows.
|
|
75
|
-
<input type="hidden" name="_csrf" value="{
|
|
32
|
+
<form class="d-inline" method="post" action="/admin/plugins/equipment-calendar/reservations/{rows.id}/reject">
|
|
33
|
+
<input type="hidden" name="_csrf" value="{csrf}">
|
|
76
34
|
<button class="btn btn-sm btn-warning" type="submit">Reject</button>
|
|
77
35
|
</form>
|
|
78
|
-
<form method="post" action="/admin/plugins/equipment-calendar/reservations/{rows.
|
|
79
|
-
<input type="hidden" name="_csrf" value="{
|
|
36
|
+
<form class="d-inline" method="post" action="/admin/plugins/equipment-calendar/reservations/{rows.id}/delete" onsubmit="return confirm('Supprimer ?');">
|
|
37
|
+
<input type="hidden" name="_csrf" value="{csrf}">
|
|
80
38
|
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
|
81
39
|
</form>
|
|
82
40
|
</td>
|
|
83
41
|
</tr>
|
|
84
|
-
|
|
42
|
+
<!-- END rows -->
|
|
85
43
|
</tbody>
|
|
86
44
|
</table>
|
|
87
45
|
</div>
|
|
88
|
-
|
|
89
|
-
<div class="d-flex justify-content-between align-items-center mt-3">
|
|
90
|
-
<div>Page {page} / {totalPages}</div>
|
|
91
|
-
<div class="btn-group">
|
|
92
|
-
{{{ if prevPage }}}
|
|
93
|
-
<a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/reservations?page={prevPage}&perPage={perPage}">Précédent</a>
|
|
94
|
-
{{{ end }}}
|
|
95
|
-
{{{ if nextPage }}}
|
|
96
|
-
<a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/reservations?page={nextPage}&perPage={perPage}">Suivant</a>
|
|
97
|
-
{{{ end }}}
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
46
|
{{{ else }}}
|
|
102
|
-
|
|
47
|
+
<div class="alert alert-info mt-3">Aucune réservation.</div>
|
|
103
48
|
{{{ end }}}
|
|
104
49
|
</div>
|
|
@@ -35,15 +35,15 @@
|
|
|
35
35
|
</div>
|
|
36
36
|
</div>
|
|
37
37
|
|
|
38
|
-
|
|
39
38
|
<div class="card card-body mb-3">
|
|
40
39
|
<h5>Affichage calendrier</h5>
|
|
41
40
|
<div class="mb-0">
|
|
42
41
|
<label class="form-label">Vue par défaut</label>
|
|
43
42
|
<select class="form-select" name="defaultView">
|
|
44
|
-
<option value="dayGridMonth"
|
|
45
|
-
<option value="
|
|
46
|
-
<option value="
|
|
43
|
+
<option value="dayGridMonth">Mois</option>
|
|
44
|
+
<option value="dayGridWeek">Semaine</option>
|
|
45
|
+
<option value="listMonth">Liste (mois)</option>
|
|
46
|
+
<option value="listWeek">Liste (semaine)</option>
|
|
47
47
|
</select>
|
|
48
48
|
</div>
|
|
49
49
|
</div>
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
<label class="form-label">Matériel</label>
|
|
44
44
|
<select class="form-select" id="ec-item-ids" name="itemIds" multiple required>
|
|
45
45
|
<!-- BEGIN items -->
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
<option value="{items.id}" data-price="{items.price}">{items.name} — {items.price} €</option>
|
|
47
|
+
<!-- END items -->
|
|
48
48
|
</select>
|
|
49
49
|
<div class="form-text">Tu peux sélectionner plusieurs matériels.</div>
|
|
50
50
|
</div>
|
|
@@ -74,7 +74,6 @@
|
|
|
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>
|
|
78
77
|
<script src="{relative_path}/plugins/nodebb-plugin-equipment-calendar/js/client.js"></script>
|
|
79
78
|
|
|
80
79
|
<script>
|
|
@@ -84,4 +83,10 @@
|
|
|
84
83
|
window.EC_TZ = '{tz}';
|
|
85
84
|
</script>
|
|
86
85
|
|
|
87
|
-
<style
|
|
86
|
+
<style>
|
|
87
|
+
.fc-event.ec-status-pending { background: rgba(108,117,125,.18); border-color: rgba(108,117,125,.45); }
|
|
88
|
+
.fc-event.ec-status-approved { background: rgba(13,110,253,.18); border-color: rgba(13,110,253,.45); }
|
|
89
|
+
.fc-event.ec-status-paid { background: rgba(25,135,84,.18); border-color: rgba(25,135,84,.45); }
|
|
90
|
+
.fc-event.ec-status-cancelled { background: rgba(220,53,69,.10); border-color: rgba(220,53,69,.35); opacity:.65; text-decoration: line-through; }
|
|
91
|
+
.fc .fc-event-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
92
|
+
</style>
|