nodebb-plugin-onekite-calendar 1.0.6 → 1.0.7

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/CHANGELOG.md CHANGED
@@ -1,18 +1,4 @@
1
- # Changelog – nodebb-plugin-onekite-calendar
2
-
3
- ## 1.0.6
4
- - Refactor interne : centralisation de l’envoi d’emails et du logging (opt-in)
5
- - Compatibilité : ajout d’alias de routes "onekite-calendar" en plus de "calendar-onekite" (API + ACP)
6
-
7
- ## 1.0.5
8
- - Widget : points colorés selon le type (mêmes couleurs que le calendrier)
9
- - Widget mobile : swipe horizontal pour naviguer semaine par semaine
10
-
11
- ## 1.0.4
12
- - Widget : affichage des évènements sous forme de points (sans texte)
13
- - Widget : survol/clic sur un point affiche le contenu (tooltip + popup)
14
- - Widget : renommage « Calendrier (2 semaines) » en « Calendrier »
15
- - Texte : remplacement de toutes les occurrences « OneKite » par « Onekite »
1
+ # Changelog – calendar-onekite
16
2
 
17
3
  ## 1.0.3
18
4
  - Suppression du texte d’archivage dans le toast de purge (plus de « 0 archivés »)
package/lib/admin.js CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  const meta = require.main.require('./src/meta');
4
4
  const user = require.main.require('./src/user');
5
+ const emailer = require.main.require('./src/emailer');
5
6
  const nconf = require.main.require('nconf');
6
7
 
7
- const { sendEmail } = require('./email');
8
- const log = require('./log');
9
-
10
8
  function forumBaseUrl() {
11
9
  const base = String(nconf.get('url') || '').trim().replace(/\/$/, '');
12
10
  return base;
@@ -20,7 +18,53 @@ function formatFR(tsOrIso) {
20
18
  return `${dd}/${mm}/${yyyy}`;
21
19
  }
22
20
 
23
- // Email helper is centralized in lib/email.js.
21
+ async function sendEmail(template, toEmail, subject, data) {
22
+ // Prefer sending by uid (NodeBB core expects uid in various places)
23
+ const uid = data && Number.isInteger(data.uid) ? data.uid : null;
24
+ if (!toEmail && !uid) return;
25
+
26
+ const settings = await meta.settings.get('calendar-onekite').catch(() => ({}));
27
+ const lang = (settings && settings.defaultLang) || (meta && meta.config && meta.config.defaultLang) || 'fr';
28
+ const params = Object.assign({}, data || {}, subject ? { subject } : {});
29
+
30
+ // If we have a uid, use the native uid-based sender first.
31
+ try {
32
+ if (uid && typeof emailer.send === 'function') {
33
+ // NodeBB: send(template, uid, params)
34
+ if (emailer.send.length >= 3) {
35
+ await emailer.send(template, uid, params);
36
+ } else {
37
+ await emailer.send(template, uid, params);
38
+ }
39
+ return;
40
+ }
41
+ } catch (err) {
42
+ console.warn('[calendar-onekite] Failed to send email', {
43
+ template,
44
+ toEmail,
45
+ err: err && err.message ? err.message : String(err),
46
+ });
47
+ }
48
+
49
+ try {
50
+ if (typeof emailer.sendToEmail === 'function') {
51
+ // NodeBB: sendToEmail(template, email, language, params)
52
+ if (emailer.sendToEmail.length >= 4) {
53
+ await emailer.sendToEmail(template, toEmail, lang, params);
54
+ } else {
55
+ // Older signature: sendToEmail(template, email, params)
56
+ await emailer.sendToEmail(template, toEmail, params);
57
+ }
58
+ return;
59
+ }
60
+ } catch (err) {
61
+ console.warn('[calendar-onekite] Failed to send email', {
62
+ template,
63
+ toEmail,
64
+ err: err && err.message ? err.message : String(err),
65
+ });
66
+ }
67
+ }
24
68
 
25
69
  function normalizeCallbackUrl(configured, meta) {
26
70
  const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
@@ -51,7 +95,7 @@ const admin = {};
51
95
 
52
96
  admin.renderAdmin = async function (req, res) {
53
97
  res.render('admin/plugins/calendar-onekite', {
54
- title: 'Calendar Onekite',
98
+ title: 'Calendar OneKite',
55
99
  });
56
100
  };
57
101
 
@@ -104,6 +148,10 @@ admin.approveReservation = async function (req, res) {
104
148
  clientId: settings.helloassoClientId,
105
149
  clientSecret: settings.helloassoClientSecret,
106
150
  });
151
+ if (!token) {
152
+
153
+ }
154
+
107
155
  let paymentUrl = null;
108
156
  if (token) {
109
157
  const requester = await user.getUserFields(r.uid, ['username', 'email']);
@@ -125,7 +173,7 @@ admin.approveReservation = async function (req, res) {
125
173
  // User return/back/error URLs must be real pages; webhook uses the plugin endpoint.
126
174
  callbackUrl: returnUrl,
127
175
  webhookUrl: webhookUrl,
128
- itemName: 'Réservation matériel Onekite',
176
+ itemName: 'Réservation matériel OneKite',
129
177
  containsDonation: false,
130
178
  metadata: { reservationId: String(rid) },
131
179
  });
@@ -140,7 +188,7 @@ admin.approveReservation = async function (req, res) {
140
188
  if (paymentUrl) {
141
189
  r.paymentUrl = paymentUrl;
142
190
  } else {
143
- log.warn('HelloAsso payment link not created (approve ACP)', { rid });
191
+ console.warn('[calendar-onekite] HelloAsso payment link not created (approve ACP)', { rid });
144
192
  }
145
193
 
146
194
  await dbLayer.saveReservation(r);
@@ -244,6 +292,98 @@ admin.purgeSpecialEventsByYear = async function (req, res) {
244
292
  return res.json({ ok: true, removed: count });
245
293
  };
246
294
 
295
+ // Debug endpoint to validate HelloAsso connectivity and item loading
296
+
297
+
298
+ admin.debugHelloAsso = async function (req, res) {
299
+ const settings = await meta.settings.get('calendar-onekite');
300
+ const env = (settings && settings.helloassoEnv) || 'prod';
301
+
302
+ // Never expose secrets in debug output
303
+ const safeSettings = {
304
+ helloassoEnv: env,
305
+ helloassoClientId: settings && settings.helloassoClientId ? String(settings.helloassoClientId) : '',
306
+ helloassoClientSecret: settings && settings.helloassoClientSecret ? '***' : '',
307
+ helloassoOrganizationSlug: settings && settings.helloassoOrganizationSlug ? String(settings.helloassoOrganizationSlug) : '',
308
+ helloassoFormType: settings && settings.helloassoFormType ? String(settings.helloassoFormType) : '',
309
+ helloassoFormSlug: settings && settings.helloassoFormSlug ? String(settings.helloassoFormSlug) : '',
310
+ };
311
+
312
+ const out = {
313
+ ok: true,
314
+ settings: safeSettings,
315
+ token: { ok: false },
316
+ // Catalog = what you actually want for a shop (available products/material)
317
+ catalog: { ok: false, count: 0, sample: [], keys: [] },
318
+ // Sold items = items present in orders (can be 0 if no sales yet)
319
+ soldItems: { ok: false, count: 0, sample: [] },
320
+ };
321
+
322
+ try {
323
+ const token = await helloasso.getAccessToken({
324
+ env,
325
+ clientId: settings.helloassoClientId,
326
+ clientSecret: settings.helloassoClientSecret,
327
+ });
328
+ if (!token) {
329
+ out.token = { ok: false, error: 'token-null' };
330
+ return res.json(out);
331
+ }
332
+ out.token = { ok: true };
333
+
334
+ // Catalog items (via /public)
335
+ try {
336
+ const y = new Date().getFullYear();
337
+ const { publicForm, items } = await helloasso.listCatalogItems({
338
+ env,
339
+ token,
340
+ organizationSlug: settings.helloassoOrganizationSlug,
341
+ formType: settings.helloassoFormType,
342
+ formSlug: `locations-materiel-${y}`,
343
+ });
344
+
345
+ const arr = Array.isArray(items) ? items : [];
346
+ out.catalog.ok = true;
347
+ out.catalog.count = arr.length;
348
+ out.catalog.keys = publicForm && typeof publicForm === 'object' ? Object.keys(publicForm) : [];
349
+ out.catalog.sample = arr.slice(0, 10).map((it) => ({
350
+ id: it.id,
351
+ name: it.name,
352
+ price: it.price ?? null,
353
+ }));
354
+ } catch (e) {
355
+ out.catalog = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [], keys: [] };
356
+ }
357
+
358
+ // Sold items
359
+ try {
360
+ const y2 = new Date().getFullYear();
361
+ const items = await helloasso.listItems({
362
+ env,
363
+ token,
364
+ organizationSlug: settings.helloassoOrganizationSlug,
365
+ formType: settings.helloassoFormType,
366
+ formSlug: `locations-materiel-${y2}`,
367
+ });
368
+ const arr = Array.isArray(items) ? items : [];
369
+ out.soldItems.ok = true;
370
+ out.soldItems.count = arr.length;
371
+ out.soldItems.sample = arr.slice(0, 10).map((it) => ({
372
+ id: it.id || it.itemId || it.reference || it.name,
373
+ name: it.name || it.label || it.itemName,
374
+ price: it.price || it.amount || it.unitPrice || null,
375
+ }));
376
+ } catch (e) {
377
+ out.soldItems = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [] };
378
+ }
379
+
380
+ return res.json(out);
381
+ } catch (e) {
382
+ out.ok = false;
383
+ out.token = { ok: false, error: String(e && e.message ? e.message : e) };
384
+ return res.json(out);
385
+ }
386
+ };
247
387
 
248
388
  // Accounting endpoint: aggregates paid reservations so you can contabilize what was rented.
249
389
  // Query params:
package/lib/api.js CHANGED
@@ -3,15 +3,14 @@
3
3
  const crypto = require('crypto');
4
4
 
5
5
  const meta = require.main.require('./src/meta');
6
+ const emailer = require.main.require('./src/emailer');
6
7
  const nconf = require.main.require('nconf');
7
8
  const user = require.main.require('./src/user');
8
9
  const groups = require.main.require('./src/groups');
9
10
  const db = require.main.require('./src/database');
10
- // logger available if you want to debug locally; we avoid noisy logs in prod.
11
+ const logger = require.main.require('./src/logger');
11
12
 
12
13
  const dbLayer = require('./db');
13
- const { sendEmail } = require('./email');
14
- const log = require('./log');
15
14
 
16
15
  // Fast membership check without N calls to groups.isMember.
17
16
  // NodeBB's groups.getUserGroups([uid]) returns an array (per uid) of group objects.
@@ -53,7 +52,36 @@ function normalizeAllowedGroups(raw) {
53
52
  const helloasso = require('./helloasso');
54
53
  const discord = require('./discord');
55
54
 
56
- // Email helper is in lib/email.js (no logs here).
55
+ // Email helper: NodeBB's Emailer signature differs across versions.
56
+ // We try the common forms. Any failure is logged for debugging.
57
+ async function sendEmail(template, toEmail, subject, data) {
58
+ if (!toEmail) return;
59
+ try {
60
+ // NodeBB core signature (historically):
61
+ // Emailer.sendToEmail(template, email, language, params[, callback])
62
+ // Subject is not a positional arg; it must be injected (either by NodeBB itself
63
+ // or via filter:email.modify). We always pass it in params.subject.
64
+ const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
65
+ const params = Object.assign({}, data || {}, subject ? { subject } : {});
66
+ if (typeof emailer.sendToEmail === 'function') {
67
+ await emailer.sendToEmail(template, toEmail, language, params);
68
+ return;
69
+ }
70
+ // Fallback for older/unusual builds (rare)
71
+ if (typeof emailer.send === 'function') {
72
+ // Some builds accept (template, email, language, params)
73
+ if (emailer.send.length >= 4) {
74
+ await emailer.send(template, toEmail, language, params);
75
+ return;
76
+ }
77
+ // Some builds accept (template, email, params)
78
+ await emailer.send(template, toEmail, params);
79
+ }
80
+ } catch (err) {
81
+ // eslint-disable-next-line no-console
82
+ console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err) });
83
+ }
84
+ }
57
85
 
58
86
  function normalizeBaseUrl(meta) {
59
87
  // Prefer meta.config.url, fallback to nconf.get('url')
@@ -74,8 +102,7 @@ function normalizeCallbackUrl(configured, meta) {
74
102
  let url = (configured || '').trim();
75
103
  if (!url) {
76
104
  // Default webhook endpoint (recommended): namespaced under /plugins
77
- // Prefer the new namespace; legacy endpoint remains available.
78
- url = base ? `${base}/plugins/onekite-calendar/helloasso` : '';
105
+ url = base ? `${base}/plugins/calendar-onekite/helloasso` : '';
79
106
  }
80
107
  if (url && url.startsWith('/') && base) {
81
108
  url = `${base}${url}`;
@@ -695,7 +722,7 @@ api.createReservation = async function (req, res) {
695
722
  }
696
723
  }
697
724
  } catch (e) {
698
- log.warn('Failed to send pending email', e && e.message ? e.message : e);
725
+ console.warn('[calendar-onekite] Failed to send pending email', e && e.message ? e.message : e);
699
726
  }
700
727
 
701
728
  // Discord webhook (optional)
@@ -761,7 +788,7 @@ api.approveReservation = async function (req, res) {
761
788
  totalAmount: (() => {
762
789
  const cents = Math.max(0, Math.round((Number(r.total) || 0) * 100));
763
790
  if (!cents) {
764
- log.warn('HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
791
+ console.warn('[calendar-onekite] HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
765
792
  }
766
793
  return cents;
767
794
  })(),
@@ -770,7 +797,7 @@ api.approveReservation = async function (req, res) {
770
797
  // Can be overridden via ACP setting `helloassoCallbackUrl`.
771
798
  callbackUrl: normalizeReturnUrl(meta),
772
799
  webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
773
- itemName: 'Réservation matériel Onekite',
800
+ itemName: 'Réservation matériel OneKite',
774
801
  containsDonation: false,
775
802
  metadata: { reservationId: String(rid) },
776
803
  });
@@ -782,7 +809,7 @@ api.approveReservation = async function (req, res) {
782
809
  r.checkoutIntentId = checkoutIntentId;
783
810
  }
784
811
  } else {
785
- log.warn('HelloAsso payment link not created (approve API)', { rid });
812
+ console.warn('[calendar-onekite] HelloAsso payment link not created (approve API)', { rid });
786
813
  }
787
814
  } catch (e) {
788
815
  // ignore payment link errors, admin can retry
package/lib/discord.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  const https = require('https');
4
- const log = require('./log');
5
4
  const { URL } = require('url');
6
5
 
7
6
  function isEnabled(v, defaultValue) {
@@ -89,7 +88,7 @@ function buildReservationMessage(kind, reservation) {
89
88
  function buildWebhookPayload(kind, reservation) {
90
89
  // Discord "regroupe" visuellement les messages consécutifs d'un même auteur.
91
90
  // En utilisant un username différent par action, on obtient un message bien distinct.
92
- const webhookUsername = kind === 'paid' ? 'Onekite • Paiement' : 'Onekite • Réservation';
91
+ const webhookUsername = kind === 'paid' ? 'OneKite • Paiement' : 'OneKite • Réservation';
93
92
 
94
93
  const calUrl = 'https://www.onekite.com/calendar';
95
94
  const username = reservation && reservation.username ? String(reservation.username) : '';
@@ -125,7 +124,7 @@ function buildWebhookPayload(kind, reservation) {
125
124
  ? 'Un paiement a été reçu pour une réservation.'
126
125
  : 'Une nouvelle demande de réservation a été créée.',
127
126
  fields,
128
- footer: { text: 'Onekite • Calendrier' },
127
+ footer: { text: 'OneKite • Calendrier' },
129
128
  timestamp: new Date().toISOString(),
130
129
  },
131
130
  ],
@@ -141,7 +140,7 @@ async function notifyReservationRequested(settings, reservation) {
141
140
  await postWebhook(url, buildWebhookPayload('request', reservation));
142
141
  } catch (e) {
143
142
  // eslint-disable-next-line no-console
144
- log.warn('Discord webhook failed (request)', e && e.message ? e.message : String(e));
143
+ console.warn('[calendar-onekite] Discord webhook failed (request)', e && e.message ? e.message : String(e));
145
144
  }
146
145
  }
147
146
 
@@ -154,7 +153,7 @@ async function notifyPaymentReceived(settings, reservation) {
154
153
  await postWebhook(url, buildWebhookPayload('paid', reservation));
155
154
  } catch (e) {
156
155
  // eslint-disable-next-line no-console
157
- log.warn('Discord webhook failed (paid)', e && e.message ? e.message : String(e));
156
+ console.warn('[calendar-onekite] Discord webhook failed (paid)', e && e.message ? e.message : String(e));
158
157
  }
159
158
  }
160
159
 
package/lib/helloasso.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  const https = require('https');
4
- const log = require('./log');
5
4
 
6
5
  function requestJson(method, url, headers = {}, bodyObj = null) {
7
6
  return new Promise((resolve) => {
@@ -285,11 +284,11 @@ async function listCatalogItems({ env, token, organizationSlug, formType, formSl
285
284
  async function createCheckoutIntent({ env, token, organizationSlug, formType, formSlug, totalAmount, payerEmail, callbackUrl, webhookUrl, itemName, containsDonation, metadata }) {
286
285
  if (!token || !organizationSlug) return null;
287
286
  if (!callbackUrl || !/^https?:\/\//i.test(String(callbackUrl))) {
288
- log.warn('HelloAsso invalid return/back/error URL', { callbackUrl });
287
+ console.warn('[calendar-onekite] HelloAsso invalid return/back/error URL', { callbackUrl });
289
288
  return null;
290
289
  }
291
290
  if (webhookUrl && !/^https?:\/\//i.test(String(webhookUrl))) {
292
- log.warn('HelloAsso invalid webhook URL', { webhookUrl });
291
+ console.warn('[calendar-onekite] HelloAsso invalid webhook URL', { webhookUrl });
293
292
  }
294
293
  // Checkout intents are created at organization level.
295
294
  const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(organizationSlug)}/checkout-intents`;
@@ -312,7 +311,7 @@ async function createCheckoutIntent({ env, token, organizationSlug, formType, fo
312
311
  // Log the error payload to help diagnose configuration issues (slug, env, urls, amount, etc.)
313
312
  try {
314
313
  // eslint-disable-next-line no-console
315
- log.warn('HelloAsso checkout-intent failed', { status, json });
314
+ console.warn('[calendar-onekite] HelloAsso checkout-intent failed', { status, json });
316
315
  } catch (e) { /* ignore */ }
317
316
  return null;
318
317
  }
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('crypto');
4
- const log = require('./log');
5
4
 
6
5
  const db = require.main.require('./src/database');
7
6
  const meta = require.main.require('./src/meta');
@@ -68,7 +67,7 @@ async function sendEmail(template, toEmail, subject, data) {
68
67
  // fall back to sendToEmail only.
69
68
  } catch (err) {
70
69
  // eslint-disable-next-line no-console
71
- log.warn('Failed to send email (webhook)', { template, toEmail, err: String(err && err.message || err) });
70
+ console.warn('[calendar-onekite] Failed to send email (webhook)', { template, toEmail, err: String(err && err.message || err) });
72
71
  }
73
72
  }
74
73
 
@@ -282,7 +281,7 @@ async function handler(req, res, next) {
282
281
 
283
282
  if (allowedIps.length && clientIp && !allowedIps.includes(clientIp)) {
284
283
  // eslint-disable-next-line no-console
285
- log.warn('HelloAsso webhook blocked by IP allowlist', { ip: clientIp, allowed: allowedIps });
284
+ console.warn('[calendar-onekite] HelloAsso webhook blocked by IP allowlist', { ip: clientIp, allowed: allowedIps });
286
285
  return res.status(403).json({ ok: false, error: 'ip-not-allowed' });
287
286
  }
288
287
 
@@ -325,7 +324,7 @@ async function handler(req, res, next) {
325
324
  }
326
325
  if (!resolvedRid) {
327
326
  // eslint-disable-next-line no-console
328
- log.warn('HelloAsso webhook missing reservationId in metadata', { eventType: payload && payload.eventType, paymentId, checkoutIntentId });
327
+ console.warn('[calendar-onekite] HelloAsso webhook missing reservationId in metadata', { eventType: payload && payload.eventType, paymentId, checkoutIntentId });
329
328
  // Do NOT mark as processed: if metadata/config is fixed later, a manual replay may be possible.
330
329
  return res.json({ ok: true, processed: false, missingReservationId: true });
331
330
  }
package/lib/scheduler.js CHANGED
@@ -3,7 +3,6 @@
3
3
  const meta = require.main.require('./src/meta');
4
4
  const db = require.main.require('./src/database');
5
5
  const dbLayer = require('./db');
6
- const log = require('./log');
7
6
 
8
7
  let timer = null;
9
8
 
@@ -84,7 +83,11 @@ async function processAwaitingPayment() {
84
83
  }
85
84
  } catch (err) {
86
85
  // eslint-disable-next-line no-console
87
- log.warn('Failed to send email (scheduler)', { template, toEmail, err: String((err && err.message) || err) });
86
+ console.warn('[calendar-onekite] Failed to send email (scheduler)', {
87
+ template,
88
+ toEmail,
89
+ err: String((err && err.message) || err),
90
+ });
88
91
  }
89
92
  }
90
93
 
package/lib/widgets.js CHANGED
@@ -41,7 +41,7 @@ widgets.defineWidgets = async function (widgetData) {
41
41
 
42
42
  list.push({
43
43
  widget: 'calendar-onekite-twoweeks',
44
- name: 'Calendrier Onekite',
44
+ name: 'Calendrier OneKite (2 semaines)',
45
45
  description: 'Affiche la semaine courante + la semaine suivante (FullCalendar via CDN).',
46
46
  content: '',
47
47
  });
@@ -54,19 +54,16 @@ widgets.renderTwoWeeksWidget = async function (data) {
54
54
  const id = makeDomId();
55
55
  const calUrl = widgetCalendarUrl();
56
56
  const apiBase = forumBaseUrl();
57
- // Prefer the new namespace, but keep a transparent fallback to the legacy one.
58
- const eventsEndpoint = `${apiBase}/api/v3/plugins/onekite-calendar/events`;
59
- const legacyEventsEndpoint = `${apiBase}/api/v3/plugins/calendar-onekite/events`;
57
+ const eventsEndpoint = `${apiBase}/api/v3/plugins/calendar-onekite/events`;
60
58
 
61
59
  const idJson = JSON.stringify(id);
62
60
  const calUrlJson = JSON.stringify(calUrl);
63
61
  const eventsEndpointJson = JSON.stringify(eventsEndpoint);
64
- const legacyEventsEndpointJson = JSON.stringify(legacyEventsEndpoint);
65
62
 
66
63
  const html = `
67
64
  <div class="onekite-twoweeks">
68
65
  <div class="d-flex justify-content-between align-items-center mb-1">
69
- <div style="font-weight: 600;">Calendrier</div>
66
+ <div style="font-weight: 600;">Calendrier (2 semaines)</div>
70
67
  <a href="${escapeHtml(calUrl)}" class="btn btn-sm btn-outline-secondary" style="line-height: 1.1;">Ouvrir</a>
71
68
  </div>
72
69
  <div id="${escapeHtml(id)}"></div>
@@ -78,7 +75,6 @@ widgets.renderTwoWeeksWidget = async function (data) {
78
75
  const containerId = ${idJson};
79
76
  const calUrl = ${calUrlJson};
80
77
  const eventsEndpoint = ${eventsEndpointJson};
81
- const legacyEventsEndpoint = ${legacyEventsEndpointJson};
82
78
 
83
79
  function loadOnce(tag, attrs) {
84
80
  return new Promise((resolve, reject) => {
@@ -141,137 +137,21 @@ widgets.renderTwoWeeksWidget = async function (data) {
141
137
  },
142
138
  navLinks: false,
143
139
  eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
144
- eventContent: function(arg) {
145
- // Render a simple dot instead of text.
146
- const wrap = document.createElement('span');
147
- wrap.className = 'onekite-dot';
148
- wrap.setAttribute('aria-label', arg.event.title || '');
149
- // Mark the node so eventDidMount can find it.
150
- wrap.setAttribute('data-onekite-dot', '1');
151
- return { domNodes: [wrap] };
152
- },
153
140
  events: function(info, successCallback, failureCallback) {
154
141
  const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
155
- const url1 = eventsEndpoint + '?' + qs.toString();
156
- fetch(url1, { credentials: 'same-origin' })
157
- .then((r) => {
158
- if (r && r.status === 404) {
159
- const url2 = legacyEventsEndpoint + '?' + qs.toString();
160
- return fetch(url2, { credentials: 'same-origin' });
161
- }
162
- return r;
163
- })
142
+ fetch(eventsEndpoint + '?' + qs.toString(), { credentials: 'same-origin' })
164
143
  .then((r) => r.json())
165
144
  .then((json) => successCallback(json || []))
166
145
  .catch((e) => failureCallback(e));
167
146
  },
168
- eventDidMount: function(info) {
169
- // Native tooltip + click popup (Bootbox when available).
170
- const title = info.event && info.event.title ? String(info.event.title) : '';
171
- const start = info.event && info.event.start ? info.event.start : null;
172
- const end = info.event && info.event.end ? info.event.end : null;
173
- const fmt = (d) => {
174
- try {
175
- return new Intl.DateTimeFormat('fr-FR', { dateStyle: 'medium' }).format(d);
176
- } catch (e) {
177
- return d ? d.toISOString().slice(0, 10) : '';
178
- }
179
- };
180
- const period = start ? (end ? (fmt(start) + ' → ' + fmt(end)) : fmt(start)) : '';
181
- const body = [title, period].filter(Boolean).join('\n');
182
- if (info.el) {
183
- // Make dot color match the FullCalendar event colors.
184
- try {
185
- const dot = info.el.querySelector && info.el.querySelector('[data-onekite-dot="1"]');
186
- if (dot) {
187
- // Prefer explicit event colors, otherwise fall back to computed styles.
188
- const bg = (info.event && (info.event.backgroundColor || info.event.borderColor)) || '';
189
- if (bg) {
190
- dot.style.backgroundColor = bg;
191
- } else {
192
- const cs = window.getComputedStyle(info.el);
193
- const bgs = cs && cs.backgroundColor;
194
- if (bgs && bgs !== 'rgba(0, 0, 0, 0)' && bgs !== 'transparent') {
195
- dot.style.backgroundColor = bgs;
196
- }
197
- }
198
- }
199
- } catch (e) {
200
- // ignore
201
- }
202
-
203
- info.el.title = body;
204
- info.el.style.cursor = 'pointer';
205
- info.el.addEventListener('click', function(ev) {
206
- ev.preventDefault();
207
- ev.stopPropagation();
208
- const html = '<div style="white-space:pre-line;">' + (body || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</div>';
209
- if (window.bootbox && typeof window.bootbox.alert === 'function') {
210
- window.bootbox.alert({ message: html });
211
- } else {
212
- // Fallback
213
- alert(body);
214
- }
215
- }, { passive: false });
216
- }
217
- },
218
- // On mobile, users can tap the calendar background to open the full page.
219
147
  dateClick: function() {
220
148
  window.location.href = calUrl;
221
149
  },
222
- // Do not redirect on eventClick: we show a popup in eventDidMount.
223
- eventClick: function(info) {
224
- try { if (info && info.jsEvent) info.jsEvent.preventDefault(); } catch (e) {}
150
+ eventClick: function() {
151
+ window.location.href = calUrl;
225
152
  },
226
153
  });
227
154
 
228
- // Mobile: swipe left/right to change week (2-week view).
229
- (function enableSwipe() {
230
- try {
231
- if (!('ontouchstart' in window)) return;
232
- const target = el; // calendar root
233
- let sx = 0;
234
- let sy = 0;
235
- let st = 0;
236
- let tracking = false;
237
-
238
- target.addEventListener('touchstart', function (e) {
239
- if (!e.touches || e.touches.length !== 1) return;
240
- const t = e.touches[0];
241
- sx = t.clientX;
242
- sy = t.clientY;
243
- st = Date.now();
244
- tracking = true;
245
- }, { passive: true });
246
-
247
- target.addEventListener('touchend', function (e) {
248
- if (!tracking) return;
249
- tracking = false;
250
- const changed = e.changedTouches && e.changedTouches[0];
251
- if (!changed) return;
252
- const dx = changed.clientX - sx;
253
- const dy = changed.clientY - sy;
254
- const adx = Math.abs(dx);
255
- const ady = Math.abs(dy);
256
- const dt = Date.now() - st;
257
-
258
- // Must be a quick-ish horizontal swipe.
259
- if (dt > 800) return;
260
- if (adx < 50) return;
261
- if (ady > adx * 0.75) return;
262
-
263
- // Move by one week per swipe, even though the view spans 2 weeks.
264
- if (dx < 0) {
265
- calendar.incrementDate({ weeks: 1 });
266
- } else {
267
- calendar.incrementDate({ weeks: -1 });
268
- }
269
- }, { passive: true });
270
- } catch (e) {
271
- // ignore
272
- }
273
- })();
274
-
275
155
  calendar.render();
276
156
  }
277
157
 
@@ -285,18 +165,7 @@ widgets.renderTwoWeeksWidget = async function (data) {
285
165
  .onekite-twoweeks .fc .fc-button { padding: .2rem .35rem; font-size: .75rem; }
286
166
  .onekite-twoweeks .fc .fc-daygrid-day-number { font-size: .75rem; padding: 2px; }
287
167
  .onekite-twoweeks .fc .fc-col-header-cell-cushion { font-size: .75rem; }
288
- .onekite-twoweeks .fc .fc-event-title { display: none; }
289
- .onekite-twoweeks .fc .fc-event-time { display: none; }
290
- .onekite-twoweeks .onekite-dot {
291
- display: inline-block;
292
- width: 8px;
293
- height: 8px;
294
- border-radius: 50%;
295
- background: var(--fc-event-bg-color, currentColor);
296
- border: 1px solid var(--fc-event-border-color, transparent);
297
- opacity: 0.85;
298
- margin: 0 2px;
299
- }
168
+ .onekite-twoweeks .fc .fc-event-title { font-size: .72rem; }
300
169
  </style>
301
170
  `;
302
171
 
package/library.js CHANGED
@@ -13,7 +13,6 @@ const admin = require('./lib/admin');
13
13
  const scheduler = require('./lib/scheduler');
14
14
  const helloassoWebhook = require('./lib/helloassoWebhook');
15
15
  const widgets = require('./lib/widgets');
16
- const { LEGACY_NAMESPACE, NAMESPACE } = require('./lib/constants');
17
16
  const bodyParser = require('body-parser');
18
17
 
19
18
  const Plugin = {};
@@ -60,30 +59,25 @@ Plugin.init = async function (params) {
60
59
  // IMPORTANT: pass an ARRAY for middlewares (even if empty), otherwise
61
60
  // setupPageRoute will throw "middlewares is not iterable".
62
61
  routeHelpers.setupPageRoute(router, '/calendar', mw(), controllers.renderCalendar);
63
- // Admin page route (keep legacy + add new)
64
- routeHelpers.setupAdminPageRoute(router, `/admin/plugins/${LEGACY_NAMESPACE}`, mw(), admin.renderAdmin);
65
- routeHelpers.setupAdminPageRoute(router, `/admin/plugins/${NAMESPACE}`, mw(), admin.renderAdmin);
62
+ routeHelpers.setupAdminPageRoute(router, '/admin/plugins/calendar-onekite', mw(), admin.renderAdmin);
66
63
 
67
64
  // Public API (JSON) — NodeBB 4.x only (v3 API)
68
- const pluginBases = [LEGACY_NAMESPACE, NAMESPACE];
69
- pluginBases.forEach((ns) => {
70
- router.get(`/api/v3/plugins/${ns}/events`, ...publicExpose, api.getEvents);
71
- router.get(`/api/v3/plugins/${ns}/items`, ...publicExpose, api.getItems);
72
- router.get(`/api/v3/plugins/${ns}/capabilities`, ...publicExpose, api.getCapabilities);
73
-
74
- router.post(`/api/v3/plugins/${ns}/reservations`, ...publicExpose, api.createReservation);
75
- router.get(`/api/v3/plugins/${ns}/reservations/:rid`, ...publicExpose, api.getReservationDetails);
76
- router.put(`/api/v3/plugins/${ns}/reservations/:rid/approve`, ...publicExpose, api.approveReservation);
77
- router.put(`/api/v3/plugins/${ns}/reservations/:rid/refuse`, ...publicExpose, api.refuseReservation);
78
- router.put(`/api/v3/plugins/${ns}/reservations/:rid/cancel`, ...publicExpose, api.cancelReservation);
79
-
80
- router.post(`/api/v3/plugins/${ns}/special-events`, ...publicExpose, api.createSpecialEvent);
81
- router.get(`/api/v3/plugins/${ns}/special-events/:eid`, ...publicExpose, api.getSpecialEventDetails);
82
- router.delete(`/api/v3/plugins/${ns}/special-events/:eid`, ...publicExpose, api.deleteSpecialEvent);
83
- });
65
+ router.get('/api/v3/plugins/calendar-onekite/events', ...publicExpose, api.getEvents);
66
+ router.get('/api/v3/plugins/calendar-onekite/items', ...publicExpose, api.getItems);
67
+ router.get('/api/v3/plugins/calendar-onekite/capabilities', ...publicExpose, api.getCapabilities);
68
+
69
+ router.post('/api/v3/plugins/calendar-onekite/reservations', ...publicExpose, api.createReservation);
70
+ router.get('/api/v3/plugins/calendar-onekite/reservations/:rid', ...publicExpose, api.getReservationDetails);
71
+ router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/approve', ...publicExpose, api.approveReservation);
72
+ router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/refuse', ...publicExpose, api.refuseReservation);
73
+ router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/cancel', ...publicExpose, api.cancelReservation);
74
+
75
+ router.post('/api/v3/plugins/calendar-onekite/special-events', ...publicExpose, api.createSpecialEvent);
76
+ router.get('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.getSpecialEventDetails);
77
+ router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.deleteSpecialEvent);
84
78
 
85
79
  // Admin API (JSON)
86
- const adminBases = [`/api/v3/admin/plugins/${LEGACY_NAMESPACE}`, `/api/v3/admin/plugins/${NAMESPACE}`];
80
+ const adminBases = ['/api/v3/admin/plugins/calendar-onekite'];
87
81
 
88
82
  adminBases.forEach((base) => {
89
83
  router.get(`${base}/settings`, ...adminMws, admin.getSettings);
@@ -94,6 +88,7 @@ Plugin.init = async function (params) {
94
88
  router.put(`${base}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
95
89
 
96
90
  router.post(`${base}/purge`, ...adminMws, admin.purgeByYear);
91
+ router.get(`${base}/debug`, ...adminMws, admin.debugHelloAsso);
97
92
  // Accounting / exports
98
93
  router.get(`${base}/accounting`, ...adminMws, admin.getAccounting);
99
94
  router.get(`${base}/accounting.csv`, ...adminMws, admin.exportAccountingCsv);
@@ -117,13 +112,11 @@ Plugin.init = async function (params) {
117
112
  // Accept webhook on both legacy root path and namespaced plugin path.
118
113
  // Some reverse proxies block unknown root paths, so /plugins/... is recommended.
119
114
  router.post('/helloasso', helloassoJson, helloassoWebhook.handler);
120
- router.post(`/plugins/${LEGACY_NAMESPACE}/helloasso`, helloassoJson, helloassoWebhook.handler);
121
- router.post(`/plugins/${NAMESPACE}/helloasso`, helloassoJson, helloassoWebhook.handler);
115
+ router.post('/plugins/calendar-onekite/helloasso', helloassoJson, helloassoWebhook.handler);
122
116
 
123
117
  // Optional: health checks
124
118
  router.get('/helloasso', (req, res) => res.json({ ok: true }));
125
- router.get(`/plugins/${LEGACY_NAMESPACE}/helloasso`, (req, res) => res.json({ ok: true }));
126
- router.get(`/plugins/${NAMESPACE}/helloasso`, (req, res) => res.json({ ok: true }));
119
+ router.get('/plugins/calendar-onekite/helloasso', (req, res) => res.json({ ok: true }));
127
120
 
128
121
  scheduler.start();
129
122
  };
@@ -131,9 +124,9 @@ Plugin.init = async function (params) {
131
124
  Plugin.addAdminNavigation = async function (header) {
132
125
  header.plugins = header.plugins || [];
133
126
  header.plugins.push({
134
- route: `/plugins/${LEGACY_NAMESPACE}`,
127
+ route: '/plugins/calendar-onekite',
135
128
  icon: 'fa-calendar',
136
- name: 'Calendar Onekite',
129
+ name: 'Calendar OneKite',
137
130
  });
138
131
  return header;
139
132
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
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
@@ -316,6 +316,14 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
316
316
  }
317
317
  }
318
318
 
319
+ async function debugHelloAsso() {
320
+ try {
321
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/debug');
322
+ } catch (e) {
323
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/debug');
324
+ }
325
+ }
326
+
319
327
  async function loadAccounting(from, to) {
320
328
  const params = new URLSearchParams();
321
329
  if (from) params.set('from', from);
@@ -328,6 +336,23 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
328
336
  const form = document.getElementById('onekite-settings-form');
329
337
  if (!form) return;
330
338
 
339
+ // Make the HelloAsso debug output readable in both light and dark ACP themes.
340
+ // NodeBB 4.x uses Bootstrap variables, so we can rely on CSS variables here.
341
+ (function injectAdminCss() {
342
+ const id = 'onekite-admin-css';
343
+ if (document.getElementById(id)) return;
344
+ const style = document.createElement('style');
345
+ style.id = id;
346
+ style.textContent = `
347
+ #onekite-debug-output.onekite-debug-output {
348
+ background: var(--bs-body-bg) !important;
349
+ color: var(--bs-body-color) !important;
350
+ border: 1px solid var(--bs-border-color) !important;
351
+ }
352
+ `;
353
+ document.head.appendChild(style);
354
+ })();
355
+
331
356
  // Load settings
332
357
  try {
333
358
  const s = await loadSettings();
@@ -649,6 +674,30 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
649
674
  });
650
675
  }
651
676
 
677
+ // Debug
678
+ const debugBtn = document.getElementById('onekite-debug-run');
679
+ if (debugBtn) {
680
+ debugBtn.addEventListener('click', async () => {
681
+ const out = document.getElementById('onekite-debug-output');
682
+ if (out) out.textContent = 'Chargement...';
683
+ try {
684
+ const result = await debugHelloAsso();
685
+ if (out) out.textContent = JSON.stringify(result, null, 2);
686
+ const catalogCount = result && result.catalog ? parseInt(result.catalog.count, 10) || 0 : 0;
687
+ const catalogOk = !!(result && result.catalog && result.catalog.ok);
688
+ // Accept "count > 0" even if ok flag is false (some proxies can strip fields, etc.)
689
+ if (catalogOk || catalogCount > 0) {
690
+ showAlert('success', `Catalogue HelloAsso: ${catalogCount} item(s)`);
691
+ } else {
692
+ showAlert('error', 'HelloAsso: impossible de récupérer le catalogue.');
693
+ }
694
+ } catch (e) {
695
+ if (out) out.textContent = String(e && e.message ? e.message : e);
696
+ showAlert('error', 'Debug impossible.');
697
+ }
698
+ });
699
+ }
700
+
652
701
  // Accounting (paid reservations)
653
702
  const accFrom = document.getElementById('onekite-acc-from');
654
703
  const accTo = document.getElementById('onekite-acc-to');
package/public/client.js CHANGED
@@ -4,7 +4,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
4
4
  'use strict';
5
5
 
6
6
  // Ensure small UI tweaks are applied even when themes override bootstrap defaults.
7
- (function ensureOnekiteStyles() {
7
+ (function ensureOneKiteStyles() {
8
8
  try {
9
9
  if (document.getElementById('onekite-inline-styles')) return;
10
10
  const style = document.createElement('style');
@@ -2,10 +2,22 @@
2
2
 
3
3
  <div class="row">
4
4
  <div class="col-lg-9">
5
- <h1>Calendar Onekite</h1>
5
+ <h1>Calendar OneKite</h1>
6
6
 
7
7
  <ul class="nav nav-tabs mt-3" role="tablist">
8
- <li class="nav-item" role="presentation">
8
+ <li class="nav-item" role="presentation">
9
+ <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#onekite-tab-settings" type="button" role="tab">Locations</button>
10
+ </li>
11
+ <li class="nav-item" role="presentation">
12
+ <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-events" type="button" role="tab">Évènements</button>
13
+ </li>
14
+ <li class="nav-item" role="presentation">
15
+ <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-pending" type="button" role="tab">Demandes en attente</button>
16
+ </li>
17
+ <li class="nav-item" role="presentation">
18
+ <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-debug" type="button" role="tab">Debug HelloAsso</button>
19
+ </li>
20
+ <li class="nav-item" role="presentation">
9
21
  <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-accounting" type="button" role="tab">Comptabilisation</button>
10
22
  </li>
11
23
  </ul>
@@ -142,7 +154,14 @@
142
154
  <h4>Demandes en attente</h4>
143
155
  <div id="onekite-pending" class="list-group"></div>
144
156
  </div>
145
- <div class="tab-pane fade" id="onekite-tab-accounting" role="tabpanel">
157
+
158
+ <div class="tab-pane fade" id="onekite-tab-debug" role="tabpanel">
159
+ <p class="text-muted">Teste la récupération du token et la liste du matériel (catalogue).</p>
160
+ <button type="button" class="btn btn-secondary me-2" id="onekite-debug-run">Tester le chargement du matériel</button>
161
+ <pre id="onekite-debug-output" class="mt-3 p-3 border rounded onekite-debug-output" style="max-height: 360px; overflow: auto;"></pre>
162
+ </div>
163
+
164
+ <div class="tab-pane fade" id="onekite-tab-accounting" role="tabpanel">
146
165
  <h4>Comptabilisation des locations (payées)</h4>
147
166
  <div class="d-flex flex-wrap gap-2 align-items-end mb-3">
148
167
  <div>
package/lib/constants.js DELETED
@@ -1,10 +0,0 @@
1
- 'use strict';
2
-
3
- // Keep backward compatibility with historical route namespaces.
4
- // New plugin name: nodebb-plugin-onekite-calendar
5
-
6
- module.exports = {
7
- LEGACY_NAMESPACE: 'calendar-onekite',
8
- NAMESPACE: 'onekite-calendar',
9
- WIDGET_ID: 'calendar-onekite-twoweeks',
10
- };
package/lib/email.js DELETED
@@ -1,39 +0,0 @@
1
- 'use strict';
2
-
3
- const meta = require.main.require('./src/meta');
4
- const emailer = require.main.require('./src/emailer');
5
-
6
- function defaultLanguage() {
7
- return (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
8
- }
9
-
10
- /**
11
- * Send a transactional email using NodeBB's emailer.
12
- *
13
- * We intentionally do not log here (per project preferences).
14
- */
15
- async function sendEmail(template, toEmail, subject, data) {
16
- if (!toEmail) return;
17
-
18
- const language = defaultLanguage();
19
- const params = Object.assign({}, data || {}, subject ? { subject } : {});
20
-
21
- // Common signature: sendToEmail(template, email, language, params)
22
- if (emailer && typeof emailer.sendToEmail === 'function') {
23
- await emailer.sendToEmail(template, toEmail, language, params);
24
- return;
25
- }
26
-
27
- // Fallbacks for older builds
28
- if (emailer && typeof emailer.send === 'function') {
29
- if (emailer.send.length >= 4) {
30
- await emailer.send(template, toEmail, language, params);
31
- return;
32
- }
33
- await emailer.send(template, toEmail, params);
34
- }
35
- }
36
-
37
- module.exports = {
38
- sendEmail,
39
- };
package/lib/log.js DELETED
@@ -1,29 +0,0 @@
1
- 'use strict';
2
-
3
- // Minimal, opt-in logging.
4
- // Enable by setting ONEKITE_CALENDAR_DEBUG=1 in the NodeBB process env.
5
-
6
- let logger;
7
- try {
8
- logger = require.main.require('./src/logger');
9
- } catch (e) {
10
- logger = null;
11
- }
12
-
13
- const enabled = () => !!process.env.ONEKITE_CALENDAR_DEBUG;
14
-
15
- function warn(msg, meta) {
16
- if (!enabled()) return;
17
- if (logger && typeof logger.warn === 'function') {
18
- logger.warn(`[onekite-calendar] ${msg}`, meta || '');
19
- }
20
- }
21
-
22
- function error(msg, meta) {
23
- if (!enabled()) return;
24
- if (logger && typeof logger.error === 'function') {
25
- logger.error(`[onekite-calendar] ${msg}`, meta || '');
26
- }
27
- }
28
-
29
- module.exports = { warn, error };