nodebb-plugin-onekite-calendar 2.0.76 → 2.0.78
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 +0 -1
- package/lib/api.js +60 -15
- package/lib/scheduler.js +0 -1
- package/package.json +1 -1
- package/public/admin.js +2 -12
- package/public/client.js +10 -1
- package/templates/emails/calendar-onekite_pending.tpl +4 -0
- package/templates/emails/calendar-onekite_pending_for_you.tpl +15 -0
package/lib/admin.js
CHANGED
|
@@ -89,7 +89,6 @@ admin.approveReservation = async function (req, res) {
|
|
|
89
89
|
r.pickupLat = String((req.body && req.body.pickupLat) || '').trim();
|
|
90
90
|
r.pickupLon = String((req.body && req.body.pickupLon) || '').trim();
|
|
91
91
|
r.approvedAt = Date.now();
|
|
92
|
-
r.paymentDeferred = !!(req.body && req.body.paymentDeferred);
|
|
93
92
|
|
|
94
93
|
try {
|
|
95
94
|
const approver = await user.getUserFields(req.uid, ['username']);
|
package/lib/api.js
CHANGED
|
@@ -1321,6 +1321,9 @@ api.createReservation = async function (req, res) {
|
|
|
1321
1321
|
|
|
1322
1322
|
const isForSelf = (targetUid === uid);
|
|
1323
1323
|
|
|
1324
|
+
// Explicit regularization flag: admin records a payment directly (any date, any target)
|
|
1325
|
+
const isRegularization = !!isValidator && !!(req.body && req.body.isRegularization);
|
|
1326
|
+
|
|
1324
1327
|
// Free only when validator creates for themselves with a future date (within configured maxDays)
|
|
1325
1328
|
const validatorFreeMaxDays = (() => {
|
|
1326
1329
|
try {
|
|
@@ -1328,10 +1331,17 @@ api.createReservation = async function (req, res) {
|
|
|
1328
1331
|
return Number.isFinite(v) ? v : 0;
|
|
1329
1332
|
} catch (e) { return 0; }
|
|
1330
1333
|
})();
|
|
1331
|
-
const isValidatorFree = !!isValidator && isForSelf && !isPastDate && (validatorFreeMaxDays <= 0 || nbDays <= validatorFreeMaxDays);
|
|
1334
|
+
const isValidatorFree = !!isValidator && !isRegularization && isForSelf && !isPastDate && (validatorFreeMaxDays <= 0 || nbDays <= validatorFreeMaxDays);
|
|
1335
|
+
|
|
1336
|
+
// Past-dated reservation for self → paid immediately (no checkbox needed)
|
|
1337
|
+
const isRegularizationSelf = !!isValidator && !isRegularization && isForSelf && isPastDate;
|
|
1338
|
+
// Past-dated reservation for a third party without explicit regularization → awaiting_payment
|
|
1339
|
+
const isRegularizationOther = !!isValidator && !isRegularization && !isForSelf && isPastDate;
|
|
1332
1340
|
|
|
1333
|
-
|
|
1334
|
-
const
|
|
1341
|
+
const isAutoPaid = isValidatorFree || isRegularizationSelf || isRegularization;
|
|
1342
|
+
const resolvedStatus = isAutoPaid ? 'paid'
|
|
1343
|
+
: isRegularizationOther ? 'awaiting_payment'
|
|
1344
|
+
: 'pending';
|
|
1335
1345
|
|
|
1336
1346
|
const resv = {
|
|
1337
1347
|
rid,
|
|
@@ -1348,14 +1358,15 @@ api.createReservation = async function (req, res) {
|
|
|
1348
1358
|
end,
|
|
1349
1359
|
startDate,
|
|
1350
1360
|
endDate,
|
|
1351
|
-
status:
|
|
1361
|
+
status: resolvedStatus,
|
|
1352
1362
|
createdAt: now,
|
|
1353
|
-
paidAt:
|
|
1354
|
-
|
|
1355
|
-
|
|
1363
|
+
paidAt: isAutoPaid ? now : 0,
|
|
1364
|
+
approvedAt: (isAutoPaid || isRegularizationOther) ? now : 0,
|
|
1365
|
+
approvedBy: (isAutoPaid || isRegularizationOther) ? uid : 0,
|
|
1366
|
+
approvedByUsername: (isAutoPaid || isRegularizationOther) ? (creatorUsername || '') : '',
|
|
1356
1367
|
isFree: !!isValidatorFree,
|
|
1357
1368
|
total: isValidatorFree ? 0 : (isNaN(total) ? 0 : total),
|
|
1358
|
-
manuallyPaid: isRegularization ? true : undefined,
|
|
1369
|
+
manuallyPaid: (isRegularizationSelf || isRegularization) ? true : undefined,
|
|
1359
1370
|
};
|
|
1360
1371
|
|
|
1361
1372
|
// Validator self-reservations are FREE (no payment required) and tracked separately in accounting.
|
|
@@ -1368,7 +1379,8 @@ api.createReservation = async function (req, res) {
|
|
|
1368
1379
|
|
|
1369
1380
|
// Audit
|
|
1370
1381
|
const auditAction = isValidatorFree ? 'reservation_self_checked'
|
|
1371
|
-
: isRegularization ? 'reservation_manually_paid'
|
|
1382
|
+
: (isRegularizationSelf || isRegularization) ? 'reservation_manually_paid'
|
|
1383
|
+
: isRegularizationOther ? 'reservation_regularization_other'
|
|
1372
1384
|
: 'reservation_requested';
|
|
1373
1385
|
await auditLog(auditAction, uid, {
|
|
1374
1386
|
targetType: 'reservation',
|
|
@@ -1385,14 +1397,13 @@ api.createReservation = async function (req, res) {
|
|
|
1385
1397
|
status: resv.status,
|
|
1386
1398
|
});
|
|
1387
1399
|
|
|
1388
|
-
if (!isValidatorFree && !isRegularization) {
|
|
1400
|
+
if (!isValidatorFree && !isRegularizationSelf && !isRegularization) {
|
|
1389
1401
|
|
|
1390
1402
|
|
|
1391
1403
|
// Notify groups by email (NodeBB emailer config)
|
|
1392
1404
|
try {
|
|
1393
1405
|
const notifyGroups = normalizeAllowedGroups(settings.notifyGroups);
|
|
1394
1406
|
if (notifyGroups.length) {
|
|
1395
|
-
const requester = await user.getUserFields(uid, ['username', 'email']);
|
|
1396
1407
|
const itemsLabel = (resv.itemNames || []).join(', ');
|
|
1397
1408
|
for (const g of notifyGroups) {
|
|
1398
1409
|
const members = await getMembersByGroupIdentifier(g);
|
|
@@ -1424,15 +1435,14 @@ api.createReservation = async function (req, res) {
|
|
|
1424
1435
|
await sendEmail('calendar-onekite_pending', u, 'Location matériel - Demande de réservation', {
|
|
1425
1436
|
uid: u,
|
|
1426
1437
|
username: md && md.username ? md.username : '',
|
|
1427
|
-
requester:
|
|
1438
|
+
requester: targetUsername,
|
|
1439
|
+
createdBy: isForSelf ? '' : (creatorUsername || ''),
|
|
1428
1440
|
itemName: itemsLabel,
|
|
1429
1441
|
itemNames: resv.itemNames || [],
|
|
1430
1442
|
dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
|
|
1431
1443
|
start: formatFR(start),
|
|
1432
1444
|
end: formatFR(end),
|
|
1433
1445
|
total: resv.total || 0,
|
|
1434
|
-
// Link to the plugin ACP page so managers can process the request quickly.
|
|
1435
|
-
// Kept as `adminUrl` to match the email template placeholder.
|
|
1436
1446
|
adminUrl: `${forumBaseUrl()}/admin/plugins/calendar-onekite`,
|
|
1437
1447
|
});
|
|
1438
1448
|
}
|
|
@@ -1443,6 +1453,23 @@ api.createReservation = async function (req, res) {
|
|
|
1443
1453
|
console.warn('[calendar-onekite] Failed to send pending email', e && e.message ? e.message : e);
|
|
1444
1454
|
}
|
|
1445
1455
|
|
|
1456
|
+
// Notify target user when a validator created the reservation on their behalf
|
|
1457
|
+
if (!isForSelf) {
|
|
1458
|
+
try {
|
|
1459
|
+
await sendEmail('calendar-onekite_pending_for_you', targetUid, 'Location matériel - Demande créée en votre nom', {
|
|
1460
|
+
uid: targetUid,
|
|
1461
|
+
username: targetUsername,
|
|
1462
|
+
createdBy: creatorUsername,
|
|
1463
|
+
itemName: (resv.itemNames || []).join(', '),
|
|
1464
|
+
itemNames: resv.itemNames || [],
|
|
1465
|
+
dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
|
|
1466
|
+
total: resv.total || 0,
|
|
1467
|
+
});
|
|
1468
|
+
} catch (e) {
|
|
1469
|
+
console.warn('[calendar-onekite] Failed to send pending_for_you email', e && e.message ? e.message : e);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1446
1473
|
// Discord webhook (optional)
|
|
1447
1474
|
try {
|
|
1448
1475
|
await discord.notifyReservationRequested(settings, {
|
|
@@ -1459,7 +1486,25 @@ api.createReservation = async function (req, res) {
|
|
|
1459
1486
|
|
|
1460
1487
|
}
|
|
1461
1488
|
|
|
1462
|
-
|
|
1489
|
+
// When a validator explicitly regularizes a reservation for another user (directly paid,
|
|
1490
|
+
// no HelloAsso), the main notification block is skipped. Still notify the target user.
|
|
1491
|
+
if (isRegularization && !isForSelf) {
|
|
1492
|
+
try {
|
|
1493
|
+
await sendEmail('calendar-onekite_paid', targetUid, 'Location matériel - Réservation confirmée', {
|
|
1494
|
+
uid: targetUid,
|
|
1495
|
+
username: targetUsername,
|
|
1496
|
+
itemName: (resv.itemNames || []).join(', '),
|
|
1497
|
+
itemNames: resv.itemNames || [],
|
|
1498
|
+
dateRange: `Du ${formatFR(start)} au ${formatFR(end)}`,
|
|
1499
|
+
icsUrl: '',
|
|
1500
|
+
googleCalUrl: '',
|
|
1501
|
+
});
|
|
1502
|
+
} catch (e) {
|
|
1503
|
+
console.warn('[calendar-onekite] Failed to send regularization paid email', e && e.message ? e.message : e);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
res.json({ ok: true, rid, status: resv.status, autoPaid: isAutoPaid });
|
|
1463
1508
|
};
|
|
1464
1509
|
|
|
1465
1510
|
api.searchUsers = async function (req, res) {
|
package/lib/scheduler.js
CHANGED
|
@@ -224,7 +224,6 @@ async function processAwaitingPayment(preIds, preReservations) {
|
|
|
224
224
|
const rid = ids[i];
|
|
225
225
|
const r = reservations[i];
|
|
226
226
|
if (!r || r.status !== 'awaiting_payment') continue;
|
|
227
|
-
if (r.paymentDeferred) continue;
|
|
228
227
|
|
|
229
228
|
const approvedAt = parseInt(r.approvedAt || r.validatedAt || 0, 10) || 0;
|
|
230
229
|
if (!approvedAt) continue;
|
package/package.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -838,10 +838,6 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
838
838
|
<label class="form-label">Heure de récupération</label>
|
|
839
839
|
<select class="form-select" id="onekite-pickup-time">${opts}</select>
|
|
840
840
|
</div>
|
|
841
|
-
<div class="form-check mb-2">
|
|
842
|
-
<input class="form-check-input" type="checkbox" id="onekite-payment-deferred" />
|
|
843
|
-
<label class="form-check-label" for="onekite-payment-deferred">Paiement différé <span class="text-muted" style="font-size:12px;">(pas d'annulation automatique)</span></label>
|
|
844
|
-
</div>
|
|
845
841
|
<div class="text-muted" style="font-size:12px;">Ces infos seront appliquées aux ${rids.length} demandes sélectionnées.</div>
|
|
846
842
|
`;
|
|
847
843
|
const dlg = bootbox.dialog({
|
|
@@ -860,9 +856,8 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
860
856
|
const pickupTime = (document.getElementById('onekite-pickup-time')?.value || '').trim();
|
|
861
857
|
const pickupLat = (document.getElementById('onekite-pickup-lat')?.value || '').trim();
|
|
862
858
|
const pickupLon = (document.getElementById('onekite-pickup-lon')?.value || '').trim();
|
|
863
|
-
const paymentDeferred = !!(document.getElementById('onekite-payment-deferred')?.checked);
|
|
864
859
|
for (const rr of rids) {
|
|
865
|
-
await approve(rr, { pickupAddress, notes, pickupTime, pickupLat, pickupLon
|
|
860
|
+
await approve(rr, { pickupAddress, notes, pickupTime, pickupLat, pickupLon });
|
|
866
861
|
}
|
|
867
862
|
showAlert('success', `${rids.length} demande(s) validée(s).`);
|
|
868
863
|
await refreshPending();
|
|
@@ -1015,10 +1010,6 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
1015
1010
|
<label class="form-label">Heure de récupération</label>
|
|
1016
1011
|
<select class="form-select" id="onekite-pickup-time">${opts}</select>
|
|
1017
1012
|
</div>
|
|
1018
|
-
<div class="form-check mb-2">
|
|
1019
|
-
<input class="form-check-input" type="checkbox" id="onekite-payment-deferred" />
|
|
1020
|
-
<label class="form-check-label" for="onekite-payment-deferred">Paiement différé <span class="text-muted" style="font-size:12px;">(pas d'annulation automatique)</span></label>
|
|
1021
|
-
</div>
|
|
1022
1013
|
`;
|
|
1023
1014
|
|
|
1024
1015
|
const dlg = bootbox.dialog({
|
|
@@ -1037,8 +1028,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
1037
1028
|
const pickupTime = (document.getElementById('onekite-pickup-time')?.value || '').trim();
|
|
1038
1029
|
const pickupLat = (document.getElementById('onekite-pickup-lat')?.value || '').trim();
|
|
1039
1030
|
const pickupLon = (document.getElementById('onekite-pickup-lon')?.value || '').trim();
|
|
1040
|
-
|
|
1041
|
-
await approve(rid, { pickupAddress, notes, pickupTime, pickupLat, pickupLon, paymentDeferred });
|
|
1031
|
+
await approve(rid, { pickupAddress, notes, pickupTime, pickupLat, pickupLon });
|
|
1042
1032
|
if (rowEl && rowEl.parentNode) rowEl.parentNode.removeChild(rowEl);
|
|
1043
1033
|
showAlert('success', 'Demande validée.');
|
|
1044
1034
|
await refreshPending();
|
package/public/client.js
CHANGED
|
@@ -1229,6 +1229,12 @@ function toDatetimeLocalValue(date) {
|
|
|
1229
1229
|
<label style="font-size:13px; font-weight:600; margin-bottom:3px; display:block;">Pour (pseudo, vide = moi-même)</label>
|
|
1230
1230
|
<input type="text" class="form-control form-control-sm" id="onekite-target-username" placeholder="Pseudo de l'adhérent" autocomplete="off">
|
|
1231
1231
|
</div>
|
|
1232
|
+
<div class="mb-2">
|
|
1233
|
+
<div class="form-check">
|
|
1234
|
+
<input class="form-check-input" type="checkbox" id="onekite-regularization">
|
|
1235
|
+
<label class="form-check-label" for="onekite-regularization" style="font-size:13px;">Régularisation <span class="text-muted" style="font-size:12px;">(passe directement en payé)</span></label>
|
|
1236
|
+
</div>
|
|
1237
|
+
</div>
|
|
1232
1238
|
` : '';
|
|
1233
1239
|
|
|
1234
1240
|
const messageHtml = `
|
|
@@ -1280,7 +1286,8 @@ function toDatetimeLocalValue(date) {
|
|
|
1280
1286
|
// Return the effective end date (exclusive) because duration shortcuts can
|
|
1281
1287
|
// change the range without updating the original FullCalendar selection.
|
|
1282
1288
|
const targetUsername = isValidatorMode ? ((document.getElementById('onekite-target-username') || {}).value || '').trim() : '';
|
|
1283
|
-
|
|
1289
|
+
const isRegularization = isValidatorMode ? !!((document.getElementById('onekite-regularization') || {}).checked) : false;
|
|
1290
|
+
resolve({ itemIds, itemNames, total, days, endDate: toLocalYmd(end), targetUsername, isRegularization });
|
|
1284
1291
|
},
|
|
1285
1292
|
},
|
|
1286
1293
|
},
|
|
@@ -1568,6 +1575,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1568
1575
|
total: chosen.total,
|
|
1569
1576
|
};
|
|
1570
1577
|
if (chosen.targetUsername) reqPayload.targetUsername = chosen.targetUsername;
|
|
1578
|
+
if (chosen.isRegularization) reqPayload.isRegularization = true;
|
|
1571
1579
|
const resp = await requestReservation(reqPayload);
|
|
1572
1580
|
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
1573
1581
|
showAlert('success', chosen.targetUsername ? `Réservation confirmée pour ${chosen.targetUsername}.` : 'Réservation confirmée.');
|
|
@@ -1659,6 +1667,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1659
1667
|
total: chosen.total,
|
|
1660
1668
|
};
|
|
1661
1669
|
if (chosen.targetUsername) reqPayload.targetUsername = chosen.targetUsername;
|
|
1670
|
+
if (chosen.isRegularization) reqPayload.isRegularization = true;
|
|
1662
1671
|
const resp = await requestReservation(reqPayload);
|
|
1663
1672
|
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
1664
1673
|
showAlert('success', chosen.targetUsername ? `Réservation confirmée pour ${chosen.targetUsername}.` : 'Réservation confirmée.');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<p>Bonjour {username},</p>
|
|
2
|
+
<p>Une demande de réservation de matériel a été créée en votre nom par {createdBy}.</p>
|
|
3
|
+
|
|
4
|
+
<p><strong>Matériel</strong></p>
|
|
5
|
+
<ul>
|
|
6
|
+
<!-- BEGIN itemNames -->
|
|
7
|
+
<li>{itemNames}</li>
|
|
8
|
+
<!-- END itemNames -->
|
|
9
|
+
</ul>
|
|
10
|
+
|
|
11
|
+
<p>{dateRange}</p>
|
|
12
|
+
|
|
13
|
+
<p><strong>Total estimé :</strong> {total} €</p>
|
|
14
|
+
|
|
15
|
+
<p>Vous recevrez un email avec les instructions de paiement une fois la demande validée par un responsable.</p>
|