nodebb-plugin-onekite-calendar 1.0.5 → 1.0.6
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 +15 -1
- package/lib/admin.js +7 -147
- package/lib/api.js +10 -37
- package/lib/constants.js +10 -0
- package/lib/discord.js +5 -4
- package/lib/email.js +39 -0
- package/lib/helloasso.js +4 -3
- package/lib/helloassoWebhook.js +4 -3
- package/lib/log.js +29 -0
- package/lib/scheduler.js +2 -5
- package/lib/widgets.js +138 -7
- package/library.js +27 -20
- package/package.json +1 -1
- package/plugin.json +2 -2
- package/public/admin.js +0 -49
- package/public/client.js +1 -1
- package/templates/admin/plugins/calendar-onekite.tpl +3 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
# Changelog –
|
|
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 »
|
|
2
16
|
|
|
3
17
|
## 1.0.3
|
|
4
18
|
- Suppression du texte d’archivage dans le toast de purge (plus de « 0 archivés »)
|
package/lib/admin.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
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');
|
|
6
5
|
const nconf = require.main.require('nconf');
|
|
7
6
|
|
|
7
|
+
const { sendEmail } = require('./email');
|
|
8
|
+
const log = require('./log');
|
|
9
|
+
|
|
8
10
|
function forumBaseUrl() {
|
|
9
11
|
const base = String(nconf.get('url') || '').trim().replace(/\/$/, '');
|
|
10
12
|
return base;
|
|
@@ -18,53 +20,7 @@ function formatFR(tsOrIso) {
|
|
|
18
20
|
return `${dd}/${mm}/${yyyy}`;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
|
|
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
|
-
}
|
|
23
|
+
// Email helper is centralized in lib/email.js.
|
|
68
24
|
|
|
69
25
|
function normalizeCallbackUrl(configured, meta) {
|
|
70
26
|
const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
|
|
@@ -95,7 +51,7 @@ const admin = {};
|
|
|
95
51
|
|
|
96
52
|
admin.renderAdmin = async function (req, res) {
|
|
97
53
|
res.render('admin/plugins/calendar-onekite', {
|
|
98
|
-
title: 'Calendar
|
|
54
|
+
title: 'Calendar Onekite',
|
|
99
55
|
});
|
|
100
56
|
};
|
|
101
57
|
|
|
@@ -148,10 +104,6 @@ admin.approveReservation = async function (req, res) {
|
|
|
148
104
|
clientId: settings.helloassoClientId,
|
|
149
105
|
clientSecret: settings.helloassoClientSecret,
|
|
150
106
|
});
|
|
151
|
-
if (!token) {
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
107
|
let paymentUrl = null;
|
|
156
108
|
if (token) {
|
|
157
109
|
const requester = await user.getUserFields(r.uid, ['username', 'email']);
|
|
@@ -173,7 +125,7 @@ admin.approveReservation = async function (req, res) {
|
|
|
173
125
|
// User return/back/error URLs must be real pages; webhook uses the plugin endpoint.
|
|
174
126
|
callbackUrl: returnUrl,
|
|
175
127
|
webhookUrl: webhookUrl,
|
|
176
|
-
itemName: 'Réservation matériel
|
|
128
|
+
itemName: 'Réservation matériel Onekite',
|
|
177
129
|
containsDonation: false,
|
|
178
130
|
metadata: { reservationId: String(rid) },
|
|
179
131
|
});
|
|
@@ -188,7 +140,7 @@ admin.approveReservation = async function (req, res) {
|
|
|
188
140
|
if (paymentUrl) {
|
|
189
141
|
r.paymentUrl = paymentUrl;
|
|
190
142
|
} else {
|
|
191
|
-
|
|
143
|
+
log.warn('HelloAsso payment link not created (approve ACP)', { rid });
|
|
192
144
|
}
|
|
193
145
|
|
|
194
146
|
await dbLayer.saveReservation(r);
|
|
@@ -292,98 +244,6 @@ admin.purgeSpecialEventsByYear = async function (req, res) {
|
|
|
292
244
|
return res.json({ ok: true, removed: count });
|
|
293
245
|
};
|
|
294
246
|
|
|
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
|
-
};
|
|
387
247
|
|
|
388
248
|
// Accounting endpoint: aggregates paid reservations so you can contabilize what was rented.
|
|
389
249
|
// Query params:
|
package/lib/api.js
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
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');
|
|
7
6
|
const nconf = require.main.require('nconf');
|
|
8
7
|
const user = require.main.require('./src/user');
|
|
9
8
|
const groups = require.main.require('./src/groups');
|
|
10
9
|
const db = require.main.require('./src/database');
|
|
11
|
-
|
|
10
|
+
// logger available if you want to debug locally; we avoid noisy logs in prod.
|
|
12
11
|
|
|
13
12
|
const dbLayer = require('./db');
|
|
13
|
+
const { sendEmail } = require('./email');
|
|
14
|
+
const log = require('./log');
|
|
14
15
|
|
|
15
16
|
// Fast membership check without N calls to groups.isMember.
|
|
16
17
|
// NodeBB's groups.getUserGroups([uid]) returns an array (per uid) of group objects.
|
|
@@ -52,36 +53,7 @@ function normalizeAllowedGroups(raw) {
|
|
|
52
53
|
const helloasso = require('./helloasso');
|
|
53
54
|
const discord = require('./discord');
|
|
54
55
|
|
|
55
|
-
// Email helper
|
|
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
|
-
}
|
|
56
|
+
// Email helper is in lib/email.js (no logs here).
|
|
85
57
|
|
|
86
58
|
function normalizeBaseUrl(meta) {
|
|
87
59
|
// Prefer meta.config.url, fallback to nconf.get('url')
|
|
@@ -102,7 +74,8 @@ function normalizeCallbackUrl(configured, meta) {
|
|
|
102
74
|
let url = (configured || '').trim();
|
|
103
75
|
if (!url) {
|
|
104
76
|
// Default webhook endpoint (recommended): namespaced under /plugins
|
|
105
|
-
|
|
77
|
+
// Prefer the new namespace; legacy endpoint remains available.
|
|
78
|
+
url = base ? `${base}/plugins/onekite-calendar/helloasso` : '';
|
|
106
79
|
}
|
|
107
80
|
if (url && url.startsWith('/') && base) {
|
|
108
81
|
url = `${base}${url}`;
|
|
@@ -722,7 +695,7 @@ api.createReservation = async function (req, res) {
|
|
|
722
695
|
}
|
|
723
696
|
}
|
|
724
697
|
} catch (e) {
|
|
725
|
-
|
|
698
|
+
log.warn('Failed to send pending email', e && e.message ? e.message : e);
|
|
726
699
|
}
|
|
727
700
|
|
|
728
701
|
// Discord webhook (optional)
|
|
@@ -788,7 +761,7 @@ api.approveReservation = async function (req, res) {
|
|
|
788
761
|
totalAmount: (() => {
|
|
789
762
|
const cents = Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
790
763
|
if (!cents) {
|
|
791
|
-
|
|
764
|
+
log.warn('HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
|
|
792
765
|
}
|
|
793
766
|
return cents;
|
|
794
767
|
})(),
|
|
@@ -797,7 +770,7 @@ api.approveReservation = async function (req, res) {
|
|
|
797
770
|
// Can be overridden via ACP setting `helloassoCallbackUrl`.
|
|
798
771
|
callbackUrl: normalizeReturnUrl(meta),
|
|
799
772
|
webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
|
|
800
|
-
itemName: 'Réservation matériel
|
|
773
|
+
itemName: 'Réservation matériel Onekite',
|
|
801
774
|
containsDonation: false,
|
|
802
775
|
metadata: { reservationId: String(rid) },
|
|
803
776
|
});
|
|
@@ -809,7 +782,7 @@ api.approveReservation = async function (req, res) {
|
|
|
809
782
|
r.checkoutIntentId = checkoutIntentId;
|
|
810
783
|
}
|
|
811
784
|
} else {
|
|
812
|
-
|
|
785
|
+
log.warn('HelloAsso payment link not created (approve API)', { rid });
|
|
813
786
|
}
|
|
814
787
|
} catch (e) {
|
|
815
788
|
// ignore payment link errors, admin can retry
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
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/discord.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const https = require('https');
|
|
4
|
+
const log = require('./log');
|
|
4
5
|
const { URL } = require('url');
|
|
5
6
|
|
|
6
7
|
function isEnabled(v, defaultValue) {
|
|
@@ -88,7 +89,7 @@ function buildReservationMessage(kind, reservation) {
|
|
|
88
89
|
function buildWebhookPayload(kind, reservation) {
|
|
89
90
|
// Discord "regroupe" visuellement les messages consécutifs d'un même auteur.
|
|
90
91
|
// En utilisant un username différent par action, on obtient un message bien distinct.
|
|
91
|
-
const webhookUsername = kind === 'paid' ? '
|
|
92
|
+
const webhookUsername = kind === 'paid' ? 'Onekite • Paiement' : 'Onekite • Réservation';
|
|
92
93
|
|
|
93
94
|
const calUrl = 'https://www.onekite.com/calendar';
|
|
94
95
|
const username = reservation && reservation.username ? String(reservation.username) : '';
|
|
@@ -124,7 +125,7 @@ function buildWebhookPayload(kind, reservation) {
|
|
|
124
125
|
? 'Un paiement a été reçu pour une réservation.'
|
|
125
126
|
: 'Une nouvelle demande de réservation a été créée.',
|
|
126
127
|
fields,
|
|
127
|
-
footer: { text: '
|
|
128
|
+
footer: { text: 'Onekite • Calendrier' },
|
|
128
129
|
timestamp: new Date().toISOString(),
|
|
129
130
|
},
|
|
130
131
|
],
|
|
@@ -140,7 +141,7 @@ async function notifyReservationRequested(settings, reservation) {
|
|
|
140
141
|
await postWebhook(url, buildWebhookPayload('request', reservation));
|
|
141
142
|
} catch (e) {
|
|
142
143
|
// eslint-disable-next-line no-console
|
|
143
|
-
|
|
144
|
+
log.warn('Discord webhook failed (request)', e && e.message ? e.message : String(e));
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
|
|
@@ -153,7 +154,7 @@ async function notifyPaymentReceived(settings, reservation) {
|
|
|
153
154
|
await postWebhook(url, buildWebhookPayload('paid', reservation));
|
|
154
155
|
} catch (e) {
|
|
155
156
|
// eslint-disable-next-line no-console
|
|
156
|
-
|
|
157
|
+
log.warn('Discord webhook failed (paid)', e && e.message ? e.message : String(e));
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
|
package/lib/email.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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/helloasso.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const https = require('https');
|
|
4
|
+
const log = require('./log');
|
|
4
5
|
|
|
5
6
|
function requestJson(method, url, headers = {}, bodyObj = null) {
|
|
6
7
|
return new Promise((resolve) => {
|
|
@@ -284,11 +285,11 @@ async function listCatalogItems({ env, token, organizationSlug, formType, formSl
|
|
|
284
285
|
async function createCheckoutIntent({ env, token, organizationSlug, formType, formSlug, totalAmount, payerEmail, callbackUrl, webhookUrl, itemName, containsDonation, metadata }) {
|
|
285
286
|
if (!token || !organizationSlug) return null;
|
|
286
287
|
if (!callbackUrl || !/^https?:\/\//i.test(String(callbackUrl))) {
|
|
287
|
-
|
|
288
|
+
log.warn('HelloAsso invalid return/back/error URL', { callbackUrl });
|
|
288
289
|
return null;
|
|
289
290
|
}
|
|
290
291
|
if (webhookUrl && !/^https?:\/\//i.test(String(webhookUrl))) {
|
|
291
|
-
|
|
292
|
+
log.warn('HelloAsso invalid webhook URL', { webhookUrl });
|
|
292
293
|
}
|
|
293
294
|
// Checkout intents are created at organization level.
|
|
294
295
|
const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(organizationSlug)}/checkout-intents`;
|
|
@@ -311,7 +312,7 @@ async function createCheckoutIntent({ env, token, organizationSlug, formType, fo
|
|
|
311
312
|
// Log the error payload to help diagnose configuration issues (slug, env, urls, amount, etc.)
|
|
312
313
|
try {
|
|
313
314
|
// eslint-disable-next-line no-console
|
|
314
|
-
|
|
315
|
+
log.warn('HelloAsso checkout-intent failed', { status, json });
|
|
315
316
|
} catch (e) { /* ignore */ }
|
|
316
317
|
return null;
|
|
317
318
|
}
|
package/lib/helloassoWebhook.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const log = require('./log');
|
|
4
5
|
|
|
5
6
|
const db = require.main.require('./src/database');
|
|
6
7
|
const meta = require.main.require('./src/meta');
|
|
@@ -67,7 +68,7 @@ async function sendEmail(template, toEmail, subject, data) {
|
|
|
67
68
|
// fall back to sendToEmail only.
|
|
68
69
|
} catch (err) {
|
|
69
70
|
// eslint-disable-next-line no-console
|
|
70
|
-
|
|
71
|
+
log.warn('Failed to send email (webhook)', { template, toEmail, err: String(err && err.message || err) });
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -281,7 +282,7 @@ async function handler(req, res, next) {
|
|
|
281
282
|
|
|
282
283
|
if (allowedIps.length && clientIp && !allowedIps.includes(clientIp)) {
|
|
283
284
|
// eslint-disable-next-line no-console
|
|
284
|
-
|
|
285
|
+
log.warn('HelloAsso webhook blocked by IP allowlist', { ip: clientIp, allowed: allowedIps });
|
|
285
286
|
return res.status(403).json({ ok: false, error: 'ip-not-allowed' });
|
|
286
287
|
}
|
|
287
288
|
|
|
@@ -324,7 +325,7 @@ async function handler(req, res, next) {
|
|
|
324
325
|
}
|
|
325
326
|
if (!resolvedRid) {
|
|
326
327
|
// eslint-disable-next-line no-console
|
|
327
|
-
|
|
328
|
+
log.warn('HelloAsso webhook missing reservationId in metadata', { eventType: payload && payload.eventType, paymentId, checkoutIntentId });
|
|
328
329
|
// Do NOT mark as processed: if metadata/config is fixed later, a manual replay may be possible.
|
|
329
330
|
return res.json({ ok: true, processed: false, missingReservationId: true });
|
|
330
331
|
}
|
package/lib/log.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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 };
|
package/lib/scheduler.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
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');
|
|
6
7
|
|
|
7
8
|
let timer = null;
|
|
8
9
|
|
|
@@ -83,11 +84,7 @@ async function processAwaitingPayment() {
|
|
|
83
84
|
}
|
|
84
85
|
} catch (err) {
|
|
85
86
|
// eslint-disable-next-line no-console
|
|
86
|
-
|
|
87
|
-
template,
|
|
88
|
-
toEmail,
|
|
89
|
-
err: String((err && err.message) || err),
|
|
90
|
-
});
|
|
87
|
+
log.warn('Failed to send email (scheduler)', { template, toEmail, err: String((err && err.message) || err) });
|
|
91
88
|
}
|
|
92
89
|
}
|
|
93
90
|
|
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
|
|
44
|
+
name: 'Calendrier Onekite',
|
|
45
45
|
description: 'Affiche la semaine courante + la semaine suivante (FullCalendar via CDN).',
|
|
46
46
|
content: '',
|
|
47
47
|
});
|
|
@@ -54,16 +54,19 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
54
54
|
const id = makeDomId();
|
|
55
55
|
const calUrl = widgetCalendarUrl();
|
|
56
56
|
const apiBase = forumBaseUrl();
|
|
57
|
-
|
|
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`;
|
|
58
60
|
|
|
59
61
|
const idJson = JSON.stringify(id);
|
|
60
62
|
const calUrlJson = JSON.stringify(calUrl);
|
|
61
63
|
const eventsEndpointJson = JSON.stringify(eventsEndpoint);
|
|
64
|
+
const legacyEventsEndpointJson = JSON.stringify(legacyEventsEndpoint);
|
|
62
65
|
|
|
63
66
|
const html = `
|
|
64
67
|
<div class="onekite-twoweeks">
|
|
65
68
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
66
|
-
<div style="font-weight: 600;">Calendrier
|
|
69
|
+
<div style="font-weight: 600;">Calendrier</div>
|
|
67
70
|
<a href="${escapeHtml(calUrl)}" class="btn btn-sm btn-outline-secondary" style="line-height: 1.1;">Ouvrir</a>
|
|
68
71
|
</div>
|
|
69
72
|
<div id="${escapeHtml(id)}"></div>
|
|
@@ -75,6 +78,7 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
75
78
|
const containerId = ${idJson};
|
|
76
79
|
const calUrl = ${calUrlJson};
|
|
77
80
|
const eventsEndpoint = ${eventsEndpointJson};
|
|
81
|
+
const legacyEventsEndpoint = ${legacyEventsEndpointJson};
|
|
78
82
|
|
|
79
83
|
function loadOnce(tag, attrs) {
|
|
80
84
|
return new Promise((resolve, reject) => {
|
|
@@ -137,21 +141,137 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
137
141
|
},
|
|
138
142
|
navLinks: false,
|
|
139
143
|
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
|
+
},
|
|
140
153
|
events: function(info, successCallback, failureCallback) {
|
|
141
154
|
const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
|
|
142
|
-
|
|
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
|
+
})
|
|
143
164
|
.then((r) => r.json())
|
|
144
165
|
.then((json) => successCallback(json || []))
|
|
145
166
|
.catch((e) => failureCallback(e));
|
|
146
167
|
},
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>') + '</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.
|
|
147
219
|
dateClick: function() {
|
|
148
220
|
window.location.href = calUrl;
|
|
149
221
|
},
|
|
150
|
-
eventClick:
|
|
151
|
-
|
|
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) {}
|
|
152
225
|
},
|
|
153
226
|
});
|
|
154
227
|
|
|
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
|
+
|
|
155
275
|
calendar.render();
|
|
156
276
|
}
|
|
157
277
|
|
|
@@ -165,7 +285,18 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
165
285
|
.onekite-twoweeks .fc .fc-button { padding: .2rem .35rem; font-size: .75rem; }
|
|
166
286
|
.onekite-twoweeks .fc .fc-daygrid-day-number { font-size: .75rem; padding: 2px; }
|
|
167
287
|
.onekite-twoweeks .fc .fc-col-header-cell-cushion { font-size: .75rem; }
|
|
168
|
-
.onekite-twoweeks .fc .fc-event-title {
|
|
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
|
+
}
|
|
169
300
|
</style>
|
|
170
301
|
`;
|
|
171
302
|
|
package/library.js
CHANGED
|
@@ -13,6 +13,7 @@ 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');
|
|
16
17
|
const bodyParser = require('body-parser');
|
|
17
18
|
|
|
18
19
|
const Plugin = {};
|
|
@@ -59,25 +60,30 @@ Plugin.init = async function (params) {
|
|
|
59
60
|
// IMPORTANT: pass an ARRAY for middlewares (even if empty), otherwise
|
|
60
61
|
// setupPageRoute will throw "middlewares is not iterable".
|
|
61
62
|
routeHelpers.setupPageRoute(router, '/calendar', mw(), controllers.renderCalendar);
|
|
62
|
-
|
|
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);
|
|
63
66
|
|
|
64
67
|
// Public API (JSON) — NodeBB 4.x only (v3 API)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
});
|
|
78
84
|
|
|
79
85
|
// Admin API (JSON)
|
|
80
|
-
const adminBases = [
|
|
86
|
+
const adminBases = [`/api/v3/admin/plugins/${LEGACY_NAMESPACE}`, `/api/v3/admin/plugins/${NAMESPACE}`];
|
|
81
87
|
|
|
82
88
|
adminBases.forEach((base) => {
|
|
83
89
|
router.get(`${base}/settings`, ...adminMws, admin.getSettings);
|
|
@@ -88,7 +94,6 @@ Plugin.init = async function (params) {
|
|
|
88
94
|
router.put(`${base}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
|
|
89
95
|
|
|
90
96
|
router.post(`${base}/purge`, ...adminMws, admin.purgeByYear);
|
|
91
|
-
router.get(`${base}/debug`, ...adminMws, admin.debugHelloAsso);
|
|
92
97
|
// Accounting / exports
|
|
93
98
|
router.get(`${base}/accounting`, ...adminMws, admin.getAccounting);
|
|
94
99
|
router.get(`${base}/accounting.csv`, ...adminMws, admin.exportAccountingCsv);
|
|
@@ -112,11 +117,13 @@ Plugin.init = async function (params) {
|
|
|
112
117
|
// Accept webhook on both legacy root path and namespaced plugin path.
|
|
113
118
|
// Some reverse proxies block unknown root paths, so /plugins/... is recommended.
|
|
114
119
|
router.post('/helloasso', helloassoJson, helloassoWebhook.handler);
|
|
115
|
-
router.post(
|
|
120
|
+
router.post(`/plugins/${LEGACY_NAMESPACE}/helloasso`, helloassoJson, helloassoWebhook.handler);
|
|
121
|
+
router.post(`/plugins/${NAMESPACE}/helloasso`, helloassoJson, helloassoWebhook.handler);
|
|
116
122
|
|
|
117
123
|
// Optional: health checks
|
|
118
124
|
router.get('/helloasso', (req, res) => res.json({ ok: true }));
|
|
119
|
-
router.get(
|
|
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 }));
|
|
120
127
|
|
|
121
128
|
scheduler.start();
|
|
122
129
|
};
|
|
@@ -124,9 +131,9 @@ Plugin.init = async function (params) {
|
|
|
124
131
|
Plugin.addAdminNavigation = async function (header) {
|
|
125
132
|
header.plugins = header.plugins || [];
|
|
126
133
|
header.plugins.push({
|
|
127
|
-
route:
|
|
134
|
+
route: `/plugins/${LEGACY_NAMESPACE}`,
|
|
128
135
|
icon: 'fa-calendar',
|
|
129
|
-
name: 'Calendar
|
|
136
|
+
name: 'Calendar Onekite',
|
|
130
137
|
});
|
|
131
138
|
return header;
|
|
132
139
|
};
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "nodebb-plugin-onekite-calendar",
|
|
3
|
-
"name": "
|
|
3
|
+
"name": "Onekite Calendar",
|
|
4
4
|
"description": "Equipment reservation calendar (FullCalendar) with admin approval & HelloAsso checkout",
|
|
5
5
|
"url": "https://www.onekite.com/calendar",
|
|
6
6
|
"hooks": [
|
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"acpScripts": [
|
|
40
40
|
"public/admin.js"
|
|
41
41
|
],
|
|
42
|
-
"version": "1.0.
|
|
42
|
+
"version": "1.0.7"
|
|
43
43
|
}
|
package/public/admin.js
CHANGED
|
@@ -316,14 +316,6 @@ 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
|
-
|
|
327
319
|
async function loadAccounting(from, to) {
|
|
328
320
|
const params = new URLSearchParams();
|
|
329
321
|
if (from) params.set('from', from);
|
|
@@ -336,23 +328,6 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
336
328
|
const form = document.getElementById('onekite-settings-form');
|
|
337
329
|
if (!form) return;
|
|
338
330
|
|
|
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
|
-
|
|
356
331
|
// Load settings
|
|
357
332
|
try {
|
|
358
333
|
const s = await loadSettings();
|
|
@@ -674,30 +649,6 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
674
649
|
});
|
|
675
650
|
}
|
|
676
651
|
|
|
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
|
-
|
|
701
652
|
// Accounting (paid reservations)
|
|
702
653
|
const accFrom = document.getElementById('onekite-acc-from');
|
|
703
654
|
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
|
|
7
|
+
(function ensureOnekiteStyles() {
|
|
8
8
|
try {
|
|
9
9
|
if (document.getElementById('onekite-inline-styles')) return;
|
|
10
10
|
const style = document.createElement('style');
|
|
@@ -2,22 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
<div class="row">
|
|
4
4
|
<div class="col-lg-9">
|
|
5
|
-
<h1>Calendar
|
|
5
|
+
<h1>Calendar Onekite</h1>
|
|
6
6
|
|
|
7
7
|
<ul class="nav nav-tabs mt-3" role="tablist">
|
|
8
|
-
|
|
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">
|
|
8
|
+
<li class="nav-item" role="presentation">
|
|
21
9
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-accounting" type="button" role="tab">Comptabilisation</button>
|
|
22
10
|
</li>
|
|
23
11
|
</ul>
|
|
@@ -154,14 +142,7 @@
|
|
|
154
142
|
<h4>Demandes en attente</h4>
|
|
155
143
|
<div id="onekite-pending" class="list-group"></div>
|
|
156
144
|
</div>
|
|
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">
|
|
145
|
+
<div class="tab-pane fade" id="onekite-tab-accounting" role="tabpanel">
|
|
165
146
|
<h4>Comptabilisation des locations (payées)</h4>
|
|
166
147
|
<div class="d-flex flex-wrap gap-2 align-items-end mb-3">
|
|
167
148
|
<div>
|