nodebb-plugin-onekite-calendar 1.0.4 → 1.0.5
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 +2 -27
- package/lib/admin.js +155 -15
- package/lib/api.js +103 -158
- package/lib/controllers.js +2 -2
- package/lib/db.js +5 -5
- package/lib/discord.js +5 -6
- package/lib/helloasso.js +3 -4
- package/lib/helloassoWebhook.js +7 -8
- package/lib/scheduler.js +11 -8
- package/lib/widgets.js +8 -128
- package/library.js +63 -39
- package/package.json +1 -1
- package/plugin.json +11 -5
- package/public/admin.js +91 -19
- package/public/client.js +23 -23
- package/templates/admin/plugins/{onekite-calendar.tpl → calendar-onekite.tpl} +23 -4
- package/templates/{onekite-calendar.tpl → calendar-onekite.tpl} +0 -11
- package/lib/constants.js +0 -7
- package/lib/email.js +0 -26
- package/lib/log.js +0 -29
- /package/templates/emails/{onekite-calendar_approved.tpl → calendar-onekite_approved.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_expired.tpl → calendar-onekite_expired.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_paid.tpl → calendar-onekite_paid.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_pending.tpl → calendar-onekite_pending.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_refused.tpl → calendar-onekite_refused.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_reminder.tpl → calendar-onekite_reminder.tpl} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,29 +1,4 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 1.1.0
|
|
4
|
-
- Ultra perf clean : JS chargé uniquement sur les pages/plugin concernées (plus de scripts globaux)
|
|
5
|
-
- Suppression legacy supplémentaire : clés DB renommées en onekite-calendar:* (aucune donnée conservée)
|
|
6
|
-
- Nettoyages internes (variables globales JS, routes/paramètres uniformisés)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## 1.0.8
|
|
10
|
-
- Suppression du legacy (namespace unique onekite-calendar, ACP/API/widgets)
|
|
11
|
-
- Suppression des fallbacks inutiles
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## 1.0.6
|
|
15
|
-
- Refactor interne : centralisation de l’envoi d’emails et du logging (opt-in)
|
|
16
|
-
- Nettoyage : suppression des alias legacy (API + ACP)
|
|
17
|
-
|
|
18
|
-
## 1.0.5
|
|
19
|
-
- Widget : points colorés selon le type (mêmes couleurs que le calendrier)
|
|
20
|
-
- Widget mobile : swipe horizontal pour naviguer semaine par semaine
|
|
21
|
-
|
|
22
|
-
## 1.0.4
|
|
23
|
-
- Widget : affichage des évènements sous forme de points (sans texte)
|
|
24
|
-
- Widget : survol/clic sur un point affiche le contenu (tooltip + popup)
|
|
25
|
-
- Widget : renommage « Calendrier (2 semaines) » en « Calendrier »
|
|
26
|
-
- Texte : remplacement de toutes les occurrences « OneKite » par « Onekite »
|
|
1
|
+
# Changelog – calendar-onekite
|
|
27
2
|
|
|
28
3
|
## 1.0.3
|
|
29
4
|
- Suppression du texte d’archivage dans le toast de purge (plus de « 0 archivés »)
|
|
@@ -43,7 +18,7 @@
|
|
|
43
18
|
- Correctifs divers de stabilité/performance (dont crash au démarrage)
|
|
44
19
|
|
|
45
20
|
## 1.0.0
|
|
46
|
-
- Première version stable du plugin onekite
|
|
21
|
+
- Première version stable du plugin calendar-onekite
|
|
47
22
|
- Gestion des réservations de matériel avec contrôle de disponibilité
|
|
48
23
|
- Calendrier FullCalendar via CDN
|
|
49
24
|
- Validation / refus des demandes depuis l’ACP
|
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']) : '';
|
|
@@ -50,18 +94,18 @@ const ADMIN_PRIV = 'admin:settings';
|
|
|
50
94
|
const admin = {};
|
|
51
95
|
|
|
52
96
|
admin.renderAdmin = async function (req, res) {
|
|
53
|
-
res.render('admin/plugins/onekite
|
|
54
|
-
title: 'Calendar
|
|
97
|
+
res.render('admin/plugins/calendar-onekite', {
|
|
98
|
+
title: 'Calendar OneKite',
|
|
55
99
|
});
|
|
56
100
|
};
|
|
57
101
|
|
|
58
102
|
admin.getSettings = async function (req, res) {
|
|
59
|
-
const settings = await meta.settings.get('onekite
|
|
103
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
60
104
|
res.json(settings || {});
|
|
61
105
|
};
|
|
62
106
|
|
|
63
107
|
admin.saveSettings = async function (req, res) {
|
|
64
|
-
await meta.settings.set('onekite
|
|
108
|
+
await meta.settings.set('calendar-onekite', req.body || {});
|
|
65
109
|
res.json({ ok: true });
|
|
66
110
|
};
|
|
67
111
|
|
|
@@ -97,13 +141,17 @@ admin.approveReservation = async function (req, res) {
|
|
|
97
141
|
}
|
|
98
142
|
|
|
99
143
|
// Create HelloAsso payment link if configured
|
|
100
|
-
const settings = await meta.settings.get('onekite
|
|
144
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
101
145
|
const env = settings.helloassoEnv || 'prod';
|
|
102
146
|
const token = await helloasso.getAccessToken({
|
|
103
147
|
env,
|
|
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']);
|
|
@@ -111,7 +159,7 @@ admin.approveReservation = async function (req, res) {
|
|
|
111
159
|
const totalAmount = Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
112
160
|
const base = forumBaseUrl();
|
|
113
161
|
const returnUrl = base ? `${base}/calendar` : '';
|
|
114
|
-
const webhookUrl = base ? `${base}/plugins/onekite
|
|
162
|
+
const webhookUrl = base ? `${base}/plugins/calendar-onekite/helloasso` : '';
|
|
115
163
|
const year = new Date(Number(r.start)).getFullYear();
|
|
116
164
|
const intent = await helloasso.createCheckoutIntent({
|
|
117
165
|
env,
|
|
@@ -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);
|
|
@@ -154,7 +202,7 @@ admin.approveReservation = async function (req, res) {
|
|
|
154
202
|
const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
|
|
155
203
|
? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
|
|
156
204
|
: '';
|
|
157
|
-
await sendEmail('
|
|
205
|
+
await sendEmail('calendar-onekite_approved', requester.email, 'Location matériel - Réservation validée', {
|
|
158
206
|
uid: parseInt(r.uid, 10),
|
|
159
207
|
username: requester.username,
|
|
160
208
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
@@ -189,7 +237,7 @@ admin.refuseReservation = async function (req, res) {
|
|
|
189
237
|
try {
|
|
190
238
|
const requester = await user.getUserFields(r.uid, ['username', 'email']);
|
|
191
239
|
if (requester && requester.email) {
|
|
192
|
-
await sendEmail('
|
|
240
|
+
await sendEmail('calendar-onekite_refused', requester.email, 'Location matériel - Réservation refusée', {
|
|
193
241
|
uid: parseInt(r.uid, 10),
|
|
194
242
|
username: requester.username,
|
|
195
243
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
@@ -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:
|
|
@@ -360,7 +500,7 @@ admin.exportAccountingCsv = async function (req, res) {
|
|
|
360
500
|
}
|
|
361
501
|
const csv = lines.join('\n');
|
|
362
502
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
363
|
-
res.setHeader('Content-Disposition', 'attachment; filename="onekite-
|
|
503
|
+
res.setHeader('Content-Disposition', 'attachment; filename="calendar-onekite-accounting.csv"');
|
|
364
504
|
return res.send(csv);
|
|
365
505
|
};
|
|
366
506
|
|