payload-reserve 1.6.0 → 2.1.0
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/README.md +55 -3
- package/dist/collections/Reservations.js +19 -7
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +11 -8
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Schedules.js +12 -6
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/collections/Services.js +19 -10
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/AvailabilityOverview/index.js +76 -18
- package/dist/components/AvailabilityOverview/index.js.map +1 -1
- package/dist/components/CalendarView/CalendarView.module.css +9 -0
- package/dist/components/CalendarView/LaneTimelineView.d.ts +4 -1
- package/dist/components/CalendarView/LaneTimelineView.js +17 -12
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
- package/dist/components/CalendarView/index.js +166 -44
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/CustomerField/index.js +8 -3
- package/dist/components/CustomerField/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +91 -18
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/defaults.js +44 -9
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +1 -1
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +56 -7
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +19 -10
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +5 -2
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/effectiveTimezone.d.ts +13 -0
- package/dist/endpoints/effectiveTimezone.js +41 -0
- package/dist/endpoints/effectiveTimezone.js.map +1 -0
- package/dist/endpoints/getSlots.js +56 -7
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +4 -1
- package/dist/endpoints/resourceAvailability.js +102 -26
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/reservations/calculateEndTime.js +48 -20
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
- package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
- package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
- package/dist/hooks/reservations/onStatusChange.js +10 -4
- package/dist/hooks/reservations/onStatusChange.js.map +1 -1
- package/dist/hooks/reservations/validateCancellation.js +3 -2
- package/dist/hooks/reservations/validateCancellation.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +23 -4
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateGuestBooking.js +3 -4
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
- package/dist/hooks/reservations/validateStatusTransition.js +2 -2
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/hooks/users/provisionStaffResource.js +5 -8
- package/dist/hooks/users/provisionStaffResource.js.map +1 -1
- package/dist/plugin.js +83 -14
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +54 -2
- package/dist/services/AvailabilityService.js +180 -46
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/translations/ar.json +1 -0
- package/dist/translations/de.json +1 -0
- package/dist/translations/en.json +1 -0
- package/dist/translations/es.json +1 -0
- package/dist/translations/fa.json +1 -0
- package/dist/translations/fr.json +1 -0
- package/dist/translations/hi.json +1 -0
- package/dist/translations/id.json +1 -0
- package/dist/translations/pl.json +1 -0
- package/dist/translations/ru.json +1 -0
- package/dist/translations/tr.json +1 -0
- package/dist/translations/zh.json +1 -0
- package/dist/types.d.ts +46 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/collectionOverrides.d.ts +14 -0
- package/dist/utilities/collectionOverrides.js +47 -0
- package/dist/utilities/collectionOverrides.js.map +1 -0
- package/dist/utilities/ownerAccess.d.ts +6 -0
- package/dist/utilities/ownerAccess.js +25 -12
- package/dist/utilities/ownerAccess.js.map +1 -1
- package/dist/utilities/reservationChanges.d.ts +17 -0
- package/dist/utilities/reservationChanges.js +88 -0
- package/dist/utilities/reservationChanges.js.map +1 -0
- package/dist/utilities/scheduleUtils.d.ts +14 -8
- package/dist/utilities/scheduleUtils.js +26 -19
- package/dist/utilities/scheduleUtils.js.map +1 -1
- package/dist/utilities/tenantTimezone.d.ts +41 -0
- package/dist/utilities/tenantTimezone.js +77 -0
- package/dist/utilities/tenantTimezone.js.map +1 -0
- package/dist/utilities/timezoneUtils.d.ts +44 -0
- package/dist/utilities/timezoneUtils.js +146 -0
- package/dist/utilities/timezoneUtils.js.map +1 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import { useConfig, useDocumentDrawer, useTranslation } from '@payloadcms/ui';
|
|
|
4
4
|
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { computeSlotStates } from '../../utilities/computeSlotStates.js';
|
|
6
6
|
import { statusToI18nKey } from '../../utilities/i18nUtils.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getDayKeyInTimezone, getHourInTimezone } from '../../utilities/timezoneUtils.js';
|
|
8
8
|
import { useTenantFilter } from '../../utilities/useTenantFilter.js';
|
|
9
9
|
import styles from './CalendarView.module.css';
|
|
10
10
|
import { LaneTimelineView } from './LaneTimelineView.js';
|
|
@@ -33,6 +33,38 @@ const CUSTOM_STATUS_PALETTE = [
|
|
|
33
33
|
'#fca5a5',
|
|
34
34
|
'#fdba74'
|
|
35
35
|
];
|
|
36
|
+
// Safe ceiling for list fetches; when totalDocs exceeds this we surface a
|
|
37
|
+
// "showing N of M" notice rather than silently truncating (review D9).
|
|
38
|
+
const MAX_LIST_LIMIT = 2000;
|
|
39
|
+
// Default visible-hour window for the week/day/lane grids; the actual window
|
|
40
|
+
// expands to include any booking outside it so nothing is hidden (review D8).
|
|
41
|
+
const DEFAULT_HOUR_START = 7;
|
|
42
|
+
const DEFAULT_HOUR_END = 20;
|
|
43
|
+
/**
|
|
44
|
+
* Visible-hour window covering `reservations` (in `timeZone`), never narrower
|
|
45
|
+
* than the default business window. Every booking's start hour gets a row, so a
|
|
46
|
+
* booking outside 7–20 is shown rather than silently dropped, and all three
|
|
47
|
+
* time views share one window.
|
|
48
|
+
*/ function computeHourWindow(reservations, timeZone) {
|
|
49
|
+
let startHour = DEFAULT_HOUR_START;
|
|
50
|
+
let endHour = DEFAULT_HOUR_END;
|
|
51
|
+
for (const r of reservations){
|
|
52
|
+
if (!r.startTime) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const sh = getHourInTimezone(new Date(r.startTime), timeZone);
|
|
56
|
+
startHour = Math.min(startHour, sh);
|
|
57
|
+
endHour = Math.max(endHour, sh + 1);
|
|
58
|
+
if (r.endTime) {
|
|
59
|
+
// round the ending hour up so a slot that ends mid-hour still has a row
|
|
60
|
+
endHour = Math.max(endHour, getHourInTimezone(new Date(r.endTime), timeZone) + 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
endHour: Math.min(endHour, 24),
|
|
65
|
+
startHour: Math.max(startHour, 0)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
36
68
|
export const CalendarView = ()=>{
|
|
37
69
|
const { config } = useConfig();
|
|
38
70
|
const { t: _t } = useTranslation();
|
|
@@ -44,6 +76,39 @@ export const CalendarView = ()=>{
|
|
|
44
76
|
const resourceSlug = slugs?.resources ?? 'resources';
|
|
45
77
|
const reservationTenantParams = useTenantFilter(reservationSlug);
|
|
46
78
|
const resourceTenantParams = useTenantFilter(resourceSlug);
|
|
79
|
+
// Day-boundary rendering uses the business timezone. In multiTenant mode that's
|
|
80
|
+
// the SELECTED tenant's zone, resolved server-side from the tenant cookie (the
|
|
81
|
+
// client can't map tenant→zone itself). Until that resolves — and for plain
|
|
82
|
+
// installs — fall back to the static global zone baked into admin config.
|
|
83
|
+
const staticReservationTimezone = config.admin?.custom?.reservationTimezone ?? 'UTC';
|
|
84
|
+
const [effectiveTimezone, setEffectiveTimezone] = useState(null);
|
|
85
|
+
const reservationTimezone = effectiveTimezone ?? staticReservationTimezone;
|
|
86
|
+
const tenantKey = JSON.stringify(reservationTenantParams);
|
|
87
|
+
useEffect(()=>{
|
|
88
|
+
let cancelled = false;
|
|
89
|
+
void (async ()=>{
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`${apiBase}/reserve/effective-timezone`, {
|
|
92
|
+
credentials: 'same-origin'
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const json = await res.json();
|
|
98
|
+
if (!cancelled && typeof json?.timeZone === 'string') {
|
|
99
|
+
setEffectiveTimezone(json.timeZone);
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// keep the static fallback
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
return ()=>{
|
|
106
|
+
cancelled = true;
|
|
107
|
+
};
|
|
108
|
+
}, [
|
|
109
|
+
apiBase,
|
|
110
|
+
tenantKey
|
|
111
|
+
]);
|
|
47
112
|
const statusMachine = config.admin?.custom?.reservationStatusMachine;
|
|
48
113
|
// The initial/pending status (what "pending" view shows)
|
|
49
114
|
const defaultStatus = statusMachine?.defaultStatus ?? 'pending';
|
|
@@ -108,6 +173,11 @@ export const CalendarView = ()=>{
|
|
|
108
173
|
const [viewMode, setViewMode] = useState('month');
|
|
109
174
|
const [reservations, setReservations] = useState([]);
|
|
110
175
|
const [loading, setLoading] = useState(true);
|
|
176
|
+
// { shown, total } when a fetch hit its cap, else null — drives a non-silent notice (D9)
|
|
177
|
+
const [truncation, setTruncation] = useState(null);
|
|
178
|
+
// Monotonic request counters so a slow earlier fetch can't overwrite a newer one (D5)
|
|
179
|
+
const reservationsSeq = useRef(0);
|
|
180
|
+
const pendingSeq = useRef(0);
|
|
111
181
|
const [drawerDocId, setDrawerDocId] = useState(null);
|
|
112
182
|
const [initialData, setInitialData] = useState(undefined);
|
|
113
183
|
// Resource filter state
|
|
@@ -166,8 +236,10 @@ export const CalendarView = ()=>{
|
|
|
166
236
|
if (viewMode === 'month') {
|
|
167
237
|
start.setDate(1);
|
|
168
238
|
start.setDate(start.getDate() - start.getDay());
|
|
169
|
-
|
|
170
|
-
|
|
239
|
+
// The grid always renders 42 cells (6 weeks) from `start`; fetch the same
|
|
240
|
+
// span so trailing weeks aren't silently empty (review D1).
|
|
241
|
+
end.setTime(start.getTime());
|
|
242
|
+
end.setDate(start.getDate() + 41);
|
|
171
243
|
} else if (viewMode === 'week') {
|
|
172
244
|
const dayOfWeek = start.getDay();
|
|
173
245
|
start.setDate(start.getDate() - dayOfWeek);
|
|
@@ -186,11 +258,12 @@ export const CalendarView = ()=>{
|
|
|
186
258
|
// Availability data for the selected resource (null when no resource selected — grid unshaded)
|
|
187
259
|
const { data: availability } = useResourceAvailability(apiBase, selectedResourceId || undefined, rangeStart, rangeEnd);
|
|
188
260
|
const fetchReservations = useCallback(async ()=>{
|
|
261
|
+
const seq = ++reservationsSeq.current;
|
|
189
262
|
setLoading(true);
|
|
190
263
|
try {
|
|
191
264
|
const params = new URLSearchParams({
|
|
192
265
|
depth: '1',
|
|
193
|
-
limit:
|
|
266
|
+
limit: String(MAX_LIST_LIMIT),
|
|
194
267
|
sort: 'startTime',
|
|
195
268
|
'where[startTime][greater_than_equal]': rangeStart.toISOString(),
|
|
196
269
|
'where[startTime][less_than_equal]': rangeEnd.toISOString(),
|
|
@@ -198,11 +271,25 @@ export const CalendarView = ()=>{
|
|
|
198
271
|
});
|
|
199
272
|
const response = await fetch(`${apiUrl}?${params}`);
|
|
200
273
|
const result = await response.json();
|
|
201
|
-
|
|
274
|
+
if (seq !== reservationsSeq.current) {
|
|
275
|
+
return;
|
|
276
|
+
} // a newer fetch superseded this one
|
|
277
|
+
const docs = result.docs ?? [];
|
|
278
|
+
setReservations(docs);
|
|
279
|
+
const total = result.totalDocs ?? docs.length;
|
|
280
|
+
setTruncation(total > docs.length ? {
|
|
281
|
+
shown: docs.length,
|
|
282
|
+
total
|
|
283
|
+
} : null);
|
|
202
284
|
} catch {
|
|
285
|
+
if (seq !== reservationsSeq.current) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
203
288
|
setReservations([]);
|
|
204
289
|
}
|
|
205
|
-
|
|
290
|
+
if (seq === reservationsSeq.current) {
|
|
291
|
+
setLoading(false);
|
|
292
|
+
}
|
|
206
293
|
}, [
|
|
207
294
|
rangeStart,
|
|
208
295
|
rangeEnd,
|
|
@@ -217,8 +304,11 @@ export const CalendarView = ()=>{
|
|
|
217
304
|
// Fetch pending count (always, for badge) — uses defaultStatus from config
|
|
218
305
|
const fetchPendingCount = useCallback(async ()=>{
|
|
219
306
|
try {
|
|
307
|
+
// limit:1 + depth:0 returns totalDocs (the full count) without downloading
|
|
308
|
+
// every pending doc — limit:0 in Payload means "no limit" (review D9).
|
|
220
309
|
const params = new URLSearchParams({
|
|
221
|
-
|
|
310
|
+
depth: '0',
|
|
311
|
+
limit: '1',
|
|
222
312
|
'where[status][equals]': defaultStatus,
|
|
223
313
|
...reservationTenantParams
|
|
224
314
|
});
|
|
@@ -240,18 +330,25 @@ export const CalendarView = ()=>{
|
|
|
240
330
|
]);
|
|
241
331
|
// Fetch pending reservations when tab is active — uses defaultStatus from config
|
|
242
332
|
const fetchPendingReservations = useCallback(async ()=>{
|
|
333
|
+
const seq = ++pendingSeq.current;
|
|
243
334
|
try {
|
|
244
335
|
const params = new URLSearchParams({
|
|
245
336
|
depth: '1',
|
|
246
|
-
limit:
|
|
337
|
+
limit: String(MAX_LIST_LIMIT),
|
|
247
338
|
sort: 'startTime',
|
|
248
339
|
'where[status][equals]': defaultStatus,
|
|
249
340
|
...reservationTenantParams
|
|
250
341
|
});
|
|
251
342
|
const response = await fetch(`${apiUrl}?${params}`);
|
|
252
343
|
const result = await response.json();
|
|
344
|
+
if (seq !== pendingSeq.current) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
253
347
|
setPendingReservations(result.docs ?? []);
|
|
254
348
|
} catch {
|
|
349
|
+
if (seq !== pendingSeq.current) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
255
352
|
setPendingReservations([]);
|
|
256
353
|
}
|
|
257
354
|
}, [
|
|
@@ -540,7 +637,8 @@ export const CalendarView = ()=>{
|
|
|
540
637
|
const getEventLabel = (r, compact)=>{
|
|
541
638
|
const time = new Date(r.startTime).toLocaleTimeString([], {
|
|
542
639
|
hour: '2-digit',
|
|
543
|
-
minute: '2-digit'
|
|
640
|
+
minute: '2-digit',
|
|
641
|
+
timeZone: reservationTimezone
|
|
544
642
|
});
|
|
545
643
|
const serviceName = getResName(r.service);
|
|
546
644
|
if (compact) {
|
|
@@ -571,11 +669,13 @@ export const CalendarView = ()=>{
|
|
|
571
669
|
const serviceName = getResName(r.service) || t('reservation:calendarUnknownService');
|
|
572
670
|
const startStr = new Date(r.startTime).toLocaleTimeString([], {
|
|
573
671
|
hour: '2-digit',
|
|
574
|
-
minute: '2-digit'
|
|
672
|
+
minute: '2-digit',
|
|
673
|
+
timeZone: reservationTimezone
|
|
575
674
|
});
|
|
576
675
|
const endStr = r.endTime ? new Date(r.endTime).toLocaleTimeString([], {
|
|
577
676
|
hour: '2-digit',
|
|
578
|
-
minute: '2-digit'
|
|
677
|
+
minute: '2-digit',
|
|
678
|
+
timeZone: reservationTimezone
|
|
579
679
|
}) : '?';
|
|
580
680
|
const customerName = getCustomerName(r.customer) || t('reservation:calendarUnknownCustomer');
|
|
581
681
|
const resourceNames = getResourceNames(r);
|
|
@@ -664,7 +764,7 @@ export const CalendarView = ()=>{
|
|
|
664
764
|
d.setDate(d.getDate() + 1);
|
|
665
765
|
}
|
|
666
766
|
const today = new Date();
|
|
667
|
-
const todayStr =
|
|
767
|
+
const todayStr = getDayKeyInTimezone(today, reservationTimezone);
|
|
668
768
|
return /*#__PURE__*/ _jsxs("div", {
|
|
669
769
|
className: styles.monthGrid,
|
|
670
770
|
children: [
|
|
@@ -681,12 +781,12 @@ export const CalendarView = ()=>{
|
|
|
681
781
|
children: d
|
|
682
782
|
}, d)),
|
|
683
783
|
days.map((day, i)=>{
|
|
684
|
-
const
|
|
685
|
-
const isToday =
|
|
686
|
-
const isOtherMonth =
|
|
784
|
+
const dayKey = getDayKeyInTimezone(day, reservationTimezone);
|
|
785
|
+
const isToday = dayKey === todayStr;
|
|
786
|
+
const isOtherMonth = dayKey.slice(0, 7) !== getDayKeyInTimezone(currentDate, reservationTimezone).slice(0, 7);
|
|
687
787
|
const dayReservations = filteredReservations.filter((r)=>{
|
|
688
|
-
const
|
|
689
|
-
return
|
|
788
|
+
const rKey = getDayKeyInTimezone(new Date(r.startTime), reservationTimezone);
|
|
789
|
+
return rKey === dayKey;
|
|
690
790
|
});
|
|
691
791
|
const clickDate = new Date(day);
|
|
692
792
|
clickDate.setHours(9, 0, 0, 0);
|
|
@@ -723,17 +823,19 @@ export const CalendarView = ()=>{
|
|
|
723
823
|
d.setDate(d.getDate() + i);
|
|
724
824
|
weekDays.push(d);
|
|
725
825
|
}
|
|
826
|
+
// Visible-hour window derived from the week's bookings (review D8)
|
|
827
|
+
const weekReservations = filteredReservations.filter((r)=>{
|
|
828
|
+
const k = getDayKeyInTimezone(new Date(r.startTime), reservationTimezone);
|
|
829
|
+
return weekDays.some((d)=>getDayKeyInTimezone(d, reservationTimezone) === k);
|
|
830
|
+
});
|
|
831
|
+
const { endHour: gridEndHour, startHour: gridStartHour } = computeHourWindow(weekReservations, reservationTimezone);
|
|
726
832
|
const hours = Array.from({
|
|
727
|
-
length:
|
|
728
|
-
}, (_, i)=>i +
|
|
729
|
-
// Grid bounds: hour 7 to hour 19 (end of last slot), step 60 min
|
|
730
|
-
const gridStartHour = 7;
|
|
731
|
-
const gridEndHour = gridStartHour + hours.length // 19
|
|
732
|
-
;
|
|
833
|
+
length: gridEndHour - gridStartHour
|
|
834
|
+
}, (_, i)=>i + gridStartHour);
|
|
733
835
|
const gridStep = 60;
|
|
734
836
|
// Build per-day slot-state maps when a resource is selected
|
|
735
837
|
const daySlotMaps = availability ? new Map(weekDays.map((day)=>{
|
|
736
|
-
const isoDay =
|
|
838
|
+
const isoDay = getDayKeyInTimezone(day, reservationTimezone);
|
|
737
839
|
const dayAvail = availability.days.find((d)=>d.date === isoDay);
|
|
738
840
|
const dayStart = new Date(day);
|
|
739
841
|
dayStart.setHours(gridStartHour, 0, 0, 0);
|
|
@@ -771,6 +873,7 @@ export const CalendarView = ()=>{
|
|
|
771
873
|
children: d.toLocaleDateString([], {
|
|
772
874
|
day: 'numeric',
|
|
773
875
|
month: 'numeric',
|
|
876
|
+
timeZone: reservationTimezone,
|
|
774
877
|
weekday: 'short'
|
|
775
878
|
})
|
|
776
879
|
}, i)),
|
|
@@ -784,14 +887,14 @@ export const CalendarView = ()=>{
|
|
|
784
887
|
]
|
|
785
888
|
}),
|
|
786
889
|
weekDays.map((day, di)=>{
|
|
890
|
+
const isoDay = getDayKeyInTimezone(day, reservationTimezone);
|
|
787
891
|
const cellReservations = filteredReservations.filter((r)=>{
|
|
788
892
|
const rDate = new Date(r.startTime);
|
|
789
|
-
return rDate
|
|
893
|
+
return getDayKeyInTimezone(rDate, reservationTimezone) === isoDay && getHourInTimezone(rDate, reservationTimezone) === hour;
|
|
790
894
|
});
|
|
791
895
|
const clickDate = new Date(day);
|
|
792
896
|
clickDate.setHours(hour, 0, 0, 0);
|
|
793
897
|
// Slot state (only when a resource is selected)
|
|
794
|
-
const isoDay = localDayKey(day);
|
|
795
898
|
const slotMap = daySlotMaps?.get(isoDay);
|
|
796
899
|
const slotInfo = slotMap?.get(clickDate.toISOString()) ?? null;
|
|
797
900
|
// Derive cell CSS class and interactivity based on slot state
|
|
@@ -856,18 +959,18 @@ export const CalendarView = ()=>{
|
|
|
856
959
|
});
|
|
857
960
|
};
|
|
858
961
|
const renderDayView = ()=>{
|
|
962
|
+
// Build slot-state map for the current day when a resource is selected
|
|
963
|
+
const currentDayKey = getDayKeyInTimezone(currentDate, reservationTimezone);
|
|
964
|
+
// Visible-hour window derived from this day's bookings (review D8)
|
|
965
|
+
const dayReservations = filteredReservations.filter((r)=>getDayKeyInTimezone(new Date(r.startTime), reservationTimezone) === currentDayKey);
|
|
966
|
+
const { endHour: gridEndHour, startHour: gridStartHour } = computeHourWindow(dayReservations, reservationTimezone);
|
|
859
967
|
const hours = Array.from({
|
|
860
|
-
length:
|
|
861
|
-
}, (_, i)=>i +
|
|
862
|
-
// Grid bounds: hour 7 to hour 21 (end of last slot), step 60 min
|
|
863
|
-
const gridStartHour = 7;
|
|
864
|
-
const gridEndHour = gridStartHour + hours.length // 21
|
|
865
|
-
;
|
|
968
|
+
length: gridEndHour - gridStartHour
|
|
969
|
+
}, (_, i)=>i + gridStartHour);
|
|
866
970
|
const gridStep = 60;
|
|
867
|
-
// Build slot-state map for the current day when a resource is selected
|
|
868
971
|
let daySlotMap = null;
|
|
869
972
|
if (availability) {
|
|
870
|
-
const isoDay =
|
|
973
|
+
const isoDay = currentDayKey;
|
|
871
974
|
const dayAvail = availability.days.find((d)=>d.date === isoDay);
|
|
872
975
|
const dayStart = new Date(currentDate);
|
|
873
976
|
dayStart.setHours(gridStartHour, 0, 0, 0);
|
|
@@ -894,7 +997,7 @@ export const CalendarView = ()=>{
|
|
|
894
997
|
children: hours.map((hour)=>{
|
|
895
998
|
const hourReservations = filteredReservations.filter((r)=>{
|
|
896
999
|
const rDate = new Date(r.startTime);
|
|
897
|
-
return rDate
|
|
1000
|
+
return getDayKeyInTimezone(rDate, reservationTimezone) === currentDayKey && getHourInTimezone(rDate, reservationTimezone) === hour;
|
|
898
1001
|
});
|
|
899
1002
|
const clickDate = new Date(currentDate);
|
|
900
1003
|
clickDate.setHours(hour, 0, 0, 0);
|
|
@@ -918,8 +1021,7 @@ export const CalendarView = ()=>{
|
|
|
918
1021
|
}
|
|
919
1022
|
}
|
|
920
1023
|
// Time-off label
|
|
921
|
-
const
|
|
922
|
-
const dayAvail = availability?.days.find((d)=>d.date === isoDay);
|
|
1024
|
+
const dayAvail = availability?.days.find((d)=>d.date === currentDayKey);
|
|
923
1025
|
const timeOffEntry = slotInfo?.state === 'time-off' ? dayAvail?.timeOff.find((to)=>new Date(to.start) <= clickDate && clickDate < new Date(to.end)) : undefined;
|
|
924
1026
|
const timeOffLabel = timeOffEntry?.type ?? timeOffEntry?.reason ?? null;
|
|
925
1027
|
const handleClick = isNonInteractive ? undefined : availability ? ()=>handleSlotClick(clickDate.toISOString()) : ()=>handleDateClick(clickDate);
|
|
@@ -1003,6 +1105,7 @@ export const CalendarView = ()=>{
|
|
|
1003
1105
|
hour: '2-digit',
|
|
1004
1106
|
minute: '2-digit',
|
|
1005
1107
|
month: 'short',
|
|
1108
|
+
timeZone: reservationTimezone,
|
|
1006
1109
|
year: 'numeric'
|
|
1007
1110
|
});
|
|
1008
1111
|
};
|
|
@@ -1159,21 +1262,25 @@ export const CalendarView = ()=>{
|
|
|
1159
1262
|
endOfWeek.setDate(endOfWeek.getDate() + 6);
|
|
1160
1263
|
return `${startOfWeek.toLocaleDateString([], {
|
|
1161
1264
|
day: 'numeric',
|
|
1162
|
-
month: 'short'
|
|
1265
|
+
month: 'short',
|
|
1266
|
+
timeZone: reservationTimezone
|
|
1163
1267
|
})} - ${endOfWeek.toLocaleDateString([], {
|
|
1164
1268
|
day: 'numeric',
|
|
1165
1269
|
month: 'short',
|
|
1270
|
+
timeZone: reservationTimezone,
|
|
1166
1271
|
year: 'numeric'
|
|
1167
1272
|
})}`;
|
|
1168
1273
|
}
|
|
1169
1274
|
return currentDate.toLocaleDateString([], {
|
|
1170
1275
|
day: 'numeric',
|
|
1171
1276
|
month: 'long',
|
|
1277
|
+
timeZone: reservationTimezone,
|
|
1172
1278
|
weekday: 'long',
|
|
1173
1279
|
year: 'numeric'
|
|
1174
1280
|
});
|
|
1175
1281
|
}, [
|
|
1176
1282
|
currentDate,
|
|
1283
|
+
reservationTimezone,
|
|
1177
1284
|
viewMode
|
|
1178
1285
|
]);
|
|
1179
1286
|
const handleDrawerSave = useCallback(()=>{
|
|
@@ -1269,6 +1376,14 @@ export const CalendarView = ()=>{
|
|
|
1269
1376
|
]
|
|
1270
1377
|
}),
|
|
1271
1378
|
viewMode !== 'pending' && renderStatusLegend(),
|
|
1379
|
+
viewMode !== 'pending' && truncation && /*#__PURE__*/ _jsx("div", {
|
|
1380
|
+
className: styles.truncationNotice,
|
|
1381
|
+
role: "status",
|
|
1382
|
+
children: t('reservation:calendarShowingNofM', {
|
|
1383
|
+
shown: String(truncation.shown),
|
|
1384
|
+
total: String(truncation.total)
|
|
1385
|
+
})
|
|
1386
|
+
}),
|
|
1272
1387
|
resources.length > 1 && /*#__PURE__*/ _jsx("div", {
|
|
1273
1388
|
className: styles.filterBar,
|
|
1274
1389
|
children: /*#__PURE__*/ _jsxs("select", {
|
|
@@ -1296,12 +1411,19 @@ export const CalendarView = ()=>{
|
|
|
1296
1411
|
viewMode === 'month' && renderMonthView(),
|
|
1297
1412
|
viewMode === 'week' && renderWeekView(),
|
|
1298
1413
|
viewMode === 'day' && renderDayView(),
|
|
1299
|
-
viewMode === 'lanes' &&
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1414
|
+
viewMode === 'lanes' && (()=>{
|
|
1415
|
+
const laneDayKey = getDayKeyInTimezone(currentDate, reservationTimezone);
|
|
1416
|
+
const { endHour, startHour } = computeHourWindow(filteredReservations.filter((r)=>getDayKeyInTimezone(new Date(r.startTime), reservationTimezone) === laneDayKey), reservationTimezone);
|
|
1417
|
+
return /*#__PURE__*/ _jsx(LaneTimelineView, {
|
|
1418
|
+
apiBase: apiBase,
|
|
1419
|
+
day: currentDate,
|
|
1420
|
+
endHour: endHour,
|
|
1421
|
+
onBook: handleLaneBook,
|
|
1422
|
+
resources: selectedResourceId ? resources.filter((r)=>r.id === selectedResourceId) : resources,
|
|
1423
|
+
startHour: startHour,
|
|
1424
|
+
timeZone: reservationTimezone
|
|
1425
|
+
});
|
|
1426
|
+
})()
|
|
1305
1427
|
]
|
|
1306
1428
|
}),
|
|
1307
1429
|
viewMode === 'pending' && renderPendingView(),
|