nodebb-plugin-onekite-calendar 1.0.21 → 1.0.22
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 +82 -119
- 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
|
-
|
|
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;
|
|
137
121
|
|
|
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
|
-
}
|
|
146
|
-
|
|
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(
|
|
378
|
-
const itemNames = Array.isArray(
|
|
331
|
+
const itemIds = Array.isArray(undefineds) ? undefineds.filter(Boolean) : [];
|
|
332
|
+
const itemNames = Array.isArray(undefineds) ? undefineds.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',
|
|
@@ -806,16 +759,20 @@ api.createReservation = async function (req, res) {
|
|
|
806
759
|
const notifyGroups = (settings.notifyGroups || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
807
760
|
if (notifyGroups.length) {
|
|
808
761
|
const requester = await user.getUserFields(uid, ['username', 'email']);
|
|
809
|
-
const itemsLabel = (
|
|
762
|
+
const itemsLabel = (undefineds || []).join(', ');
|
|
810
763
|
for (const g of notifyGroups) {
|
|
811
764
|
const members = await getMembersByGroupIdentifier(g);
|
|
812
|
-
const uids =
|
|
765
|
+
const uids = normalizeUids(members);
|
|
813
766
|
|
|
814
767
|
// Batch fetch user email/username when supported by this NodeBB version.
|
|
815
768
|
let usersData = [];
|
|
816
769
|
try {
|
|
817
770
|
if (typeof user.getUsersFields === 'function') {
|
|
818
771
|
usersData = await user.getUsersFields(uids, ['username', 'email']);
|
|
772
|
+
// Some NodeBB versions omit uid in returned rows; re-attach it from input order.
|
|
773
|
+
if (Array.isArray(usersData)) {
|
|
774
|
+
usersData = usersData.map((row, idx) => (row ? Object.assign({ uid: uids[idx] }, row) : null));
|
|
775
|
+
}
|
|
819
776
|
} else {
|
|
820
777
|
usersData = await Promise.all(uids.map(async (memberUid) => {
|
|
821
778
|
try { return await user.getUserFields(memberUid, ['username', 'email']); }
|
|
@@ -827,12 +784,15 @@ api.createReservation = async function (req, res) {
|
|
|
827
784
|
}
|
|
828
785
|
|
|
829
786
|
for (const md of (usersData || [])) {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
787
|
+
const memberUid = md && (md.uid || md.userId || md.userid || md.user_id);
|
|
788
|
+
const u = parseInt(memberUid, 10);
|
|
789
|
+
if (Number.isInteger(u) && u > 0) {
|
|
790
|
+
await sendEmail('calendar-onekite_pending', u, 'Location matériel - Demande de réservation', {
|
|
791
|
+
uid: u,
|
|
792
|
+
username: md && md.username ? md.username : '',
|
|
833
793
|
requester: requester.username,
|
|
834
794
|
itemName: itemsLabel,
|
|
835
|
-
itemNames:
|
|
795
|
+
itemNames: undefineds || [],
|
|
836
796
|
dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
|
|
837
797
|
start: formatFR(start),
|
|
838
798
|
end: formatFR(end),
|
|
@@ -852,8 +812,8 @@ api.createReservation = async function (req, res) {
|
|
|
852
812
|
rid: resv.rid,
|
|
853
813
|
uid: resv.uid,
|
|
854
814
|
username: resv.username || '',
|
|
855
|
-
itemIds:
|
|
856
|
-
itemNames:
|
|
815
|
+
itemIds: undefineds || [],
|
|
816
|
+
itemNames: undefineds || [],
|
|
857
817
|
start: resv.start,
|
|
858
818
|
end: resv.end,
|
|
859
819
|
status: resv.status,
|
|
@@ -877,8 +837,7 @@ api.approveReservation = async function (req, res) {
|
|
|
877
837
|
if (r.status !== 'pending') return res.status(400).json({ error: 'bad-status' });
|
|
878
838
|
|
|
879
839
|
r.status = 'awaiting_payment';
|
|
880
|
-
|
|
881
|
-
r.pickupAddress = String((req.body && (req.body.pickupAddress || req.body.adminNote)) || '').trim();
|
|
840
|
+
r.pickupAddress = String((req.body && req.body.pickupAddress) || '').trim();
|
|
882
841
|
r.notes = String((req.body && req.body.notes) || '').trim();
|
|
883
842
|
r.pickupTime = String((req.body && req.body.pickupTime) || '').trim();
|
|
884
843
|
r.pickupLat = String((req.body && req.body.pickupLat) || '').trim();
|
|
@@ -918,11 +877,11 @@ api.approveReservation = async function (req, res) {
|
|
|
918
877
|
// Can be overridden via ACP setting `helloassoCallbackUrl`.
|
|
919
878
|
callbackUrl: normalizeReturnUrl(meta),
|
|
920
879
|
webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
|
|
921
|
-
itemName: buildHelloAssoItemName('',
|
|
880
|
+
itemName: buildHelloAssoItemName('', Array.isArray(r.itemNames) ? r.itemNames : [], r.start, r.end),
|
|
922
881
|
containsDonation: false,
|
|
923
882
|
metadata: {
|
|
924
883
|
reservationId: String(rid),
|
|
925
|
-
items: (Array.isArray(r.itemNames) ? r.itemNames :
|
|
884
|
+
items: (Array.isArray(r.itemNames) ? r.itemNames : []).filter(Boolean),
|
|
926
885
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
927
886
|
},
|
|
928
887
|
});
|
|
@@ -943,17 +902,19 @@ api.approveReservation = async function (req, res) {
|
|
|
943
902
|
await dbLayer.saveReservation(r);
|
|
944
903
|
|
|
945
904
|
// Email requester
|
|
946
|
-
const
|
|
947
|
-
|
|
905
|
+
const requesterUid = parseInt(r.uid, 10);
|
|
906
|
+
const requester = await user.getUserFields(requesterUid, ['username']);
|
|
907
|
+
if (requesterUid) {
|
|
948
908
|
const latNum = Number(r.pickupLat);
|
|
949
909
|
const lonNum = Number(r.pickupLon);
|
|
950
910
|
const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
|
|
951
911
|
? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
|
|
952
912
|
: '';
|
|
953
|
-
await sendEmail('calendar-onekite_approved',
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
913
|
+
await sendEmail('calendar-onekite_approved', requesterUid, 'Location matériel - Réservation validée', {
|
|
914
|
+
uid: requesterUid,
|
|
915
|
+
username: requester && requester.username ? requester.username : '',
|
|
916
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
917
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
957
918
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
958
919
|
start: formatFR(r.start),
|
|
959
920
|
end: formatFR(r.end),
|
|
@@ -987,12 +948,14 @@ api.refuseReservation = async function (req, res) {
|
|
|
987
948
|
r.refusedReason = String((req.body && (req.body.reason || req.body.refusedReason || req.body.refuseReason)) || '').trim();
|
|
988
949
|
await dbLayer.saveReservation(r);
|
|
989
950
|
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
951
|
+
const requesterUid2 = parseInt(r.uid, 10);
|
|
952
|
+
const requester = await user.getUserFields(requesterUid2, ['username']);
|
|
953
|
+
if (requesterUid2) {
|
|
954
|
+
await sendEmail('calendar-onekite_refused', requesterUid2, 'Location matériel - Demande de réservation', {
|
|
955
|
+
uid: requesterUid2,
|
|
956
|
+
username: requester && requester.username ? requester.username : '',
|
|
957
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
958
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
996
959
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
997
960
|
start: formatFR(r.start),
|
|
998
961
|
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
|
: '';
|