nodebb-plugin-onekite-calendar 2.0.18 → 2.0.20
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 +4 -0
- package/lib/admin.js +109 -2
- package/lib/api.js +4 -4
- package/package.json +1 -1
- package/pkg/package/lib/admin.js +111 -3
- package/pkg/package/lib/api.js +4 -4
- package/pkg/package/package.json +1 -1
- package/pkg/package/plugin.json +1 -1
- package/pkg/package/public/client.js +11 -5
- package/plugin.json +1 -1
- package/public/client.js +11 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog – calendar-onekite
|
|
2
2
|
|
|
3
|
+
## 1.3.13
|
|
4
|
+
- Réservations : il est désormais possible de réserver pour le jour même (seules les dates passées sont refusées).
|
|
5
|
+
- Annulation : les validateurs peuvent annuler une réservation déjà payée.
|
|
6
|
+
|
|
3
7
|
## 1.3.12
|
|
4
8
|
- Expiration automatique : envoi d’un email au demandeur avec la raison :
|
|
5
9
|
- « Demande non prise en charge dans le temps imparti » (demande en attente)
|
package/lib/admin.js
CHANGED
|
@@ -432,6 +432,72 @@ admin.getAccounting = async function (req, res) {
|
|
|
432
432
|
let paidCount = 0;
|
|
433
433
|
let grandTotal = 0;
|
|
434
434
|
|
|
435
|
+
// Helper: calendar-day difference (end exclusive) from YYYY-MM-DD strings (UTC midnights).
|
|
436
|
+
const calendarDaysExclusiveYmd = (startYmd, endYmd) => {
|
|
437
|
+
try {
|
|
438
|
+
const s = String(startYmd || '').trim();
|
|
439
|
+
const e = String(endYmd || '').trim();
|
|
440
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(s) || !/^\d{4}-\d{2}-\d{2}$/.test(e)) return null;
|
|
441
|
+
const [sy, sm, sd] = s.split('-').map((x) => parseInt(x, 10));
|
|
442
|
+
const [ey, em, ed] = e.split('-').map((x) => parseInt(x, 10));
|
|
443
|
+
const sUtc = Date.UTC(sy, sm - 1, sd);
|
|
444
|
+
const eUtc = Date.UTC(ey, em - 1, ed);
|
|
445
|
+
const diff = Math.floor((eUtc - sUtc) / (24 * 60 * 60 * 1000));
|
|
446
|
+
return Math.max(1, diff);
|
|
447
|
+
} catch (e) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const yearFromTs = (ts) => {
|
|
453
|
+
const d = new Date(Number(ts));
|
|
454
|
+
return Number.isFinite(d.getTime()) ? d.getFullYear() : new Date().getFullYear();
|
|
455
|
+
};
|
|
456
|
+
const formSlugForYear = (y) => `locations-materiel-${y}`;
|
|
457
|
+
|
|
458
|
+
// Cache HelloAsso catalog price lookups by year to compute *per-item* totals.
|
|
459
|
+
// This prevents a bug where the full reservation total was previously counted on every item line.
|
|
460
|
+
let settingsCache = null;
|
|
461
|
+
let tokenCache = null;
|
|
462
|
+
const catalogByYear = new Map(); // year -> Map(itemId -> priceCents)
|
|
463
|
+
const getCatalogPriceMapForYear = async (year) => {
|
|
464
|
+
const y = Number(year);
|
|
465
|
+
if (!Number.isFinite(y)) return null;
|
|
466
|
+
if (catalogByYear.has(y)) return catalogByYear.get(y);
|
|
467
|
+
try {
|
|
468
|
+
if (!settingsCache) settingsCache = await meta.settings.get('calendar-onekite');
|
|
469
|
+
if (!settingsCache || !settingsCache.helloassoClientId || !settingsCache.helloassoClientSecret || !settingsCache.helloassoOrganizationSlug || !settingsCache.helloassoFormType) {
|
|
470
|
+
catalogByYear.set(y, null);
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
if (!tokenCache) {
|
|
474
|
+
tokenCache = await helloasso.getAccessToken({
|
|
475
|
+
env: settingsCache.helloassoEnv || 'prod',
|
|
476
|
+
clientId: settingsCache.helloassoClientId,
|
|
477
|
+
clientSecret: settingsCache.helloassoClientSecret,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (!tokenCache) {
|
|
481
|
+
catalogByYear.set(y, null);
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const { items: catalog } = await helloasso.listCatalogItems({
|
|
486
|
+
env: settingsCache.helloassoEnv,
|
|
487
|
+
token: tokenCache,
|
|
488
|
+
organizationSlug: settingsCache.helloassoOrganizationSlug,
|
|
489
|
+
formType: settingsCache.helloassoFormType,
|
|
490
|
+
formSlug: formSlugForYear(y),
|
|
491
|
+
});
|
|
492
|
+
const byId = new Map((catalog || []).map((it) => [String(it.id), (typeof it.price === 'number' ? it.price : 0)]));
|
|
493
|
+
catalogByYear.set(y, byId);
|
|
494
|
+
return byId;
|
|
495
|
+
} catch (e) {
|
|
496
|
+
catalogByYear.set(y, null);
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
435
501
|
for (const rid of ids) {
|
|
436
502
|
const r = await dbLayer.getReservation(rid);
|
|
437
503
|
if (!r) continue;
|
|
@@ -473,14 +539,55 @@ admin.getAccounting = async function (req, res) {
|
|
|
473
539
|
|
|
474
540
|
// Only count paid (non-free) reservations into revenue by item.
|
|
475
541
|
if (!isFree) {
|
|
476
|
-
|
|
542
|
+
// Compute per-item totals (price_per_day * calendar_days) when possible.
|
|
543
|
+
// Fallback: split reservation total equally across items.
|
|
544
|
+
const idsForRes = (Array.isArray(r.itemIds) && r.itemIds.length) ? r.itemIds.map(String) : (r.itemId ? [String(r.itemId)] : []);
|
|
545
|
+
const days = calendarDaysExclusiveYmd(r.startDate, r.endDate) || 1;
|
|
546
|
+
const year = yearFromTs(start);
|
|
547
|
+
|
|
548
|
+
let perItemTotals = null; // Map(itemId -> euros)
|
|
549
|
+
const priceMap = await getCatalogPriceMapForYear(year);
|
|
550
|
+
if (priceMap && idsForRes.length) {
|
|
551
|
+
const m = new Map();
|
|
552
|
+
for (const id of idsForRes) {
|
|
553
|
+
const cents = priceMap.get(String(id)) || 0;
|
|
554
|
+
if (cents > 0) {
|
|
555
|
+
m.set(String(id), (cents * days) / 100);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Use catalog-based totals only if we got at least one valid price.
|
|
559
|
+
if (m.size) perItemTotals = m;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const nItems = Math.max(1, itemNames.length || idsForRes.length || 1);
|
|
563
|
+
const fallbackEach = (Number(total) || 0) / nItems;
|
|
564
|
+
|
|
565
|
+
// Aggregate by *display name* (stable for admins), using the computed per-item totals.
|
|
566
|
+
// If we have ids and names, we try to align by index; otherwise we apply fallback.
|
|
567
|
+
for (let i = 0; i < itemNames.length; i++) {
|
|
568
|
+
const name = itemNames[i];
|
|
477
569
|
const key = String(name || '').trim();
|
|
478
570
|
if (!key) continue;
|
|
571
|
+
const id = idsForRes[i] != null ? String(idsForRes[i]) : null;
|
|
572
|
+
const per = (id && perItemTotals && perItemTotals.has(String(id))) ? perItemTotals.get(String(id)) : fallbackEach;
|
|
479
573
|
const cur = byItem.get(key) || { item: key, count: 0, total: 0 };
|
|
480
574
|
cur.count += 1;
|
|
481
|
-
cur.total +=
|
|
575
|
+
cur.total += (Number(per) || 0);
|
|
482
576
|
byItem.set(key, cur);
|
|
483
577
|
}
|
|
578
|
+
|
|
579
|
+
// If we have ids but no names (edge cases), keep a reasonable summary by id.
|
|
580
|
+
if (!itemNames.length && idsForRes.length) {
|
|
581
|
+
for (const id of idsForRes) {
|
|
582
|
+
const key = String(id || '').trim();
|
|
583
|
+
if (!key) continue;
|
|
584
|
+
const per = (perItemTotals && perItemTotals.has(String(id))) ? perItemTotals.get(String(id)) : fallbackEach;
|
|
585
|
+
const cur = byItem.get(key) || { item: key, count: 0, total: 0 };
|
|
586
|
+
cur.count += 1;
|
|
587
|
+
cur.total += (Number(per) || 0);
|
|
588
|
+
byItem.set(key, cur);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
484
591
|
}
|
|
485
592
|
}
|
|
486
593
|
|
package/lib/api.js
CHANGED
|
@@ -866,16 +866,16 @@ api.createReservation = async function (req, res) {
|
|
|
866
866
|
// A validator is "free" only if the rental duration is within the configured threshold.
|
|
867
867
|
const isValidatorFree = !!isValidator && (validatorFreeMaxDays <= 0 || nbDays <= validatorFreeMaxDays);
|
|
868
868
|
|
|
869
|
-
// Business rule: a reservation cannot start
|
|
869
|
+
// Business rule: a reservation cannot start in the past.
|
|
870
870
|
// We compare against server-local midnight. (Front-end also prevents it.)
|
|
871
871
|
try {
|
|
872
872
|
const today0 = new Date();
|
|
873
873
|
today0.setHours(0, 0, 0, 0);
|
|
874
|
-
const
|
|
875
|
-
if (start <
|
|
874
|
+
const today0ts = today0.getTime();
|
|
875
|
+
if (start < today0ts) {
|
|
876
876
|
return res.status(400).json({
|
|
877
877
|
error: 'date-too-soon',
|
|
878
|
-
message: "Impossible de réserver pour
|
|
878
|
+
message: "Impossible de réserver pour une date passée.",
|
|
879
879
|
});
|
|
880
880
|
}
|
|
881
881
|
} catch (e) {
|
package/package.json
CHANGED
package/pkg/package/lib/admin.js
CHANGED
|
@@ -110,6 +110,7 @@ admin.listPending = async function (req, res) {
|
|
|
110
110
|
.filter(v => Number.isInteger(v) && v > 0)));
|
|
111
111
|
|
|
112
112
|
if (uids.length) {
|
|
113
|
+
// user.getUsersFields exists in NodeBB 4.x
|
|
113
114
|
const users = await user.getUsersFields(uids, ['uid', 'username', 'userslug']);
|
|
114
115
|
const byUid = new Map((users || []).filter(Boolean).map(u => [String(u.uid), u]));
|
|
115
116
|
for (const r of pending) {
|
|
@@ -121,7 +122,7 @@ admin.listPending = async function (req, res) {
|
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
} catch (e) {
|
|
124
|
-
// Best-effort only
|
|
125
|
+
// Best-effort only; keep API working even if user lookups fail
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
res.json(pending);
|
|
@@ -431,6 +432,72 @@ admin.getAccounting = async function (req, res) {
|
|
|
431
432
|
let paidCount = 0;
|
|
432
433
|
let grandTotal = 0;
|
|
433
434
|
|
|
435
|
+
// Helper: calendar-day difference (end exclusive) from YYYY-MM-DD strings (UTC midnights).
|
|
436
|
+
const calendarDaysExclusiveYmd = (startYmd, endYmd) => {
|
|
437
|
+
try {
|
|
438
|
+
const s = String(startYmd || '').trim();
|
|
439
|
+
const e = String(endYmd || '').trim();
|
|
440
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(s) || !/^\d{4}-\d{2}-\d{2}$/.test(e)) return null;
|
|
441
|
+
const [sy, sm, sd] = s.split('-').map((x) => parseInt(x, 10));
|
|
442
|
+
const [ey, em, ed] = e.split('-').map((x) => parseInt(x, 10));
|
|
443
|
+
const sUtc = Date.UTC(sy, sm - 1, sd);
|
|
444
|
+
const eUtc = Date.UTC(ey, em - 1, ed);
|
|
445
|
+
const diff = Math.floor((eUtc - sUtc) / (24 * 60 * 60 * 1000));
|
|
446
|
+
return Math.max(1, diff);
|
|
447
|
+
} catch (e) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const yearFromTs = (ts) => {
|
|
453
|
+
const d = new Date(Number(ts));
|
|
454
|
+
return Number.isFinite(d.getTime()) ? d.getFullYear() : new Date().getFullYear();
|
|
455
|
+
};
|
|
456
|
+
const formSlugForYear = (y) => `locations-materiel-${y}`;
|
|
457
|
+
|
|
458
|
+
// Cache HelloAsso catalog price lookups by year to compute *per-item* totals.
|
|
459
|
+
// This prevents a bug where the full reservation total was previously counted on every item line.
|
|
460
|
+
let settingsCache = null;
|
|
461
|
+
let tokenCache = null;
|
|
462
|
+
const catalogByYear = new Map(); // year -> Map(itemId -> priceCents)
|
|
463
|
+
const getCatalogPriceMapForYear = async (year) => {
|
|
464
|
+
const y = Number(year);
|
|
465
|
+
if (!Number.isFinite(y)) return null;
|
|
466
|
+
if (catalogByYear.has(y)) return catalogByYear.get(y);
|
|
467
|
+
try {
|
|
468
|
+
if (!settingsCache) settingsCache = await meta.settings.get('calendar-onekite');
|
|
469
|
+
if (!settingsCache || !settingsCache.helloassoClientId || !settingsCache.helloassoClientSecret || !settingsCache.helloassoOrganizationSlug || !settingsCache.helloassoFormType) {
|
|
470
|
+
catalogByYear.set(y, null);
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
if (!tokenCache) {
|
|
474
|
+
tokenCache = await helloasso.getAccessToken({
|
|
475
|
+
env: settingsCache.helloassoEnv || 'prod',
|
|
476
|
+
clientId: settingsCache.helloassoClientId,
|
|
477
|
+
clientSecret: settingsCache.helloassoClientSecret,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (!tokenCache) {
|
|
481
|
+
catalogByYear.set(y, null);
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const { items: catalog } = await helloasso.listCatalogItems({
|
|
486
|
+
env: settingsCache.helloassoEnv,
|
|
487
|
+
token: tokenCache,
|
|
488
|
+
organizationSlug: settingsCache.helloassoOrganizationSlug,
|
|
489
|
+
formType: settingsCache.helloassoFormType,
|
|
490
|
+
formSlug: formSlugForYear(y),
|
|
491
|
+
});
|
|
492
|
+
const byId = new Map((catalog || []).map((it) => [String(it.id), (typeof it.price === 'number' ? it.price : 0)]));
|
|
493
|
+
catalogByYear.set(y, byId);
|
|
494
|
+
return byId;
|
|
495
|
+
} catch (e) {
|
|
496
|
+
catalogByYear.set(y, null);
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
434
501
|
for (const rid of ids) {
|
|
435
502
|
const r = await dbLayer.getReservation(rid);
|
|
436
503
|
if (!r) continue;
|
|
@@ -472,14 +539,55 @@ admin.getAccounting = async function (req, res) {
|
|
|
472
539
|
|
|
473
540
|
// Only count paid (non-free) reservations into revenue by item.
|
|
474
541
|
if (!isFree) {
|
|
475
|
-
|
|
542
|
+
// Compute per-item totals (price_per_day * calendar_days) when possible.
|
|
543
|
+
// Fallback: split reservation total equally across items.
|
|
544
|
+
const idsForRes = (Array.isArray(r.itemIds) && r.itemIds.length) ? r.itemIds.map(String) : (r.itemId ? [String(r.itemId)] : []);
|
|
545
|
+
const days = calendarDaysExclusiveYmd(r.startDate, r.endDate) || 1;
|
|
546
|
+
const year = yearFromTs(start);
|
|
547
|
+
|
|
548
|
+
let perItemTotals = null; // Map(itemId -> euros)
|
|
549
|
+
const priceMap = await getCatalogPriceMapForYear(year);
|
|
550
|
+
if (priceMap && idsForRes.length) {
|
|
551
|
+
const m = new Map();
|
|
552
|
+
for (const id of idsForRes) {
|
|
553
|
+
const cents = priceMap.get(String(id)) || 0;
|
|
554
|
+
if (cents > 0) {
|
|
555
|
+
m.set(String(id), (cents * days) / 100);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Use catalog-based totals only if we got at least one valid price.
|
|
559
|
+
if (m.size) perItemTotals = m;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const nItems = Math.max(1, itemNames.length || idsForRes.length || 1);
|
|
563
|
+
const fallbackEach = (Number(total) || 0) / nItems;
|
|
564
|
+
|
|
565
|
+
// Aggregate by *display name* (stable for admins), using the computed per-item totals.
|
|
566
|
+
// If we have ids and names, we try to align by index; otherwise we apply fallback.
|
|
567
|
+
for (let i = 0; i < itemNames.length; i++) {
|
|
568
|
+
const name = itemNames[i];
|
|
476
569
|
const key = String(name || '').trim();
|
|
477
570
|
if (!key) continue;
|
|
571
|
+
const id = idsForRes[i] != null ? String(idsForRes[i]) : null;
|
|
572
|
+
const per = (id && perItemTotals && perItemTotals.has(String(id))) ? perItemTotals.get(String(id)) : fallbackEach;
|
|
478
573
|
const cur = byItem.get(key) || { item: key, count: 0, total: 0 };
|
|
479
574
|
cur.count += 1;
|
|
480
|
-
cur.total +=
|
|
575
|
+
cur.total += (Number(per) || 0);
|
|
481
576
|
byItem.set(key, cur);
|
|
482
577
|
}
|
|
578
|
+
|
|
579
|
+
// If we have ids but no names (edge cases), keep a reasonable summary by id.
|
|
580
|
+
if (!itemNames.length && idsForRes.length) {
|
|
581
|
+
for (const id of idsForRes) {
|
|
582
|
+
const key = String(id || '').trim();
|
|
583
|
+
if (!key) continue;
|
|
584
|
+
const per = (perItemTotals && perItemTotals.has(String(id))) ? perItemTotals.get(String(id)) : fallbackEach;
|
|
585
|
+
const cur = byItem.get(key) || { item: key, count: 0, total: 0 };
|
|
586
|
+
cur.count += 1;
|
|
587
|
+
cur.total += (Number(per) || 0);
|
|
588
|
+
byItem.set(key, cur);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
483
591
|
}
|
|
484
592
|
}
|
|
485
593
|
|
package/pkg/package/lib/api.js
CHANGED
|
@@ -866,16 +866,16 @@ api.createReservation = async function (req, res) {
|
|
|
866
866
|
// A validator is "free" only if the rental duration is within the configured threshold.
|
|
867
867
|
const isValidatorFree = !!isValidator && (validatorFreeMaxDays <= 0 || nbDays <= validatorFreeMaxDays);
|
|
868
868
|
|
|
869
|
-
// Business rule: a reservation cannot start
|
|
869
|
+
// Business rule: a reservation cannot start in the past.
|
|
870
870
|
// We compare against server-local midnight. (Front-end also prevents it.)
|
|
871
871
|
try {
|
|
872
872
|
const today0 = new Date();
|
|
873
873
|
today0.setHours(0, 0, 0, 0);
|
|
874
|
-
const
|
|
875
|
-
if (start <
|
|
874
|
+
const today0ts = today0.getTime();
|
|
875
|
+
if (start < today0ts) {
|
|
876
876
|
return res.status(400).json({
|
|
877
877
|
error: 'date-too-soon',
|
|
878
|
-
message: "Impossible de réserver pour
|
|
878
|
+
message: "Impossible de réserver pour une date passée.",
|
|
879
879
|
});
|
|
880
880
|
}
|
|
881
881
|
} catch (e) {
|
package/pkg/package/package.json
CHANGED
package/pkg/package/plugin.json
CHANGED
|
@@ -1346,14 +1346,14 @@ function toDatetimeLocalValue(date) {
|
|
|
1346
1346
|
return;
|
|
1347
1347
|
}
|
|
1348
1348
|
|
|
1349
|
-
// Business rule: reservations cannot start
|
|
1349
|
+
// Business rule: reservations cannot start in the past.
|
|
1350
1350
|
// (We validate again on the server, but this gives immediate feedback.)
|
|
1351
1351
|
try {
|
|
1352
1352
|
const startDateCheck = toLocalYmd(info.start);
|
|
1353
1353
|
const todayCheck = toLocalYmd(new Date());
|
|
1354
|
-
if (startDateCheck
|
|
1354
|
+
if (startDateCheck < todayCheck) {
|
|
1355
1355
|
lastDateRuleToastAt = Date.now();
|
|
1356
|
-
showAlert('error', "Impossible de réserver pour
|
|
1356
|
+
showAlert('error', "Impossible de réserver pour une date passée.");
|
|
1357
1357
|
calendar.unselect();
|
|
1358
1358
|
isDialogOpen = false;
|
|
1359
1359
|
return;
|
|
@@ -1413,7 +1413,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1413
1413
|
} else if (code === '400' && payload && (payload.error === 'date-too-soon' || payload.code === 'date-too-soon')) {
|
|
1414
1414
|
// If we already showed the client-side toast a moment ago, avoid a duplicate.
|
|
1415
1415
|
if (!lastDateRuleToastAt || (Date.now() - lastDateRuleToastAt) > 1500) {
|
|
1416
|
-
showAlert('error', String(payload.message || "Impossible de réserver pour
|
|
1416
|
+
showAlert('error', String(payload.message || "Impossible de réserver pour une date passée."));
|
|
1417
1417
|
}
|
|
1418
1418
|
} else {
|
|
1419
1419
|
const msgRaw = payload && (payload.message || payload.error || payload.msg)
|
|
@@ -1699,7 +1699,13 @@ function toDatetimeLocalValue(date) {
|
|
|
1699
1699
|
const ownerUid = String(ev.extendedProps && ev.extendedProps.uid ? ev.extendedProps.uid : '');
|
|
1700
1700
|
const isOwner = uidNow && ownerUid && uidNow === ownerUid;
|
|
1701
1701
|
const showModeration = canModerate && isPending;
|
|
1702
|
-
|
|
1702
|
+
// Cancellation rules:
|
|
1703
|
+
// - Owner: can cancel only before payment is completed.
|
|
1704
|
+
// - Validators/admins (canModerate): can cancel even when already paid.
|
|
1705
|
+
const showCancel = (
|
|
1706
|
+
(isOwner && ['pending', 'awaiting_payment'].includes(status)) ||
|
|
1707
|
+
(canModerate && ['pending', 'awaiting_payment', 'approved', 'paid'].includes(status))
|
|
1708
|
+
);
|
|
1703
1709
|
const paymentUrl = String((p && p.paymentUrl) || '');
|
|
1704
1710
|
const showPay = isOwner && status === 'awaiting_payment' && /^https?:\/\//i.test(paymentUrl);
|
|
1705
1711
|
const buttons = {
|
package/plugin.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1356,14 +1356,14 @@ function toDatetimeLocalValue(date) {
|
|
|
1356
1356
|
return;
|
|
1357
1357
|
}
|
|
1358
1358
|
|
|
1359
|
-
// Business rule: reservations cannot start
|
|
1359
|
+
// Business rule: reservations cannot start in the past.
|
|
1360
1360
|
// (We validate again on the server, but this gives immediate feedback.)
|
|
1361
1361
|
try {
|
|
1362
1362
|
const startDateCheck = toLocalYmd(info.start);
|
|
1363
1363
|
const todayCheck = toLocalYmd(new Date());
|
|
1364
|
-
if (startDateCheck
|
|
1364
|
+
if (startDateCheck < todayCheck) {
|
|
1365
1365
|
lastDateRuleToastAt = Date.now();
|
|
1366
|
-
showAlert('error', "Impossible de réserver pour
|
|
1366
|
+
showAlert('error', "Impossible de réserver pour une date passée.");
|
|
1367
1367
|
calendar.unselect();
|
|
1368
1368
|
isDialogOpen = false;
|
|
1369
1369
|
return;
|
|
@@ -1423,7 +1423,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1423
1423
|
} else if (code === '400' && payload && (payload.error === 'date-too-soon' || payload.code === 'date-too-soon')) {
|
|
1424
1424
|
// If we already showed the client-side toast a moment ago, avoid a duplicate.
|
|
1425
1425
|
if (!lastDateRuleToastAt || (Date.now() - lastDateRuleToastAt) > 1500) {
|
|
1426
|
-
showAlert('error', String(payload.message || "Impossible de réserver pour
|
|
1426
|
+
showAlert('error', String(payload.message || "Impossible de réserver pour une date passée."));
|
|
1427
1427
|
}
|
|
1428
1428
|
} else {
|
|
1429
1429
|
const msgRaw = payload && (payload.message || payload.error || payload.msg)
|
|
@@ -1709,7 +1709,13 @@ function toDatetimeLocalValue(date) {
|
|
|
1709
1709
|
const ownerUid = String(ev.extendedProps && ev.extendedProps.uid ? ev.extendedProps.uid : '');
|
|
1710
1710
|
const isOwner = uidNow && ownerUid && uidNow === ownerUid;
|
|
1711
1711
|
const showModeration = canModerate && isPending;
|
|
1712
|
-
|
|
1712
|
+
// Cancellation rules:
|
|
1713
|
+
// - Owner: can cancel only before payment is completed.
|
|
1714
|
+
// - Validators/admins (canModerate): can cancel even when already paid.
|
|
1715
|
+
const showCancel = (
|
|
1716
|
+
(isOwner && ['pending', 'awaiting_payment'].includes(status)) ||
|
|
1717
|
+
(canModerate && ['pending', 'awaiting_payment', 'approved', 'paid'].includes(status))
|
|
1718
|
+
);
|
|
1713
1719
|
const paymentUrl = String((p && p.paymentUrl) || '');
|
|
1714
1720
|
const showPay = isOwner && status === 'awaiting_payment' && /^https?:\/\//i.test(paymentUrl);
|
|
1715
1721
|
const buttons = {
|