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 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
- router.get('/equipment/calendar', middleware.buildHeader, renderCalendarPage);
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: startMs ? new Date(startMs).toISOString() : '',
834
- end: endMs ? new Date(endMs).toISOString() : '',
835
- createdAt: createdAt ? new Date(createdAt).toISOString() : '',
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: DateTime.fromMillis(r.startMs).toFormat('dd/LL/yyyy HH:mm'),
1223
- end: DateTime.fromMillis(r.endMs).toFormat('dd/LL/yyyy HH:mm'),
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: Date.now(),
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: new Date(startMs).toISOString().slice(0, 10),
1323
- endIso: new Date(endMs - 24*60*60*1000).toISOString().slice(0, 10),
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: Date.now(),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-equipment-calendar",
3
- "version": "8.1.8",
3
+ "version": "8.8.9",
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": "3.0.0-stable4f-parisdates",
28
+ "version": "3.0.0-stable4j-calendar-actions",
29
29
  "minver": "4.7.1"
30
30
  }
@@ -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
  });
@@ -82,3 +82,8 @@
82
82
  window.EC_CAN_CREATE = {canCreateJs};
83
83
  window.EC_TZ = '{tz}';
84
84
  </script>
85
+
86
+ <script>
87
+ window.EC_CSRF = '{csrf_token}';
88
+ window.EC_IS_APPROVER = {isApprover};
89
+ </script>