nodebb-plugin-calendar-onekite 11.1.46 → 11.1.48

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
@@ -25,7 +25,8 @@ async function sendEmail(template, toEmail, subject, data) {
25
25
  return;
26
26
  }
27
27
  if (emailer.send.length === 3) {
28
- await emailer.send(template, toEmail, data);
28
+ const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
29
+ await emailer.send(template, toEmail, dataWithSubject);
29
30
  return;
30
31
  }
31
32
  await emailer.send(template, toEmail, subject, data);
@@ -35,6 +36,26 @@ async function sendEmail(template, toEmail, subject, data) {
35
36
  }
36
37
  }
37
38
 
39
+ function normalizeCallbackUrl(configured, meta) {
40
+ const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
41
+ let url = (configured || '').trim();
42
+ if (!url) {
43
+ url = base ? `${base}/helloasso` : '';
44
+ }
45
+ if (url && url.startsWith('/') && base) {
46
+ url = `${base}${url}`;
47
+ }
48
+ return url;
49
+ }
50
+
51
+ function normalizeReturnUrl(meta) {
52
+ const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
53
+ const b = String(base || '').trim().replace(/\/$/, '');
54
+ if (!b) return '';
55
+ return `${b}/calendar`;
56
+ }
57
+
58
+
38
59
  const dbLayer = require('./db');
39
60
  const helloasso = require('./helloasso');
40
61
 
@@ -89,6 +110,9 @@ admin.approveReservation = async function (req, res) {
89
110
  clientId: settings.helloassoClientId,
90
111
  clientSecret: settings.helloassoClientSecret,
91
112
  });
113
+ if (!token) {
114
+ console.warn('[calendar-onekite] HelloAsso access token not obtained (approve ACP)', { rid: r && r.rid });
115
+ }
92
116
 
93
117
  let paymentUrl = null;
94
118
  if (token) {
package/lib/api.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const crypto = require('crypto');
4
4
 
5
5
  const meta = require.main.require('./src/meta');
6
+ const nconf = require.main.require('nconf');
6
7
  const user = require.main.require('./src/user');
7
8
  const groups = require.main.require('./src/groups');
8
9
 
@@ -17,6 +18,16 @@ async function sendEmail(template, toEmail, subject, data) {
17
18
  try {
18
19
  // Newer NodeBB builds expose sendToEmail
19
20
  if (typeof emailer.sendToEmail === 'function') {
21
+ if (emailer.sendToEmail.length >= 4) {
22
+ await emailer.sendToEmail(template, toEmail, subject, data);
23
+ return;
24
+ }
25
+ if (emailer.sendToEmail.length === 3) {
26
+ const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
27
+ await emailer.sendToEmail(template, toEmail, dataWithSubject);
28
+ return;
29
+ }
30
+ // Fallback
20
31
  await emailer.sendToEmail(template, toEmail, subject, data);
21
32
  return;
22
33
  }
@@ -27,8 +38,10 @@ async function sendEmail(template, toEmail, subject, data) {
27
38
  return;
28
39
  }
29
40
  // Some builds: (template, email, data)
41
+ // In that case, subject is expected inside `data.subject`.
30
42
  if (emailer.send.length === 3) {
31
- await emailer.send(template, toEmail, data);
43
+ const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
44
+ await emailer.send(template, toEmail, dataWithSubject);
32
45
  return;
33
46
  }
34
47
  // Fallback: try 4-args anyway
@@ -41,6 +54,42 @@ async function sendEmail(template, toEmail, subject, data) {
41
54
  }
42
55
  }
43
56
 
57
+ function normalizeBaseUrl(meta) {
58
+ // Prefer meta.config.url, fallback to nconf.get('url')
59
+ let base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
60
+ if (!base) {
61
+ base = String(nconf.get('url') || '').trim();
62
+ }
63
+ base = String(base || '').trim().replace(/\/$/, '');
64
+ // Ensure absolute with scheme
65
+ if (base && !/^https?:\/\//i.test(base)) {
66
+ base = `https://${base.replace(/^\/\//, '')}`;
67
+ }
68
+ return base;
69
+ }
70
+
71
+ function normalizeCallbackUrl(configured, meta) {
72
+ const base = normalizeBaseUrl(meta);
73
+ let url = (configured || '').trim();
74
+ if (!url) {
75
+ url = base ? `${base}/helloasso` : '';
76
+ }
77
+ if (url && url.startsWith('/') && base) {
78
+ url = `${base}${url}`;
79
+ }
80
+ // Ensure scheme for absolute URLs
81
+ if (url && !/^https?:\/\//i.test(url)) {
82
+ url = `https://${url.replace(/^\/\//, '')}`;
83
+ }
84
+ return url;
85
+ }
86
+
87
+ function normalizeReturnUrl(meta) {
88
+ const base = normalizeBaseUrl(meta);
89
+ return base ? `${base}/calendar` : '';
90
+ }
91
+
92
+
44
93
  function overlap(aStart, aEnd, bStart, bEnd) {
45
94
  return aStart < bEnd && bStart < aEnd;
46
95
  }
@@ -168,6 +217,9 @@ api.getItems = async function (req, res) {
168
217
  clientId: settings.helloassoClientId,
169
218
  clientSecret: settings.helloassoClientSecret,
170
219
  });
220
+ if (!token) {
221
+ console.warn('[calendar-onekite] HelloAsso access token not obtained (approve API)', { rid });
222
+ }
171
223
 
172
224
  if (!token) {
173
225
  return res.json([]);
@@ -321,11 +373,18 @@ api.approveReservation = async function (req, res) {
321
373
  formType: settings2.helloassoFormType,
322
374
  formSlug: settings2.helloassoFormSlug,
323
375
  // r.total is stored as an estimated total in euros; HelloAsso expects cents.
324
- totalAmount: Math.max(0, Math.round((Number(r.total) || 0) * 100)),
376
+ totalAmount: (() => {
377
+ const cents = Math.max(0, Math.round((Number(r.total) || 0) * 100));
378
+ if (!cents) {
379
+ console.warn('[calendar-onekite] HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
380
+ }
381
+ return cents;
382
+ })(),
325
383
  payerEmail: payer && payer.email ? payer.email : '',
326
384
  // By default, point to the forum base url so the webhook hits this NodeBB instance.
327
385
  // Can be overridden via ACP setting `helloassoCallbackUrl`.
328
- callbackUrl: (settings2.helloassoCallbackUrl || (meta.config && meta.config.url ? `${meta.config.url}/helloasso` : '')),
386
+ callbackUrl: normalizeReturnUrl(meta),
387
+ webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
329
388
  itemName: 'Réservation matériel OneKite',
330
389
  containsDonation: false,
331
390
  metadata: { reservationId: String(rid) },
package/lib/helloasso.js CHANGED
@@ -174,8 +174,15 @@ async function listCatalogItems({ env, token, organizationSlug, formType, formSl
174
174
  };
175
175
  }
176
176
 
177
- async function createCheckoutIntent({ env, token, organizationSlug, formType, formSlug, totalAmount, payerEmail, callbackUrl, itemName, containsDonation, metadata }) {
177
+ async function createCheckoutIntent({ env, token, organizationSlug, formType, formSlug, totalAmount, payerEmail, callbackUrl, webhookUrl, itemName, containsDonation, metadata }) {
178
178
  if (!token || !organizationSlug) return null;
179
+ if (!callbackUrl || !/^https?:\/\//i.test(String(callbackUrl))) {
180
+ console.warn('[calendar-onekite] HelloAsso invalid return/back/error URL', { callbackUrl });
181
+ return null;
182
+ }
183
+ if (webhookUrl && !/^https?:\/\//i.test(String(webhookUrl))) {
184
+ console.warn('[calendar-onekite] HelloAsso invalid webhook URL', { webhookUrl });
185
+ }
179
186
  // Checkout intents are created at organization level.
180
187
  const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(organizationSlug)}/checkout-intents`;
181
188
  const payload = {
@@ -188,15 +195,21 @@ async function createCheckoutIntent({ env, token, organizationSlug, formType, fo
188
195
  backUrl: callbackUrl || '',
189
196
  errorUrl: callbackUrl || '',
190
197
  returnUrl: callbackUrl || '',
191
- notificationUrl: callbackUrl || '',
198
+ notificationUrl: webhookUrl || callbackUrl || '',
192
199
  };
193
200
  const { status, json } = await requestJson('POST', url, { Authorization: `Bearer ${token}` }, payload);
194
201
  if (status >= 200 && status < 300 && json) {
195
202
  return json.redirectUrl || json.checkoutUrl || json.url || null;
196
203
  }
204
+ // Log the error payload to help diagnose configuration issues (slug, env, urls, amount, etc.)
205
+ try {
206
+ // eslint-disable-next-line no-console
207
+ console.warn('[calendar-onekite] HelloAsso checkout-intent failed', { status, json });
208
+ } catch (e) { /* ignore */ }
197
209
  return null;
198
210
  }
199
211
 
212
+
200
213
  module.exports = {
201
214
  getAccessToken,
202
215
  listItems,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "11.1.46",
3
+ "version": "11.1.48",
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/public/admin.js CHANGED
@@ -77,7 +77,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
77
77
  for (const r of list) {
78
78
  const created = r.createdAt ? fmtFR(r.createdAt) : '';
79
79
  const itemNames = Array.isArray(r.itemNames) && r.itemNames.length ? r.itemNames : [r.itemName || r.itemId].filter(Boolean);
80
- const itemsHtml = itemNames.map(n => `<div>• ${escapeHtml(String(n))}</div>`).join('');
80
+ const itemsHtml = `<ul style="margin: 0 0 10px 18px;">${itemNames.map(n => `<li>${escapeHtml(String(n))}</li>`).join('')}</ul>`;
81
81
  const div = document.createElement('div');
82
82
  div.className = 'list-group-item';
83
83
  div.innerHTML = `
@@ -107,9 +107,12 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
107
107
  });
108
108
 
109
109
  approveBtn.addEventListener('click', async () => {
110
+ const itemNamesModal = Array.isArray(r.itemNames) && r.itemNames.length ? r.itemNames : [r.itemName || r.itemId].filter(Boolean);
111
+ const itemsHtmlModal = `<ul style="margin: 0 0 10px 18px;">${itemNamesModal.map(n => `<li>${escapeHtml(String(n))}</li>`).join('')}</ul>`;
112
+
110
113
  const opts = (() => {
111
114
  const out = [];
112
- for (let h = 0; h < 24; h++) {
115
+ for (let h = 7; h < 24; h++) {
113
116
  for (let m = 0; m < 60; m += 5) {
114
117
  out.push(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`);
115
118
  }
@@ -162,7 +165,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
162
165
  function timeOptions(stepMinutes) {
163
166
  const step = stepMinutes || 5;
164
167
  const out = [];
165
- for (let h = 0; h < 24; h++) {
168
+ for (let h = 7; h < 24; h++) {
166
169
  for (let m = 0; m < 60; m += step) {
167
170
  const hh = String(h).padStart(2, '0');
168
171
  const mm = String(m).padStart(2, '0');
package/public/client.js CHANGED
@@ -286,14 +286,20 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
286
286
  const p = ev.extendedProps || {};
287
287
  const rid = p.rid || ev.id;
288
288
  const status = p.status || '';
289
- const itemsLabel = Array.isArray(p.itemNames) ? p.itemNames.join(', ') : (ev.title || '');
289
+ const itemsHtml = (() => {
290
+ const names = Array.isArray(p.itemNames) ? p.itemNames : (p.itemName ? [p.itemName] : []);
291
+ if (names.length) {
292
+ return `<ul style="margin:0 0 0 1.1rem; padding:0;">${names.map(n => `<li>${String(n).replace(/</g,'&lt;').replace(/>/g,'&gt;')}</li>`).join('')}</ul>`;
293
+ }
294
+ return String(ev.title || '').replace(/</g,'&lt;').replace(/>/g,'&gt;');
295
+ })();
290
296
  const period = `${formatDt(ev.start)} → ${formatDt(ev.end)}`;
291
297
 
292
298
  const canModerate = !!p.canModerate;
293
299
  const isPending = status === 'pending';
294
300
 
295
301
  const baseHtml = `
296
- <div class="mb-2"><strong>Matériel</strong><br>${String(itemsLabel).replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
302
+ <div class="mb-2"><strong>Matériel</strong><br>${itemsHtml}</div>
297
303
  <div class="mb-2"><strong>Période</strong><br>${period}</div>
298
304
  <div class="text-muted" style="font-size: 12px;">Statut: ${statusLabel(status)}</div>
299
305
  `;
@@ -339,9 +345,11 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
339
345
  label: 'Valider',
340
346
  className: 'btn-success',
341
347
  callback: async () => {
348
+ const itemNames = Array.isArray(p.itemNames) && p.itemNames.length ? p.itemNames : String(itemsLabel || '').split(',').map(s => s.trim()).filter(Boolean);
349
+ const itemsHtml = itemNames.map(n => `<li>${String(n).replace(/</g, '&lt;').replace(/>/g, '&gt;')}</li>`).join('');
342
350
  const opts = (() => {
343
351
  const out = [];
344
- for (let h = 0; h < 24; h++) {
352
+ for (let h = 7; h < 24; h++) {
345
353
  for (let m = 0; m < 60; m += 5) {
346
354
  out.push(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`);
347
355
  }
@@ -416,4 +424,4 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
416
424
  return {
417
425
  init,
418
426
  };
419
- });
427
+ });
@@ -19,5 +19,10 @@
19
19
  <!-- ENDIF pickupTime -->
20
20
 
21
21
  <!-- IF paymentUrl -->
22
- <p><strong>Lien de paiement :</strong> <a href="{paymentUrl}">{paymentUrl}</a></p>
22
+ <p>
23
+ <a href="{paymentUrl}" style="display:inline-block;padding:12px 18px;border-radius:6px;text-decoration:none;border:1px solid #333;">
24
+ Payer maintenant
25
+ </a>
26
+ </p>
27
+ <p style="font-size: 12px; color: #666;">Si le bouton ne fonctionne pas, utilisez ce lien : <a href="{paymentUrl}">{paymentUrl}</a></p>
23
28
  <!-- ENDIF paymentUrl -->