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