nodebb-plugin-calendar-onekite 11.2.6 → 11.2.8

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/lib/admin.js CHANGED
@@ -228,6 +228,8 @@ admin.refuseReservation = async function (req, res) {
228
228
  if (!r) return res.status(404).json({ error: 'not-found' });
229
229
 
230
230
  r.status = 'refused';
231
+ r.refusedAt = Date.now();
232
+ r.refusedReason = String((req.body && (req.body.reason || req.body.refusedReason || req.body.refuseReason)) || '').trim();
231
233
  await dbLayer.saveReservation(r);
232
234
 
233
235
  try {
@@ -236,9 +238,12 @@ admin.refuseReservation = async function (req, res) {
236
238
  await sendEmail('calendar-onekite_refused', requester.email, 'Location matériel - Réservation refusée', {
237
239
  uid: parseInt(r.uid, 10),
238
240
  username: requester.username,
239
- itemName: r.itemName,
241
+ itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
242
+ itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
243
+ dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
240
244
  start: formatFR(r.start),
241
245
  end: formatFR(r.end),
246
+ refusedReason: r.refusedReason || '',
242
247
  });
243
248
  }
244
249
  } catch (e) {}
package/lib/api.js CHANGED
@@ -643,6 +643,7 @@ api.refuseReservation = async function (req, res) {
643
643
 
644
644
  r.status = 'refused';
645
645
  r.refusedAt = Date.now();
646
+ r.refusedReason = String((req.body && (req.body.reason || req.body.refusedReason || req.body.refuseReason)) || '').trim();
646
647
  await dbLayer.saveReservation(r);
647
648
 
648
649
  const requester = await user.getUserFields(r.uid, ['username', 'email']);
@@ -654,6 +655,7 @@ api.refuseReservation = async function (req, res) {
654
655
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
655
656
  start: formatFR(r.start),
656
657
  end: formatFR(r.end),
658
+ refusedReason: r.refusedReason || '',
657
659
  });
658
660
  }
659
661
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "11.2.6",
3
+ "version": "11.2.8",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -31,5 +31,5 @@
31
31
  "acpScripts": [
32
32
  "public/admin.js"
33
33
  ],
34
- "version": "1.0.48"
34
+ "version": "1.0.49"
35
35
  }
package/public/admin.js CHANGED
@@ -408,8 +408,11 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
408
408
  });
409
409
  }
410
410
 
411
- async function refuse(rid) {
412
- return await fetchJson(`/api/v3/admin/plugins/calendar-onekite/reservations/${rid}/refuse`, { method: 'PUT' });
411
+ async function refuse(rid, payload) {
412
+ return await fetchJson(`/api/v3/admin/plugins/calendar-onekite/reservations/${rid}/refuse`, {
413
+ method: 'PUT',
414
+ body: JSON.stringify(payload || {}),
415
+ });
413
416
  }
414
417
 
415
418
  async function purge(year) {
@@ -453,6 +456,23 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
453
456
  const form = document.getElementById('onekite-settings-form');
454
457
  if (!form) return;
455
458
 
459
+ // Make the HelloAsso debug output readable in both light and dark ACP themes.
460
+ // NodeBB 4.x uses Bootstrap variables, so we can rely on CSS variables here.
461
+ (function injectAdminCss() {
462
+ const id = 'onekite-admin-css';
463
+ if (document.getElementById(id)) return;
464
+ const style = document.createElement('style');
465
+ style.id = id;
466
+ style.textContent = `
467
+ #onekite-debug-output.onekite-debug-output {
468
+ background: var(--bs-body-bg) !important;
469
+ color: var(--bs-body-color) !important;
470
+ border: 1px solid var(--bs-border-color) !important;
471
+ }
472
+ `;
473
+ document.head.appendChild(style);
474
+ })();
475
+
456
476
  // Load settings
457
477
  try {
458
478
  const s = await loadSettings();
@@ -528,6 +548,9 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
528
548
  const rid = btn.getAttribute('data-rid');
529
549
  if (!rid) return;
530
550
 
551
+ // Remove the row immediately on success for a snappier UX
552
+ const rowEl = btn.closest('tr') || btn.closest('.onekite-pending-row');
553
+
531
554
  try {
532
555
  if (action === 'approve') {
533
556
  const opts = timeOptions(5).map(t => `<option value="${t}">${t}</option>`).join('');
@@ -555,6 +578,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
555
578
  const adminNote = (document.getElementById('onekite-admin-note')?.value || '').trim();
556
579
  const pickupTime = (document.getElementById('onekite-pickup-time')?.value || '').trim();
557
580
  await approve(rid, { adminNote, pickupTime });
581
+ if (rowEl && rowEl.parentNode) rowEl.parentNode.removeChild(rowEl);
558
582
  bootbox.alert('Réservation validée.');
559
583
  resolve(true);
560
584
  } catch (e) {
@@ -568,8 +592,38 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
568
592
  });
569
593
  });
570
594
  } else if (action === 'refuse') {
571
- await refuse(rid);
572
- bootbox.alert('Réservation refusée.');
595
+ const html = `
596
+ <div class="mb-3">
597
+ <label class="form-label">Raison du refus (incluse dans l'email)</label>
598
+ <textarea class="form-control" id="onekite-refuse-reason" rows="3" placeholder="Ex: matériel indisponible, dates impossibles, dossier incomplet..."></textarea>
599
+ </div>
600
+ `;
601
+ await new Promise((resolve) => {
602
+ bootbox.dialog({
603
+ title: 'Refuser la réservation',
604
+ message: html,
605
+ buttons: {
606
+ cancel: { label: 'Annuler', className: 'btn-secondary', callback: () => resolve(false) },
607
+ ok: {
608
+ label: 'Refuser',
609
+ className: 'btn-danger',
610
+ callback: async () => {
611
+ try {
612
+ const reason = (document.getElementById('onekite-refuse-reason')?.value || '').trim();
613
+ await refuse(rid, { reason });
614
+ if (rowEl && rowEl.parentNode) rowEl.parentNode.removeChild(rowEl);
615
+ bootbox.alert('Réservation refusée.');
616
+ resolve(true);
617
+ } catch (e) {
618
+ showAlert('error', 'Refus impossible.');
619
+ resolve(false);
620
+ }
621
+ return false;
622
+ },
623
+ },
624
+ },
625
+ });
626
+ });
573
627
  }
574
628
  await refreshPending();
575
629
  } catch (e) {
package/public/client.js CHANGED
@@ -19,8 +19,8 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
19
19
  width: 100%;
20
20
  max-width: 100%;
21
21
  box-sizing: border-box;
22
- padding-right: 3rem; /* leave room for caret */
23
- min-width: 7rem;
22
+ padding-right: 2.25rem; /* leave room for caret */
23
+ min-width: 0;
24
24
  text-overflow: clip;
25
25
  white-space: nowrap;
26
26
  }
@@ -125,10 +125,10 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
125
125
  <div class="col-12 col-md-6">
126
126
  <label class="form-label">Début</label>
127
127
  <div class="row g-2">
128
- <div class="col-7">
128
+ <div class="col-6">
129
129
  <input type="date" class="form-control" id="onekite-se-start-date" value="${escapeHtml(toDateInputValue(seStart))}" />
130
130
  </div>
131
- <div class="col-5">
131
+ <div class="col-6">
132
132
  <select class="form-select onekite-time-select" id="onekite-se-start-time">${seTimeOptions(seStartTime, false)}</select>
133
133
  </div>
134
134
  </div>
@@ -136,10 +136,10 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
136
136
  <div class="col-12 col-md-6">
137
137
  <label class="form-label">Fin</label>
138
138
  <div class="row g-2">
139
- <div class="col-7">
139
+ <div class="col-6">
140
140
  <input type="date" class="form-control" id="onekite-se-end-date" value="${escapeHtml(toDateInputValue(seEnd))}" />
141
141
  </div>
142
- <div class="col-5">
142
+ <div class="col-6">
143
143
  <select class="form-select onekite-time-select" id="onekite-se-end-time">${seTimeOptions(seEndTime, true)}</select>
144
144
  </div>
145
145
  </div>
@@ -602,9 +602,10 @@ function attachAddressAutocomplete(inputEl, onPick) {
602
602
  });
603
603
  }
604
604
 
605
- async function refuseReservation(rid) {
605
+ async function refuseReservation(rid, payload) {
606
606
  return await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/refuse`, {
607
607
  method: 'PUT',
608
+ body: JSON.stringify(payload || {}),
608
609
  });
609
610
  }
610
611
 
@@ -784,6 +785,9 @@ function toDatetimeLocalValue(date) {
784
785
 
785
786
  function setMode(next) {
786
787
  mode = next;
788
+ if (mode === 'special') {
789
+ showAlert('info', 'Mode évènement : sélectionnez une date ou une plage');
790
+ }
787
791
  refreshDesktopModeButton();
788
792
  try {
789
793
  const mb = document.querySelector('#onekite-mobile-controls .onekite-mode-btn');
@@ -996,9 +1000,12 @@ function toDatetimeLocalValue(date) {
996
1000
  }
997
1001
  isDialogOpen = true;
998
1002
  try {
999
- // Default to a same-day event: 00:00 -> 23:59
1000
- const end = new Date(start.getTime() + (23 * 60 + 59) * 60 * 1000);
1001
- const payload = await openSpecialEventDialog({ start, end, allDay: true });
1003
+ // Default to a same-day event in local time: 07:00 -> 23:59
1004
+ const start2 = new Date(start);
1005
+ start2.setHours(7, 0, 0, 0);
1006
+ const end2 = new Date(start);
1007
+ end2.setHours(23, 59, 0, 0);
1008
+ const payload = await openSpecialEventDialog({ start: start2, end: end2, allDay: false });
1002
1009
  setMode('reservation');
1003
1010
  if (!payload) {
1004
1011
  isDialogOpen = false;
@@ -1123,12 +1130,18 @@ function toDatetimeLocalValue(date) {
1123
1130
  const canModerate = !!p.canModerate;
1124
1131
  const isPending = status === 'pending';
1125
1132
 
1133
+ const refusedReason = String(p.refusedReason || p.refuseReason || '').trim();
1134
+ const refusedReasonHtml = (status === 'refused' && refusedReason)
1135
+ ? `<div class="mb-2"><strong>Raison du refus</strong><br>${escapeHtml(refusedReason)}</div>`
1136
+ : '';
1137
+
1126
1138
  const baseHtml = `
1127
1139
  ${userLine}
1128
1140
  <div class="mb-2"><strong>Matériel</strong><br>${itemsHtml}</div>
1129
1141
  <div class="mb-2"><strong>Période</strong><br>${period}</div>
1130
1142
  ${validatedByHtml}
1131
1143
  ${pickupHtml}
1144
+ ${refusedReasonHtml}
1132
1145
  <div class="text-muted" style="font-size: 12px;">Statut: ${statusLabel(status)}</div>
1133
1146
  `;
1134
1147
 
@@ -1171,16 +1184,41 @@ function toDatetimeLocalValue(date) {
1171
1184
  }
1172
1185
  if (showModeration) {
1173
1186
  buttons.refuse = {
1174
- label: 'Supprimer',
1187
+ label: 'Refuser',
1175
1188
  className: 'btn-outline-danger',
1176
1189
  callback: async () => {
1177
- try {
1178
- await refuseReservation(rid);
1179
- showAlert('success', 'Demande supprimée.');
1180
- calendar.refetchEvents();
1181
- } catch (e) {
1182
- showAlert('error', 'Suppression impossible.');
1183
- }
1190
+ const html = `
1191
+ <div class="mb-3">
1192
+ <label class="form-label">Raison du refus</label>
1193
+ <textarea class="form-control" id="onekite-refuse-reason" rows="3" placeholder="Ex: matériel indisponible, dates impossibles, dossier incomplet..."></textarea>
1194
+ </div>
1195
+ `;
1196
+ return await new Promise((resolve) => {
1197
+ bootbox.dialog({
1198
+ title: 'Refuser la réservation',
1199
+ message: html,
1200
+ buttons: {
1201
+ cancel: { label: 'Annuler', className: 'btn-secondary', callback: () => resolve(false) },
1202
+ ok: {
1203
+ label: 'Refuser',
1204
+ className: 'btn-danger',
1205
+ callback: async () => {
1206
+ try {
1207
+ const reason = (document.getElementById('onekite-refuse-reason')?.value || '').trim();
1208
+ await refuseReservation(rid, { reason });
1209
+ showAlert('success', 'Demande refusée.');
1210
+ calendar.refetchEvents();
1211
+ resolve(true);
1212
+ } catch (e) {
1213
+ showAlert('error', 'Refus impossible.');
1214
+ resolve(false);
1215
+ }
1216
+ return false;
1217
+ },
1218
+ },
1219
+ },
1220
+ });
1221
+ });
1184
1222
  },
1185
1223
  };
1186
1224
  buttons.approve = {
@@ -138,7 +138,7 @@
138
138
  <div class="tab-pane fade" id="onekite-tab-debug" role="tabpanel">
139
139
  <p class="text-muted">Teste la récupération du token et la liste du matériel (catalogue).</p>
140
140
  <button type="button" class="btn btn-secondary me-2" id="onekite-debug-run">Tester le chargement du matériel</button>
141
- <pre id="onekite-debug-output" class="mt-3 p-3 bg-light" style="max-height: 360px; overflow: auto;"></pre>
141
+ <pre id="onekite-debug-output" class="mt-3 p-3 border rounded onekite-debug-output" style="max-height: 360px; overflow: auto;"></pre>
142
142
  </div>
143
143
 
144
144
  <div class="tab-pane fade" id="onekite-tab-accounting" role="tabpanel">
@@ -1,7 +1,7 @@
1
1
  <p>Bonjour {username},</p>
2
2
  <p>Votre demande de location de matériel a été refusée.</p>
3
3
 
4
- <p><strong>Matériel</strong></p>
4
+ <p><strong>Matériel réservé</strong></p>
5
5
  <ul>
6
6
  <!-- BEGIN itemNames -->
7
7
  <li>{itemNames}</li>
@@ -9,3 +9,7 @@
9
9
  </ul>
10
10
 
11
11
  <p>{dateRange}</p>
12
+
13
+ <!-- IF refusedReason -->
14
+ <p><strong>Raison du refus</strong><br>{refusedReason}</p>
15
+ <!-- ENDIF refusedReason -->