nodebb-plugin-onekite-calendar 1.0.25 → 1.0.27
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/lib/admin.js +9 -12
- package/lib/api.js +71 -56
- package/lib/helloassoWebhook.js +7 -9
- package/lib/scheduler.js +7 -10
- package/library.js +32 -45
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/admin.js +3 -3
- package/public/client.js +9 -4
package/lib/admin.js
CHANGED
|
@@ -41,11 +41,8 @@ async function sendEmail(template, uid, subject, data) {
|
|
|
41
41
|
try {
|
|
42
42
|
if (typeof emailer.send !== 'function') return;
|
|
43
43
|
// NodeBB 4.x: send(template, uid, params)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} else {
|
|
47
|
-
await emailer.send(template, toUid);
|
|
48
|
-
}
|
|
44
|
+
// Do NOT branch on function.length (unreliable once wrapped/bound).
|
|
45
|
+
await emailer.send(template, toUid, params);
|
|
49
46
|
} catch (err) {
|
|
50
47
|
console.warn('[calendar-onekite] Failed to send email', {
|
|
51
48
|
template,
|
|
@@ -162,11 +159,11 @@ admin.approveReservation = async function (req, res) {
|
|
|
162
159
|
// User return/back/error URLs must be real pages; webhook uses the plugin endpoint.
|
|
163
160
|
callbackUrl: returnUrl,
|
|
164
161
|
webhookUrl: webhookUrl,
|
|
165
|
-
itemName: buildHelloAssoItemName('Réservation matériel Onekite',
|
|
162
|
+
itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
|
|
166
163
|
containsDonation: false,
|
|
167
164
|
metadata: {
|
|
168
165
|
reservationId: String(rid),
|
|
169
|
-
items: (Array.isArray(r.itemNames) ? r.itemNames : []).filter(Boolean),
|
|
166
|
+
items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
|
|
170
167
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
171
168
|
},
|
|
172
169
|
});
|
|
@@ -199,8 +196,8 @@ admin.approveReservation = async function (req, res) {
|
|
|
199
196
|
await sendEmail('calendar-onekite_approved', requesterUid, 'Location matériel - Réservation validée', {
|
|
200
197
|
uid: requesterUid,
|
|
201
198
|
username: requester && requester.username ? requester.username : '',
|
|
202
|
-
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
203
|
-
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
199
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
200
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
204
201
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
205
202
|
paymentUrl: paymentUrl || '',
|
|
206
203
|
pickupAddress: r.pickupAddress || '',
|
|
@@ -235,8 +232,8 @@ admin.refuseReservation = async function (req, res) {
|
|
|
235
232
|
await sendEmail('calendar-onekite_refused', requesterUid, 'Location matériel - Réservation refusée', {
|
|
236
233
|
uid: requesterUid,
|
|
237
234
|
username: requester && requester.username ? requester.username : '',
|
|
238
|
-
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
239
|
-
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
235
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
236
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
240
237
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
241
238
|
start: formatFR(r.start),
|
|
242
239
|
end: formatFR(r.end),
|
|
@@ -419,7 +416,7 @@ admin.getAccounting = async function (req, res) {
|
|
|
419
416
|
|
|
420
417
|
const itemNames = Array.isArray(r.itemNames) && r.itemNames.length
|
|
421
418
|
? r.itemNames
|
|
422
|
-
: [];
|
|
419
|
+
: (r.itemName ? [r.itemName] : []);
|
|
423
420
|
|
|
424
421
|
const total = Number(r.total) || 0;
|
|
425
422
|
const startDate = formatFR(r.start);
|
package/lib/api.js
CHANGED
|
@@ -123,12 +123,11 @@ async function sendEmail(template, uid, subject, data) {
|
|
|
123
123
|
|
|
124
124
|
try {
|
|
125
125
|
if (typeof emailer.send !== 'function') return;
|
|
126
|
-
// NodeBB: send(template, uid, params)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
126
|
+
// NodeBB 4.x: send(template, uid, params)
|
|
127
|
+
// NOTE: Do NOT branch on function.length: it is unreliable once the function
|
|
128
|
+
// is wrapped/bound (common in production builds) and can lead to params being
|
|
129
|
+
// dropped, resulting in empty email bodies and missing subjects.
|
|
130
|
+
await emailer.send(template, toUid, params);
|
|
132
131
|
} catch (err) {
|
|
133
132
|
// eslint-disable-next-line no-console
|
|
134
133
|
console.warn('[calendar-onekite] Failed to send email', { template, uid: toUid, err: String((err && err.message) || err) });
|
|
@@ -328,18 +327,15 @@ function eventsFor(resv) {
|
|
|
328
327
|
const startIsoDate = new Date(parseInt(resv.start, 10)).toISOString().slice(0, 10);
|
|
329
328
|
const endIsoDate = new Date(parseInt(resv.end, 10)).toISOString().slice(0, 10);
|
|
330
329
|
|
|
331
|
-
const itemIds = Array.isArray(resv.itemIds) ? resv.itemIds.
|
|
332
|
-
const itemNames = Array.isArray(resv.itemNames) ? resv.itemNames.
|
|
330
|
+
const itemIds = Array.isArray(resv.itemIds) ? resv.itemIds : (resv.itemId ? [resv.itemId] : []);
|
|
331
|
+
const itemNames = Array.isArray(resv.itemNames) ? resv.itemNames : (resv.itemName ? [resv.itemName] : []);
|
|
333
332
|
|
|
334
333
|
// One line = one material: return one calendar event per item
|
|
335
|
-
if (!itemIds.length && !itemNames.length) {
|
|
336
|
-
return [];
|
|
337
|
-
}
|
|
338
334
|
const out = [];
|
|
339
|
-
const count = Math.max(itemIds.length, itemNames.length);
|
|
335
|
+
const count = Math.max(itemIds.length, itemNames.length, 1);
|
|
340
336
|
for (let i = 0; i < count; i++) {
|
|
341
|
-
const itemId = String(itemIds[i] || itemIds[0] ||
|
|
342
|
-
const itemName = String(itemNames[i] || itemNames[0] || itemId);
|
|
337
|
+
const itemId = String(itemIds[i] || itemIds[0] || resv.itemId || '');
|
|
338
|
+
const itemName = String(itemNames[i] || itemNames[0] || resv.itemName || itemId);
|
|
343
339
|
out.push({
|
|
344
340
|
// keep id unique per item for FullCalendar, but keep the real rid in extendedProps.rid
|
|
345
341
|
id: `${resv.rid}:${itemId || i}`,
|
|
@@ -356,14 +352,15 @@ function eventsFor(resv) {
|
|
|
356
352
|
uid: resv.uid,
|
|
357
353
|
approvedBy: resv.approvedBy || 0,
|
|
358
354
|
approvedByUsername: resv.approvedByUsername || '',
|
|
359
|
-
itemIds: itemIds,
|
|
360
|
-
itemNames: itemNames,
|
|
355
|
+
itemIds: itemIds.filter(Boolean),
|
|
356
|
+
itemNames: itemNames.filter(Boolean),
|
|
361
357
|
itemIdLine: itemId,
|
|
362
358
|
itemNameLine: itemName,
|
|
363
359
|
},
|
|
364
360
|
});
|
|
365
361
|
}
|
|
366
362
|
return out;
|
|
363
|
+
}
|
|
367
364
|
|
|
368
365
|
function eventsForSpecial(ev) {
|
|
369
366
|
const start = new Date(parseInt(ev.start, 10));
|
|
@@ -533,8 +530,8 @@ api.getReservationDetails = async function (req, res) {
|
|
|
533
530
|
status: r.status,
|
|
534
531
|
uid: r.uid,
|
|
535
532
|
username: r.username || '',
|
|
536
|
-
itemNames: Array.isArray(r.itemNames) ? r.itemNames : [],
|
|
537
|
-
itemIds: Array.isArray(r.itemIds) ? r.itemIds : [],
|
|
533
|
+
itemNames: Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : []),
|
|
534
|
+
itemIds: Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : []),
|
|
538
535
|
start: r.start,
|
|
539
536
|
end: r.end,
|
|
540
537
|
approvedByUsername: r.approvedByUsername || '',
|
|
@@ -698,9 +695,9 @@ api.createReservation = async function (req, res) {
|
|
|
698
695
|
return res.status(400).json({ error: "bad-dates" });
|
|
699
696
|
}
|
|
700
697
|
|
|
701
|
-
//
|
|
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) : [];
|
|
698
|
+
// Support both legacy single itemId and new itemIds[] payload
|
|
699
|
+
const itemIds = Array.isArray(req.body.itemIds) ? req.body.itemIds.map(String) : ((req.body.itemId ? [String(req.body.itemId)] : []));
|
|
700
|
+
const itemNames = Array.isArray(req.body.itemNames) ? req.body.itemNames.map(String) : (req.body.itemName ? [String(req.body.itemName)] : []);
|
|
704
701
|
|
|
705
702
|
const total = typeof req.body.total === 'number' ? req.body.total : parseFloat(String(req.body.total || '0'));
|
|
706
703
|
|
|
@@ -719,7 +716,7 @@ api.createReservation = async function (req, res) {
|
|
|
719
716
|
const exStart = parseInt(existing.start, 10);
|
|
720
717
|
const exEnd = parseInt(existing.end, 10);
|
|
721
718
|
if (!(exStart < end && start < exEnd)) continue;
|
|
722
|
-
const exItemIds = Array.isArray(existing.itemIds) ? existing.itemIds : [];
|
|
719
|
+
const exItemIds = Array.isArray(existing.itemIds) ? existing.itemIds : (existing.itemId ? [existing.itemId] : []);
|
|
723
720
|
const shared = exItemIds.filter(x => itemIds.includes(String(x)));
|
|
724
721
|
if (shared.length) {
|
|
725
722
|
conflicts.push({ rid: existing.rid, itemIds: shared, status: existing.status });
|
|
@@ -744,6 +741,9 @@ api.createReservation = async function (req, res) {
|
|
|
744
741
|
username: username || null,
|
|
745
742
|
itemIds,
|
|
746
743
|
itemNames: itemNames.length ? itemNames : itemIds,
|
|
744
|
+
// keep legacy fields for backward compatibility
|
|
745
|
+
itemId: itemIds[0],
|
|
746
|
+
itemName: (itemNames[0] || itemIds[0]),
|
|
747
747
|
start,
|
|
748
748
|
end,
|
|
749
749
|
status: 'pending',
|
|
@@ -754,35 +754,51 @@ api.createReservation = async function (req, res) {
|
|
|
754
754
|
// Save
|
|
755
755
|
await dbLayer.saveReservation(resv);
|
|
756
756
|
|
|
757
|
-
// Notify groups by email (NodeBB
|
|
757
|
+
// Notify groups by email (NodeBB emailer config)
|
|
758
758
|
try {
|
|
759
|
-
const notifyGroups =
|
|
760
|
-
.split(/[\n,;]+/)
|
|
761
|
-
.map(s => s.trim())
|
|
762
|
-
.filter(Boolean);
|
|
763
|
-
|
|
759
|
+
const notifyGroups = (settings.notifyGroups || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
764
760
|
if (notifyGroups.length) {
|
|
765
|
-
const requester = await user.getUserFields(uid, ['username']);
|
|
766
|
-
const itemsLabel = (
|
|
767
|
-
|
|
761
|
+
const requester = await user.getUserFields(uid, ['username', 'email']);
|
|
762
|
+
const itemsLabel = (resv.itemNames || []).join(', ');
|
|
768
763
|
for (const g of notifyGroups) {
|
|
769
764
|
const members = await getMembersByGroupIdentifier(g);
|
|
770
765
|
const uids = normalizeUids(members);
|
|
771
766
|
|
|
772
|
-
|
|
767
|
+
// Batch fetch user email/username when supported by this NodeBB version.
|
|
768
|
+
let usersData = [];
|
|
769
|
+
try {
|
|
770
|
+
if (typeof user.getUsersFields === 'function') {
|
|
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
|
+
}
|
|
776
|
+
} else {
|
|
777
|
+
usersData = await Promise.all(uids.map(async (memberUid) => {
|
|
778
|
+
try { return await user.getUserFields(memberUid, ['username', 'email']); }
|
|
779
|
+
catch (e) { return null; }
|
|
780
|
+
}));
|
|
781
|
+
}
|
|
782
|
+
} catch (e) {
|
|
783
|
+
usersData = [];
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
for (const md of (usersData || [])) {
|
|
787
|
+
const memberUid = md && (md.uid || md.userId || md.userid || md.user_id);
|
|
773
788
|
const u = parseInt(memberUid, 10);
|
|
774
|
-
if (
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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 : '',
|
|
793
|
+
requester: requester.username,
|
|
794
|
+
itemName: itemsLabel,
|
|
795
|
+
itemNames: resv.itemNames || [],
|
|
796
|
+
dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
|
|
797
|
+
start: formatFR(start),
|
|
798
|
+
end: formatFR(end),
|
|
799
|
+
total: resv.total || 0,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
786
802
|
}
|
|
787
803
|
}
|
|
788
804
|
}
|
|
@@ -796,8 +812,8 @@ api.createReservation = async function (req, res) {
|
|
|
796
812
|
rid: resv.rid,
|
|
797
813
|
uid: resv.uid,
|
|
798
814
|
username: resv.username || '',
|
|
799
|
-
itemIds: itemIds || [],
|
|
800
|
-
itemNames: itemNames || [],
|
|
815
|
+
itemIds: resv.itemIds || [],
|
|
816
|
+
itemNames: resv.itemNames || [],
|
|
801
817
|
start: resv.start,
|
|
802
818
|
end: resv.end,
|
|
803
819
|
status: resv.status,
|
|
@@ -821,7 +837,8 @@ api.approveReservation = async function (req, res) {
|
|
|
821
837
|
if (r.status !== 'pending') return res.status(400).json({ error: 'bad-status' });
|
|
822
838
|
|
|
823
839
|
r.status = 'awaiting_payment';
|
|
824
|
-
|
|
840
|
+
// Backwards compatible: old clients sent `adminNote` to describe the pickup place.
|
|
841
|
+
r.pickupAddress = String((req.body && (req.body.pickupAddress || req.body.adminNote)) || '').trim();
|
|
825
842
|
r.notes = String((req.body && req.body.notes) || '').trim();
|
|
826
843
|
r.pickupTime = String((req.body && req.body.pickupTime) || '').trim();
|
|
827
844
|
r.pickupLat = String((req.body && req.body.pickupLat) || '').trim();
|
|
@@ -861,11 +878,11 @@ api.approveReservation = async function (req, res) {
|
|
|
861
878
|
// Can be overridden via ACP setting `helloassoCallbackUrl`.
|
|
862
879
|
callbackUrl: normalizeReturnUrl(meta),
|
|
863
880
|
webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
|
|
864
|
-
itemName: buildHelloAssoItemName('',
|
|
881
|
+
itemName: buildHelloAssoItemName('', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
|
|
865
882
|
containsDonation: false,
|
|
866
883
|
metadata: {
|
|
867
884
|
reservationId: String(rid),
|
|
868
|
-
items: (Array.isArray(r.itemNames) ? r.itemNames : []).filter(Boolean),
|
|
885
|
+
items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
|
|
869
886
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
870
887
|
},
|
|
871
888
|
});
|
|
@@ -897,8 +914,8 @@ api.approveReservation = async function (req, res) {
|
|
|
897
914
|
await sendEmail('calendar-onekite_approved', requesterUid, 'Location matériel - Réservation validée', {
|
|
898
915
|
uid: requesterUid,
|
|
899
916
|
username: requester && requester.username ? requester.username : '',
|
|
900
|
-
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
901
|
-
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
917
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
918
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
902
919
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
903
920
|
start: formatFR(r.start),
|
|
904
921
|
end: formatFR(r.end),
|
|
@@ -938,8 +955,8 @@ api.refuseReservation = async function (req, res) {
|
|
|
938
955
|
await sendEmail('calendar-onekite_refused', requesterUid2, 'Location matériel - Demande de réservation', {
|
|
939
956
|
uid: requesterUid2,
|
|
940
957
|
username: requester && requester.username ? requester.username : '',
|
|
941
|
-
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
942
|
-
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
958
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
959
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
943
960
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
944
961
|
start: formatFR(r.start),
|
|
945
962
|
end: formatFR(r.end),
|
|
@@ -1000,6 +1017,4 @@ api.cancelReservation = async function (req, res) {
|
|
|
1000
1017
|
return res.json({ ok: true, status: 'cancelled' });
|
|
1001
1018
|
};
|
|
1002
1019
|
|
|
1003
|
-
module.exports = api;
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1020
|
+
module.exports = api;
|
package/lib/helloassoWebhook.js
CHANGED
|
@@ -32,11 +32,9 @@ async function sendEmail(template, uid, subject, data) {
|
|
|
32
32
|
if (typeof emailer.send !== 'function') return;
|
|
33
33
|
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
34
34
|
// NodeBB 4.x: send(template, uid, params)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
await emailer.send(template, toUid);
|
|
39
|
-
}
|
|
35
|
+
// Do NOT branch on function.length: it is unreliable once wrapped/bound and
|
|
36
|
+
// can lead to params being dropped (empty email bodies / missing subjects).
|
|
37
|
+
await emailer.send(template, toUid, params);
|
|
40
38
|
} catch (err) {
|
|
41
39
|
// eslint-disable-next-line no-console
|
|
42
40
|
console.warn('[calendar-onekite] Failed to send email (webhook)', { template, uid: toUid, err: String((err && err.message) || err) });
|
|
@@ -330,8 +328,8 @@ async function handler(req, res, next) {
|
|
|
330
328
|
await sendEmail('calendar-onekite_paid', requesterUid, 'Location matériel - Paiement reçu', {
|
|
331
329
|
uid: requesterUid,
|
|
332
330
|
username: requester && requester.username ? requester.username : '',
|
|
333
|
-
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
334
|
-
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
331
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
332
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
335
333
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
336
334
|
paymentReceiptUrl: r.paymentReceiptUrl || '',
|
|
337
335
|
});
|
|
@@ -343,8 +341,8 @@ async function handler(req, res, next) {
|
|
|
343
341
|
rid: r.rid,
|
|
344
342
|
uid: r.uid,
|
|
345
343
|
username: (requester && requester.username) ? requester.username : (r.username || ''),
|
|
346
|
-
itemIds:
|
|
347
|
-
itemNames:
|
|
344
|
+
itemIds: r.itemIds || (r.itemId ? [r.itemId] : []),
|
|
345
|
+
itemNames: r.itemNames || (r.itemName ? [r.itemName] : []),
|
|
348
346
|
start: r.start,
|
|
349
347
|
end: r.end,
|
|
350
348
|
status: r.status,
|
package/lib/scheduler.js
CHANGED
|
@@ -66,12 +66,9 @@ async function processAwaitingPayment() {
|
|
|
66
66
|
|
|
67
67
|
try {
|
|
68
68
|
if (typeof emailer.send !== 'function') return;
|
|
69
|
-
// NodeBB: send(template, uid, params)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
} else {
|
|
73
|
-
await emailer.send(template, toUid);
|
|
74
|
-
}
|
|
69
|
+
// NodeBB 4.x: send(template, uid, params)
|
|
70
|
+
// Do NOT branch on function.length (unreliable once wrapped/bound).
|
|
71
|
+
await emailer.send(template, toUid, params);
|
|
75
72
|
} catch (err) {
|
|
76
73
|
// eslint-disable-next-line no-console
|
|
77
74
|
console.warn('[calendar-onekite] Failed to send email (scheduler)', {
|
|
@@ -118,8 +115,8 @@ async function processAwaitingPayment() {
|
|
|
118
115
|
await sendEmail('calendar-onekite_reminder', toUid, 'Location matériel - Rappel', {
|
|
119
116
|
uid: toUid,
|
|
120
117
|
username: (u && u.username) ? u.username : '',
|
|
121
|
-
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
122
|
-
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
118
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
119
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
123
120
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
124
121
|
paymentUrl: r.paymentUrl || '',
|
|
125
122
|
delayMinutes: holdMins,
|
|
@@ -152,8 +149,8 @@ async function processAwaitingPayment() {
|
|
|
152
149
|
await sendEmail('calendar-onekite_expired', toUid, 'Location matériel - Rappel', {
|
|
153
150
|
uid: toUid,
|
|
154
151
|
username: (u && u.username) ? u.username : '',
|
|
155
|
-
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : ''),
|
|
156
|
-
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : []),
|
|
152
|
+
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
153
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
157
154
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
158
155
|
delayMinutes: holdMins,
|
|
159
156
|
});
|
package/library.js
CHANGED
|
@@ -20,26 +20,6 @@ const Plugin = {};
|
|
|
20
20
|
const isFn = (fn) => typeof fn === 'function';
|
|
21
21
|
const mw = (...fns) => fns.filter(isFn);
|
|
22
22
|
|
|
23
|
-
// Express throws during route registration if any callback is undefined.
|
|
24
|
-
// To keep NodeBB bootable and surface a clearer error, we wrap handlers and
|
|
25
|
-
// provide a deterministic 500 handler when a target function is missing.
|
|
26
|
-
function safeHandler(fn, name) {
|
|
27
|
-
if (typeof fn !== 'function') {
|
|
28
|
-
return function missingHandler(req, res) {
|
|
29
|
-
// eslint-disable-next-line no-console
|
|
30
|
-
console.error(`[calendar-onekite] Missing route handler: ${name}`);
|
|
31
|
-
return res.status(500).json({ error: 'missing-handler', handler: name });
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
return async function wrappedHandler(req, res, next) {
|
|
35
|
-
try {
|
|
36
|
-
return await fn(req, res, next);
|
|
37
|
-
} catch (err) {
|
|
38
|
-
return next(err);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
23
|
Plugin.init = async function (params) {
|
|
44
24
|
const { router, middleware } = params;
|
|
45
25
|
|
|
@@ -78,43 +58,44 @@ Plugin.init = async function (params) {
|
|
|
78
58
|
// Page routes (HTML)
|
|
79
59
|
// IMPORTANT: pass an ARRAY for middlewares (even if empty), otherwise
|
|
80
60
|
// setupPageRoute will throw "middlewares is not iterable".
|
|
81
|
-
routeHelpers.setupPageRoute(router, '/calendar', mw(),
|
|
82
|
-
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/calendar-onekite', mw(),
|
|
61
|
+
routeHelpers.setupPageRoute(router, '/calendar', mw(), controllers.renderCalendar);
|
|
62
|
+
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/calendar-onekite', mw(), admin.renderAdmin);
|
|
83
63
|
|
|
84
64
|
// Public API (JSON) — NodeBB 4.x only (v3 API)
|
|
85
|
-
router.get('/api/v3/plugins/calendar-onekite/events', ...publicExpose,
|
|
86
|
-
router.get('/api/v3/plugins/calendar-onekite/items', ...publicExpose,
|
|
87
|
-
router.get('/api/v3/plugins/calendar-onekite/capabilities', ...publicExpose,
|
|
65
|
+
router.get('/api/v3/plugins/calendar-onekite/events', ...publicExpose, api.getEvents);
|
|
66
|
+
router.get('/api/v3/plugins/calendar-onekite/items', ...publicExpose, api.getItems);
|
|
67
|
+
router.get('/api/v3/plugins/calendar-onekite/capabilities', ...publicExpose, api.getCapabilities);
|
|
88
68
|
|
|
89
|
-
router.post('/api/v3/plugins/calendar-onekite/reservations', ...publicExpose,
|
|
90
|
-
router.get('/api/v3/plugins/calendar-onekite/reservations/:rid', ...publicExpose,
|
|
91
|
-
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/approve', ...publicExpose,
|
|
92
|
-
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/refuse', ...publicExpose,
|
|
93
|
-
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/cancel', ...publicExpose,
|
|
69
|
+
router.post('/api/v3/plugins/calendar-onekite/reservations', ...publicExpose, api.createReservation);
|
|
70
|
+
router.get('/api/v3/plugins/calendar-onekite/reservations/:rid', ...publicExpose, api.getReservationDetails);
|
|
71
|
+
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/approve', ...publicExpose, api.approveReservation);
|
|
72
|
+
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/refuse', ...publicExpose, api.refuseReservation);
|
|
73
|
+
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/cancel', ...publicExpose, api.cancelReservation);
|
|
94
74
|
|
|
95
|
-
router.post('/api/v3/plugins/calendar-onekite/special-events', ...publicExpose,
|
|
96
|
-
router.get('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose,
|
|
97
|
-
router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose,
|
|
75
|
+
router.post('/api/v3/plugins/calendar-onekite/special-events', ...publicExpose, api.createSpecialEvent);
|
|
76
|
+
router.get('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.getSpecialEventDetails);
|
|
77
|
+
router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.deleteSpecialEvent);
|
|
98
78
|
|
|
99
79
|
// Admin API (JSON)
|
|
100
80
|
const adminBases = ['/api/v3/admin/plugins/calendar-onekite'];
|
|
101
81
|
|
|
102
82
|
adminBases.forEach((base) => {
|
|
103
|
-
router.get(`${base}/settings`, ...adminMws,
|
|
104
|
-
router.put(`${base}/settings`, ...adminMws,
|
|
83
|
+
router.get(`${base}/settings`, ...adminMws, admin.getSettings);
|
|
84
|
+
router.put(`${base}/settings`, ...adminMws, admin.saveSettings);
|
|
105
85
|
|
|
106
|
-
router.get(`${base}/pending`, ...adminMws,
|
|
107
|
-
router.put(`${base}/reservations/:rid/approve`, ...adminMws,
|
|
108
|
-
router.put(`${base}/reservations/:rid/refuse`, ...adminMws,
|
|
86
|
+
router.get(`${base}/pending`, ...adminMws, admin.listPending);
|
|
87
|
+
router.put(`${base}/reservations/:rid/approve`, ...adminMws, admin.approveReservation);
|
|
88
|
+
router.put(`${base}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
|
|
109
89
|
|
|
110
|
-
router.post(`${base}/purge`, ...adminMws,
|
|
90
|
+
router.post(`${base}/purge`, ...adminMws, admin.purgeByYear);
|
|
91
|
+
router.get(`${base}/debug`, ...adminMws, admin.debugHelloAsso);
|
|
111
92
|
// Accounting / exports
|
|
112
|
-
router.get(`${base}/accounting`, ...adminMws,
|
|
113
|
-
router.get(`${base}/accounting.csv`, ...adminMws,
|
|
114
|
-
router.post(`${base}/accounting/purge`, ...adminMws,
|
|
93
|
+
router.get(`${base}/accounting`, ...adminMws, admin.getAccounting);
|
|
94
|
+
router.get(`${base}/accounting.csv`, ...adminMws, admin.exportAccountingCsv);
|
|
95
|
+
router.post(`${base}/accounting/purge`, ...adminMws, admin.purgeAccounting);
|
|
115
96
|
|
|
116
97
|
// Purge special events by year
|
|
117
|
-
router.post(`${base}/special-events/purge`, ...adminMws,
|
|
98
|
+
router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
|
|
118
99
|
});
|
|
119
100
|
|
|
120
101
|
// HelloAsso callback endpoint (hardened)
|
|
@@ -128,8 +109,13 @@ Plugin.init = async function (params) {
|
|
|
128
109
|
},
|
|
129
110
|
type: ['application/json', 'application/*+json'],
|
|
130
111
|
});
|
|
131
|
-
|
|
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
|
+
router.post('/plugins/calendar-onekite/helloasso', helloassoJson, helloassoWebhook.handler);
|
|
132
116
|
|
|
117
|
+
// Optional: health checks
|
|
118
|
+
router.get('/helloasso', (req, res) => res.json({ ok: true }));
|
|
133
119
|
router.get('/plugins/calendar-onekite/helloasso', (req, res) => res.json({ ok: true }));
|
|
134
120
|
|
|
135
121
|
scheduler.start();
|
|
@@ -147,7 +133,8 @@ Plugin.addAdminNavigation = async function (header) {
|
|
|
147
133
|
|
|
148
134
|
|
|
149
135
|
// Ensure our transactional emails always get a subject.
|
|
150
|
-
//
|
|
136
|
+
// NodeBB's Emailer.sendToEmail signature expects (template, email, language, params),
|
|
137
|
+
// so plugins typically inject/modify the subject via this hook.
|
|
151
138
|
Plugin.emailModify = async function (data) {
|
|
152
139
|
try {
|
|
153
140
|
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) ? r.itemNames : [];
|
|
224
|
+
const itemNames = Array.isArray(r.itemNames) && r.itemNames.length ? r.itemNames : [r.itemName || r.itemId].filter(Boolean);
|
|
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}</strong></div>
|
|
231
|
+
<div><strong>${itemsHtml || escapeHtml(r.itemName || '')}</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
|
+
: ([r.itemName || r.itemId].filter(Boolean)));
|
|
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) || (ev.extendedProps && ev.extendedProps.itemId ? [ev.extendedProps.itemId] : []);
|
|
791
791
|
ids.forEach((id) => blocked.add(String(id)));
|
|
792
792
|
});
|
|
793
793
|
} catch (e) {}
|
|
@@ -1300,7 +1300,12 @@ 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
|
|
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
|
+
});
|
|
1304
1309
|
}
|
|
1305
1310
|
} catch (e) {
|
|
1306
1311
|
// ignore detail fetch errors; fall back to minimal props
|
|
@@ -1365,7 +1370,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1365
1370
|
? `<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>`
|
|
1366
1371
|
: '';
|
|
1367
1372
|
const itemsHtml = (() => {
|
|
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) : []);
|
|
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) : (p.itemName ? [p.itemName] : []));
|
|
1369
1374
|
if (names.length) {
|
|
1370
1375
|
return `<ul style="margin:0 0 0 1.1rem; padding:0;">${names.map(n => `<li>${String(n).replace(/</g,'<').replace(/>/g,'>')}</li>`).join('')}</ul>`;
|
|
1371
1376
|
}
|
|
@@ -1500,7 +1505,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1500
1505
|
callback: async () => {
|
|
1501
1506
|
const itemNames = Array.isArray(p.itemNames) && p.itemNames.length
|
|
1502
1507
|
? p.itemNames
|
|
1503
|
-
: (typeof p.itemNames === 'string' && p.itemNames.trim() ? p.itemNames.split(',').map(s=>s.trim()).filter(Boolean) : []);
|
|
1508
|
+
: (typeof p.itemNames === 'string' && p.itemNames.trim() ? p.itemNames.split(',').map(s=>s.trim()).filter(Boolean) : (p.itemName ? [p.itemName] : []));
|
|
1504
1509
|
const itemsListHtml = itemNames.length
|
|
1505
1510
|
? `<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>`
|
|
1506
1511
|
: '';
|