nodebb-plugin-onekite-calendar 1.0.6 → 1.0.8

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,67 @@ function formatFR(tsOrIso) {
20
18
  return `${dd}/${mm}/${yyyy}`;
21
19
  }
22
20
 
23
- // Email helper is centralized in lib/email.js.
21
+ function buildHelloAssoItemName(baseLabel, itemNames, start, end) {
22
+ const base = String(baseLabel || 'Réservation matériel Onekite').trim();
23
+ const items = Array.isArray(itemNames) ? itemNames.map((s) => String(s || '').trim()).filter(Boolean) : [];
24
+ const range = (start && end) ? `Du ${formatFR(start)} au ${formatFR(end)}` : '';
25
+ const lines = [base];
26
+ items.forEach((it) => lines.push(`• ${it}`));
27
+ if (range) lines.push(range);
28
+ let out = lines.join('\n').trim();
29
+ if (out.length > 250) {
30
+ out = out.slice(0, 249).trimEnd() + '…';
31
+ }
32
+ return out;
33
+ }
34
+
35
+ async function sendEmail(template, toEmail, subject, data) {
36
+ // Prefer sending by uid (NodeBB core expects uid in various places)
37
+ const uid = data && Number.isInteger(data.uid) ? data.uid : null;
38
+ if (!toEmail && !uid) return;
39
+
40
+ const settings = await meta.settings.get('calendar-onekite').catch(() => ({}));
41
+ const lang = (settings && settings.defaultLang) || (meta && meta.config && meta.config.defaultLang) || 'fr';
42
+ const params = Object.assign({}, data || {}, subject ? { subject } : {});
43
+
44
+ // If we have a uid, use the native uid-based sender first.
45
+ try {
46
+ if (uid && typeof emailer.send === 'function') {
47
+ // NodeBB: send(template, uid, params)
48
+ if (emailer.send.length >= 3) {
49
+ await emailer.send(template, uid, params);
50
+ } else {
51
+ await emailer.send(template, uid, params);
52
+ }
53
+ return;
54
+ }
55
+ } catch (err) {
56
+ console.warn('[calendar-onekite] Failed to send email', {
57
+ template,
58
+ toEmail,
59
+ err: err && err.message ? err.message : String(err),
60
+ });
61
+ }
62
+
63
+ try {
64
+ if (typeof emailer.sendToEmail === 'function') {
65
+ // NodeBB: sendToEmail(template, email, language, params)
66
+ if (emailer.sendToEmail.length >= 4) {
67
+ await emailer.sendToEmail(template, toEmail, lang, params);
68
+ } else {
69
+ // Older signature: sendToEmail(template, email, params)
70
+ await emailer.sendToEmail(template, toEmail, params);
71
+ }
72
+ return;
73
+ }
74
+ } catch (err) {
75
+ console.warn('[calendar-onekite] Failed to send email', {
76
+ template,
77
+ toEmail,
78
+ err: err && err.message ? err.message : String(err),
79
+ });
80
+ }
81
+ }
24
82
 
25
83
  function normalizeCallbackUrl(configured, meta) {
26
84
  const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
@@ -104,6 +162,10 @@ admin.approveReservation = async function (req, res) {
104
162
  clientId: settings.helloassoClientId,
105
163
  clientSecret: settings.helloassoClientSecret,
106
164
  });
165
+ if (!token) {
166
+
167
+ }
168
+
107
169
  let paymentUrl = null;
108
170
  if (token) {
109
171
  const requester = await user.getUserFields(r.uid, ['username', 'email']);
@@ -125,9 +187,13 @@ admin.approveReservation = async function (req, res) {
125
187
  // User return/back/error URLs must be real pages; webhook uses the plugin endpoint.
126
188
  callbackUrl: returnUrl,
127
189
  webhookUrl: webhookUrl,
128
- itemName: 'Réservation matériel Onekite',
190
+ itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
129
191
  containsDonation: false,
130
- metadata: { reservationId: String(rid) },
192
+ metadata: {
193
+ reservationId: String(rid),
194
+ items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
195
+ dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
196
+ },
131
197
  });
132
198
  paymentUrl = intent && (intent.paymentUrl || intent.redirectUrl)
133
199
  ? (intent.paymentUrl || intent.redirectUrl)
@@ -140,7 +206,7 @@ admin.approveReservation = async function (req, res) {
140
206
  if (paymentUrl) {
141
207
  r.paymentUrl = paymentUrl;
142
208
  } else {
143
- log.warn('HelloAsso payment link not created (approve ACP)', { rid });
209
+ console.warn('[calendar-onekite] HelloAsso payment link not created (approve ACP)', { rid });
144
210
  }
145
211
 
146
212
  await dbLayer.saveReservation(r);
@@ -244,6 +310,98 @@ admin.purgeSpecialEventsByYear = async function (req, res) {
244
310
  return res.json({ ok: true, removed: count });
245
311
  };
246
312
 
313
+ // Debug endpoint to validate HelloAsso connectivity and item loading
314
+
315
+
316
+ admin.debugHelloAsso = async function (req, res) {
317
+ const settings = await meta.settings.get('calendar-onekite');
318
+ const env = (settings && settings.helloassoEnv) || 'prod';
319
+
320
+ // Never expose secrets in debug output
321
+ const safeSettings = {
322
+ helloassoEnv: env,
323
+ helloassoClientId: settings && settings.helloassoClientId ? String(settings.helloassoClientId) : '',
324
+ helloassoClientSecret: settings && settings.helloassoClientSecret ? '***' : '',
325
+ helloassoOrganizationSlug: settings && settings.helloassoOrganizationSlug ? String(settings.helloassoOrganizationSlug) : '',
326
+ helloassoFormType: settings && settings.helloassoFormType ? String(settings.helloassoFormType) : '',
327
+ helloassoFormSlug: settings && settings.helloassoFormSlug ? String(settings.helloassoFormSlug) : '',
328
+ };
329
+
330
+ const out = {
331
+ ok: true,
332
+ settings: safeSettings,
333
+ token: { ok: false },
334
+ // Catalog = what you actually want for a shop (available products/material)
335
+ catalog: { ok: false, count: 0, sample: [], keys: [] },
336
+ // Sold items = items present in orders (can be 0 if no sales yet)
337
+ soldItems: { ok: false, count: 0, sample: [] },
338
+ };
339
+
340
+ try {
341
+ const token = await helloasso.getAccessToken({
342
+ env,
343
+ clientId: settings.helloassoClientId,
344
+ clientSecret: settings.helloassoClientSecret,
345
+ });
346
+ if (!token) {
347
+ out.token = { ok: false, error: 'token-null' };
348
+ return res.json(out);
349
+ }
350
+ out.token = { ok: true };
351
+
352
+ // Catalog items (via /public)
353
+ try {
354
+ const y = new Date().getFullYear();
355
+ const { publicForm, items } = await helloasso.listCatalogItems({
356
+ env,
357
+ token,
358
+ organizationSlug: settings.helloassoOrganizationSlug,
359
+ formType: settings.helloassoFormType,
360
+ formSlug: `locations-materiel-${y}`,
361
+ });
362
+
363
+ const arr = Array.isArray(items) ? items : [];
364
+ out.catalog.ok = true;
365
+ out.catalog.count = arr.length;
366
+ out.catalog.keys = publicForm && typeof publicForm === 'object' ? Object.keys(publicForm) : [];
367
+ out.catalog.sample = arr.slice(0, 10).map((it) => ({
368
+ id: it.id,
369
+ name: it.name,
370
+ price: it.price ?? null,
371
+ }));
372
+ } catch (e) {
373
+ out.catalog = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [], keys: [] };
374
+ }
375
+
376
+ // Sold items
377
+ try {
378
+ const y2 = new Date().getFullYear();
379
+ const items = await helloasso.listItems({
380
+ env,
381
+ token,
382
+ organizationSlug: settings.helloassoOrganizationSlug,
383
+ formType: settings.helloassoFormType,
384
+ formSlug: `locations-materiel-${y2}`,
385
+ });
386
+ const arr = Array.isArray(items) ? items : [];
387
+ out.soldItems.ok = true;
388
+ out.soldItems.count = arr.length;
389
+ out.soldItems.sample = arr.slice(0, 10).map((it) => ({
390
+ id: it.id || it.itemId || it.reference || it.name,
391
+ name: it.name || it.label || it.itemName,
392
+ price: it.price || it.amount || it.unitPrice || null,
393
+ }));
394
+ } catch (e) {
395
+ out.soldItems = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [] };
396
+ }
397
+
398
+ return res.json(out);
399
+ } catch (e) {
400
+ out.ok = false;
401
+ out.token = { ok: false, error: String(e && e.message ? e.message : e) };
402
+ return res.json(out);
403
+ }
404
+ };
247
405
 
248
406
  // Accounting endpoint: aggregates paid reservations so you can contabilize what was rented.
249
407
  // 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}`;
@@ -106,6 +133,21 @@ function formatFR(tsOrIso) {
106
133
  return `${dd}/${mm}/${yyyy}`;
107
134
  }
108
135
 
136
+ function buildHelloAssoItemName(baseLabel, itemNames, start, end) {
137
+ const base = String(baseLabel || 'Réservation matériel Onekite').trim();
138
+ const items = Array.isArray(itemNames) ? itemNames.map((s) => String(s || '').trim()).filter(Boolean) : [];
139
+ const range = (start && end) ? `Du ${formatFR(start)} au ${formatFR(end)}` : '';
140
+ const lines = [base];
141
+ items.forEach((it) => lines.push(`• ${it}`));
142
+ if (range) lines.push(range);
143
+ let out = lines.join('\n').trim();
144
+ // HelloAsso constraint: itemName max 250 chars
145
+ if (out.length > 250) {
146
+ out = out.slice(0, 249).trimEnd() + '…';
147
+ }
148
+ return out;
149
+ }
150
+
109
151
  function toTs(v) {
110
152
  if (v === undefined || v === null || v === '') return NaN;
111
153
  // Accept milliseconds timestamps passed as strings or numbers.
@@ -695,7 +737,7 @@ api.createReservation = async function (req, res) {
695
737
  }
696
738
  }
697
739
  } catch (e) {
698
- log.warn('Failed to send pending email', e && e.message ? e.message : e);
740
+ console.warn('[calendar-onekite] Failed to send pending email', e && e.message ? e.message : e);
699
741
  }
700
742
 
701
743
  // Discord webhook (optional)
@@ -761,7 +803,7 @@ api.approveReservation = async function (req, res) {
761
803
  totalAmount: (() => {
762
804
  const cents = Math.max(0, Math.round((Number(r.total) || 0) * 100));
763
805
  if (!cents) {
764
- log.warn('HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
806
+ console.warn('[calendar-onekite] HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
765
807
  }
766
808
  return cents;
767
809
  })(),
@@ -770,9 +812,13 @@ api.approveReservation = async function (req, res) {
770
812
  // Can be overridden via ACP setting `helloassoCallbackUrl`.
771
813
  callbackUrl: normalizeReturnUrl(meta),
772
814
  webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
773
- itemName: 'Réservation matériel Onekite',
815
+ itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
774
816
  containsDonation: false,
775
- metadata: { reservationId: String(rid) },
817
+ metadata: {
818
+ reservationId: String(rid),
819
+ items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
820
+ dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
821
+ },
776
822
  });
777
823
  const paymentUrl = intent && (intent.paymentUrl || intent.redirectUrl) ? (intent.paymentUrl || intent.redirectUrl) : (typeof intent === 'string' ? intent : null);
778
824
  const checkoutIntentId = intent && intent.checkoutIntentId ? String(intent.checkoutIntentId) : null;
@@ -782,7 +828,7 @@ api.approveReservation = async function (req, res) {
782
828
  r.checkoutIntentId = checkoutIntentId;
783
829
  }
784
830
  } else {
785
- log.warn('HelloAsso payment link not created (approve API)', { rid });
831
+ console.warn('[calendar-onekite] HelloAsso payment link not created (approve API)', { rid });
786
832
  }
787
833
  } catch (e) {
788
834
  // 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) {
@@ -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',
45
45
  description: 'Affiche la semaine courante + la semaine suivante (FullCalendar via CDN).',
46
46
  content: '',
47
47
  });
@@ -54,14 +54,11 @@ 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">
@@ -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) => {
@@ -121,6 +117,33 @@ widgets.renderTwoWeeksWidget = async function (data) {
121
117
 
122
118
  await ensureFullCalendar();
123
119
 
120
+ // Basic lightweight tooltip (no dependencies, works on hover + tap)
121
+ const tip = document.createElement('div');
122
+ tip.className = 'onekite-cal-tooltip';
123
+ tip.style.display = 'none';
124
+ document.body.appendChild(tip);
125
+
126
+ function setTipContent(html) {
127
+ tip.innerHTML = html;
128
+ }
129
+
130
+ function showTipAt(x, y) {
131
+ tip.style.left = Math.max(8, x + 12) + 'px';
132
+ tip.style.top = Math.max(8, y + 12) + 'px';
133
+ tip.style.display = 'block';
134
+ }
135
+
136
+ function hideTip() {
137
+ tip.style.display = 'none';
138
+ }
139
+
140
+ document.addEventListener('click', (e) => {
141
+ // Close when clicking outside an event
142
+ if (!e.target || !e.target.closest || !e.target.closest('.fc-event')) {
143
+ hideTip();
144
+ }
145
+ }, { passive: true });
146
+
124
147
  // Define a 2-week dayGrid view
125
148
  const calendar = new window.FullCalendar.Calendar(el, {
126
149
  initialView: 'dayGridTwoWeek',
@@ -141,138 +164,92 @@ widgets.renderTwoWeeksWidget = async function (data) {
141
164
  },
142
165
  navLinks: false,
143
166
  eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
167
+ // Render as a colored dot (like FullCalendar list dots)
144
168
  eventContent: function(arg) {
145
- // Render a simple dot instead of text.
169
+ const bg = (arg.event.backgroundColor || (arg.event.extendedProps && arg.event.extendedProps.backgroundColor) || '').trim();
170
+ const border = (arg.event.borderColor || '').trim();
171
+ const color = bg || border || '#3788d8';
146
172
  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');
173
+ wrap.className = 'onekite-dot-wrap';
174
+ const dot = document.createElement('span');
175
+ dot.className = 'onekite-dot';
176
+ dot.style.backgroundColor = color;
177
+ wrap.appendChild(dot);
151
178
  return { domNodes: [wrap] };
152
179
  },
153
180
  events: function(info, successCallback, failureCallback) {
154
181
  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
- })
182
+ fetch(eventsEndpoint + '?' + qs.toString(), { credentials: 'same-origin' })
164
183
  .then((r) => r.json())
165
184
  .then((json) => successCallback(json || []))
166
185
  .catch((e) => failureCallback(e));
167
186
  },
187
+ dateClick: function() { window.location.href = calUrl; },
168
188
  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
- dateClick: function() {
220
- window.location.href = calUrl;
221
- },
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) {}
189
+ try {
190
+ const ev = info.event;
191
+ const ep = ev.extendedProps || {};
192
+ const title = (ep.itemNameLine || ep.title || ev.title || '').toString();
193
+ const status = (ep.status || ep.type || '').toString();
194
+ const start = ev.start ? new Date(ev.start) : null;
195
+ const end = ev.end ? new Date(ev.end) : null;
196
+ const pad2 = (n) => String(n).padStart(2, '0');
197
+ const fmt = (d) => d ? `${pad2(d.getDate())}/${pad2(d.getMonth() + 1)}/${String(d.getFullYear()).slice(-2)}` : '';
198
+ const range = (start && end) ? `Du ${fmt(start)} au ${fmt(end)}` : '';
199
+ const html = `
200
+ <div style="font-weight:600; margin-bottom:2px;">${escapeHtml(title)}</div>
201
+ ${range ? `<div style=\"opacity:.85\">${escapeHtml(range)}</div>` : ''}
202
+ ${status ? `<div style=\"opacity:.75; margin-top:2px; font-size:.85em;\">${escapeHtml(status)}</div>` : ''}
203
+ `;
204
+
205
+ // Hover (desktop)
206
+ info.el.addEventListener('mouseenter', (e) => {
207
+ setTipContent(html);
208
+ const rect = info.el.getBoundingClientRect();
209
+ showTipAt(rect.left + window.scrollX, rect.top + window.scrollY);
210
+ }, { passive: true });
211
+ info.el.addEventListener('mouseleave', hideTip, { passive: true });
212
+
213
+ // Tap/click (mobile)
214
+ info.el.addEventListener('click', (e) => {
215
+ e.preventDefault();
216
+ e.stopPropagation();
217
+ setTipContent(html);
218
+ const pt = (e.touches && e.touches[0]) ? e.touches[0] : e;
219
+ showTipAt((pt.clientX || 0) + window.scrollX, (pt.clientY || 0) + window.scrollY);
220
+ });
221
+ } catch (e) {}
225
222
  },
226
223
  });
227
224
 
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
225
  calendar.render();
226
+
227
+ // Mobile swipe (left/right) to navigate weeks
228
+ try {
229
+ let touchStartX = null;
230
+ let touchStartY = null;
231
+ el.addEventListener('touchstart', (e) => {
232
+ const t = e.touches && e.touches[0];
233
+ if (!t) return;
234
+ touchStartX = t.clientX;
235
+ touchStartY = t.clientY;
236
+ }, { passive: true });
237
+ el.addEventListener('touchend', (e) => {
238
+ const t = e.changedTouches && e.changedTouches[0];
239
+ if (!t || touchStartX === null || touchStartY === null) return;
240
+ const dx = t.clientX - touchStartX;
241
+ const dy = t.clientY - touchStartY;
242
+ touchStartX = null;
243
+ touchStartY = null;
244
+ if (Math.abs(dx) < 55) return;
245
+ if (Math.abs(dx) < Math.abs(dy) * 1.2) return; // mostly vertical
246
+ if (dx < 0) {
247
+ calendar.next();
248
+ } else {
249
+ calendar.prev();
250
+ }
251
+ }, { passive: true });
252
+ } catch (e) {}
276
253
  }
277
254
 
278
255
  // Widgets can be rendered after ajaxify; delay a tick.
@@ -285,17 +262,22 @@ widgets.renderTwoWeeksWidget = async function (data) {
285
262
  .onekite-twoweeks .fc .fc-button { padding: .2rem .35rem; font-size: .75rem; }
286
263
  .onekite-twoweeks .fc .fc-daygrid-day-number { font-size: .75rem; padding: 2px; }
287
264
  .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;
265
+ .onekite-twoweeks .fc .fc-event { background: transparent; border: none; }
266
+ .onekite-twoweeks .fc .fc-event-main { color: inherit; }
267
+ .onekite-twoweeks .fc .fc-event-title { display:none; }
268
+ .onekite-dot-wrap { display:flex; align-items:center; justify-content:center; width: 100%; }
269
+ .onekite-dot { width: 10px; height: 10px; border-radius: 999px; display:inline-block; }
270
+ .onekite-cal-tooltip {
271
+ position: absolute;
272
+ z-index: 99999;
273
+ max-width: 260px;
274
+ background: rgba(0,0,0,.92);
275
+ color: #fff;
276
+ padding: 8px 10px;
277
+ border-radius: 10px;
278
+ font-size: 0.9rem;
279
+ box-shadow: 0 8px 24px rgba(0,0,0,.25);
280
+ pointer-events: none;
299
281
  }
300
282
  </style>
301
283
  `;
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,7 +124,7 @@ 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
129
  name: 'Calendar Onekite',
137
130
  });
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.8",
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/plugin.json CHANGED
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "1.0.7"
42
+ "version": "1.0.3"
43
43
  }
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');
@@ -5,7 +5,19 @@
5
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 };