nodebb-plugin-onekite-calendar 2.0.1 → 2.0.3
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 +11 -0
- package/lib/api.js +63 -3
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/client.js +22 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog – calendar-onekite
|
|
2
2
|
|
|
3
|
+
## 1.2.10
|
|
4
|
+
- HelloAsso : calcul des jours 100% fiable (différence en jours calendaires Y/M/J, sans dépendance aux heures/au fuseau/DST)
|
|
5
|
+
- FullCalendar : endDate traitée comme exclusive partout (UI + checkout)
|
|
6
|
+
- HelloAsso : montant du checkout recalculé côté serveur à partir du catalogue (prix/jour × nbJours)
|
|
7
|
+
|
|
8
|
+
## 1.2.9
|
|
9
|
+
- Modale réservation : retour du grisé des matériels indisponibles (API events expose à nouveau itemIds)
|
|
10
|
+
|
|
11
|
+
## 1.2.8
|
|
12
|
+
- Popup réservation : correction « Durée rapide » (la période envoyée correspond bien à la durée sélectionnée)
|
|
13
|
+
|
|
3
14
|
## 1.2.7
|
|
4
15
|
- UI : suppression du bouton flottant mobile « + Réserver »
|
|
5
16
|
- Client : nettoyage du code associé (suppression du bloc FAB)
|
package/lib/api.js
CHANGED
|
@@ -226,6 +226,24 @@ function toTs(v) {
|
|
|
226
226
|
return d.getTime();
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// Calendar-day difference (end exclusive) computed purely from Y/M/D.
|
|
230
|
+
// Uses UTC midnights to avoid any dependency on local timezone or DST.
|
|
231
|
+
function calendarDaysExclusiveYmd(startYmd, endYmd) {
|
|
232
|
+
try {
|
|
233
|
+
const m1 = /^\d{4}-\d{2}-\d{2}$/.exec(String(startYmd || '').trim());
|
|
234
|
+
const m2 = /^\d{4}-\d{2}-\d{2}$/.exec(String(endYmd || '').trim());
|
|
235
|
+
if (!m1 || !m2) return null;
|
|
236
|
+
const [sy, sm, sd] = startYmd.split('-').map((x) => parseInt(x, 10));
|
|
237
|
+
const [ey, em, ed] = endYmd.split('-').map((x) => parseInt(x, 10));
|
|
238
|
+
const sUtc = Date.UTC(sy, sm - 1, sd);
|
|
239
|
+
const eUtc = Date.UTC(ey, em - 1, ed);
|
|
240
|
+
const diff = Math.floor((eUtc - sUtc) / (24 * 60 * 60 * 1000));
|
|
241
|
+
return Math.max(1, diff);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
229
247
|
function yearFromTs(ts) {
|
|
230
248
|
const d = new Date(Number(ts));
|
|
231
249
|
return Number.isFinite(d.getTime()) ? d.getFullYear() : new Date().getFullYear();
|
|
@@ -468,6 +486,10 @@ api.getEvents = async function (req, res) {
|
|
|
468
486
|
rid: p.rid,
|
|
469
487
|
status: p.status,
|
|
470
488
|
uid: p.uid,
|
|
489
|
+
// Needed client-side to gray out unavailable items in the reservation modal.
|
|
490
|
+
// Not sensitive: it is already visible in event titles and prevents double booking.
|
|
491
|
+
itemIds: Array.isArray(p.itemIds) ? p.itemIds.map(String) : [],
|
|
492
|
+
itemIdLine: p.itemIdLine ? String(p.itemIdLine) : '',
|
|
471
493
|
canModerate: canMod,
|
|
472
494
|
...(widgetMode ? { reservedByUsername: String(r.username || '') } : {}),
|
|
473
495
|
},
|
|
@@ -725,12 +747,19 @@ api.createReservation = async function (req, res) {
|
|
|
725
747
|
const ok = await canRequest(uid, settings, startPreview);
|
|
726
748
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
727
749
|
|
|
728
|
-
const
|
|
729
|
-
const
|
|
750
|
+
const startRaw = req.body.start;
|
|
751
|
+
const endRaw = req.body.end;
|
|
752
|
+
const start = parseInt(toTs(startRaw), 10);
|
|
753
|
+
const end = parseInt(toTs(endRaw), 10);
|
|
730
754
|
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) {
|
|
731
755
|
return res.status(400).json({ error: "bad-dates" });
|
|
732
756
|
}
|
|
733
757
|
|
|
758
|
+
// Keep the original date-only strings when available. This allows
|
|
759
|
+
// calendar-day calculations that are 100% independent of hours/timezones/DST.
|
|
760
|
+
const startDate = (typeof startRaw === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(startRaw.trim())) ? startRaw.trim() : null;
|
|
761
|
+
const endDate = (typeof endRaw === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(endRaw.trim())) ? endRaw.trim() : null;
|
|
762
|
+
|
|
734
763
|
// Business rule: a reservation cannot start on the current day or in the past.
|
|
735
764
|
// We compare against server-local midnight. (Front-end also prevents it.)
|
|
736
765
|
try {
|
|
@@ -798,6 +827,8 @@ api.createReservation = async function (req, res) {
|
|
|
798
827
|
itemName: (itemNames[0] || itemIds[0]),
|
|
799
828
|
start,
|
|
800
829
|
end,
|
|
830
|
+
startDate,
|
|
831
|
+
endDate,
|
|
801
832
|
status: 'pending',
|
|
802
833
|
createdAt: now,
|
|
803
834
|
total: isNaN(total) ? 0 : total,
|
|
@@ -916,6 +947,33 @@ api.approveReservation = async function (req, res) {
|
|
|
916
947
|
const token = await helloasso.getAccessToken({ env: settings2.helloassoEnv || 'prod', clientId: settings2.helloassoClientId, clientSecret: settings2.helloassoClientSecret });
|
|
917
948
|
const payer = await user.getUserFields(r.uid, ['email']);
|
|
918
949
|
const year = yearFromTs(r.start);
|
|
950
|
+
|
|
951
|
+
// Reliable calendar-day count (end is EXCLUSIVE, FullCalendar rule)
|
|
952
|
+
const days = calendarDaysExclusiveYmd(r.startDate, r.endDate) || 1;
|
|
953
|
+
|
|
954
|
+
// Recompute total from HelloAsso catalog to avoid any dependency on hours/DST
|
|
955
|
+
// and to ensure checkout amount is always consistent.
|
|
956
|
+
let recomputedTotalCents = null;
|
|
957
|
+
try {
|
|
958
|
+
const { items: catalog } = await helloasso.listCatalogItems({
|
|
959
|
+
env: settings2.helloassoEnv,
|
|
960
|
+
token,
|
|
961
|
+
organizationSlug: settings2.helloassoOrganizationSlug,
|
|
962
|
+
formType: settings2.helloassoFormType,
|
|
963
|
+
formSlug: autoFormSlugForYear(year),
|
|
964
|
+
});
|
|
965
|
+
const byId = new Map((catalog || []).map((it) => [String(it.id), (typeof it.price === 'number' ? it.price : 0)]));
|
|
966
|
+
const ids = (Array.isArray(r.itemIds) ? r.itemIds : (r.itemId ? [r.itemId] : [])).map(String);
|
|
967
|
+
const sumCentsPerDay = ids.reduce((acc, id) => acc + (byId.get(String(id)) || 0), 0);
|
|
968
|
+
if (sumCentsPerDay > 0) {
|
|
969
|
+
recomputedTotalCents = Math.max(0, Math.round(sumCentsPerDay * days));
|
|
970
|
+
// Keep stored total in sync (euros) for emails/UX.
|
|
971
|
+
r.total = recomputedTotalCents / 100;
|
|
972
|
+
}
|
|
973
|
+
} catch (e) {
|
|
974
|
+
// ignore recompute failures; fallback to stored total
|
|
975
|
+
}
|
|
976
|
+
|
|
919
977
|
const intent = await helloasso.createCheckoutIntent({
|
|
920
978
|
env: settings2.helloassoEnv,
|
|
921
979
|
token,
|
|
@@ -925,7 +983,9 @@ api.approveReservation = async function (req, res) {
|
|
|
925
983
|
formSlug: autoFormSlugForYear(year),
|
|
926
984
|
// r.total is stored as an estimated total in euros; HelloAsso expects cents.
|
|
927
985
|
totalAmount: (() => {
|
|
928
|
-
const cents =
|
|
986
|
+
const cents = (typeof recomputedTotalCents === 'number')
|
|
987
|
+
? recomputedTotalCents
|
|
988
|
+
: Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
929
989
|
if (!cents) {
|
|
930
990
|
console.warn('[calendar-onekite] HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
|
|
931
991
|
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/client.js
CHANGED
|
@@ -806,6 +806,17 @@ function attachAddressAutocomplete(inputEl, onPick) {
|
|
|
806
806
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
807
807
|
}
|
|
808
808
|
|
|
809
|
+
// Calendar-day difference with end treated as EXCLUSIVE (FullCalendar rule).
|
|
810
|
+
// Ignores hours/timezones/DST by projecting local Y/M/D onto UTC midnights.
|
|
811
|
+
function calendarDaysExclusive(startDate, endDate) {
|
|
812
|
+
const s = new Date(startDate);
|
|
813
|
+
const e = new Date(endDate);
|
|
814
|
+
const sUtc = Date.UTC(s.getFullYear(), s.getMonth(), s.getDate());
|
|
815
|
+
const eUtc = Date.UTC(e.getFullYear(), e.getMonth(), e.getDate());
|
|
816
|
+
const diff = Math.floor((eUtc - sUtc) / (24 * 60 * 60 * 1000));
|
|
817
|
+
return Math.max(1, diff);
|
|
818
|
+
}
|
|
819
|
+
|
|
809
820
|
function toDatetimeLocalValue(date) {
|
|
810
821
|
const d = new Date(date);
|
|
811
822
|
const pad = (n) => String(n).padStart(2, '0');
|
|
@@ -816,9 +827,9 @@ function toDatetimeLocalValue(date) {
|
|
|
816
827
|
const start = selectionInfo.start;
|
|
817
828
|
let end = selectionInfo.end;
|
|
818
829
|
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
let days =
|
|
830
|
+
// Days (end is exclusive in FullCalendar) — compute in calendar days only
|
|
831
|
+
// (no dependency on hours, timezone or DST).
|
|
832
|
+
let days = calendarDaysExclusive(start, end);
|
|
822
833
|
|
|
823
834
|
// Fetch existing events overlapping the selection to disable already reserved items.
|
|
824
835
|
let blocked = new Set();
|
|
@@ -923,7 +934,9 @@ function toDatetimeLocalValue(date) {
|
|
|
923
934
|
const itemNames = cbs.map(cb => cb.getAttribute('data-name'));
|
|
924
935
|
const sum = cbs.reduce((acc, cb) => acc + (parseFloat(cb.getAttribute('data-price') || '0') || 0), 0);
|
|
925
936
|
const total = (sum / 100) * days;
|
|
926
|
-
|
|
937
|
+
// Return the effective end date (exclusive) because duration shortcuts can
|
|
938
|
+
// change the range without updating the original FullCalendar selection.
|
|
939
|
+
resolve({ itemIds, itemNames, total, days, endDate: toLocalYmd(end) });
|
|
927
940
|
},
|
|
928
941
|
},
|
|
929
942
|
},
|
|
@@ -995,7 +1008,7 @@ function toDatetimeLocalValue(date) {
|
|
|
995
1008
|
// end is exclusive -> add nd days to start
|
|
996
1009
|
end = new Date(start);
|
|
997
1010
|
end.setDate(end.getDate() + nd);
|
|
998
|
-
days =
|
|
1011
|
+
days = calendarDaysExclusive(start, end);
|
|
999
1012
|
if (daysEl) {
|
|
1000
1013
|
daysEl.textContent = `(${days} jour${days > 1 ? 's' : ''})`;
|
|
1001
1014
|
}
|
|
@@ -1258,7 +1271,10 @@ function toDatetimeLocalValue(date) {
|
|
|
1258
1271
|
}
|
|
1259
1272
|
// Send date strings (no hours) so reservations are day-based.
|
|
1260
1273
|
const startDate = toLocalYmd(info.start);
|
|
1261
|
-
|
|
1274
|
+
// NOTE: FullCalendar's `info.end` reflects the original selection.
|
|
1275
|
+
// If the user used "Durée rapide", the effective end date is held
|
|
1276
|
+
// inside the dialog (returned as `chosen.endDate`).
|
|
1277
|
+
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(info.end);
|
|
1262
1278
|
await requestReservation({
|
|
1263
1279
|
start: startDate,
|
|
1264
1280
|
end: endDate,
|