nodebb-plugin-equipment-calendar 8.1.8 → 8.8.9
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 +63 -10
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/js/client.js +116 -0
- package/public/templates/equipment-calendar/calendar.tpl +5 -0
package/library.js
CHANGED
|
@@ -462,6 +462,26 @@ async function getReservation(rid) {
|
|
|
462
462
|
return await db.getObject(resKey(id));
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
|
|
466
|
+
async function setReservationStatus(rid, status) {
|
|
467
|
+
const r = await getReservation(rid);
|
|
468
|
+
if (!r) throw new Error('not_found');
|
|
469
|
+
r.status = status;
|
|
470
|
+
await db.setObject(resKey(rid), r);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function deleteReservation(rid) {
|
|
474
|
+
const r = await getReservation(rid);
|
|
475
|
+
if (!r) return;
|
|
476
|
+
// remove from global index
|
|
477
|
+
try { await db.sortedSetRemove('equipmentCalendar:reservations', rid); } catch (e) {}
|
|
478
|
+
// remove from item index
|
|
479
|
+
try { await db.sortedSetRemove(itemIndexKey(r.itemId), rid); } catch (e) {}
|
|
480
|
+
// delete object
|
|
481
|
+
await db.delete(resKey(rid));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
|
|
465
485
|
function normalizeReservation(obj) {
|
|
466
486
|
const startMs = Number(obj.startMs);
|
|
467
487
|
const endMs = Number(obj.endMs);
|
|
@@ -751,7 +771,36 @@ plugin.init = async function (params) {
|
|
|
751
771
|
});
|
|
752
772
|
|
|
753
773
|
// Page routes are attached via filter:router.page as well, but we also add directly to be safe:
|
|
754
|
-
|
|
774
|
+
|
|
775
|
+
// Calendar inline actions (approve/reject/delete) via API
|
|
776
|
+
router.post('/api/equipment/reservations/:rid/action', middleware.applyCSRF, async (req, res) => {
|
|
777
|
+
try {
|
|
778
|
+
const settings = await getSettings();
|
|
779
|
+
const rid = String(req.params.rid || '').trim();
|
|
780
|
+
const action = String(req.body.action || '').trim();
|
|
781
|
+
if (!rid || !action) return res.status(400).json({ error: 'missing' });
|
|
782
|
+
|
|
783
|
+
// Only approvers/admins can moderate
|
|
784
|
+
const allowed = await isApprover(req.uid, settings);
|
|
785
|
+
if (!allowed) return res.status(403).json({ error: 'forbidden' });
|
|
786
|
+
|
|
787
|
+
if (action === 'approve') {
|
|
788
|
+
await setReservationStatus(rid, 'approved');
|
|
789
|
+
} else if (action === 'reject') {
|
|
790
|
+
await setReservationStatus(rid, 'rejected');
|
|
791
|
+
} else if (action === 'delete') {
|
|
792
|
+
await deleteReservation(rid);
|
|
793
|
+
} else {
|
|
794
|
+
return res.status(400).json({ error: 'bad_action' });
|
|
795
|
+
}
|
|
796
|
+
return res.json({ ok: true });
|
|
797
|
+
} catch (e) {
|
|
798
|
+
winston.error('[equipment-calendar] api action error', e);
|
|
799
|
+
return res.status(500).json({ error: 'server' });
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
router.get('/equipment/calendar', middleware.buildHeader, renderCalendarPage);
|
|
755
804
|
router.get('/equipment/approvals', middleware.buildHeader, renderApprovalsPage);
|
|
756
805
|
|
|
757
806
|
router.post('/equipment/reservations/create', middleware.applyCSRF, handleCreateReservation);
|
|
@@ -830,9 +879,9 @@ async function renderAdminReservationsPage(req, res) {
|
|
|
830
879
|
itemName: (itemById[String(r.itemId || '')] && itemById[String(r.itemId || '')].name) || String(r.itemId || ''),
|
|
831
880
|
uid: String(r.uid || ''),
|
|
832
881
|
status: String(r.status || ''),
|
|
833
|
-
start:
|
|
834
|
-
end:
|
|
835
|
-
createdAt: createdAt
|
|
882
|
+
start: r.startIso,
|
|
883
|
+
end: addDaysIso(r.endIso, 1),
|
|
884
|
+
createdAt: formatDateTimeFR(r.createdAt || 0),
|
|
836
885
|
notesUser: notes,
|
|
837
886
|
});
|
|
838
887
|
}
|
|
@@ -1219,8 +1268,8 @@ async function renderApprovalsPage(req, res) {
|
|
|
1219
1268
|
id: r.id,
|
|
1220
1269
|
itemName: item ? item.name : r.itemId,
|
|
1221
1270
|
requester: nameByUid[r.uid] || `uid:${r.uid}`,
|
|
1222
|
-
start:
|
|
1223
|
-
end:
|
|
1271
|
+
start: r.startIso,
|
|
1272
|
+
end: addDaysIso(r.endIso, 1),
|
|
1224
1273
|
status: r.status,
|
|
1225
1274
|
paymentUrl: r.ha_paymentUrl || '',
|
|
1226
1275
|
};
|
|
@@ -1259,7 +1308,7 @@ async function createReservationForItem(req, res, settings, itemId, startMs, end
|
|
|
1259
1308
|
status: 'pending',
|
|
1260
1309
|
notesUser: notesUser || '',
|
|
1261
1310
|
bookingId: bookingId || '',
|
|
1262
|
-
createdAt:
|
|
1311
|
+
createdAt: formatDateTimeFR(r.createdAt || 0),
|
|
1263
1312
|
};
|
|
1264
1313
|
await saveReservation(data);
|
|
1265
1314
|
if (bookingId) {
|
|
@@ -1289,6 +1338,10 @@ async function handleCreateReservation(req, res) {
|
|
|
1289
1338
|
const end = parseDateInput(endVal);
|
|
1290
1339
|
if (!start || !end) return res.status(400).send('dates required');
|
|
1291
1340
|
|
|
1341
|
+
const startIso = (/^\d{4}-\d{2}-\d{2}$/.test(startIsoInput) ? startIsoInput : `${start.getUTCFullYear()}-${String(start.getUTCMonth()+1).padStart(2,'0')}-${String(start.getUTCDate()).padStart(2,'0')}`);
|
|
1342
|
+
const endIso = (/^\d{4}-\d{2}-\d{2}$/.test(endIsoInput) ? endIsoInput : `${end.getUTCFullYear()}-${String(end.getUTCMonth()+1).padStart(2,'0')}-${String(end.getUTCDate()).padStart(2,'0')}`);
|
|
1343
|
+
|
|
1344
|
+
|
|
1292
1345
|
const startMs = Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate());
|
|
1293
1346
|
let endMs = Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate());
|
|
1294
1347
|
if (endMs <= startMs) endMs = startMs + 24 * 60 * 60 * 1000;
|
|
@@ -1319,14 +1372,14 @@ async function handleCreateReservation(req, res) {
|
|
|
1319
1372
|
itemId,
|
|
1320
1373
|
startMs,
|
|
1321
1374
|
endMs,
|
|
1322
|
-
startIso:
|
|
1323
|
-
endIso:
|
|
1375
|
+
startIso: startIso, 10),
|
|
1376
|
+
endIso: endIso, 10),
|
|
1324
1377
|
days,
|
|
1325
1378
|
unitPrice,
|
|
1326
1379
|
total,
|
|
1327
1380
|
notesUser,
|
|
1328
1381
|
status: 'pending',
|
|
1329
|
-
createdAt:
|
|
1382
|
+
createdAt: formatDateTimeFR(r.createdAt || 0),
|
|
1330
1383
|
};
|
|
1331
1384
|
// eslint-disable-next-line no-await-in-loop
|
|
1332
1385
|
await saveReservation(r);
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/js/client.js
CHANGED
|
@@ -83,4 +83,120 @@ function updateTotalPrice() {
|
|
|
83
83
|
out.textContent = txt + ' €';
|
|
84
84
|
} catch (e) {}
|
|
85
85
|
})();
|
|
86
|
+
|
|
87
|
+
// EC_CALENDAR_INIT
|
|
88
|
+
function addDaysIsoLocal(iso, days) {
|
|
89
|
+
const m = String(iso || '').match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
90
|
+
if (!m) return iso;
|
|
91
|
+
const y = parseInt(m[1], 10);
|
|
92
|
+
const mo = parseInt(m[2], 10) - 1;
|
|
93
|
+
const d = parseInt(m[3], 10);
|
|
94
|
+
const dt = new Date(Date.UTC(y, mo, d));
|
|
95
|
+
dt.setUTCDate(dt.getUTCDate() + (days || 0));
|
|
96
|
+
const pad = (x) => String(x).padStart(2, '0');
|
|
97
|
+
return dt.getUTCFullYear() + '-' + pad(dt.getUTCMonth() + 1) + '-' + pad(dt.getUTCDate());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function openCreateModal(startIso, endIsoInclusive) {
|
|
101
|
+
const modalEl = document.getElementById('ec-create-modal');
|
|
102
|
+
if (!modalEl) return;
|
|
103
|
+
|
|
104
|
+
const startInput = document.getElementById('ec-start-date');
|
|
105
|
+
const endInput = document.getElementById('ec-end-date');
|
|
106
|
+
const hiddenStart = document.getElementById('ec-start-iso');
|
|
107
|
+
const hiddenEnd = document.getElementById('ec-end-iso');
|
|
108
|
+
|
|
109
|
+
if (startInput) startInput.value = startIso;
|
|
110
|
+
if (endInput) endInput.value = endIsoInclusive;
|
|
111
|
+
if (hiddenStart) hiddenStart.value = startIso;
|
|
112
|
+
if (hiddenEnd) hiddenEnd.value = endIsoInclusive;
|
|
113
|
+
|
|
114
|
+
// sync hidden when user edits
|
|
115
|
+
if (startInput && hiddenStart) startInput.onchange = () => { hiddenStart.value = startInput.value; updateTotalPrice(); };
|
|
116
|
+
if (endInput && hiddenEnd) endInput.onchange = () => { hiddenEnd.value = endInput.value; updateTotalPrice(); };
|
|
117
|
+
|
|
118
|
+
updateTotalPrice();
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const modal = bootstrap.Modal.getOrCreateInstance(modalEl, { backdrop: true, keyboard: true });
|
|
122
|
+
modal.show();
|
|
123
|
+
} catch (e) {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function initCalendar() {
|
|
127
|
+
const calEl = document.getElementById('ec-calendar');
|
|
128
|
+
if (!calEl || typeof window.FullCalendar === 'undefined') return;
|
|
129
|
+
|
|
130
|
+
const events = Array.isArray(window.EC_EVENTS) ? window.EC_EVENTS : [];
|
|
131
|
+
const blocks = Array.isArray(window.EC_BLOCKS) ? window.EC_BLOCKS : [];
|
|
132
|
+
|
|
133
|
+
const calendar = new window.FullCalendar.Calendar(calEl, {
|
|
134
|
+
initialView: 'dayGridMonth',
|
|
135
|
+
listDaySideFormat: false,
|
|
136
|
+
listDayFormat: { weekday: 'long', day: '2-digit', month: '2-digit', year: 'numeric' },
|
|
137
|
+
headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,dayGridWeek,listWeek' },
|
|
138
|
+
selectable: !!window.EC_CAN_CREATE,
|
|
139
|
+
selectMirror: true,
|
|
140
|
+
dayMaxEventRows: true,
|
|
141
|
+
displayEventTime: false,
|
|
142
|
+
eventDisplay: 'block',
|
|
143
|
+
eventContent: function(arg) {
|
|
144
|
+
try {
|
|
145
|
+
var status = (arg.event.extendedProps && arg.event.extendedProps.status) ? arg.event.extendedProps.status : '';
|
|
146
|
+
var iconClass = '';
|
|
147
|
+
if (status === 'pending') iconClass = 'fa-hourglass-half text-warning';
|
|
148
|
+
else if (status === 'paid') iconClass = 'fa-check-circle text-success';
|
|
149
|
+
else if (status === 'approved') iconClass = 'fa-check text-primary';
|
|
150
|
+
else if (status === 'rejected' || status === 'cancelled') iconClass = 'fa-times-circle text-danger';
|
|
151
|
+
var wrap = document.createElement('div');
|
|
152
|
+
wrap.className = 'ec-event';
|
|
153
|
+
if (iconClass) {
|
|
154
|
+
var i = document.createElement('i');
|
|
155
|
+
i.className = 'fa ' + iconClass + ' ec-icon';
|
|
156
|
+
wrap.appendChild(i);
|
|
157
|
+
}
|
|
158
|
+
var t = document.createElement('span');
|
|
159
|
+
t.className = 'ec-title';
|
|
160
|
+
t.textContent = arg.event.title || '';
|
|
161
|
+
wrap.appendChild(t);
|
|
162
|
+
return { domNodes: [wrap] };
|
|
163
|
+
} catch (e) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
timeZone: 'local',
|
|
168
|
+
events: events,
|
|
169
|
+
select: function (info) {
|
|
170
|
+
if (!window.EC_CAN_CREATE) return;
|
|
171
|
+
const startIso = info.startStr; // YYYY-MM-DD
|
|
172
|
+
const endIsoInclusive = addDaysIsoLocal(info.endStr, -1); // endStr is exclusive
|
|
173
|
+
openCreateModal(startIso, endIsoInclusive);
|
|
174
|
+
},
|
|
175
|
+
dateClick: function (info) {
|
|
176
|
+
if (!window.EC_CAN_CREATE) return;
|
|
177
|
+
openCreateModal(info.dateStr, info.dateStr);
|
|
178
|
+
},
|
|
179
|
+
eventClick: function (info) {
|
|
180
|
+
// simple details popup
|
|
181
|
+
const r = info.event.extendedProps || {};
|
|
182
|
+
const title = info.event.title || 'Réservation';
|
|
183
|
+
const start = info.event.startStr || '';
|
|
184
|
+
const end = info.event.endStr ? addDaysIsoLocal(info.event.endStr, -1) : '';
|
|
185
|
+
bootbox.alert(`<strong>${title}</strong><br>Du ${start} au ${end}<br>Statut: ${r.status || ''}`);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
calendar.render();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
$(window).on('action:ajaxify.end', function (ev, data) {
|
|
193
|
+
if (data && data.url && data.url.indexOf('/calendar') !== -1) {
|
|
194
|
+
initCalendar();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
$(function () {
|
|
199
|
+
initCalendar();
|
|
200
|
+
});
|
|
201
|
+
|
|
86
202
|
});
|