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.
Files changed (95) hide show
  1. package/README.md +40 -3
  2. package/dist/collections/Reservations.js +19 -7
  3. package/dist/collections/Reservations.js.map +1 -1
  4. package/dist/collections/Resources.js +11 -8
  5. package/dist/collections/Resources.js.map +1 -1
  6. package/dist/collections/Schedules.js +12 -6
  7. package/dist/collections/Schedules.js.map +1 -1
  8. package/dist/collections/Services.js +19 -10
  9. package/dist/collections/Services.js.map +1 -1
  10. package/dist/components/AvailabilityOverview/index.js +70 -26
  11. package/dist/components/AvailabilityOverview/index.js.map +1 -1
  12. package/dist/components/CalendarView/CalendarView.module.css +9 -0
  13. package/dist/components/CalendarView/LaneTimelineView.d.ts +4 -1
  14. package/dist/components/CalendarView/LaneTimelineView.js +17 -12
  15. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  16. package/dist/components/CalendarView/index.js +154 -53
  17. package/dist/components/CalendarView/index.js.map +1 -1
  18. package/dist/components/CustomerField/index.js +8 -3
  19. package/dist/components/CustomerField/index.js.map +1 -1
  20. package/dist/components/DashboardWidget/DashboardWidgetServer.js +97 -21
  21. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  22. package/dist/defaults.js +46 -8
  23. package/dist/defaults.js.map +1 -1
  24. package/dist/endpoints/cancelBooking.js +1 -1
  25. package/dist/endpoints/cancelBooking.js.map +1 -1
  26. package/dist/endpoints/checkAvailability.js +56 -7
  27. package/dist/endpoints/checkAvailability.js.map +1 -1
  28. package/dist/endpoints/createBooking.js +19 -10
  29. package/dist/endpoints/createBooking.js.map +1 -1
  30. package/dist/endpoints/customerSearch.js +5 -2
  31. package/dist/endpoints/customerSearch.js.map +1 -1
  32. package/dist/endpoints/getSlots.js +56 -7
  33. package/dist/endpoints/getSlots.js.map +1 -1
  34. package/dist/endpoints/resourceAvailability.d.ts +2 -1
  35. package/dist/endpoints/resourceAvailability.js +85 -25
  36. package/dist/endpoints/resourceAvailability.js.map +1 -1
  37. package/dist/hooks/reservations/calculateEndTime.js +48 -20
  38. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  39. package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
  40. package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
  41. package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
  42. package/dist/hooks/reservations/onStatusChange.js +10 -4
  43. package/dist/hooks/reservations/onStatusChange.js.map +1 -1
  44. package/dist/hooks/reservations/validateCancellation.js +3 -2
  45. package/dist/hooks/reservations/validateCancellation.js.map +1 -1
  46. package/dist/hooks/reservations/validateConflicts.js +23 -4
  47. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  48. package/dist/hooks/reservations/validateGuestBooking.js +3 -4
  49. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  50. package/dist/hooks/reservations/validateStatusTransition.js +2 -2
  51. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  52. package/dist/hooks/users/provisionStaffResource.js +5 -8
  53. package/dist/hooks/users/provisionStaffResource.js.map +1 -1
  54. package/dist/plugin.js +82 -13
  55. package/dist/plugin.js.map +1 -1
  56. package/dist/services/AvailabilityService.d.ts +54 -2
  57. package/dist/services/AvailabilityService.js +180 -46
  58. package/dist/services/AvailabilityService.js.map +1 -1
  59. package/dist/translations/ar.json +1 -0
  60. package/dist/translations/de.json +1 -0
  61. package/dist/translations/en.json +1 -0
  62. package/dist/translations/es.json +1 -0
  63. package/dist/translations/fa.json +1 -0
  64. package/dist/translations/fr.json +1 -0
  65. package/dist/translations/hi.json +1 -0
  66. package/dist/translations/id.json +1 -0
  67. package/dist/translations/pl.json +1 -0
  68. package/dist/translations/ru.json +1 -0
  69. package/dist/translations/tr.json +1 -0
  70. package/dist/translations/zh.json +1 -0
  71. package/dist/types.d.ts +50 -1
  72. package/dist/types.js +2 -0
  73. package/dist/types.js.map +1 -1
  74. package/dist/utilities/collectionOverrides.d.ts +14 -0
  75. package/dist/utilities/collectionOverrides.js +47 -0
  76. package/dist/utilities/collectionOverrides.js.map +1 -0
  77. package/dist/utilities/ownerAccess.d.ts +6 -0
  78. package/dist/utilities/ownerAccess.js +25 -12
  79. package/dist/utilities/ownerAccess.js.map +1 -1
  80. package/dist/utilities/reservationChanges.d.ts +17 -0
  81. package/dist/utilities/reservationChanges.js +88 -0
  82. package/dist/utilities/reservationChanges.js.map +1 -0
  83. package/dist/utilities/scheduleUtils.d.ts +14 -8
  84. package/dist/utilities/scheduleUtils.js +26 -19
  85. package/dist/utilities/scheduleUtils.js.map +1 -1
  86. package/dist/utilities/tenantFilter.d.ts +25 -0
  87. package/dist/utilities/tenantFilter.js +56 -0
  88. package/dist/utilities/tenantFilter.js.map +1 -0
  89. package/dist/utilities/timezoneUtils.d.ts +39 -0
  90. package/dist/utilities/timezoneUtils.js +134 -0
  91. package/dist/utilities/timezoneUtils.js.map +1 -0
  92. package/dist/utilities/useTenantFilter.d.ts +6 -0
  93. package/dist/utilities/useTenantFilter.js +28 -0
  94. package/dist/utilities/useTenantFilter.js.map +1 -0
  95. 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 { localDayKey } from '../../utilities/slotUtils.js';
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
- slugs?.resources
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
- end.setMonth(end.getMonth() + 1, 0);
165
- end.setDate(end.getDate() + (6 - end.getDay()));
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: '500',
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
- setReservations(result.docs ?? []);
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
- setLoading(false);
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
- limit: '0',
215
- 'where[status][equals]': defaultStatus
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: '500',
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 = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
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 dayStr = `${day.getFullYear()}-${day.getMonth()}-${day.getDate()}`;
674
- const isToday = dayStr === todayStr;
675
- const isOtherMonth = day.getMonth() !== currentDate.getMonth();
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 rDate = new Date(r.startTime);
678
- return rDate.getFullYear() === day.getFullYear() && rDate.getMonth() === day.getMonth() && rDate.getDate() === day.getDate();
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: 12
717
- }, (_, i)=>i + 7);
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 = localDayKey(day);
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.getFullYear() === day.getFullYear() && rDate.getMonth() === day.getMonth() && rDate.getDate() === day.getDate() && rDate.getHours() === hour;
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: 14
850
- }, (_, i)=>i + 7);
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 = localDayKey(currentDate);
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.getFullYear() === currentDate.getFullYear() && rDate.getMonth() === currentDate.getMonth() && rDate.getDate() === currentDate.getDate() && rDate.getHours() === hour;
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 isoDay = localDayKey(currentDate);
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' && /*#__PURE__*/ _jsx(LaneTimelineView, {
1289
- apiBase: apiBase,
1290
- day: currentDate,
1291
- onBook: handleLaneBook,
1292
- resources: selectedResourceId ? resources.filter((r)=>r.id === selectedResourceId) : resources
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(),