nodebb-plugin-onekite-calendar 1.0.21 → 1.0.23
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 +0 -2
- package/lib/admin.js +29 -52
- package/lib/api.js +94 -147
- package/lib/helloassoWebhook.js +20 -47
- package/lib/scheduler.js +31 -52
- package/library.js +1 -7
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/admin.js +3 -3
- package/public/client.js +4 -9
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
- Suppression du texte d’archivage dans le toast de purge (plus de « 0 archivés »)
|
|
5
5
|
- Renommage du plugin : nodebb-plugin-onekite-calendar
|
|
6
6
|
- Discord : notification « ❌ Réservation annulée » (annulation manuelle + annulation automatique) + option ACP
|
|
7
|
-
- Groupes ACP : les champs CSV acceptent aussi ; et retours à la ligne
|
|
8
|
-
- Notifications email (groupes) : résolution robuste nom/slug et normalisation des membres (uids)
|
|
9
7
|
|
|
10
8
|
## 1.0.2
|
|
11
9
|
- Purge calendrier : suppression réelle des réservations (aucune logique d’archivage)
|
package/lib/admin.js
CHANGED
|
@@ -32,49 +32,24 @@ function buildHelloAssoItemName(baseLabel, itemNames, start, end) {
|
|
|
32
32
|
return out;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async function sendEmail(template,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!toEmail && !uid) return;
|
|
35
|
+
async function sendEmail(template, uid, subject, data) {
|
|
36
|
+
const toUid = Number.isInteger(uid) ? uid : (uid ? parseInt(uid, 10) : NaN);
|
|
37
|
+
if (!Number.isInteger(toUid) || toUid <= 0) return;
|
|
39
38
|
|
|
40
|
-
const settings = await meta.settings.get('calendar-onekite').catch(() => ({}));
|
|
41
|
-
const lang = (settings && settings.defaultLang) || (meta && meta.config && meta.config.defaultLang) || 'fr';
|
|
42
39
|
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
43
40
|
|
|
44
|
-
// If we have a uid, use the native uid-based sender first.
|
|
45
41
|
try {
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
return;
|
|
42
|
+
if (typeof emailer.send !== 'function') return;
|
|
43
|
+
// NodeBB 4.x: send(template, uid, params)
|
|
44
|
+
if (emailer.send.length >= 3) {
|
|
45
|
+
await emailer.send(template, toUid, params);
|
|
46
|
+
} else {
|
|
47
|
+
await emailer.send(template, toUid);
|
|
54
48
|
}
|
|
55
49
|
} catch (err) {
|
|
56
50
|
console.warn('[calendar-onekite] Failed to send email', {
|
|
57
51
|
template,
|
|
58
|
-
|
|
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,
|
|
52
|
+
uid: toUid,
|
|
78
53
|
err: err && err.message ? err.message : String(err),
|
|
79
54
|
});
|
|
80
55
|
}
|
|
@@ -187,11 +162,11 @@ admin.approveReservation = async function (req, res) {
|
|
|
187
162
|
// User return/back/error URLs must be real pages; webhook uses the plugin endpoint.
|
|
188
163
|
callbackUrl: returnUrl,
|
|
189
164
|
webhookUrl: webhookUrl,
|
|
190
|
-
itemName: buildHelloAssoItemName('Réservation matériel Onekite',
|
|
165
|
+
itemName: buildHelloAssoItemName('Réservation matériel Onekite', Array.isArray(r.itemNames) ? r.itemNames : [], r.start, r.end),
|
|
191
166
|
containsDonation: false,
|
|
192
167
|
metadata: {
|
|
193
168
|
reservationId: String(rid),
|
|
194
|
-
items: (Array.isArray(r.itemNames) ? r.itemNames :
|
|
169
|
+
items: (Array.isArray(r.itemNames) ? r.itemNames : []).filter(Boolean),
|
|
195
170
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
196
171
|
},
|
|
197
172
|
});
|
|
@@ -213,18 +188,19 @@ admin.approveReservation = async function (req, res) {
|
|
|
213
188
|
|
|
214
189
|
// Email requester
|
|
215
190
|
try {
|
|
216
|
-
const
|
|
217
|
-
|
|
191
|
+
const requesterUid = parseInt(r.uid, 10);
|
|
192
|
+
const requester = await user.getUserFields(requesterUid, ['username']);
|
|
193
|
+
if (requesterUid) {
|
|
218
194
|
const latNum = Number(r.pickupLat);
|
|
219
195
|
const lonNum = Number(r.pickupLon);
|
|
220
196
|
const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
|
|
221
197
|
? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
|
|
222
198
|
: '';
|
|
223
|
-
await sendEmail('calendar-onekite_approved',
|
|
224
|
-
uid:
|
|
225
|
-
username: requester.username,
|
|
226
|
-
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') :
|
|
227
|
-
itemNames: (Array.isArray(r.itemNames) ? r.itemNames :
|
|
199
|
+
await sendEmail('calendar-onekite_approved', requesterUid, 'Location matériel - Réservation validée', {
|
|
200
|
+
uid: requesterUid,
|
|
201
|
+
username: requester && requester.username ? requester.username : '',
|
|
202
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
203
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
228
204
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
229
205
|
paymentUrl: paymentUrl || '',
|
|
230
206
|
pickupAddress: r.pickupAddress || '',
|
|
@@ -253,13 +229,14 @@ admin.refuseReservation = async function (req, res) {
|
|
|
253
229
|
await dbLayer.saveReservation(r);
|
|
254
230
|
|
|
255
231
|
try {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
232
|
+
const requesterUid = parseInt(r.uid, 10);
|
|
233
|
+
const requester = await user.getUserFields(requesterUid, ['username']);
|
|
234
|
+
if (requesterUid) {
|
|
235
|
+
await sendEmail('calendar-onekite_refused', requesterUid, 'Location matériel - Réservation refusée', {
|
|
236
|
+
uid: requesterUid,
|
|
237
|
+
username: requester && requester.username ? requester.username : '',
|
|
238
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
239
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
263
240
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
264
241
|
start: formatFR(r.start),
|
|
265
242
|
end: formatFR(r.end),
|
|
@@ -442,7 +419,7 @@ admin.getAccounting = async function (req, res) {
|
|
|
442
419
|
|
|
443
420
|
const itemNames = Array.isArray(r.itemNames) && r.itemNames.length
|
|
444
421
|
? r.itemNames
|
|
445
|
-
:
|
|
422
|
+
: [];
|
|
446
423
|
|
|
447
424
|
const total = Number(r.total) || 0;
|
|
448
425
|
const startDate = formatFR(r.start);
|
package/lib/api.js
CHANGED
|
@@ -20,27 +20,6 @@ async function getMembersByGroupIdentifier(groupIdentifier) {
|
|
|
20
20
|
const id = String(groupIdentifier || '').trim();
|
|
21
21
|
if (!id) return [];
|
|
22
22
|
|
|
23
|
-
const toUids = (arr) => {
|
|
24
|
-
if (!Array.isArray(arr)) return [];
|
|
25
|
-
// NodeBB may return an array of uids (numbers/strings), or an array of user objects (with uid).
|
|
26
|
-
const uids = arr.map((v) => {
|
|
27
|
-
if (v == null) return null;
|
|
28
|
-
if (typeof v === 'number') return v;
|
|
29
|
-
if (typeof v === 'string') {
|
|
30
|
-
const n = parseInt(v, 10);
|
|
31
|
-
return Number.isFinite(n) ? n : null;
|
|
32
|
-
}
|
|
33
|
-
if (typeof v === 'object') {
|
|
34
|
-
const maybe = v.uid ?? v.userId ?? v.id;
|
|
35
|
-
const n = parseInt(String(maybe), 10);
|
|
36
|
-
return Number.isFinite(n) ? n : null;
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}).filter((n) => Number.isFinite(n));
|
|
40
|
-
// de-dupe
|
|
41
|
-
return [...new Set(uids)];
|
|
42
|
-
};
|
|
43
|
-
|
|
44
23
|
// First try direct.
|
|
45
24
|
let members = [];
|
|
46
25
|
try {
|
|
@@ -48,17 +27,7 @@ async function getMembersByGroupIdentifier(groupIdentifier) {
|
|
|
48
27
|
} catch (e) {
|
|
49
28
|
members = [];
|
|
50
29
|
}
|
|
51
|
-
|
|
52
|
-
if (directUids.length) return directUids;
|
|
53
|
-
|
|
54
|
-
// Some NodeBB builds expose getMembersByGroupSlug (slug -> uids)
|
|
55
|
-
if (typeof groups.getMembersByGroupSlug === 'function') {
|
|
56
|
-
try {
|
|
57
|
-
const bySlug = await groups.getMembersByGroupSlug(id, 0, -1);
|
|
58
|
-
const bySlugUids = toUids(bySlug);
|
|
59
|
-
if (bySlugUids.length) return bySlugUids;
|
|
60
|
-
} catch (e) {}
|
|
61
|
-
}
|
|
30
|
+
if (Array.isArray(members) && members.length) return members;
|
|
62
31
|
|
|
63
32
|
// Then try slug -> groupName mapping when available.
|
|
64
33
|
if (typeof groups.getGroupNameByGroupSlug === 'function') {
|
|
@@ -81,12 +50,27 @@ async function getMembersByGroupIdentifier(groupIdentifier) {
|
|
|
81
50
|
} catch (e) {
|
|
82
51
|
members = [];
|
|
83
52
|
}
|
|
84
|
-
|
|
85
|
-
if (byNameUids.length) return byNameUids;
|
|
53
|
+
if (Array.isArray(members) && members.length) return members;
|
|
86
54
|
}
|
|
87
55
|
}
|
|
88
56
|
|
|
89
|
-
return [];
|
|
57
|
+
return Array.isArray(members) ? members : [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
function normalizeUids(members) {
|
|
62
|
+
if (!Array.isArray(members)) return [];
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const m of members) {
|
|
65
|
+
if (Number.isInteger(m)) { out.push(m); continue; }
|
|
66
|
+
if (typeof m === 'string' && m.trim() && !Number.isNaN(parseInt(m, 10))) { out.push(parseInt(m, 10)); continue; }
|
|
67
|
+
if (m && typeof m === 'object' && (Number.isInteger(m.uid) || (typeof m.uid === 'string' && m.uid.trim()))) {
|
|
68
|
+
const u = Number.isInteger(m.uid) ? m.uid : parseInt(m.uid, 10);
|
|
69
|
+
if (!Number.isNaN(u)) out.push(u);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// de-dupe
|
|
73
|
+
return Array.from(new Set(out));
|
|
90
74
|
}
|
|
91
75
|
|
|
92
76
|
// Fast membership check without N calls to groups.isMember.
|
|
@@ -120,8 +104,7 @@ function normalizeAllowedGroups(raw) {
|
|
|
120
104
|
if (Array.isArray(parsed)) return parsed.map(v => String(v).trim()).filter(Boolean);
|
|
121
105
|
} catch (e) {}
|
|
122
106
|
}
|
|
123
|
-
|
|
124
|
-
return s.split(/[,;\n]+/).map(v => String(v).trim().replace(/^"+|"+$/g, '')).filter(Boolean);
|
|
107
|
+
return s.split(',').map(v => String(v).trim().replace(/^"+|"+$/g, '')).filter(Boolean);
|
|
125
108
|
}
|
|
126
109
|
|
|
127
110
|
// NOTE: Avoid per-group async checks (groups.isMember) when possible.
|
|
@@ -130,54 +113,25 @@ function normalizeAllowedGroups(raw) {
|
|
|
130
113
|
const helloasso = require('./helloasso');
|
|
131
114
|
const discord = require('./discord');
|
|
132
115
|
|
|
133
|
-
// Email helper
|
|
134
|
-
//
|
|
135
|
-
async function sendEmail(template,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// Language can come from plugin settings or NodeBB config depending on version.
|
|
139
|
-
let lang = 'fr';
|
|
140
|
-
try {
|
|
141
|
-
const s = await meta.settings.get('calendar-onekite').catch(() => ({}));
|
|
142
|
-
lang = (s && s.defaultLang) || (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
143
|
-
} catch (e) {
|
|
144
|
-
lang = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
145
|
-
}
|
|
116
|
+
// Email helper (NodeBB 4.x): always send by uid.
|
|
117
|
+
// Subject must be provided inside params.subject.
|
|
118
|
+
async function sendEmail(template, uid, subject, data) {
|
|
119
|
+
const toUid = Number.isInteger(uid) ? uid : (uid ? parseInt(uid, 10) : NaN);
|
|
120
|
+
if (!Number.isInteger(toUid) || toUid <= 0) return;
|
|
146
121
|
|
|
147
|
-
// Subject is not a positional arg; it must be injected via params.subject.
|
|
148
122
|
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
149
123
|
|
|
150
|
-
// Prefer sendToEmail when available (most consistent across versions).
|
|
151
124
|
try {
|
|
152
|
-
if (typeof emailer.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
await emailer.sendToEmail(template, toEmail, params);
|
|
159
|
-
}
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
} catch (err) {
|
|
163
|
-
// eslint-disable-next-line no-console
|
|
164
|
-
console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String((err && err.message) || err) });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Very old/unusual builds: try send() but only if it clearly accepts an email.
|
|
168
|
-
try {
|
|
169
|
-
if (typeof emailer.send === 'function') {
|
|
170
|
-
// Some builds accept (template, email, language, params)
|
|
171
|
-
if (emailer.send.length >= 4) {
|
|
172
|
-
await emailer.send(template, toEmail, lang, params);
|
|
173
|
-
} else {
|
|
174
|
-
// Some builds accept (template, email, params)
|
|
175
|
-
await emailer.send(template, toEmail, params);
|
|
176
|
-
}
|
|
125
|
+
if (typeof emailer.send !== 'function') return;
|
|
126
|
+
// NodeBB: send(template, uid, params)
|
|
127
|
+
if (emailer.send.length >= 3) {
|
|
128
|
+
await emailer.send(template, toUid, params);
|
|
129
|
+
} else {
|
|
130
|
+
await emailer.send(template, toUid);
|
|
177
131
|
}
|
|
178
132
|
} catch (err) {
|
|
179
133
|
// eslint-disable-next-line no-console
|
|
180
|
-
console.warn('[calendar-onekite] Failed to send email', { template,
|
|
134
|
+
console.warn('[calendar-onekite] Failed to send email', { template, uid: toUid, err: String((err && err.message) || err) });
|
|
181
135
|
}
|
|
182
136
|
}
|
|
183
137
|
|
|
@@ -374,15 +328,18 @@ function eventsFor(resv) {
|
|
|
374
328
|
const startIsoDate = new Date(parseInt(resv.start, 10)).toISOString().slice(0, 10);
|
|
375
329
|
const endIsoDate = new Date(parseInt(resv.end, 10)).toISOString().slice(0, 10);
|
|
376
330
|
|
|
377
|
-
const itemIds = Array.isArray(resv.itemIds) ? resv.itemIds
|
|
378
|
-
const itemNames = Array.isArray(resv.itemNames) ? resv.itemNames
|
|
331
|
+
const itemIds = Array.isArray(resv.itemIds) ? resv.itemIds.filter(Boolean) : [];
|
|
332
|
+
const itemNames = Array.isArray(resv.itemNames) ? resv.itemNames.filter(Boolean) : [];
|
|
379
333
|
|
|
380
334
|
// One line = one material: return one calendar event per item
|
|
335
|
+
if (!itemIds.length && !itemNames.length) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
381
338
|
const out = [];
|
|
382
|
-
const count = Math.max(itemIds.length, itemNames.length
|
|
339
|
+
const count = Math.max(itemIds.length, itemNames.length);
|
|
383
340
|
for (let i = 0; i < count; i++) {
|
|
384
|
-
const itemId = String(itemIds[i] || itemIds[0] ||
|
|
385
|
-
const itemName = String(itemNames[i] || itemNames[0] ||
|
|
341
|
+
const itemId = String(itemIds[i] || itemIds[0] || i);
|
|
342
|
+
const itemName = String(itemNames[i] || itemNames[0] || itemId);
|
|
386
343
|
out.push({
|
|
387
344
|
// keep id unique per item for FullCalendar, but keep the real rid in extendedProps.rid
|
|
388
345
|
id: `${resv.rid}:${itemId || i}`,
|
|
@@ -399,15 +356,14 @@ function eventsFor(resv) {
|
|
|
399
356
|
uid: resv.uid,
|
|
400
357
|
approvedBy: resv.approvedBy || 0,
|
|
401
358
|
approvedByUsername: resv.approvedByUsername || '',
|
|
402
|
-
itemIds: itemIds
|
|
403
|
-
itemNames: itemNames
|
|
359
|
+
itemIds: itemIds,
|
|
360
|
+
itemNames: itemNames,
|
|
404
361
|
itemIdLine: itemId,
|
|
405
362
|
itemNameLine: itemName,
|
|
406
363
|
},
|
|
407
364
|
});
|
|
408
365
|
}
|
|
409
366
|
return out;
|
|
410
|
-
}
|
|
411
367
|
|
|
412
368
|
function eventsForSpecial(ev) {
|
|
413
369
|
const start = new Date(parseInt(ev.start, 10));
|
|
@@ -577,8 +533,8 @@ api.getReservationDetails = async function (req, res) {
|
|
|
577
533
|
status: r.status,
|
|
578
534
|
uid: r.uid,
|
|
579
535
|
username: r.username || '',
|
|
580
|
-
itemNames: Array.isArray(r.itemNames) ? r.itemNames :
|
|
581
|
-
itemIds: Array.isArray(r.itemIds) ? r.itemIds :
|
|
536
|
+
itemNames: Array.isArray(r.itemNames) ? r.itemNames : [],
|
|
537
|
+
itemIds: Array.isArray(r.itemIds) ? r.itemIds : [],
|
|
582
538
|
start: r.start,
|
|
583
539
|
end: r.end,
|
|
584
540
|
approvedByUsername: r.approvedByUsername || '',
|
|
@@ -742,9 +698,9 @@ api.createReservation = async function (req, res) {
|
|
|
742
698
|
return res.status(400).json({ error: "bad-dates" });
|
|
743
699
|
}
|
|
744
700
|
|
|
745
|
-
//
|
|
746
|
-
const itemIds = Array.isArray(req.body.itemIds) ? req.body.itemIds.map(String) :
|
|
747
|
-
const itemNames = Array.isArray(req.body.itemNames) ? req.body.itemNames.map(String) :
|
|
701
|
+
// NodeBB 4.x only: require modern payload
|
|
702
|
+
const itemIds = Array.isArray(req.body.itemIds) ? req.body.itemIds.map(String) : [];
|
|
703
|
+
const itemNames = Array.isArray(req.body.itemNames) ? req.body.itemNames.map(String) : [];
|
|
748
704
|
|
|
749
705
|
const total = typeof req.body.total === 'number' ? req.body.total : parseFloat(String(req.body.total || '0'));
|
|
750
706
|
|
|
@@ -763,7 +719,7 @@ api.createReservation = async function (req, res) {
|
|
|
763
719
|
const exStart = parseInt(existing.start, 10);
|
|
764
720
|
const exEnd = parseInt(existing.end, 10);
|
|
765
721
|
if (!(exStart < end && start < exEnd)) continue;
|
|
766
|
-
const exItemIds = Array.isArray(existing.itemIds) ? existing.itemIds :
|
|
722
|
+
const exItemIds = Array.isArray(existing.itemIds) ? existing.itemIds : [];
|
|
767
723
|
const shared = exItemIds.filter(x => itemIds.includes(String(x)));
|
|
768
724
|
if (shared.length) {
|
|
769
725
|
conflicts.push({ rid: existing.rid, itemIds: shared, status: existing.status });
|
|
@@ -788,9 +744,6 @@ api.createReservation = async function (req, res) {
|
|
|
788
744
|
username: username || null,
|
|
789
745
|
itemIds,
|
|
790
746
|
itemNames: itemNames.length ? itemNames : itemIds,
|
|
791
|
-
// keep legacy fields for backward compatibility
|
|
792
|
-
itemId: itemIds[0],
|
|
793
|
-
itemName: (itemNames[0] || itemIds[0]),
|
|
794
747
|
start,
|
|
795
748
|
end,
|
|
796
749
|
status: 'pending',
|
|
@@ -801,44 +754,35 @@ api.createReservation = async function (req, res) {
|
|
|
801
754
|
// Save
|
|
802
755
|
await dbLayer.saveReservation(resv);
|
|
803
756
|
|
|
804
|
-
// Notify groups by email (NodeBB
|
|
757
|
+
// Notify groups by email (NodeBB 4.x, UID-only)
|
|
805
758
|
try {
|
|
806
|
-
const notifyGroups = (settings.notifyGroups || '')
|
|
759
|
+
const notifyGroups = String(settings.notifyGroups || '')
|
|
760
|
+
.split(/[\n,;]+/)
|
|
761
|
+
.map(s => s.trim())
|
|
762
|
+
.filter(Boolean);
|
|
763
|
+
|
|
807
764
|
if (notifyGroups.length) {
|
|
808
|
-
const requester = await user.getUserFields(uid, ['username'
|
|
809
|
-
const itemsLabel = (
|
|
765
|
+
const requester = await user.getUserFields(uid, ['username']);
|
|
766
|
+
const itemsLabel = (itemNames && itemNames.length ? itemNames : itemIds).join(', ');
|
|
767
|
+
|
|
810
768
|
for (const g of notifyGroups) {
|
|
811
769
|
const members = await getMembersByGroupIdentifier(g);
|
|
812
|
-
const uids =
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
})
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
for (const md of (usersData || [])) {
|
|
830
|
-
if (md && md.email) {
|
|
831
|
-
await sendEmail('calendar-onekite_pending', md.email, 'Location matériel - Demande de réservation', {
|
|
832
|
-
username: md.username,
|
|
833
|
-
requester: requester.username,
|
|
834
|
-
itemName: itemsLabel,
|
|
835
|
-
itemNames: resv.itemNames || [],
|
|
836
|
-
dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
|
|
837
|
-
start: formatFR(start),
|
|
838
|
-
end: formatFR(end),
|
|
839
|
-
total: resv.total || 0,
|
|
840
|
-
});
|
|
841
|
-
}
|
|
770
|
+
const uids = normalizeUids(members);
|
|
771
|
+
|
|
772
|
+
for (const memberUid of uids) {
|
|
773
|
+
const u = parseInt(memberUid, 10);
|
|
774
|
+
if (!Number.isInteger(u) || u <= 0) continue;
|
|
775
|
+
|
|
776
|
+
await sendEmail('calendar-onekite_pending', u, 'Location matériel - Demande de réservation', {
|
|
777
|
+
uid: u,
|
|
778
|
+
requester: requester && requester.username ? requester.username : '',
|
|
779
|
+
itemName: itemsLabel,
|
|
780
|
+
itemNames: (itemNames && itemNames.length ? itemNames : itemIds),
|
|
781
|
+
dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
|
|
782
|
+
start: formatFR(start),
|
|
783
|
+
end: formatFR(end),
|
|
784
|
+
total: resv.total || 0,
|
|
785
|
+
});
|
|
842
786
|
}
|
|
843
787
|
}
|
|
844
788
|
}
|
|
@@ -852,8 +796,8 @@ api.createReservation = async function (req, res) {
|
|
|
852
796
|
rid: resv.rid,
|
|
853
797
|
uid: resv.uid,
|
|
854
798
|
username: resv.username || '',
|
|
855
|
-
itemIds:
|
|
856
|
-
itemNames:
|
|
799
|
+
itemIds: itemIds || [],
|
|
800
|
+
itemNames: itemNames || [],
|
|
857
801
|
start: resv.start,
|
|
858
802
|
end: resv.end,
|
|
859
803
|
status: resv.status,
|
|
@@ -877,8 +821,7 @@ api.approveReservation = async function (req, res) {
|
|
|
877
821
|
if (r.status !== 'pending') return res.status(400).json({ error: 'bad-status' });
|
|
878
822
|
|
|
879
823
|
r.status = 'awaiting_payment';
|
|
880
|
-
|
|
881
|
-
r.pickupAddress = String((req.body && (req.body.pickupAddress || req.body.adminNote)) || '').trim();
|
|
824
|
+
r.pickupAddress = String((req.body && req.body.pickupAddress) || '').trim();
|
|
882
825
|
r.notes = String((req.body && req.body.notes) || '').trim();
|
|
883
826
|
r.pickupTime = String((req.body && req.body.pickupTime) || '').trim();
|
|
884
827
|
r.pickupLat = String((req.body && req.body.pickupLat) || '').trim();
|
|
@@ -918,11 +861,11 @@ api.approveReservation = async function (req, res) {
|
|
|
918
861
|
// Can be overridden via ACP setting `helloassoCallbackUrl`.
|
|
919
862
|
callbackUrl: normalizeReturnUrl(meta),
|
|
920
863
|
webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
|
|
921
|
-
itemName: buildHelloAssoItemName('',
|
|
864
|
+
itemName: buildHelloAssoItemName('', Array.isArray(r.itemNames) ? r.itemNames : [], r.start, r.end),
|
|
922
865
|
containsDonation: false,
|
|
923
866
|
metadata: {
|
|
924
867
|
reservationId: String(rid),
|
|
925
|
-
items: (Array.isArray(r.itemNames) ? r.itemNames :
|
|
868
|
+
items: (Array.isArray(r.itemNames) ? r.itemNames : []).filter(Boolean),
|
|
926
869
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
927
870
|
},
|
|
928
871
|
});
|
|
@@ -943,17 +886,19 @@ api.approveReservation = async function (req, res) {
|
|
|
943
886
|
await dbLayer.saveReservation(r);
|
|
944
887
|
|
|
945
888
|
// Email requester
|
|
946
|
-
const
|
|
947
|
-
|
|
889
|
+
const requesterUid = parseInt(r.uid, 10);
|
|
890
|
+
const requester = await user.getUserFields(requesterUid, ['username']);
|
|
891
|
+
if (requesterUid) {
|
|
948
892
|
const latNum = Number(r.pickupLat);
|
|
949
893
|
const lonNum = Number(r.pickupLon);
|
|
950
894
|
const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
|
|
951
895
|
? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
|
|
952
896
|
: '';
|
|
953
|
-
await sendEmail('calendar-onekite_approved',
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
897
|
+
await sendEmail('calendar-onekite_approved', requesterUid, 'Location matériel - Réservation validée', {
|
|
898
|
+
uid: requesterUid,
|
|
899
|
+
username: requester && requester.username ? requester.username : '',
|
|
900
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
901
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
957
902
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
958
903
|
start: formatFR(r.start),
|
|
959
904
|
end: formatFR(r.end),
|
|
@@ -987,12 +932,14 @@ api.refuseReservation = async function (req, res) {
|
|
|
987
932
|
r.refusedReason = String((req.body && (req.body.reason || req.body.refusedReason || req.body.refuseReason)) || '').trim();
|
|
988
933
|
await dbLayer.saveReservation(r);
|
|
989
934
|
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
935
|
+
const requesterUid2 = parseInt(r.uid, 10);
|
|
936
|
+
const requester = await user.getUserFields(requesterUid2, ['username']);
|
|
937
|
+
if (requesterUid2) {
|
|
938
|
+
await sendEmail('calendar-onekite_refused', requesterUid2, 'Location matériel - Demande de réservation', {
|
|
939
|
+
uid: requesterUid2,
|
|
940
|
+
username: requester && requester.username ? requester.username : '',
|
|
941
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
942
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
996
943
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
997
944
|
start: formatFR(r.start),
|
|
998
945
|
end: formatFR(r.end),
|
package/lib/helloassoWebhook.js
CHANGED
|
@@ -24,50 +24,22 @@ const SETTINGS_KEY = 'calendar-onekite';
|
|
|
24
24
|
// Replay protection: store processed payment ids.
|
|
25
25
|
const PROCESSED_KEY = 'calendar-onekite:helloasso:processedPayments';
|
|
26
26
|
|
|
27
|
-
async function sendEmail(template,
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
27
|
+
async function sendEmail(template, uid, subject, data) {
|
|
28
|
+
const toUid = Number.isInteger(uid) ? uid : (uid ? parseInt(uid, 10) : NaN);
|
|
29
|
+
if (!Number.isInteger(toUid) || toUid <= 0) return;
|
|
30
30
|
try {
|
|
31
31
|
const emailer = require.main.require('./src/emailer');
|
|
32
|
-
|
|
33
|
-
// sendToEmail(template, email, language, params)
|
|
34
|
-
// Subject must be provided inside params.subject (and can be finalized via filter:email.modify).
|
|
35
|
-
const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
32
|
+
if (typeof emailer.send !== 'function') return;
|
|
36
33
|
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
uid = await user.getUidByEmail(toEmail);
|
|
44
|
-
if (uid && typeof uid === 'string') uid = parseInt(uid, 10);
|
|
45
|
-
} catch (e) {
|
|
46
|
-
uid = null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (uid && typeof emailer.send === 'function') {
|
|
50
|
-
// NodeBB: send(template, uid, params)
|
|
51
|
-
await emailer.send(template, uid, params);
|
|
52
|
-
return;
|
|
34
|
+
// NodeBB 4.x: send(template, uid, params)
|
|
35
|
+
if (emailer.send.length >= 3) {
|
|
36
|
+
await emailer.send(template, toUid, params);
|
|
37
|
+
} else {
|
|
38
|
+
await emailer.send(template, toUid);
|
|
53
39
|
}
|
|
54
|
-
|
|
55
|
-
if (typeof emailer.sendToEmail === 'function') {
|
|
56
|
-
// Prefer the canonical signature.
|
|
57
|
-
// If a fork uses a shorter signature, we'll still try passing params as 3rd arg.
|
|
58
|
-
if (emailer.sendToEmail.length >= 4) {
|
|
59
|
-
await emailer.sendToEmail(template, toEmail, language, params);
|
|
60
|
-
} else {
|
|
61
|
-
await emailer.sendToEmail(template, toEmail, params);
|
|
62
|
-
}
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Do not call emailer.send with an email address (it expects uid). If we reach here,
|
|
67
|
-
// fall back to sendToEmail only.
|
|
68
40
|
} catch (err) {
|
|
69
41
|
// eslint-disable-next-line no-console
|
|
70
|
-
console.warn('[calendar-onekite] Failed to send email (webhook)', { template,
|
|
42
|
+
console.warn('[calendar-onekite] Failed to send email (webhook)', { template, uid: toUid, err: String((err && err.message) || err) });
|
|
71
43
|
}
|
|
72
44
|
}
|
|
73
45
|
|
|
@@ -352,13 +324,14 @@ async function handler(req, res, next) {
|
|
|
352
324
|
} catch (e) {}
|
|
353
325
|
|
|
354
326
|
// Notify requester
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
327
|
+
const requesterUid = parseInt(r.uid, 10);
|
|
328
|
+
const requester = await user.getUserFields(requesterUid, ['username']);
|
|
329
|
+
if (requesterUid) {
|
|
330
|
+
await sendEmail('calendar-onekite_paid', requesterUid, 'Location matériel - Paiement reçu', {
|
|
331
|
+
uid: requesterUid,
|
|
332
|
+
username: requester && requester.username ? requester.username : '',
|
|
333
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
334
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
362
335
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
363
336
|
paymentReceiptUrl: r.paymentReceiptUrl || '',
|
|
364
337
|
});
|
|
@@ -370,8 +343,8 @@ async function handler(req, res, next) {
|
|
|
370
343
|
rid: r.rid,
|
|
371
344
|
uid: r.uid,
|
|
372
345
|
username: (requester && requester.username) ? requester.username : (r.username || ''),
|
|
373
|
-
itemIds:
|
|
374
|
-
itemNames:
|
|
346
|
+
itemIds: Array.isArray(r.itemIds) ? r.itemIds : [],
|
|
347
|
+
itemNames: Array.isArray(r.itemNames) ? r.itemNames : [],
|
|
375
348
|
start: r.start,
|
|
376
349
|
end: r.end,
|
|
377
350
|
status: r.status,
|
package/lib/scheduler.js
CHANGED
|
@@ -57,55 +57,26 @@ async function processAwaitingPayment() {
|
|
|
57
57
|
const emailer = require.main.require('./src/emailer');
|
|
58
58
|
const user = require.main.require('./src/user');
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// NodeBB 4.x: always send by uid.
|
|
61
|
+
async function sendEmail(template, uid, subject, data) {
|
|
62
|
+
const toUid = Number.isInteger(uid) ? uid : (uid ? parseInt(uid, 10) : NaN);
|
|
63
|
+
if (!Number.isInteger(toUid) || toUid <= 0) return;
|
|
62
64
|
|
|
63
|
-
// Language can come from plugin settings or NodeBB config depending on version.
|
|
64
|
-
let lang = 'fr';
|
|
65
|
-
try {
|
|
66
|
-
const s = await meta.settings.get('calendar-onekite').catch(() => ({}));
|
|
67
|
-
lang = (s && s.defaultLang) || (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
68
|
-
} catch (e) {
|
|
69
|
-
lang = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Subject is NOT a positional argument; it must be provided in params.subject.
|
|
73
65
|
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
74
66
|
|
|
75
67
|
try {
|
|
76
|
-
if (typeof emailer.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
await emailer.sendToEmail(template, toEmail, params);
|
|
83
|
-
}
|
|
84
|
-
return;
|
|
68
|
+
if (typeof emailer.send !== 'function') return;
|
|
69
|
+
// NodeBB: send(template, uid, params)
|
|
70
|
+
if (emailer.send.length >= 3) {
|
|
71
|
+
await emailer.send(template, toUid, params);
|
|
72
|
+
} else {
|
|
73
|
+
await emailer.send(template, toUid);
|
|
85
74
|
}
|
|
86
75
|
} catch (err) {
|
|
87
76
|
// eslint-disable-next-line no-console
|
|
88
77
|
console.warn('[calendar-onekite] Failed to send email (scheduler)', {
|
|
89
78
|
template,
|
|
90
|
-
|
|
91
|
-
err: String((err && err.message) || err),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Very old/unusual builds: try send() but only if it clearly accepts an email.
|
|
96
|
-
try {
|
|
97
|
-
if (typeof emailer.send === 'function') {
|
|
98
|
-
if (emailer.send.length >= 4) {
|
|
99
|
-
await emailer.send(template, toEmail, lang, params);
|
|
100
|
-
} else {
|
|
101
|
-
await emailer.send(template, toEmail, params);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
} catch (err) {
|
|
105
|
-
// eslint-disable-next-line no-console
|
|
106
|
-
console.warn('[calendar-onekite] Failed to send email (scheduler)', {
|
|
107
|
-
template,
|
|
108
|
-
toEmail,
|
|
79
|
+
uid: toUid,
|
|
109
80
|
err: String((err && err.message) || err),
|
|
110
81
|
});
|
|
111
82
|
}
|
|
@@ -140,18 +111,22 @@ async function processAwaitingPayment() {
|
|
|
140
111
|
await dbLayer.saveReservation(r);
|
|
141
112
|
continue;
|
|
142
113
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
114
|
+
|
|
115
|
+
const toUid = parseInt(r.uid, 10);
|
|
116
|
+
const u = await user.getUserFields(toUid, ['username']);
|
|
117
|
+
if (toUid) {
|
|
118
|
+
await sendEmail('calendar-onekite_reminder', toUid, 'Location matériel - Rappel', {
|
|
119
|
+
uid: toUid,
|
|
120
|
+
username: (u && u.username) ? u.username : '',
|
|
121
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
122
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
149
123
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
150
124
|
paymentUrl: r.paymentUrl || '',
|
|
151
125
|
delayMinutes: holdMins,
|
|
152
126
|
pickupLine: r.pickupTime ? (r.adminNote ? `${r.pickupTime} à ${r.adminNote}` : r.pickupTime) : '',
|
|
153
127
|
});
|
|
154
128
|
}
|
|
129
|
+
|
|
155
130
|
r.reminderSent = true;
|
|
156
131
|
r.reminderAt = now;
|
|
157
132
|
await dbLayer.saveReservation(r);
|
|
@@ -170,12 +145,15 @@ async function processAwaitingPayment() {
|
|
|
170
145
|
const firstDiscord = await db.setAdd(discordKey, rid);
|
|
171
146
|
const shouldDiscord = !!firstDiscord;
|
|
172
147
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
148
|
+
const toUid = parseInt(r.uid, 10);
|
|
149
|
+
const u = await user.getUserFields(toUid, ['username']);
|
|
150
|
+
|
|
151
|
+
if (shouldEmail && toUid) {
|
|
152
|
+
await sendEmail('calendar-onekite_expired', toUid, 'Location matériel - Rappel', {
|
|
153
|
+
uid: toUid,
|
|
154
|
+
username: (u && u.username) ? u.username : '',
|
|
155
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
156
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
179
157
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
180
158
|
delayMinutes: holdMins,
|
|
181
159
|
});
|
|
@@ -197,6 +175,7 @@ async function processAwaitingPayment() {
|
|
|
197
175
|
});
|
|
198
176
|
} catch (e) {}
|
|
199
177
|
}
|
|
178
|
+
|
|
200
179
|
await dbLayer.removeReservation(rid);
|
|
201
180
|
}
|
|
202
181
|
}
|
package/library.js
CHANGED
|
@@ -109,13 +109,8 @@ Plugin.init = async function (params) {
|
|
|
109
109
|
},
|
|
110
110
|
type: ['application/json', 'application/*+json'],
|
|
111
111
|
});
|
|
112
|
-
// Accept webhook on both legacy root path and namespaced plugin path.
|
|
113
|
-
// Some reverse proxies block unknown root paths, so /plugins/... is recommended.
|
|
114
|
-
router.post('/helloasso', helloassoJson, helloassoWebhook.handler);
|
|
115
112
|
router.post('/plugins/calendar-onekite/helloasso', helloassoJson, helloassoWebhook.handler);
|
|
116
113
|
|
|
117
|
-
// Optional: health checks
|
|
118
|
-
router.get('/helloasso', (req, res) => res.json({ ok: true }));
|
|
119
114
|
router.get('/plugins/calendar-onekite/helloasso', (req, res) => res.json({ ok: true }));
|
|
120
115
|
|
|
121
116
|
scheduler.start();
|
|
@@ -133,8 +128,7 @@ Plugin.addAdminNavigation = async function (header) {
|
|
|
133
128
|
|
|
134
129
|
|
|
135
130
|
// Ensure our transactional emails always get a subject.
|
|
136
|
-
//
|
|
137
|
-
// so plugins typically inject/modify the subject via this hook.
|
|
131
|
+
// Ensure our transactional emails always get a subject (NodeBB email hook).
|
|
138
132
|
Plugin.emailModify = async function (data) {
|
|
139
133
|
try {
|
|
140
134
|
if (!data || !data.template) return data;
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -221,14 +221,14 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
221
221
|
for (const r of list) {
|
|
222
222
|
if (r && r.rid) pendingCache.set(String(r.rid), r);
|
|
223
223
|
const created = r.createdAt ? fmtFR(r.createdAt) : '';
|
|
224
|
-
const itemNames = Array.isArray(r.itemNames)
|
|
224
|
+
const itemNames = Array.isArray(r.itemNames) ? r.itemNames : [];
|
|
225
225
|
const itemsHtml = `<ul style="margin: 0 0 10px 18px;">${itemNames.map(n => `<li>${escapeHtml(String(n))}</li>`).join('')}</ul>`;
|
|
226
226
|
const div = document.createElement('div');
|
|
227
227
|
div.className = 'list-group-item onekite-pending-row';
|
|
228
228
|
div.innerHTML = `
|
|
229
229
|
<div class="d-flex justify-content-between align-items-start gap-2">
|
|
230
230
|
<div style="min-width: 0;">
|
|
231
|
-
<div><strong>${itemsHtml
|
|
231
|
+
<div><strong>${itemsHtml}</strong></div>
|
|
232
232
|
<div class="text-muted" style="font-size: 12px;">Créée: ${escapeHtml(created)}</div>
|
|
233
233
|
<div class="text-muted" style="font-size: 12px;">Période: ${escapeHtml(new Date(parseInt(r.start, 10)).toLocaleDateString('fr-FR'))} → ${escapeHtml(new Date(parseInt(r.end, 10)).toLocaleDateString('fr-FR'))}</div>
|
|
234
234
|
</div>
|
|
@@ -492,7 +492,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
492
492
|
? r.itemNames
|
|
493
493
|
: (typeof r.itemNames === 'string' && r.itemNames.trim()
|
|
494
494
|
? r.itemNames.split(',').map(s => s.trim()).filter(Boolean)
|
|
495
|
-
: ([
|
|
495
|
+
: ([]));
|
|
496
496
|
const itemsListHtml = itemNames.length
|
|
497
497
|
? `<div class="mb-2"><strong>Matériel</strong><ul style="margin:0.25rem 0 0 1.1rem; padding:0;">${itemNames.map(n => `<li>${escapeHtml(String(n))}</li>`).join('')}</ul></div>`
|
|
498
498
|
: '';
|
package/public/client.js
CHANGED
|
@@ -787,7 +787,7 @@ function toDatetimeLocalValue(date) {
|
|
|
787
787
|
(evs || []).forEach((ev) => {
|
|
788
788
|
const st = (ev.extendedProps && ev.extendedProps.status) || '';
|
|
789
789
|
if (!['pending', 'awaiting_payment', 'approved', 'paid'].includes(st)) return;
|
|
790
|
-
const ids = (ev.extendedProps && ev.extendedProps.itemIds) ||
|
|
790
|
+
const ids = (ev.extendedProps && ev.extendedProps.itemIds) || [];
|
|
791
791
|
ids.forEach((id) => blocked.add(String(id)));
|
|
792
792
|
});
|
|
793
793
|
} catch (e) {}
|
|
@@ -1300,12 +1300,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1300
1300
|
p = Object.assign({}, p0, details);
|
|
1301
1301
|
} else if (p0.type === 'special' && p0.eid) {
|
|
1302
1302
|
const details = await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(String(p0.eid))}`);
|
|
1303
|
-
p = Object.assign({}, p0, details
|
|
1304
|
-
// keep backward compat with older field names used by templates below
|
|
1305
|
-
pickupAddress: details.address || details.pickupAddress || p0.pickupAddress,
|
|
1306
|
-
pickupLat: details.lat || details.pickupLat || p0.pickupLat,
|
|
1307
|
-
pickupLon: details.lon || details.pickupLon || p0.pickupLon,
|
|
1308
|
-
});
|
|
1303
|
+
p = Object.assign({}, p0, details);
|
|
1309
1304
|
}
|
|
1310
1305
|
} catch (e) {
|
|
1311
1306
|
// ignore detail fetch errors; fall back to minimal props
|
|
@@ -1370,7 +1365,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1370
1365
|
? `<div class="mb-2"><strong>Réservée par</strong><br><a class="onekite-user-link" href="${window.location.origin}/user/${encodeURIComponent(username)}">${escapeHtml(username)}</a></div>`
|
|
1371
1366
|
: '';
|
|
1372
1367
|
const itemsHtml = (() => {
|
|
1373
|
-
const names = Array.isArray(p.itemNames) ? p.itemNames : (typeof p.itemNames === 'string' && p.itemNames.trim() ? p.itemNames.split(',').map(s=>s.trim()).filter(Boolean) :
|
|
1368
|
+
const names = Array.isArray(p.itemNames) ? p.itemNames : (typeof p.itemNames === 'string' && p.itemNames.trim() ? p.itemNames.split(',').map(s=>s.trim()).filter(Boolean) : []);
|
|
1374
1369
|
if (names.length) {
|
|
1375
1370
|
return `<ul style="margin:0 0 0 1.1rem; padding:0;">${names.map(n => `<li>${String(n).replace(/</g,'<').replace(/>/g,'>')}</li>`).join('')}</ul>`;
|
|
1376
1371
|
}
|
|
@@ -1505,7 +1500,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1505
1500
|
callback: async () => {
|
|
1506
1501
|
const itemNames = Array.isArray(p.itemNames) && p.itemNames.length
|
|
1507
1502
|
? p.itemNames
|
|
1508
|
-
: (typeof p.itemNames === 'string' && p.itemNames.trim() ? p.itemNames.split(',').map(s=>s.trim()).filter(Boolean) :
|
|
1503
|
+
: (typeof p.itemNames === 'string' && p.itemNames.trim() ? p.itemNames.split(',').map(s=>s.trim()).filter(Boolean) : []);
|
|
1509
1504
|
const itemsListHtml = itemNames.length
|
|
1510
1505
|
? `<div class="mb-2"><strong>Matériel</strong><ul style="margin:0.25rem 0 0 1.1rem; padding:0;">${itemNames.map(n => `<li>${String(n).replace(/</g, '<').replace(/>/g, '>')}</li>`).join('')}</ul></div>`
|
|
1511
1506
|
: '';
|