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 +56 -26
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/js/client.js +24 -98
- package/public/templates/equipment-calendar/calendar.tpl +1 -10
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.
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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(
|
|
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
|
-
//
|
|
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).
|
|
555
|
-
const end = DateTime.fromMillis(res.endMs).
|
|
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'
|
|
558
|
-
if (res.status === 'paid_validated'
|
|
559
|
-
if (res.status === 'rejected' || res.status === 'cancelled') { className = 'ec-status-
|
|
560
|
-
|
|
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:
|
|
568
|
-
|
|
569
|
-
extendedProps: {
|
|
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
package/plugin.json
CHANGED
package/public/js/client.js
CHANGED
|
@@ -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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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}
|
|
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>
|