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 +1 -15
- package/lib/admin.js +165 -7
- package/lib/api.js +57 -11
- package/lib/discord.js +2 -3
- package/lib/helloasso.js +3 -4
- package/lib/helloassoWebhook.js +3 -4
- package/lib/scheduler.js +5 -2
- package/lib/widgets.js +116 -134
- package/library.js +19 -26
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/admin.js +49 -0
- package/templates/admin/plugins/calendar-onekite.tpl +21 -2
- 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,67 @@ function formatFR(tsOrIso) {
|
|
|
20
18
|
return `${dd}/${mm}/${yyyy}`;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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}`;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
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
|
|
289
|
-
.onekite-twoweeks .fc .fc-event-
|
|
290
|
-
.onekite-twoweeks .
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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,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:
|
|
127
|
+
route: '/plugins/calendar-onekite',
|
|
135
128
|
icon: 'fa-calendar',
|
|
136
129
|
name: 'Calendar Onekite',
|
|
137
130
|
});
|
package/package.json
CHANGED
package/plugin.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');
|
|
@@ -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
|
-
|
|
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 };
|