nodebb-plugin-equipment-calendar 6.0.0 → 8.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
@@ -256,21 +256,15 @@ async function fetchHelloAssoItems(settings) {
256
256
 
257
257
  function pushItem(it, tierName) {
258
258
  if (!it) return;
259
- const id = String(it.id || it.itemId || it.productId || it.code || it.slug || '').trim();
260
- const baseName = String(it.name || it.label || it.title || '').trim();
261
- const name = (tierName && baseName) ? `${tierName} — ${baseName}` : (baseName || tierName || id);
262
- let amount = it.amount ?? it.price ?? it.unitPrice ?? it.totalAmount ?? it.initialAmount ?? it.minimumAmount;
263
- if (amount && typeof amount === 'object') {
264
- amount = amount.amount ?? amount.value ?? amount.total ?? amount.price ?? amount.cents ?? amount.centAmount ?? 0;
265
- }
266
- let price = 0;
267
- if (typeof amount === 'number') price = amount;
268
- else if (typeof amount === 'string') {
269
- const s = amount.replace(',', '.').replace(/[^0-9.\-]/g, '');
270
- price = parseFloat(s) || 0;
271
- }
272
- if (!id) return;
273
- out.push({ id, name, price, location: '' });
259
+ const id = String(it.id || it.itemId || it.reference || it.slug || it.code || it.name || '').trim();
260
+ const name = String(it.name || it.label || it.title || '').trim() || (tierName ? String(tierName) : id);
261
+ if (!id || !name) return;
262
+ const amount = it.amount || it.price || it.unitPrice || it.totalAmount || it.initialAmount;
263
+ const priceCents = (typeof amount === 'number' ? amount : parseInt(amount, 10)) || 0;
264
+ const raw = (typeof priceCents === 'number' ? priceCents : parseInt(priceCents, 10)) || 0;
265
+ // Heuristic: HelloAsso amounts are often in cents; convert when it looks like cents.
266
+ const price = (raw >= 1000 && raw % 100 === 0) ? (raw / 100) : raw;
267
+ out.push({ id, name, price, priceRaw: raw });
274
268
  }
275
269
 
276
270
  // Try a few known layouts
@@ -307,7 +301,7 @@ async function getActiveItems() {
307
301
  const items = (Array.isArray(rawItems) ? rawItems : []).map((it) => ({
308
302
  id: String(it.id || '').trim(),
309
303
  name: String(it.name || '').trim(),
310
- price: Number((it.price !== undefined ? it.price : it.priceCents) || 0) || 0,
304
+ price: (Number(it.price || 0) || ((Number(it.priceCents || 0) || 0) >= 1000 && (Number(it.priceCents||0)%100===0) ? (Number(it.priceCents||0)/100) : (Number(it.priceCents||0)||0)) || 0),
311
305
  active: true,
312
306
  })).filter(it => it.id && it.name);
313
307
  return items;
@@ -356,8 +350,9 @@ async function getBookingRids(bookingId) {
356
350
 
357
351
  async function saveReservation(res) {
358
352
  await db.setObject(resKey(res.id), res);
353
+ // per-item index (calendar fetch by item)
359
354
  await db.sortedSetAdd(itemIndexKey(res.itemId), res.startMs, res.id);
360
- // Global index for ACP listing
355
+ // global index (ACP list)
361
356
  await db.sortedSetAdd('equipmentCalendar:reservations', res.startMs, res.id);
362
357
  }
363
358
 
@@ -551,22 +546,28 @@ function verifyWebhook(req, secret) {
551
546
 
552
547
  // --- Rendering helpers ---
553
548
  function toEvent(res, item, requesterName, canSeeRequester) {
554
- const start = DateTime.fromMillis(res.startMs).toISODate();
555
- const end = DateTime.fromMillis(res.endMs).toISODate();
549
+ const start = DateTime.fromMillis(res.startMs).toISO();
550
+ const end = DateTime.fromMillis(res.endMs).toISO();
551
+
552
+ let icon = '⏳';
556
553
  let className = 'ec-status-pending';
557
- if (res.status === 'approved_waiting_payment' || res.status === 'approved') { className = 'ec-status-approved'; }
558
- if (res.status === 'paid_validated' || res.status === 'paid') { className = 'ec-status-paid'; }
559
- if (res.status === 'rejected' || res.status === 'cancelled') { className = 'ec-status-cancelled'; }
560
- const titleParts = [item ? item.name : res.itemId];
554
+ if (res.status === 'approved_waiting_payment') { icon = '💳'; className = 'ec-status-awaitpay'; }
555
+ if (res.status === 'paid_validated') { icon = ''; className = 'ec-status-valid'; }
556
+ if (res.status === 'rejected' || res.status === 'cancelled') { icon = '❌'; className = 'ec-status-cancel'; }
557
+
558
+ const titleParts = [icon, item ? item.name : res.itemId];
561
559
  if (canSeeRequester && requesterName) titleParts.push(`- ${requesterName}`);
562
560
  return {
563
561
  id: res.id,
564
562
  title: titleParts.join(' '),
565
563
  start,
566
564
  end,
567
- allDay: true,
568
- classNames: [className],
569
- extendedProps: { status: res.status, itemId: res.itemId },
565
+ allDay: false,
566
+ className,
567
+ extendedProps: {
568
+ status: res.status,
569
+ itemId: res.itemId,
570
+ },
570
571
  };
571
572
  }
572
573
 
@@ -1464,3 +1465,32 @@ function startPaymentTimeoutScheduler() {
1464
1465
  }
1465
1466
 
1466
1467
  module.exports = plugin;
1468
+
1469
+
1470
+ async function handleGetReservation(req, res) {
1471
+ if (!req.uid) return res.status(403).json({ error: 'not-logged-in' });
1472
+ const rid = String(req.params.id || '');
1473
+ const r = await db.getObject(resKey(rid));
1474
+ if (!r || !r.id) return res.status(404).json({ error: 'not-found' });
1475
+
1476
+ const settings = await getSettings();
1477
+ const isAdmin = await user.isAdminOrGlobalMod(req.uid);
1478
+ const isOwner = String(r.uid) === String(req.uid);
1479
+ const canSee = isAdmin || isOwner || String(settings.showRequesterToAll || '0') === '1';
1480
+ if (!canSee) return res.status(403).json({ error: 'forbidden' });
1481
+
1482
+ res.json({
1483
+ id: r.id,
1484
+ bookingId: r.bookingId || '',
1485
+ itemId: r.itemId,
1486
+ itemName: r.itemName || '',
1487
+ startMs: parseInt(r.startMs, 10) || 0,
1488
+ endMs: parseInt(r.endMs, 10) || 0,
1489
+ days: parseInt(r.days, 10) || 1,
1490
+ status: r.status || 'pending',
1491
+ total: Number(r.total || 0) || 0,
1492
+ unitPrice: Number(r.unitPrice || 0) || 0,
1493
+ notesUser: r.notesUser || '',
1494
+ uid: r.uid,
1495
+ });
1496
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-equipment-calendar",
3
- "version": "6.0.0",
3
+ "version": "8.0.0",
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-stable1",
28
+ "version": "0.7.5",
29
29
  "minver": "4.7.1"
30
30
  }
@@ -29,108 +29,34 @@ require(['jquery', 'bootstrap'], function ($, bootstrap) {
29
29
  }
30
30
 
31
31
  function updateTotalPrice() {
32
+ try {
32
33
  const sel = document.getElementById('ec-item-ids');
34
+ if (sel) sel.addEventListener('change', updateTotalPrice);
33
35
  const out = document.getElementById('ec-total-price');
34
- const daysEl = document.getElementById('ec-total-days');
35
- if (!sel || !out) return;
36
-
37
- let unitTotal = 0;
38
- Array.from(sel.selectedOptions || []).forEach((opt) => {
39
- const p = parseFloat(opt.getAttribute('data-price') || '0');
40
- if (!Number.isNaN(p)) unitTotal += p;
41
- });
42
-
43
- const days = getReservationDays();
44
- if (daysEl) daysEl.textContent = days + (days > 1 ? ' jours' : ' jour');
45
-
46
- const finalTotal = unitTotal * days;
47
- const txt = Number.isInteger(finalTotal) ? String(finalTotal) : finalTotal.toFixed(2);
48
- out.textContent = txt + ' €';
49
- }
50
-
51
- function syncHiddenIsoFields() {
52
- const s = document.getElementById('ec-start-date')?.value;
53
- const e = document.getElementById('ec-end-date')?.value;
54
- const startIsoEl = document.getElementById('ec-start-iso');
55
- const endIsoEl = document.getElementById('ec-end-iso');
56
- if (!s || !e || !startIsoEl || !endIsoEl) return;
57
-
58
- // Start inclusive at 00:00Z; End exclusive = day after end date at 00:00Z
59
- const startMs = parseDateInputToMs(s);
60
- const endMsExclusive = parseDateInputToMs(e) + 24 * 60 * 60 * 1000;
61
- startIsoEl.value = new Date(startMs).toISOString();
62
- endIsoEl.value = new Date(endMsExclusive).toISOString();
63
- }
64
-
65
- function openModalWithRange(startMs, endMsExclusive) {
66
- // Convert to date inputs: end date = endExclusive -1 day
67
- const startDate = toIsoDateUTC(startMs);
68
- const endDate = toIsoDateUTC(endMsExclusive - 24 * 60 * 60 * 1000);
69
-
70
- const startEl = document.getElementById('ec-start-date');
71
- const endEl = document.getElementById('ec-end-date');
72
- if (startEl) startEl.value = startDate;
73
- if (endEl) endEl.value = endDate;
74
-
75
- syncHiddenIsoFields();
76
- updateTotalPrice();
77
-
78
- const modalEl = document.getElementById('ec-create-modal');
79
- const BS = (bootstrap && bootstrap.Modal) ? bootstrap : (window.bootstrap && window.bootstrap.Modal ? window.bootstrap : null);
80
- if (modalEl && BS && BS.Modal) {
81
- BS.Modal.getOrCreateInstance(modalEl).show();
82
- } else {
83
- // Fallback: if bootstrap modal isn't available, scroll to form area
84
- modalEl?.scrollIntoView({ behavior: 'smooth', block: 'center' });
36
+ const startEl = document.getElementById('ec-start');
37
+ const endEl = document.getElementById('ec-end');
38
+ if (startEl) startEl.addEventListener('change', updateTotalPrice);
39
+ if (endEl) endEl.addEventListener('change', updateTotalPrice);
40
+ if (!sel || !out || !startEl || !endEl) return;
41
+
42
+ const start = startEl.value ? new Date(startEl.value + 'T00:00:00Z') : null;
43
+ const end = endEl.value ? new Date(endEl.value + 'T00:00:00Z') : null;
44
+ let days = 1;
45
+ if (start && end) {
46
+ const diff = (end.getTime() - start.getTime());
47
+ days = Math.max(1, Math.round(diff / (24*60*60*1000)));
85
48
  }
86
- }
87
-
88
- function initCalendar() {
89
- const calendarEl = document.getElementById('ec-calendar');
90
- if (!calendarEl || !window.FullCalendar) return;
91
-
92
- const canCreate = !!window.EC_CAN_CREATE;
93
- const events = Array.isArray(window.EC_EVENTS) ? window.EC_EVENTS : [];
94
-
95
- const calendar = new window.FullCalendar.Calendar(calendarEl, {
96
- initialView: 'dayGridMonth',
97
- locale: 'fr',
98
- headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,dayGridWeek,listMonth,listWeek' },
99
- selectable: canCreate,
100
- selectMirror: true,
101
- events,
102
- dateClick: function (info) {
103
- if (!canCreate) return;
104
- const startMs = Date.UTC(info.date.getUTCFullYear(), info.date.getUTCMonth(), info.date.getUTCDate());
105
- const endMs = startMs + 24 * 60 * 60 * 1000;
106
- console.debug('[equipment-calendar] open modal', { startMs, endMs });
107
- openModalWithRange(startMs, endMs);
108
- },
109
- select: function (info) {
110
- if (!canCreate) return;
111
- const startMs = Date.UTC(info.start.getUTCFullYear(), info.start.getUTCMonth(), info.start.getUTCDate());
112
- const endMs = Date.UTC(info.end.getUTCFullYear(), info.end.getUTCMonth(), info.end.getUTCDate());
113
- console.debug('[equipment-calendar] open modal', { startMs, endMs });
114
- openModalWithRange(startMs, endMs);
115
- },
116
- });
117
49
 
118
- calendar.render();
119
-
120
- document.getElementById('ec-item-ids')?.addEventListener('change', updateTotalPrice);
121
- document.getElementById('ec-start-date')?.addEventListener('change', function () {
122
- syncHiddenIsoFields();
123
- updateTotalPrice();
124
- });
125
- document.getElementById('ec-end-date')?.addEventListener('change', function () {
126
- syncHiddenIsoFields();
127
- updateTotalPrice();
128
- });
129
-
130
- updateTotalPrice();
131
- }
50
+ let sum = 0;
51
+ const opts = Array.from(sel.selectedOptions || []);
52
+ for (const opt of opts) {
53
+ const raw = (opt.getAttribute('data-price') || '').trim().replace(',', '.');
54
+ const price = parseFloat(raw);
55
+ if (!Number.isNaN(price)) sum += price;
56
+ }
132
57
 
133
- $(window).off('action:ajaxify.end.ec').on('action:ajaxify.end.ec', initCalendar);
134
- $(initCalendar);
58
+ const total = sum * days;
59
+ out.textContent = total.toFixed(2).replace('.00','') + ' €';
60
+ } catch (e) rememberError(e);
135
61
  })();
136
62
  });
@@ -43,7 +43,7 @@
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
- <option value="{items.id}" data-price="{items.price}">{items.name} — {items.price} €</option>
46
+ <option value="{items.id}" data-price="{items.price}">{items.name}</option>
47
47
  <!-- END items -->
48
48
  </select>
49
49
  <div class="form-text">Tu peux sélectionner plusieurs matériels.</div>
@@ -82,12 +82,3 @@
82
82
  window.EC_CAN_CREATE = {canCreateJs};
83
83
  window.EC_TZ = '{tz}';
84
84
  </script>
85
-
86
- <style>
87
- /* Event styling */
88
- .fc .fc-event-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
89
- .fc-event.ec-status-pending { background: rgba(108,117,125,.18); border-color: rgba(108,117,125,.45); }
90
- .fc-event.ec-status-approved { background: rgba(13,110,253,.18); border-color: rgba(13,110,253,.45); }
91
- .fc-event.ec-status-paid { background: rgba(25,135,84,.18); border-color: rgba(25,135,84,.45); }
92
- .fc-event.ec-status-cancelled { background: rgba(220,53,69,.10); border-color: rgba(220,53,69,.35); opacity:.65; text-decoration: line-through; }
93
- </style>