nodebb-plugin-equipment-calendar 0.8.6 → 0.9.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 +33 -31
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/templates/admin/plugins/equipment-calendar.tpl +3 -44
package/library.js
CHANGED
|
@@ -27,12 +27,12 @@ const DEFAULT_SETTINGS = {
|
|
|
27
27
|
creatorGroups: 'registered-users',
|
|
28
28
|
approverGroup: 'administrators',
|
|
29
29
|
notifyGroup: 'administrators',
|
|
30
|
-
// JSON array of items: [{ "id": "cam1", "name": "Caméra A", "
|
|
30
|
+
// JSON array of items: [{ "id": "cam1", "name": "Caméra A", "price": 5000, "location": "Stock A", "active": true }]
|
|
31
31
|
itemsJson: '[]',
|
|
32
32
|
itemsSource: 'manual',
|
|
33
33
|
ha_itemsFormType: '',
|
|
34
34
|
ha_itemsFormSlug: '',
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
ha_calendarItemNamePrefix: 'Location matériel',
|
|
37
37
|
paymentTimeoutMinutes: 10,
|
|
38
38
|
// HelloAsso
|
|
@@ -49,18 +49,11 @@ const DEFAULT_SETTINGS = {
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
function parseLocationMap(locationMapJson) {
|
|
53
|
-
try {
|
|
54
|
-
const obj = JSON.parse(locationMapJson || '{}');
|
|
55
|
-
return (obj && typeof obj === 'object') ? obj : {};
|
|
56
|
-
} catch (e) {
|
|
57
|
-
return {};
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
52
|
|
|
61
53
|
let haTokenCache = null; // { accessToken, refreshToken, expMs }
|
|
62
54
|
|
|
63
|
-
async function getHelloAssoAccessToken(settings,
|
|
55
|
+
async function getHelloAssoAccessToken(settings,
|
|
56
|
+
saved, opts = {}) {
|
|
64
57
|
const now = Date.now();
|
|
65
58
|
if (!opts.force && haTokenCache && haTokenCache.accessToken && haTokenCache.expMs && now < haTokenCache.expMs - 30_000) {
|
|
66
59
|
return haTokenCache.accessToken;
|
|
@@ -121,7 +114,8 @@ async function getHelloAssoAccessToken(settings, opts = {}) {
|
|
|
121
114
|
}
|
|
122
115
|
|
|
123
116
|
|
|
124
|
-
async function createHelloAssoCheckoutIntent(settings,
|
|
117
|
+
async function createHelloAssoCheckoutIntent(settings,
|
|
118
|
+
saved, bookingId, reservations) {
|
|
125
119
|
const org = String(settings.ha_organizationSlug || '').trim();
|
|
126
120
|
if (!org) throw new Error('HelloAsso organization slug missing');
|
|
127
121
|
|
|
@@ -136,7 +130,7 @@ async function createHelloAssoCheckoutIntent(settings, bookingId, reservations)
|
|
|
136
130
|
|
|
137
131
|
const names = reservations.map(r => (byId[r.itemId] && byId[r.itemId].name) || r.itemId);
|
|
138
132
|
const totalAmount = reservations.reduce((sum, r) => {
|
|
139
|
-
const price = (byId[r.itemId] && parseInt(byId[r.itemId].
|
|
133
|
+
const price = (byId[r.itemId] && parseInt(byId[r.itemId].price, 10)) || 0;
|
|
140
134
|
return sum + (Number.isFinite(price) ? price : 0);
|
|
141
135
|
}, 0);
|
|
142
136
|
|
|
@@ -180,7 +174,8 @@ async function createHelloAssoCheckoutIntent(settings, bookingId, reservations)
|
|
|
180
174
|
return await resp.json();
|
|
181
175
|
}
|
|
182
176
|
|
|
183
|
-
async function fetchHelloAssoCheckoutIntent(settings,
|
|
177
|
+
async function fetchHelloAssoCheckoutIntent(settings,
|
|
178
|
+
saved, checkoutIntentId) {
|
|
184
179
|
const org = String(settings.ha_organizationSlug || '').trim();
|
|
185
180
|
const token = await getHelloAssoAccessToken(settings);
|
|
186
181
|
const url = `https://api.helloasso.com/v5/organizations/${encodeURIComponent(org)}/checkout-intents/${encodeURIComponent(checkoutIntentId)}`;
|
|
@@ -248,8 +243,8 @@ async function fetchHelloAssoItems(settings) {
|
|
|
248
243
|
const name = String(it.name || it.label || it.title || '').trim() || (tierName ? String(tierName) : id);
|
|
249
244
|
if (!id || !name) return;
|
|
250
245
|
const amount = it.amount || it.price || it.unitPrice || it.totalAmount || it.initialAmount;
|
|
251
|
-
const
|
|
252
|
-
out.push({ id, name,
|
|
246
|
+
const price = (typeof amount === 'number' ? amount : parseInt(amount, 10)) || 0;
|
|
247
|
+
out.push({ id, name, price: String(price), location: '' });
|
|
253
248
|
}
|
|
254
249
|
|
|
255
250
|
// Try a few known layouts
|
|
@@ -285,7 +280,7 @@ function parseItems(itemsJson) {
|
|
|
285
280
|
return arr.map(it => ({
|
|
286
281
|
id: String(it.id || '').trim(),
|
|
287
282
|
name: String(it.name || '').trim(),
|
|
288
|
-
|
|
283
|
+
price: Number(it.price || 0),
|
|
289
284
|
location: String(it.location || '').trim(),
|
|
290
285
|
active: it.active !== false,
|
|
291
286
|
})).filter(it => it.id && it.name);
|
|
@@ -297,9 +292,7 @@ function parseItems(itemsJson) {
|
|
|
297
292
|
async function getActiveItems(settings) {
|
|
298
293
|
const source = String(settings.itemsSource || 'manual');
|
|
299
294
|
if (source === 'helloasso') {
|
|
300
|
-
const rawItems = await fetchHelloAssoItems(settings);
|
|
301
|
-
const locMap = parseLocationMap(settings.ha_locationMapJson);
|
|
302
|
-
return (rawItems || []).map((it) => {
|
|
295
|
+
const rawItems = await fetchHelloAssoItems(settings); return (rawItems || []).map((it) => {
|
|
303
296
|
const id = String(it.id || it.itemId || it.reference || it.slug || it.name || '').trim();
|
|
304
297
|
const name = String(it.name || it.label || it.title || id).trim();
|
|
305
298
|
// Price handling is not displayed in the public calendar, keep a field if you want later
|
|
@@ -307,13 +300,12 @@ async function getActiveItems(settings) {
|
|
|
307
300
|
(it.price && (it.price.value || it.price.amount)) ||
|
|
308
301
|
(it.amount && (it.amount.value || it.amount)) ||
|
|
309
302
|
it.price;
|
|
310
|
-
const
|
|
303
|
+
const price = typeof price === 'number' ? price : 0;
|
|
311
304
|
|
|
312
305
|
return {
|
|
313
306
|
id: id || name,
|
|
314
307
|
name,
|
|
315
|
-
|
|
316
|
-
priceCents,
|
|
308
|
+
price,
|
|
317
309
|
active: true,
|
|
318
310
|
source: 'helloasso',
|
|
319
311
|
};
|
|
@@ -513,14 +505,15 @@ async function helloAssoGetAccessToken(settings) {
|
|
|
513
505
|
return resp.data.access_token;
|
|
514
506
|
}
|
|
515
507
|
|
|
516
|
-
async function helloAssoCreateCheckout(settings,
|
|
508
|
+
async function helloAssoCreateCheckout(settings,
|
|
509
|
+
saved, token, reservation, item) {
|
|
517
510
|
// Minimal: create a checkout intent and return redirectUrl
|
|
518
511
|
// This endpoint/payload may need adaptation depending on your HelloAsso setup.
|
|
519
512
|
const org = settings.ha_organizationSlug;
|
|
520
513
|
if (!org) throw new Error('HelloAsso organizationSlug missing');
|
|
521
514
|
|
|
522
515
|
const url = `https://api.helloasso.com/v5/organizations/${encodeURIComponent(org)}/checkout-intents`;
|
|
523
|
-
const amountCents = Math.max(0, Number(item.
|
|
516
|
+
const amountCents = Math.max(0, Number(item.price || 0));
|
|
524
517
|
|
|
525
518
|
const payload = {
|
|
526
519
|
totalAmount: amountCents,
|
|
@@ -771,6 +764,7 @@ async function renderAdminReservationsPage(req, res) {
|
|
|
771
764
|
res.render('admin/plugins/equipment-calendar-reservations', {
|
|
772
765
|
title: 'Equipment Calendar - Réservations',
|
|
773
766
|
settings,
|
|
767
|
+
saved,
|
|
774
768
|
rows: pageRows,
|
|
775
769
|
hasRows: pageRows.length > 0,
|
|
776
770
|
itemOptions: itemOptions.map(o => ({ ...o, selected: o.id === itemId })),
|
|
@@ -842,7 +836,8 @@ async function handleHelloAssoTest(req, res) {
|
|
|
842
836
|
const clear = String(req.query.clear || '') === '1';
|
|
843
837
|
// force=1 skips in-memory cache and refresh_token; clear=1 wipes stored refresh token
|
|
844
838
|
haTokenCache = null;
|
|
845
|
-
await getHelloAssoAccessToken(settings,
|
|
839
|
+
await getHelloAssoAccessToken(settings,
|
|
840
|
+
saved, { force, clearStored: clear });
|
|
846
841
|
const items = await fetchHelloAssoItems(settings);
|
|
847
842
|
const list = Array.isArray(items) ? items : (Array.isArray(items.data) ? items.data : []);
|
|
848
843
|
count = list.length;
|
|
@@ -865,6 +860,7 @@ async function handleHelloAssoTest(req, res) {
|
|
|
865
860
|
message,
|
|
866
861
|
count,
|
|
867
862
|
settings,
|
|
863
|
+
saved,
|
|
868
864
|
sampleItems,
|
|
869
865
|
hasSampleItems,
|
|
870
866
|
hasSampleItems: sampleItems && sampleItems.length > 0,
|
|
@@ -873,9 +869,11 @@ async function handleHelloAssoTest(req, res) {
|
|
|
873
869
|
|
|
874
870
|
async function renderAdminPage(req, res) {
|
|
875
871
|
const settings = await getSettings();
|
|
872
|
+
const saved = String(req.query.saved || '') === '1';
|
|
876
873
|
res.render('admin/plugins/equipment-calendar', {
|
|
877
874
|
title: 'Equipment Calendar',
|
|
878
875
|
settings,
|
|
876
|
+
saved,
|
|
879
877
|
saved: req.query && String(req.query.saved || '') === '1',
|
|
880
878
|
purged: req.query && parseInt(req.query.purged, 10) || 0,
|
|
881
879
|
view_dayGridMonth: (settings.defaultView || 'dayGridMonth') === 'dayGridMonth',
|
|
@@ -900,7 +898,8 @@ async function handleHelloAssoCallback(req, res) {
|
|
|
900
898
|
|
|
901
899
|
try {
|
|
902
900
|
if (!checkoutIntentId) throw new Error('checkoutIntentId manquant');
|
|
903
|
-
const checkout = await fetchHelloAssoCheckoutIntent(settings,
|
|
901
|
+
const checkout = await fetchHelloAssoCheckoutIntent(settings,
|
|
902
|
+
saved, checkoutIntentId);
|
|
904
903
|
|
|
905
904
|
const paid = isCheckoutPaid(checkout);
|
|
906
905
|
if (paid) {
|
|
@@ -1150,7 +1149,8 @@ async function renderApprovalsPage(req, res) {
|
|
|
1150
1149
|
|
|
1151
1150
|
// --- Actions ---
|
|
1152
1151
|
|
|
1153
|
-
async function createReservationForItem(req, res, settings,
|
|
1152
|
+
async function createReservationForItem(req, res, settings,
|
|
1153
|
+
saved, itemId, startMs, endMs, notesUser, bookingId) {
|
|
1154
1154
|
const items = await getActiveItems(settings);
|
|
1155
1155
|
const item = items.find(i => i.id === itemId);
|
|
1156
1156
|
if (!item) {
|
|
@@ -1194,7 +1194,8 @@ async function handleCreateReservation(req, res) {
|
|
|
1194
1194
|
const itemIdsRaw = String(req.body.itemIds || req.body.itemId || '').trim();
|
|
1195
1195
|
const itemIds = itemIdsRaw.split(',').map(s => s.trim()).filter(Boolean);
|
|
1196
1196
|
if (!itemIds.length) {
|
|
1197
|
-
|
|
1197
|
+
req.flash('error', 'Veuillez sélectionner au moins un matériel.');
|
|
1198
|
+
return res.redirect('/equipment/calendar');
|
|
1198
1199
|
}
|
|
1199
1200
|
const item = items.find(i => i.id === itemId);
|
|
1200
1201
|
if (!item) return res.status(400).send('Invalid item');
|
|
@@ -1274,7 +1275,8 @@ async function handleApproveReservation(req, res) {
|
|
|
1274
1275
|
return res.status(400).send('HelloAsso not configured');
|
|
1275
1276
|
}
|
|
1276
1277
|
const token = await helloAssoGetAccessToken(settings);
|
|
1277
|
-
const checkout = await helloAssoCreateCheckout(settings,
|
|
1278
|
+
const checkout = await helloAssoCreateCheckout(settings,
|
|
1279
|
+
saved, token, reservation, item);
|
|
1278
1280
|
reservation.ha_checkoutIntentId = checkout.checkoutIntentId;
|
|
1279
1281
|
reservation.ha_paymentUrl = checkout.paymentUrl;
|
|
1280
1282
|
}
|
|
@@ -1426,7 +1428,7 @@ async function handleAdminSave(req, res) {
|
|
|
1426
1428
|
itemsSource: String(req.body.itemsSource || DEFAULT_SETTINGS.itemsSource),
|
|
1427
1429
|
ha_itemsFormType: String(req.body.ha_itemsFormType || DEFAULT_SETTINGS.ha_itemsFormType),
|
|
1428
1430
|
ha_itemsFormSlug: String(req.body.ha_itemsFormSlug || DEFAULT_SETTINGS.ha_itemsFormSlug),
|
|
1429
|
-
|
|
1431
|
+
|
|
1430
1432
|
ha_clientId: String(req.body.ha_clientId || ''),
|
|
1431
1433
|
ha_clientSecret: String(req.body.ha_clientSecret || ''),
|
|
1432
1434
|
ha_organizationSlug: String(req.body.ha_organizationSlug || ''),
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -28,55 +28,14 @@
|
|
|
28
28
|
<div class="alert alert-warning">
|
|
29
29
|
Le champ "Matériel" doit être un JSON valide (array). Exemple :
|
|
30
30
|
<pre class="mb-0">[
|
|
31
|
-
{ "id": "cam1", "name": "Caméra A", "
|
|
32
|
-
{ "id": "light1", "name": "Projecteur", "
|
|
31
|
+
{ "id": "cam1", "name": "Caméra A", "price": 50, "": "Stock A", "active": true },
|
|
32
|
+
{ "id": "light1", "name": "Projecteur", "price": 20, "": "Stock B", "active": true }
|
|
33
33
|
]</pre>
|
|
34
|
-
<div class="mt-2">Note : <strong>
|
|
34
|
+
<div class="mt-2">Note : <strong>price est en euros</strong> dans ce plugin (ex: 50 = 50€).</div>
|
|
35
35
|
</div>
|
|
36
36
|
|
|
37
37
|
<div class="card card-body mb-3">
|
|
38
38
|
<h5>Permissions</h5>
|
|
39
|
-
<div class="mb-3">
|
|
40
|
-
<label class="form-label">Groupes autorisés à créer (CSV)</label>
|
|
41
|
-
<input name="creatorGroups" class="form-control" value="{settings.creatorGroups}">
|
|
42
|
-
</div>
|
|
43
|
-
<div class="mb-3">
|
|
44
|
-
<label class="form-label">Groupe validateur</label>
|
|
45
|
-
<input name="approverGroup" class="form-control" value="{settings.approverGroup}">
|
|
46
|
-
</div>
|
|
47
|
-
<div class="mb-3">
|
|
48
|
-
<label class="form-label">Groupe notifié (emails/notifs)</label>
|
|
49
|
-
<input name="notifyGroup" class="form-control" value="{settings.notifyGroup}">
|
|
50
|
-
</div>
|
|
51
|
-
<div class="form-check">
|
|
52
|
-
<input class="form-check-input" type="checkbox" name="showRequesterToAll" id="showRequesterToAll" {{{ if view_showRequesterToAll }}}checked{{{ end }}}>
|
|
53
|
-
<label class="form-check-label" for="showRequesterToAll">Afficher le demandeur à tout le monde</label>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
|
|
57
|
-
<div class="card card-body mb-3">
|
|
58
|
-
<h5>Matériel</h5>
|
|
59
|
-
<div class="mb-3">
|
|
60
|
-
<label class="form-label">Source</label>
|
|
61
|
-
<select class="form-select" name="itemsSource">
|
|
62
|
-
<option value="manual">Manuel (JSON)</option>
|
|
63
|
-
<option value="helloasso">HelloAsso (articles d’un formulaire)</option>
|
|
64
|
-
</select>
|
|
65
|
-
<div class="form-text">Si HelloAsso est choisi, la liste du matériel est récupérée via l’API HelloAsso (items d’un formulaire).</div>
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<div class="mb-3">
|
|
69
|
-
<label class="form-label">HelloAsso formType (ex: shop, event, membership, donation)</label>
|
|
70
|
-
<input class="form-control" name="ha_itemsFormType" value="{settings.ha_itemsFormType}">
|
|
71
|
-
</div>
|
|
72
|
-
<div class="mb-3">
|
|
73
|
-
<label class="form-label">HelloAsso formSlug</label>
|
|
74
|
-
<input class="form-control" name="ha_itemsFormSlug" value="{settings.ha_itemsFormSlug}">
|
|
75
|
-
</div>
|
|
76
|
-
<div class="mb-3">
|
|
77
|
-
<label class="form-label">Mapping lieux (JSON) par id d’article HelloAsso</label>
|
|
78
|
-
<textarea class="form-control" rows="4" name="ha_locationMapJson">{settings.ha_locationMapJson}</textarea>
|
|
79
|
-
<div class="form-text">Ex: { "12345": "Local A", "67890": "Local B" }</div>
|
|
80
39
|
</div>
|
|
81
40
|
|
|
82
41
|
<div class="mb-3">
|