nodebb-plugin-onekite-calendar 2.0.80 → 2.0.83
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/api.js +112 -10
- package/package.json +1 -1
- package/public/client.js +22 -3
package/lib/api.js
CHANGED
|
@@ -31,6 +31,7 @@ const {
|
|
|
31
31
|
signCalendarLink,
|
|
32
32
|
ymdToCompact,
|
|
33
33
|
dtToGCalUtc,
|
|
34
|
+
getSetting,
|
|
34
35
|
} = shared;
|
|
35
36
|
|
|
36
37
|
const helloasso = require('./helloasso');
|
|
@@ -757,6 +758,8 @@ api.leaveSpecialEvent = async function (req, res) {
|
|
|
757
758
|
api.getCapabilities = async function (req, res) {
|
|
758
759
|
const settings = await getSettings();
|
|
759
760
|
const uid = req.uid || 0;
|
|
761
|
+
const pendingHoldMinutes = parseInt(getSetting(settings, 'pendingHoldMinutes', '5'), 10) || 5;
|
|
762
|
+
const paymentHoldMinutes = parseInt(getSetting(settings, 'paymentHoldMinutes', getSetting(settings, 'holdMinutes', '60')), 10) || 60;
|
|
760
763
|
if (!uid) {
|
|
761
764
|
return res.json({
|
|
762
765
|
canModerate: false,
|
|
@@ -765,6 +768,8 @@ api.getCapabilities = async function (req, res) {
|
|
|
765
768
|
canCreateOuting: false,
|
|
766
769
|
canCreateReservation: false,
|
|
767
770
|
specialEventCategoryCid: 0,
|
|
771
|
+
pendingHoldMinutes,
|
|
772
|
+
paymentHoldMinutes,
|
|
768
773
|
});
|
|
769
774
|
}
|
|
770
775
|
const [canMod, canSpecialC, canSpecialD, canReq] = await Promise.all([
|
|
@@ -781,6 +786,8 @@ api.getCapabilities = async function (req, res) {
|
|
|
781
786
|
canCreateOuting: canMod || canReq,
|
|
782
787
|
canCreateReservation: canReq,
|
|
783
788
|
specialEventCategoryCid: parseInt(settings && settings.specialEventCategoryId, 10) || 0,
|
|
789
|
+
pendingHoldMinutes,
|
|
790
|
+
paymentHoldMinutes,
|
|
784
791
|
});
|
|
785
792
|
};
|
|
786
793
|
|
|
@@ -1369,7 +1376,69 @@ api.createReservation = async function (req, res) {
|
|
|
1369
1376
|
manuallyPaid: (isRegularizationSelf || isRegularization) ? true : undefined,
|
|
1370
1377
|
};
|
|
1371
1378
|
|
|
1372
|
-
//
|
|
1379
|
+
// For past-date reservations created for another user (isRegularizationOther):
|
|
1380
|
+
// create the HelloAsso checkout intent immediately so the user has a payment link right away.
|
|
1381
|
+
if (isRegularizationOther) {
|
|
1382
|
+
try {
|
|
1383
|
+
const token = await helloasso.getAccessToken({ env: settings.helloassoEnv || 'prod', clientId: settings.helloassoClientId, clientSecret: settings.helloassoClientSecret });
|
|
1384
|
+
const payer = await user.getUserFields(targetUid, ['email']);
|
|
1385
|
+
const year = yearFromTs(resv.start);
|
|
1386
|
+
const days = (startDate && endDate) ? (calendarDaysExclusiveYmd(startDate, endDate) || 1) : nbDays;
|
|
1387
|
+
|
|
1388
|
+
let recomputedTotalCents = null;
|
|
1389
|
+
try {
|
|
1390
|
+
const { items: catalog } = await helloasso.listCatalogItems({
|
|
1391
|
+
env: settings.helloassoEnv,
|
|
1392
|
+
token,
|
|
1393
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
1394
|
+
formType: settings.helloassoFormType,
|
|
1395
|
+
formSlug: autoFormSlugForYear(year),
|
|
1396
|
+
});
|
|
1397
|
+
const byId = new Map((catalog || []).map((it) => [String(it.id), (typeof it.price === 'number' ? it.price : 0)]));
|
|
1398
|
+
const ids = itemIds.map(String);
|
|
1399
|
+
const sumCentsPerDay = ids.reduce((acc, id) => acc + (byId.get(id) || 0), 0);
|
|
1400
|
+
if (sumCentsPerDay > 0) {
|
|
1401
|
+
recomputedTotalCents = Math.max(0, Math.round(sumCentsPerDay * days));
|
|
1402
|
+
resv.total = recomputedTotalCents / 100;
|
|
1403
|
+
}
|
|
1404
|
+
} catch (e) {}
|
|
1405
|
+
|
|
1406
|
+
const intent = await helloasso.createCheckoutIntent({
|
|
1407
|
+
env: settings.helloassoEnv,
|
|
1408
|
+
token,
|
|
1409
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
1410
|
+
formType: settings.helloassoFormType,
|
|
1411
|
+
formSlug: autoFormSlugForYear(year),
|
|
1412
|
+
totalAmount: (() => {
|
|
1413
|
+
const cents = (typeof recomputedTotalCents === 'number')
|
|
1414
|
+
? recomputedTotalCents
|
|
1415
|
+
: Math.max(0, Math.round((Number(resv.total) || 0) * 100));
|
|
1416
|
+
if (!cents) console.warn('[calendar-onekite] HelloAsso totalAmount is 0 (regularizationOther)', { rid });
|
|
1417
|
+
return cents;
|
|
1418
|
+
})(),
|
|
1419
|
+
payerEmail: payer && payer.email ? payer.email : '',
|
|
1420
|
+
callbackUrl: normalizeReturnUrl(),
|
|
1421
|
+
webhookUrl: normalizeCallbackUrl(settings.helloassoCallbackUrl),
|
|
1422
|
+
itemName: buildHelloAssoItemName('', resv.itemNames || [], resv.start, resv.end),
|
|
1423
|
+
containsDonation: false,
|
|
1424
|
+
metadata: {
|
|
1425
|
+
reservationId: String(rid),
|
|
1426
|
+
items: resv.itemNames || [],
|
|
1427
|
+
dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
|
|
1428
|
+
},
|
|
1429
|
+
});
|
|
1430
|
+
const intentPaymentUrl = intent && (intent.paymentUrl || intent.redirectUrl) ? (intent.paymentUrl || intent.redirectUrl) : (typeof intent === 'string' ? intent : null);
|
|
1431
|
+
const intentCheckoutId = intent && intent.checkoutIntentId ? String(intent.checkoutIntentId) : null;
|
|
1432
|
+
if (intentPaymentUrl) {
|
|
1433
|
+
resv.paymentUrl = intentPaymentUrl;
|
|
1434
|
+
if (intentCheckoutId) resv.checkoutIntentId = intentCheckoutId;
|
|
1435
|
+
} else {
|
|
1436
|
+
console.warn('[calendar-onekite] HelloAsso payment link not created (regularizationOther)', { rid });
|
|
1437
|
+
}
|
|
1438
|
+
} catch (e) {
|
|
1439
|
+
console.warn('[calendar-onekite] Failed to create HelloAsso checkout for regularizationOther', e && e.message ? e.message : e);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1373
1442
|
|
|
1374
1443
|
// Save
|
|
1375
1444
|
await dbLayer.saveReservation(resv);
|
|
@@ -1456,15 +1525,48 @@ api.createReservation = async function (req, res) {
|
|
|
1456
1525
|
// Notify target user when a validator created the reservation on their behalf
|
|
1457
1526
|
if (!isForSelf) {
|
|
1458
1527
|
try {
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1528
|
+
if (isRegularizationOther) {
|
|
1529
|
+
// Reservation is already awaiting_payment: send the approved email with payment link
|
|
1530
|
+
await sendEmail('calendar-onekite_approved', targetUid, 'Location matériel - Réservation validée', {
|
|
1531
|
+
uid: targetUid,
|
|
1532
|
+
username: targetUsername,
|
|
1533
|
+
itemName: (resv.itemNames || []).join(', '),
|
|
1534
|
+
itemNames: resv.itemNames || [],
|
|
1535
|
+
dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
|
|
1536
|
+
start: formatFR(start),
|
|
1537
|
+
end: formatFR(end),
|
|
1538
|
+
pickupAddress: '',
|
|
1539
|
+
notes: '',
|
|
1540
|
+
pickupTime: '',
|
|
1541
|
+
pickupLat: '',
|
|
1542
|
+
pickupLon: '',
|
|
1543
|
+
mapUrl: '',
|
|
1544
|
+
paymentUrl: resv.paymentUrl || '',
|
|
1545
|
+
validatedBy: creatorUsername || '',
|
|
1546
|
+
validatedByUrl: creatorUsername ? `${forumBaseUrl()}/user/${encodeURIComponent(String(creatorUsername))}` : '',
|
|
1547
|
+
...buildCalendarLinks({
|
|
1548
|
+
type: 'reservation',
|
|
1549
|
+
id: String(resv.rid),
|
|
1550
|
+
uid: targetUid,
|
|
1551
|
+
title: (resv.itemNames && resv.itemNames.length) ? `Location - ${resv.itemNames.join(', ')}` : 'Location',
|
|
1552
|
+
details: (resv.itemNames && resv.itemNames.length) ? `Matériel: ${resv.itemNames.join(', ')}` : '',
|
|
1553
|
+
location: '',
|
|
1554
|
+
allDay: true,
|
|
1555
|
+
startYmd: startDate || new Date(start).toISOString().slice(0, 10),
|
|
1556
|
+
endYmd: endDate || new Date(end).toISOString().slice(0, 10),
|
|
1557
|
+
}),
|
|
1558
|
+
});
|
|
1559
|
+
} else {
|
|
1560
|
+
await sendEmail('calendar-onekite_pending_for_you', targetUid, 'Location matériel - Demande créée en votre nom', {
|
|
1561
|
+
uid: targetUid,
|
|
1562
|
+
username: targetUsername,
|
|
1563
|
+
createdBy: creatorUsername,
|
|
1564
|
+
itemName: (resv.itemNames || []).join(', '),
|
|
1565
|
+
itemNames: resv.itemNames || [],
|
|
1566
|
+
dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
|
|
1567
|
+
total: resv.total || 0,
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1468
1570
|
} catch (e) {
|
|
1469
1571
|
console.warn('[calendar-onekite] Failed to send pending_for_you email', e && e.message ? e.message : e);
|
|
1470
1572
|
}
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1123,6 +1123,13 @@ function toDatetimeLocalValue(date) {
|
|
|
1123
1123
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
1124
1124
|
}
|
|
1125
1125
|
|
|
1126
|
+
function fmtDuration(minutes) {
|
|
1127
|
+
if (minutes < 60) return `${minutes} min`;
|
|
1128
|
+
const h = Math.floor(minutes / 60);
|
|
1129
|
+
const rem = minutes % 60;
|
|
1130
|
+
return rem ? `${h} h ${rem} min` : `${h} h`;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1126
1133
|
async function openReservationDialog(selectionInfo, items, opts) {
|
|
1127
1134
|
const isValidatorMode = !!(opts && opts.isValidator);
|
|
1128
1135
|
const start = selectionInfo.start;
|
|
@@ -1237,6 +1244,15 @@ function toDatetimeLocalValue(date) {
|
|
|
1237
1244
|
</div>
|
|
1238
1245
|
` : '';
|
|
1239
1246
|
|
|
1247
|
+
const holdPending = parseInt((opts && opts.pendingHoldMinutes), 10) || 5;
|
|
1248
|
+
const holdPayment = parseInt((opts && opts.paymentHoldMinutes), 10) || 60;
|
|
1249
|
+
const delayBannerHtml = `
|
|
1250
|
+
<div class="mt-2 p-2 rounded" style="font-size: 12px; background: var(--bs-info-bg-subtle, #cff4fc); border: 1px solid var(--bs-info-border-subtle, #9eeaf9); color: var(--bs-info-text-emphasis, #055160);">
|
|
1251
|
+
<strong>Délais :</strong>
|
|
1252
|
+
validation sous <strong>${fmtDuration(holdPending)}</strong> · paiement sous <strong>${fmtDuration(holdPayment)}</strong> après validation
|
|
1253
|
+
</div>
|
|
1254
|
+
`;
|
|
1255
|
+
|
|
1240
1256
|
const messageHtml = `
|
|
1241
1257
|
<div class="mb-2" id="onekite-period"><strong>Période</strong><br>${formatDt(start)} → ${formatDt(endDisplay)} <span class="text-muted" id="onekite-days">(${days} jour${days > 1 ? 's' : ''})</span></div>
|
|
1242
1258
|
${shortcutsHtml}
|
|
@@ -1253,6 +1269,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1253
1269
|
<div id="onekite-total" style="font-size: 18px;"><strong>0,00 €</strong></div>
|
|
1254
1270
|
</div>
|
|
1255
1271
|
<div class="text-muted" style="font-size: 12px;">Les matériels grisés sont déjà réservés ou en attente.</div>
|
|
1272
|
+
${delayBannerHtml}
|
|
1256
1273
|
`;
|
|
1257
1274
|
|
|
1258
1275
|
return new Promise((resolve) => {
|
|
@@ -1369,7 +1386,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1369
1386
|
const q = normalize(searchEl.value.trim());
|
|
1370
1387
|
document.querySelectorAll('#onekite-items [data-itemid]').forEach((row) => {
|
|
1371
1388
|
const name = normalize(row.querySelector('input.onekite-item-cb')?.getAttribute('data-name'));
|
|
1372
|
-
row.
|
|
1389
|
+
row.classList.toggle('d-none', !(!q || name.includes(q)));
|
|
1373
1390
|
});
|
|
1374
1391
|
});
|
|
1375
1392
|
}
|
|
@@ -1429,6 +1446,8 @@ function toDatetimeLocalValue(date) {
|
|
|
1429
1446
|
const canCreateReservation = !!caps.canCreateReservation;
|
|
1430
1447
|
const isValidator = !!caps.isValidator;
|
|
1431
1448
|
const specialEventCategoryCid = parseInt(caps.specialEventCategoryCid, 10) || 0;
|
|
1449
|
+
const pendingHoldMinutes = parseInt(caps.pendingHoldMinutes, 10) || 5;
|
|
1450
|
+
const paymentHoldMinutes = parseInt(caps.paymentHoldMinutes, 10) || 60;
|
|
1432
1451
|
|
|
1433
1452
|
// Creation chooser: Location / Prévision de sortie / Évènement (si autorisé).
|
|
1434
1453
|
|
|
@@ -1577,7 +1596,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1577
1596
|
showAlert('error', 'Aucun matériel disponible (items HelloAsso non chargés).');
|
|
1578
1597
|
return;
|
|
1579
1598
|
}
|
|
1580
|
-
const chosen = await openReservationDialog(sel, items, { isValidator });
|
|
1599
|
+
const chosen = await openReservationDialog(sel, items, { isValidator, pendingHoldMinutes, paymentHoldMinutes });
|
|
1581
1600
|
if (!chosen || !chosen.itemIds || !chosen.itemIds.length) return;
|
|
1582
1601
|
const startDate = toLocalYmd(sel.start);
|
|
1583
1602
|
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(sel.end);
|
|
@@ -1669,7 +1688,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1669
1688
|
showAlert('error', 'Aucun matériel disponible (items HelloAsso non chargés).');
|
|
1670
1689
|
return;
|
|
1671
1690
|
}
|
|
1672
|
-
const chosen = await openReservationDialog(sel, items, { isValidator });
|
|
1691
|
+
const chosen = await openReservationDialog(sel, items, { isValidator, pendingHoldMinutes, paymentHoldMinutes });
|
|
1673
1692
|
if (!chosen || !chosen.itemIds || !chosen.itemIds.length) return;
|
|
1674
1693
|
const startDate = toLocalYmd(sel.start);
|
|
1675
1694
|
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(sel.end);
|