payload-reserve 1.5.0 → 2.0.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 +40 -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 +70 -26
- 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 +154 -53
- 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 +97 -21
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/defaults.js +46 -8
- 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/getSlots.js +56 -7
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +2 -1
- package/dist/endpoints/resourceAvailability.js +85 -25
- 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 +82 -13
- 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 +50 -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/tenantFilter.d.ts +25 -0
- package/dist/utilities/tenantFilter.js +56 -0
- package/dist/utilities/tenantFilter.js.map +1 -0
- package/dist/utilities/timezoneUtils.d.ts +39 -0
- package/dist/utilities/timezoneUtils.js +134 -0
- package/dist/utilities/timezoneUtils.js.map +1 -0
- package/dist/utilities/useTenantFilter.d.ts +6 -0
- package/dist/utilities/useTenantFilter.js +28 -0
- package/dist/utilities/useTenantFilter.js.map +1 -0
- package/package.json +2 -1
|
@@ -4,7 +4,8 @@ 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
|
+
import { useTenantFilter } from '../../utilities/useTenantFilter.js';
|
|
8
9
|
import styles from './CalendarView.module.css';
|
|
9
10
|
import { LaneTimelineView } from './LaneTimelineView.js';
|
|
10
11
|
import { useResourceAvailability } from './useResourceAvailability.js';
|
|
@@ -32,6 +33,38 @@ const CUSTOM_STATUS_PALETTE = [
|
|
|
32
33
|
'#fca5a5',
|
|
33
34
|
'#fdba74'
|
|
34
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
|
+
}
|
|
35
68
|
export const CalendarView = ()=>{
|
|
36
69
|
const { config } = useConfig();
|
|
37
70
|
const { t: _t } = useTranslation();
|
|
@@ -40,6 +73,10 @@ export const CalendarView = ()=>{
|
|
|
40
73
|
const reservationSlug = slugs?.reservations ?? 'reservations';
|
|
41
74
|
const apiUrl = `${config.serverURL ?? ''}${config.routes.api}/${reservationSlug}`;
|
|
42
75
|
const apiBase = `${config.serverURL ?? ''}${config.routes.api}`;
|
|
76
|
+
const resourceSlug = slugs?.resources ?? 'resources';
|
|
77
|
+
const reservationTenantParams = useTenantFilter(reservationSlug);
|
|
78
|
+
const resourceTenantParams = useTenantFilter(resourceSlug);
|
|
79
|
+
const reservationTimezone = config.admin?.custom?.reservationTimezone ?? 'UTC';
|
|
43
80
|
const statusMachine = config.admin?.custom?.reservationStatusMachine;
|
|
44
81
|
// The initial/pending status (what "pending" view shows)
|
|
45
82
|
const defaultStatus = statusMachine?.defaultStatus ?? 'pending';
|
|
@@ -104,6 +141,11 @@ export const CalendarView = ()=>{
|
|
|
104
141
|
const [viewMode, setViewMode] = useState('month');
|
|
105
142
|
const [reservations, setReservations] = useState([]);
|
|
106
143
|
const [loading, setLoading] = useState(true);
|
|
144
|
+
// { shown, total } when a fetch hit its cap, else null — drives a non-silent notice (D9)
|
|
145
|
+
const [truncation, setTruncation] = useState(null);
|
|
146
|
+
// Monotonic request counters so a slow earlier fetch can't overwrite a newer one (D5)
|
|
147
|
+
const reservationsSeq = useRef(0);
|
|
148
|
+
const pendingSeq = useRef(0);
|
|
107
149
|
const [drawerDocId, setDrawerDocId] = useState(null);
|
|
108
150
|
const [initialData, setInitialData] = useState(undefined);
|
|
109
151
|
// Resource filter state
|
|
@@ -130,12 +172,12 @@ export const CalendarView = ()=>{
|
|
|
130
172
|
useEffect(()=>{
|
|
131
173
|
const fetchResources = async ()=>{
|
|
132
174
|
try {
|
|
133
|
-
const resourceSlug = slugs?.resources ?? 'resources';
|
|
134
175
|
const params = new URLSearchParams({
|
|
135
176
|
depth: '0',
|
|
136
177
|
limit: '100',
|
|
137
178
|
sort: 'name',
|
|
138
|
-
'where[active][equals]': 'true'
|
|
179
|
+
'where[active][equals]': 'true',
|
|
180
|
+
...resourceTenantParams
|
|
139
181
|
});
|
|
140
182
|
const url = `${config.serverURL ?? ''}${config.routes.api}/${resourceSlug}?${params}`;
|
|
141
183
|
const response = await fetch(url);
|
|
@@ -153,7 +195,8 @@ export const CalendarView = ()=>{
|
|
|
153
195
|
}, [
|
|
154
196
|
config.routes.api,
|
|
155
197
|
config.serverURL,
|
|
156
|
-
|
|
198
|
+
resourceSlug,
|
|
199
|
+
resourceTenantParams
|
|
157
200
|
]);
|
|
158
201
|
const { rangeEnd, rangeStart } = useMemo(()=>{
|
|
159
202
|
const start = new Date(currentDate);
|
|
@@ -161,8 +204,10 @@ export const CalendarView = ()=>{
|
|
|
161
204
|
if (viewMode === 'month') {
|
|
162
205
|
start.setDate(1);
|
|
163
206
|
start.setDate(start.getDate() - start.getDay());
|
|
164
|
-
|
|
165
|
-
|
|
207
|
+
// The grid always renders 42 cells (6 weeks) from `start`; fetch the same
|
|
208
|
+
// span so trailing weeks aren't silently empty (review D1).
|
|
209
|
+
end.setTime(start.getTime());
|
|
210
|
+
end.setDate(start.getDate() + 41);
|
|
166
211
|
} else if (viewMode === 'week') {
|
|
167
212
|
const dayOfWeek = start.getDay();
|
|
168
213
|
start.setDate(start.getDate() - dayOfWeek);
|
|
@@ -181,26 +226,43 @@ export const CalendarView = ()=>{
|
|
|
181
226
|
// Availability data for the selected resource (null when no resource selected — grid unshaded)
|
|
182
227
|
const { data: availability } = useResourceAvailability(apiBase, selectedResourceId || undefined, rangeStart, rangeEnd);
|
|
183
228
|
const fetchReservations = useCallback(async ()=>{
|
|
229
|
+
const seq = ++reservationsSeq.current;
|
|
184
230
|
setLoading(true);
|
|
185
231
|
try {
|
|
186
232
|
const params = new URLSearchParams({
|
|
187
233
|
depth: '1',
|
|
188
|
-
limit:
|
|
234
|
+
limit: String(MAX_LIST_LIMIT),
|
|
189
235
|
sort: 'startTime',
|
|
190
236
|
'where[startTime][greater_than_equal]': rangeStart.toISOString(),
|
|
191
|
-
'where[startTime][less_than_equal]': rangeEnd.toISOString()
|
|
237
|
+
'where[startTime][less_than_equal]': rangeEnd.toISOString(),
|
|
238
|
+
...reservationTenantParams
|
|
192
239
|
});
|
|
193
240
|
const response = await fetch(`${apiUrl}?${params}`);
|
|
194
241
|
const result = await response.json();
|
|
195
|
-
|
|
242
|
+
if (seq !== reservationsSeq.current) {
|
|
243
|
+
return;
|
|
244
|
+
} // a newer fetch superseded this one
|
|
245
|
+
const docs = result.docs ?? [];
|
|
246
|
+
setReservations(docs);
|
|
247
|
+
const total = result.totalDocs ?? docs.length;
|
|
248
|
+
setTruncation(total > docs.length ? {
|
|
249
|
+
shown: docs.length,
|
|
250
|
+
total
|
|
251
|
+
} : null);
|
|
196
252
|
} catch {
|
|
253
|
+
if (seq !== reservationsSeq.current) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
197
256
|
setReservations([]);
|
|
198
257
|
}
|
|
199
|
-
|
|
258
|
+
if (seq === reservationsSeq.current) {
|
|
259
|
+
setLoading(false);
|
|
260
|
+
}
|
|
200
261
|
}, [
|
|
201
262
|
rangeStart,
|
|
202
263
|
rangeEnd,
|
|
203
|
-
apiUrl
|
|
264
|
+
apiUrl,
|
|
265
|
+
reservationTenantParams
|
|
204
266
|
]);
|
|
205
267
|
useEffect(()=>{
|
|
206
268
|
void fetchReservations();
|
|
@@ -210,9 +272,13 @@ export const CalendarView = ()=>{
|
|
|
210
272
|
// Fetch pending count (always, for badge) — uses defaultStatus from config
|
|
211
273
|
const fetchPendingCount = useCallback(async ()=>{
|
|
212
274
|
try {
|
|
275
|
+
// limit:1 + depth:0 returns totalDocs (the full count) without downloading
|
|
276
|
+
// every pending doc — limit:0 in Payload means "no limit" (review D9).
|
|
213
277
|
const params = new URLSearchParams({
|
|
214
|
-
|
|
215
|
-
'
|
|
278
|
+
depth: '0',
|
|
279
|
+
limit: '1',
|
|
280
|
+
'where[status][equals]': defaultStatus,
|
|
281
|
+
...reservationTenantParams
|
|
216
282
|
});
|
|
217
283
|
const response = await fetch(`${apiUrl}?${params}`);
|
|
218
284
|
const result = await response.json();
|
|
@@ -222,7 +288,8 @@ export const CalendarView = ()=>{
|
|
|
222
288
|
}
|
|
223
289
|
}, [
|
|
224
290
|
apiUrl,
|
|
225
|
-
defaultStatus
|
|
291
|
+
defaultStatus,
|
|
292
|
+
reservationTenantParams
|
|
226
293
|
]);
|
|
227
294
|
useEffect(()=>{
|
|
228
295
|
void fetchPendingCount();
|
|
@@ -231,22 +298,31 @@ export const CalendarView = ()=>{
|
|
|
231
298
|
]);
|
|
232
299
|
// Fetch pending reservations when tab is active — uses defaultStatus from config
|
|
233
300
|
const fetchPendingReservations = useCallback(async ()=>{
|
|
301
|
+
const seq = ++pendingSeq.current;
|
|
234
302
|
try {
|
|
235
303
|
const params = new URLSearchParams({
|
|
236
304
|
depth: '1',
|
|
237
|
-
limit:
|
|
305
|
+
limit: String(MAX_LIST_LIMIT),
|
|
238
306
|
sort: 'startTime',
|
|
239
|
-
'where[status][equals]': defaultStatus
|
|
307
|
+
'where[status][equals]': defaultStatus,
|
|
308
|
+
...reservationTenantParams
|
|
240
309
|
});
|
|
241
310
|
const response = await fetch(`${apiUrl}?${params}`);
|
|
242
311
|
const result = await response.json();
|
|
312
|
+
if (seq !== pendingSeq.current) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
243
315
|
setPendingReservations(result.docs ?? []);
|
|
244
316
|
} catch {
|
|
317
|
+
if (seq !== pendingSeq.current) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
245
320
|
setPendingReservations([]);
|
|
246
321
|
}
|
|
247
322
|
}, [
|
|
248
323
|
apiUrl,
|
|
249
|
-
defaultStatus
|
|
324
|
+
defaultStatus,
|
|
325
|
+
reservationTenantParams
|
|
250
326
|
]);
|
|
251
327
|
useEffect(()=>{
|
|
252
328
|
if (viewMode === 'pending') {
|
|
@@ -529,7 +605,8 @@ export const CalendarView = ()=>{
|
|
|
529
605
|
const getEventLabel = (r, compact)=>{
|
|
530
606
|
const time = new Date(r.startTime).toLocaleTimeString([], {
|
|
531
607
|
hour: '2-digit',
|
|
532
|
-
minute: '2-digit'
|
|
608
|
+
minute: '2-digit',
|
|
609
|
+
timeZone: reservationTimezone
|
|
533
610
|
});
|
|
534
611
|
const serviceName = getResName(r.service);
|
|
535
612
|
if (compact) {
|
|
@@ -560,11 +637,13 @@ export const CalendarView = ()=>{
|
|
|
560
637
|
const serviceName = getResName(r.service) || t('reservation:calendarUnknownService');
|
|
561
638
|
const startStr = new Date(r.startTime).toLocaleTimeString([], {
|
|
562
639
|
hour: '2-digit',
|
|
563
|
-
minute: '2-digit'
|
|
640
|
+
minute: '2-digit',
|
|
641
|
+
timeZone: reservationTimezone
|
|
564
642
|
});
|
|
565
643
|
const endStr = r.endTime ? new Date(r.endTime).toLocaleTimeString([], {
|
|
566
644
|
hour: '2-digit',
|
|
567
|
-
minute: '2-digit'
|
|
645
|
+
minute: '2-digit',
|
|
646
|
+
timeZone: reservationTimezone
|
|
568
647
|
}) : '?';
|
|
569
648
|
const customerName = getCustomerName(r.customer) || t('reservation:calendarUnknownCustomer');
|
|
570
649
|
const resourceNames = getResourceNames(r);
|
|
@@ -653,7 +732,7 @@ export const CalendarView = ()=>{
|
|
|
653
732
|
d.setDate(d.getDate() + 1);
|
|
654
733
|
}
|
|
655
734
|
const today = new Date();
|
|
656
|
-
const todayStr =
|
|
735
|
+
const todayStr = getDayKeyInTimezone(today, reservationTimezone);
|
|
657
736
|
return /*#__PURE__*/ _jsxs("div", {
|
|
658
737
|
className: styles.monthGrid,
|
|
659
738
|
children: [
|
|
@@ -670,12 +749,12 @@ export const CalendarView = ()=>{
|
|
|
670
749
|
children: d
|
|
671
750
|
}, d)),
|
|
672
751
|
days.map((day, i)=>{
|
|
673
|
-
const
|
|
674
|
-
const isToday =
|
|
675
|
-
const isOtherMonth =
|
|
752
|
+
const dayKey = getDayKeyInTimezone(day, reservationTimezone);
|
|
753
|
+
const isToday = dayKey === todayStr;
|
|
754
|
+
const isOtherMonth = dayKey.slice(0, 7) !== getDayKeyInTimezone(currentDate, reservationTimezone).slice(0, 7);
|
|
676
755
|
const dayReservations = filteredReservations.filter((r)=>{
|
|
677
|
-
const
|
|
678
|
-
return
|
|
756
|
+
const rKey = getDayKeyInTimezone(new Date(r.startTime), reservationTimezone);
|
|
757
|
+
return rKey === dayKey;
|
|
679
758
|
});
|
|
680
759
|
const clickDate = new Date(day);
|
|
681
760
|
clickDate.setHours(9, 0, 0, 0);
|
|
@@ -712,17 +791,19 @@ export const CalendarView = ()=>{
|
|
|
712
791
|
d.setDate(d.getDate() + i);
|
|
713
792
|
weekDays.push(d);
|
|
714
793
|
}
|
|
794
|
+
// Visible-hour window derived from the week's bookings (review D8)
|
|
795
|
+
const weekReservations = filteredReservations.filter((r)=>{
|
|
796
|
+
const k = getDayKeyInTimezone(new Date(r.startTime), reservationTimezone);
|
|
797
|
+
return weekDays.some((d)=>getDayKeyInTimezone(d, reservationTimezone) === k);
|
|
798
|
+
});
|
|
799
|
+
const { endHour: gridEndHour, startHour: gridStartHour } = computeHourWindow(weekReservations, reservationTimezone);
|
|
715
800
|
const hours = Array.from({
|
|
716
|
-
length:
|
|
717
|
-
}, (_, i)=>i +
|
|
718
|
-
// Grid bounds: hour 7 to hour 19 (end of last slot), step 60 min
|
|
719
|
-
const gridStartHour = 7;
|
|
720
|
-
const gridEndHour = gridStartHour + hours.length // 19
|
|
721
|
-
;
|
|
801
|
+
length: gridEndHour - gridStartHour
|
|
802
|
+
}, (_, i)=>i + gridStartHour);
|
|
722
803
|
const gridStep = 60;
|
|
723
804
|
// Build per-day slot-state maps when a resource is selected
|
|
724
805
|
const daySlotMaps = availability ? new Map(weekDays.map((day)=>{
|
|
725
|
-
const isoDay =
|
|
806
|
+
const isoDay = getDayKeyInTimezone(day, reservationTimezone);
|
|
726
807
|
const dayAvail = availability.days.find((d)=>d.date === isoDay);
|
|
727
808
|
const dayStart = new Date(day);
|
|
728
809
|
dayStart.setHours(gridStartHour, 0, 0, 0);
|
|
@@ -760,6 +841,7 @@ export const CalendarView = ()=>{
|
|
|
760
841
|
children: d.toLocaleDateString([], {
|
|
761
842
|
day: 'numeric',
|
|
762
843
|
month: 'numeric',
|
|
844
|
+
timeZone: reservationTimezone,
|
|
763
845
|
weekday: 'short'
|
|
764
846
|
})
|
|
765
847
|
}, i)),
|
|
@@ -773,14 +855,14 @@ export const CalendarView = ()=>{
|
|
|
773
855
|
]
|
|
774
856
|
}),
|
|
775
857
|
weekDays.map((day, di)=>{
|
|
858
|
+
const isoDay = getDayKeyInTimezone(day, reservationTimezone);
|
|
776
859
|
const cellReservations = filteredReservations.filter((r)=>{
|
|
777
860
|
const rDate = new Date(r.startTime);
|
|
778
|
-
return rDate
|
|
861
|
+
return getDayKeyInTimezone(rDate, reservationTimezone) === isoDay && getHourInTimezone(rDate, reservationTimezone) === hour;
|
|
779
862
|
});
|
|
780
863
|
const clickDate = new Date(day);
|
|
781
864
|
clickDate.setHours(hour, 0, 0, 0);
|
|
782
865
|
// Slot state (only when a resource is selected)
|
|
783
|
-
const isoDay = localDayKey(day);
|
|
784
866
|
const slotMap = daySlotMaps?.get(isoDay);
|
|
785
867
|
const slotInfo = slotMap?.get(clickDate.toISOString()) ?? null;
|
|
786
868
|
// Derive cell CSS class and interactivity based on slot state
|
|
@@ -845,18 +927,18 @@ export const CalendarView = ()=>{
|
|
|
845
927
|
});
|
|
846
928
|
};
|
|
847
929
|
const renderDayView = ()=>{
|
|
930
|
+
// Build slot-state map for the current day when a resource is selected
|
|
931
|
+
const currentDayKey = getDayKeyInTimezone(currentDate, reservationTimezone);
|
|
932
|
+
// Visible-hour window derived from this day's bookings (review D8)
|
|
933
|
+
const dayReservations = filteredReservations.filter((r)=>getDayKeyInTimezone(new Date(r.startTime), reservationTimezone) === currentDayKey);
|
|
934
|
+
const { endHour: gridEndHour, startHour: gridStartHour } = computeHourWindow(dayReservations, reservationTimezone);
|
|
848
935
|
const hours = Array.from({
|
|
849
|
-
length:
|
|
850
|
-
}, (_, i)=>i +
|
|
851
|
-
// Grid bounds: hour 7 to hour 21 (end of last slot), step 60 min
|
|
852
|
-
const gridStartHour = 7;
|
|
853
|
-
const gridEndHour = gridStartHour + hours.length // 21
|
|
854
|
-
;
|
|
936
|
+
length: gridEndHour - gridStartHour
|
|
937
|
+
}, (_, i)=>i + gridStartHour);
|
|
855
938
|
const gridStep = 60;
|
|
856
|
-
// Build slot-state map for the current day when a resource is selected
|
|
857
939
|
let daySlotMap = null;
|
|
858
940
|
if (availability) {
|
|
859
|
-
const isoDay =
|
|
941
|
+
const isoDay = currentDayKey;
|
|
860
942
|
const dayAvail = availability.days.find((d)=>d.date === isoDay);
|
|
861
943
|
const dayStart = new Date(currentDate);
|
|
862
944
|
dayStart.setHours(gridStartHour, 0, 0, 0);
|
|
@@ -883,7 +965,7 @@ export const CalendarView = ()=>{
|
|
|
883
965
|
children: hours.map((hour)=>{
|
|
884
966
|
const hourReservations = filteredReservations.filter((r)=>{
|
|
885
967
|
const rDate = new Date(r.startTime);
|
|
886
|
-
return rDate
|
|
968
|
+
return getDayKeyInTimezone(rDate, reservationTimezone) === currentDayKey && getHourInTimezone(rDate, reservationTimezone) === hour;
|
|
887
969
|
});
|
|
888
970
|
const clickDate = new Date(currentDate);
|
|
889
971
|
clickDate.setHours(hour, 0, 0, 0);
|
|
@@ -907,8 +989,7 @@ export const CalendarView = ()=>{
|
|
|
907
989
|
}
|
|
908
990
|
}
|
|
909
991
|
// Time-off label
|
|
910
|
-
const
|
|
911
|
-
const dayAvail = availability?.days.find((d)=>d.date === isoDay);
|
|
992
|
+
const dayAvail = availability?.days.find((d)=>d.date === currentDayKey);
|
|
912
993
|
const timeOffEntry = slotInfo?.state === 'time-off' ? dayAvail?.timeOff.find((to)=>new Date(to.start) <= clickDate && clickDate < new Date(to.end)) : undefined;
|
|
913
994
|
const timeOffLabel = timeOffEntry?.type ?? timeOffEntry?.reason ?? null;
|
|
914
995
|
const handleClick = isNonInteractive ? undefined : availability ? ()=>handleSlotClick(clickDate.toISOString()) : ()=>handleDateClick(clickDate);
|
|
@@ -992,6 +1073,7 @@ export const CalendarView = ()=>{
|
|
|
992
1073
|
hour: '2-digit',
|
|
993
1074
|
minute: '2-digit',
|
|
994
1075
|
month: 'short',
|
|
1076
|
+
timeZone: reservationTimezone,
|
|
995
1077
|
year: 'numeric'
|
|
996
1078
|
});
|
|
997
1079
|
};
|
|
@@ -1148,21 +1230,25 @@ export const CalendarView = ()=>{
|
|
|
1148
1230
|
endOfWeek.setDate(endOfWeek.getDate() + 6);
|
|
1149
1231
|
return `${startOfWeek.toLocaleDateString([], {
|
|
1150
1232
|
day: 'numeric',
|
|
1151
|
-
month: 'short'
|
|
1233
|
+
month: 'short',
|
|
1234
|
+
timeZone: reservationTimezone
|
|
1152
1235
|
})} - ${endOfWeek.toLocaleDateString([], {
|
|
1153
1236
|
day: 'numeric',
|
|
1154
1237
|
month: 'short',
|
|
1238
|
+
timeZone: reservationTimezone,
|
|
1155
1239
|
year: 'numeric'
|
|
1156
1240
|
})}`;
|
|
1157
1241
|
}
|
|
1158
1242
|
return currentDate.toLocaleDateString([], {
|
|
1159
1243
|
day: 'numeric',
|
|
1160
1244
|
month: 'long',
|
|
1245
|
+
timeZone: reservationTimezone,
|
|
1161
1246
|
weekday: 'long',
|
|
1162
1247
|
year: 'numeric'
|
|
1163
1248
|
});
|
|
1164
1249
|
}, [
|
|
1165
1250
|
currentDate,
|
|
1251
|
+
reservationTimezone,
|
|
1166
1252
|
viewMode
|
|
1167
1253
|
]);
|
|
1168
1254
|
const handleDrawerSave = useCallback(()=>{
|
|
@@ -1258,6 +1344,14 @@ export const CalendarView = ()=>{
|
|
|
1258
1344
|
]
|
|
1259
1345
|
}),
|
|
1260
1346
|
viewMode !== 'pending' && renderStatusLegend(),
|
|
1347
|
+
viewMode !== 'pending' && truncation && /*#__PURE__*/ _jsx("div", {
|
|
1348
|
+
className: styles.truncationNotice,
|
|
1349
|
+
role: "status",
|
|
1350
|
+
children: t('reservation:calendarShowingNofM', {
|
|
1351
|
+
shown: String(truncation.shown),
|
|
1352
|
+
total: String(truncation.total)
|
|
1353
|
+
})
|
|
1354
|
+
}),
|
|
1261
1355
|
resources.length > 1 && /*#__PURE__*/ _jsx("div", {
|
|
1262
1356
|
className: styles.filterBar,
|
|
1263
1357
|
children: /*#__PURE__*/ _jsxs("select", {
|
|
@@ -1285,12 +1379,19 @@ export const CalendarView = ()=>{
|
|
|
1285
1379
|
viewMode === 'month' && renderMonthView(),
|
|
1286
1380
|
viewMode === 'week' && renderWeekView(),
|
|
1287
1381
|
viewMode === 'day' && renderDayView(),
|
|
1288
|
-
viewMode === 'lanes' &&
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1382
|
+
viewMode === 'lanes' && (()=>{
|
|
1383
|
+
const laneDayKey = getDayKeyInTimezone(currentDate, reservationTimezone);
|
|
1384
|
+
const { endHour, startHour } = computeHourWindow(filteredReservations.filter((r)=>getDayKeyInTimezone(new Date(r.startTime), reservationTimezone) === laneDayKey), reservationTimezone);
|
|
1385
|
+
return /*#__PURE__*/ _jsx(LaneTimelineView, {
|
|
1386
|
+
apiBase: apiBase,
|
|
1387
|
+
day: currentDate,
|
|
1388
|
+
endHour: endHour,
|
|
1389
|
+
onBook: handleLaneBook,
|
|
1390
|
+
resources: selectedResourceId ? resources.filter((r)=>r.id === selectedResourceId) : resources,
|
|
1391
|
+
startHour: startHour,
|
|
1392
|
+
timeZone: reservationTimezone
|
|
1393
|
+
});
|
|
1394
|
+
})()
|
|
1294
1395
|
]
|
|
1295
1396
|
}),
|
|
1296
1397
|
viewMode === 'pending' && renderPendingView(),
|