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 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", "priceCents": 5000, "location": "Stock A", "active": true }]
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
- ha_locationMapJson: '{}',
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, opts = {}) {
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, bookingId, reservations) {
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].priceCents, 10)) || 0;
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, checkoutIntentId) {
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 priceCents = (typeof amount === 'number' ? amount : parseInt(amount, 10)) || 0;
252
- out.push({ id, name, priceCents: String(priceCents), location: '' });
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
- priceCents: Number(it.priceCents || 0),
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 priceCents = typeof price === 'number' ? price : 0;
303
+ const price = typeof price === 'number' ? price : 0;
311
304
 
312
305
  return {
313
306
  id: id || name,
314
307
  name,
315
- location: String(locMap[id] || ''),
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, token, reservation, item) {
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.priceCents || 0));
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, { force, clearStored: clear });
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, checkoutIntentId);
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, itemId, startMs, endMs, notesUser, bookingId) {
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
- return res.status(400).send('itemId required');
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, token, reservation, item);
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
- ha_locationMapJson: String(req.body.ha_locationMapJson || DEFAULT_SETTINGS.ha_locationMapJson),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-equipment-calendar",
3
- "version": "0.8.6",
3
+ "version": "0.9.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": "0.5.0",
28
+ "version": "0.5.2",
29
29
  "minver": "4.7.1"
30
30
  }
@@ -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", "priceCents": 50, "location": "Stock A", "active": true },
32
- { "id": "light1", "name": "Projecteur", "priceCents": 20, "location": "Stock B", "active": true }
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>priceCents est en euros</strong> dans ce plugin (ex: 50 = 50€).</div>
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">