payload-reserve 1.6.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 (89) 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 +43 -18
  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 +134 -44
  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 +79 -17
  21. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  22. package/dist/defaults.js +42 -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 +81 -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 +39 -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/timezoneUtils.d.ts +39 -0
  87. package/dist/utilities/timezoneUtils.js +134 -0
  88. package/dist/utilities/timezoneUtils.js.map +1 -0
  89. 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 { localDayKey } from '../../utilities/slotUtils.js';
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,7 @@ export const CalendarView = ()=>{
44
76
  const resourceSlug = slugs?.resources ?? 'resources';
45
77
  const reservationTenantParams = useTenantFilter(reservationSlug);
46
78
  const resourceTenantParams = useTenantFilter(resourceSlug);
79
+ const reservationTimezone = config.admin?.custom?.reservationTimezone ?? 'UTC';
47
80
  const statusMachine = config.admin?.custom?.reservationStatusMachine;
48
81
  // The initial/pending status (what "pending" view shows)
49
82
  const defaultStatus = statusMachine?.defaultStatus ?? 'pending';
@@ -108,6 +141,11 @@ export const CalendarView = ()=>{
108
141
  const [viewMode, setViewMode] = useState('month');
109
142
  const [reservations, setReservations] = useState([]);
110
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);
111
149
  const [drawerDocId, setDrawerDocId] = useState(null);
112
150
  const [initialData, setInitialData] = useState(undefined);
113
151
  // Resource filter state
@@ -166,8 +204,10 @@ export const CalendarView = ()=>{
166
204
  if (viewMode === 'month') {
167
205
  start.setDate(1);
168
206
  start.setDate(start.getDate() - start.getDay());
169
- end.setMonth(end.getMonth() + 1, 0);
170
- 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);
171
211
  } else if (viewMode === 'week') {
172
212
  const dayOfWeek = start.getDay();
173
213
  start.setDate(start.getDate() - dayOfWeek);
@@ -186,11 +226,12 @@ export const CalendarView = ()=>{
186
226
  // Availability data for the selected resource (null when no resource selected — grid unshaded)
187
227
  const { data: availability } = useResourceAvailability(apiBase, selectedResourceId || undefined, rangeStart, rangeEnd);
188
228
  const fetchReservations = useCallback(async ()=>{
229
+ const seq = ++reservationsSeq.current;
189
230
  setLoading(true);
190
231
  try {
191
232
  const params = new URLSearchParams({
192
233
  depth: '1',
193
- limit: '500',
234
+ limit: String(MAX_LIST_LIMIT),
194
235
  sort: 'startTime',
195
236
  'where[startTime][greater_than_equal]': rangeStart.toISOString(),
196
237
  'where[startTime][less_than_equal]': rangeEnd.toISOString(),
@@ -198,11 +239,25 @@ export const CalendarView = ()=>{
198
239
  });
199
240
  const response = await fetch(`${apiUrl}?${params}`);
200
241
  const result = await response.json();
201
- 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);
202
252
  } catch {
253
+ if (seq !== reservationsSeq.current) {
254
+ return;
255
+ }
203
256
  setReservations([]);
204
257
  }
205
- setLoading(false);
258
+ if (seq === reservationsSeq.current) {
259
+ setLoading(false);
260
+ }
206
261
  }, [
207
262
  rangeStart,
208
263
  rangeEnd,
@@ -217,8 +272,11 @@ export const CalendarView = ()=>{
217
272
  // Fetch pending count (always, for badge) — uses defaultStatus from config
218
273
  const fetchPendingCount = useCallback(async ()=>{
219
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).
220
277
  const params = new URLSearchParams({
221
- limit: '0',
278
+ depth: '0',
279
+ limit: '1',
222
280
  'where[status][equals]': defaultStatus,
223
281
  ...reservationTenantParams
224
282
  });
@@ -240,18 +298,25 @@ export const CalendarView = ()=>{
240
298
  ]);
241
299
  // Fetch pending reservations when tab is active — uses defaultStatus from config
242
300
  const fetchPendingReservations = useCallback(async ()=>{
301
+ const seq = ++pendingSeq.current;
243
302
  try {
244
303
  const params = new URLSearchParams({
245
304
  depth: '1',
246
- limit: '500',
305
+ limit: String(MAX_LIST_LIMIT),
247
306
  sort: 'startTime',
248
307
  'where[status][equals]': defaultStatus,
249
308
  ...reservationTenantParams
250
309
  });
251
310
  const response = await fetch(`${apiUrl}?${params}`);
252
311
  const result = await response.json();
312
+ if (seq !== pendingSeq.current) {
313
+ return;
314
+ }
253
315
  setPendingReservations(result.docs ?? []);
254
316
  } catch {
317
+ if (seq !== pendingSeq.current) {
318
+ return;
319
+ }
255
320
  setPendingReservations([]);
256
321
  }
257
322
  }, [
@@ -540,7 +605,8 @@ export const CalendarView = ()=>{
540
605
  const getEventLabel = (r, compact)=>{
541
606
  const time = new Date(r.startTime).toLocaleTimeString([], {
542
607
  hour: '2-digit',
543
- minute: '2-digit'
608
+ minute: '2-digit',
609
+ timeZone: reservationTimezone
544
610
  });
545
611
  const serviceName = getResName(r.service);
546
612
  if (compact) {
@@ -571,11 +637,13 @@ export const CalendarView = ()=>{
571
637
  const serviceName = getResName(r.service) || t('reservation:calendarUnknownService');
572
638
  const startStr = new Date(r.startTime).toLocaleTimeString([], {
573
639
  hour: '2-digit',
574
- minute: '2-digit'
640
+ minute: '2-digit',
641
+ timeZone: reservationTimezone
575
642
  });
576
643
  const endStr = r.endTime ? new Date(r.endTime).toLocaleTimeString([], {
577
644
  hour: '2-digit',
578
- minute: '2-digit'
645
+ minute: '2-digit',
646
+ timeZone: reservationTimezone
579
647
  }) : '?';
580
648
  const customerName = getCustomerName(r.customer) || t('reservation:calendarUnknownCustomer');
581
649
  const resourceNames = getResourceNames(r);
@@ -664,7 +732,7 @@ export const CalendarView = ()=>{
664
732
  d.setDate(d.getDate() + 1);
665
733
  }
666
734
  const today = new Date();
667
- const todayStr = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
735
+ const todayStr = getDayKeyInTimezone(today, reservationTimezone);
668
736
  return /*#__PURE__*/ _jsxs("div", {
669
737
  className: styles.monthGrid,
670
738
  children: [
@@ -681,12 +749,12 @@ export const CalendarView = ()=>{
681
749
  children: d
682
750
  }, d)),
683
751
  days.map((day, i)=>{
684
- const dayStr = `${day.getFullYear()}-${day.getMonth()}-${day.getDate()}`;
685
- const isToday = dayStr === todayStr;
686
- 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);
687
755
  const dayReservations = filteredReservations.filter((r)=>{
688
- const rDate = new Date(r.startTime);
689
- 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;
690
758
  });
691
759
  const clickDate = new Date(day);
692
760
  clickDate.setHours(9, 0, 0, 0);
@@ -723,17 +791,19 @@ export const CalendarView = ()=>{
723
791
  d.setDate(d.getDate() + i);
724
792
  weekDays.push(d);
725
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);
726
800
  const hours = Array.from({
727
- length: 12
728
- }, (_, i)=>i + 7);
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
- ;
801
+ length: gridEndHour - gridStartHour
802
+ }, (_, i)=>i + gridStartHour);
733
803
  const gridStep = 60;
734
804
  // Build per-day slot-state maps when a resource is selected
735
805
  const daySlotMaps = availability ? new Map(weekDays.map((day)=>{
736
- const isoDay = localDayKey(day);
806
+ const isoDay = getDayKeyInTimezone(day, reservationTimezone);
737
807
  const dayAvail = availability.days.find((d)=>d.date === isoDay);
738
808
  const dayStart = new Date(day);
739
809
  dayStart.setHours(gridStartHour, 0, 0, 0);
@@ -771,6 +841,7 @@ export const CalendarView = ()=>{
771
841
  children: d.toLocaleDateString([], {
772
842
  day: 'numeric',
773
843
  month: 'numeric',
844
+ timeZone: reservationTimezone,
774
845
  weekday: 'short'
775
846
  })
776
847
  }, i)),
@@ -784,14 +855,14 @@ export const CalendarView = ()=>{
784
855
  ]
785
856
  }),
786
857
  weekDays.map((day, di)=>{
858
+ const isoDay = getDayKeyInTimezone(day, reservationTimezone);
787
859
  const cellReservations = filteredReservations.filter((r)=>{
788
860
  const rDate = new Date(r.startTime);
789
- 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;
790
862
  });
791
863
  const clickDate = new Date(day);
792
864
  clickDate.setHours(hour, 0, 0, 0);
793
865
  // Slot state (only when a resource is selected)
794
- const isoDay = localDayKey(day);
795
866
  const slotMap = daySlotMaps?.get(isoDay);
796
867
  const slotInfo = slotMap?.get(clickDate.toISOString()) ?? null;
797
868
  // Derive cell CSS class and interactivity based on slot state
@@ -856,18 +927,18 @@ export const CalendarView = ()=>{
856
927
  });
857
928
  };
858
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);
859
935
  const hours = Array.from({
860
- length: 14
861
- }, (_, i)=>i + 7);
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
- ;
936
+ length: gridEndHour - gridStartHour
937
+ }, (_, i)=>i + gridStartHour);
866
938
  const gridStep = 60;
867
- // Build slot-state map for the current day when a resource is selected
868
939
  let daySlotMap = null;
869
940
  if (availability) {
870
- const isoDay = localDayKey(currentDate);
941
+ const isoDay = currentDayKey;
871
942
  const dayAvail = availability.days.find((d)=>d.date === isoDay);
872
943
  const dayStart = new Date(currentDate);
873
944
  dayStart.setHours(gridStartHour, 0, 0, 0);
@@ -894,7 +965,7 @@ export const CalendarView = ()=>{
894
965
  children: hours.map((hour)=>{
895
966
  const hourReservations = filteredReservations.filter((r)=>{
896
967
  const rDate = new Date(r.startTime);
897
- 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;
898
969
  });
899
970
  const clickDate = new Date(currentDate);
900
971
  clickDate.setHours(hour, 0, 0, 0);
@@ -918,8 +989,7 @@ export const CalendarView = ()=>{
918
989
  }
919
990
  }
920
991
  // Time-off label
921
- const isoDay = localDayKey(currentDate);
922
- const dayAvail = availability?.days.find((d)=>d.date === isoDay);
992
+ const dayAvail = availability?.days.find((d)=>d.date === currentDayKey);
923
993
  const timeOffEntry = slotInfo?.state === 'time-off' ? dayAvail?.timeOff.find((to)=>new Date(to.start) <= clickDate && clickDate < new Date(to.end)) : undefined;
924
994
  const timeOffLabel = timeOffEntry?.type ?? timeOffEntry?.reason ?? null;
925
995
  const handleClick = isNonInteractive ? undefined : availability ? ()=>handleSlotClick(clickDate.toISOString()) : ()=>handleDateClick(clickDate);
@@ -1003,6 +1073,7 @@ export const CalendarView = ()=>{
1003
1073
  hour: '2-digit',
1004
1074
  minute: '2-digit',
1005
1075
  month: 'short',
1076
+ timeZone: reservationTimezone,
1006
1077
  year: 'numeric'
1007
1078
  });
1008
1079
  };
@@ -1159,21 +1230,25 @@ export const CalendarView = ()=>{
1159
1230
  endOfWeek.setDate(endOfWeek.getDate() + 6);
1160
1231
  return `${startOfWeek.toLocaleDateString([], {
1161
1232
  day: 'numeric',
1162
- month: 'short'
1233
+ month: 'short',
1234
+ timeZone: reservationTimezone
1163
1235
  })} - ${endOfWeek.toLocaleDateString([], {
1164
1236
  day: 'numeric',
1165
1237
  month: 'short',
1238
+ timeZone: reservationTimezone,
1166
1239
  year: 'numeric'
1167
1240
  })}`;
1168
1241
  }
1169
1242
  return currentDate.toLocaleDateString([], {
1170
1243
  day: 'numeric',
1171
1244
  month: 'long',
1245
+ timeZone: reservationTimezone,
1172
1246
  weekday: 'long',
1173
1247
  year: 'numeric'
1174
1248
  });
1175
1249
  }, [
1176
1250
  currentDate,
1251
+ reservationTimezone,
1177
1252
  viewMode
1178
1253
  ]);
1179
1254
  const handleDrawerSave = useCallback(()=>{
@@ -1269,6 +1344,14 @@ export const CalendarView = ()=>{
1269
1344
  ]
1270
1345
  }),
1271
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
+ }),
1272
1355
  resources.length > 1 && /*#__PURE__*/ _jsx("div", {
1273
1356
  className: styles.filterBar,
1274
1357
  children: /*#__PURE__*/ _jsxs("select", {
@@ -1296,12 +1379,19 @@ export const CalendarView = ()=>{
1296
1379
  viewMode === 'month' && renderMonthView(),
1297
1380
  viewMode === 'week' && renderWeekView(),
1298
1381
  viewMode === 'day' && renderDayView(),
1299
- viewMode === 'lanes' && /*#__PURE__*/ _jsx(LaneTimelineView, {
1300
- apiBase: apiBase,
1301
- day: currentDate,
1302
- onBook: handleLaneBook,
1303
- resources: selectedResourceId ? resources.filter((r)=>r.id === selectedResourceId) : resources
1304
- })
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
+ })()
1305
1395
  ]
1306
1396
  }),
1307
1397
  viewMode === 'pending' && renderPendingView(),