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.
Files changed (95) hide show
  1. package/README.md +55 -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 +76 -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 +166 -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 +91 -18
  21. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  22. package/dist/defaults.js +44 -9
  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/effectiveTimezone.d.ts +13 -0
  33. package/dist/endpoints/effectiveTimezone.js +41 -0
  34. package/dist/endpoints/effectiveTimezone.js.map +1 -0
  35. package/dist/endpoints/getSlots.js +56 -7
  36. package/dist/endpoints/getSlots.js.map +1 -1
  37. package/dist/endpoints/resourceAvailability.d.ts +4 -1
  38. package/dist/endpoints/resourceAvailability.js +102 -26
  39. package/dist/endpoints/resourceAvailability.js.map +1 -1
  40. package/dist/hooks/reservations/calculateEndTime.js +48 -20
  41. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  42. package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
  43. package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
  44. package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
  45. package/dist/hooks/reservations/onStatusChange.js +10 -4
  46. package/dist/hooks/reservations/onStatusChange.js.map +1 -1
  47. package/dist/hooks/reservations/validateCancellation.js +3 -2
  48. package/dist/hooks/reservations/validateCancellation.js.map +1 -1
  49. package/dist/hooks/reservations/validateConflicts.js +23 -4
  50. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  51. package/dist/hooks/reservations/validateGuestBooking.js +3 -4
  52. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  53. package/dist/hooks/reservations/validateStatusTransition.js +2 -2
  54. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  55. package/dist/hooks/users/provisionStaffResource.js +5 -8
  56. package/dist/hooks/users/provisionStaffResource.js.map +1 -1
  57. package/dist/plugin.js +83 -14
  58. package/dist/plugin.js.map +1 -1
  59. package/dist/services/AvailabilityService.d.ts +54 -2
  60. package/dist/services/AvailabilityService.js +180 -46
  61. package/dist/services/AvailabilityService.js.map +1 -1
  62. package/dist/translations/ar.json +1 -0
  63. package/dist/translations/de.json +1 -0
  64. package/dist/translations/en.json +1 -0
  65. package/dist/translations/es.json +1 -0
  66. package/dist/translations/fa.json +1 -0
  67. package/dist/translations/fr.json +1 -0
  68. package/dist/translations/hi.json +1 -0
  69. package/dist/translations/id.json +1 -0
  70. package/dist/translations/pl.json +1 -0
  71. package/dist/translations/ru.json +1 -0
  72. package/dist/translations/tr.json +1 -0
  73. package/dist/translations/zh.json +1 -0
  74. package/dist/types.d.ts +46 -1
  75. package/dist/types.js +2 -0
  76. package/dist/types.js.map +1 -1
  77. package/dist/utilities/collectionOverrides.d.ts +14 -0
  78. package/dist/utilities/collectionOverrides.js +47 -0
  79. package/dist/utilities/collectionOverrides.js.map +1 -0
  80. package/dist/utilities/ownerAccess.d.ts +6 -0
  81. package/dist/utilities/ownerAccess.js +25 -12
  82. package/dist/utilities/ownerAccess.js.map +1 -1
  83. package/dist/utilities/reservationChanges.d.ts +17 -0
  84. package/dist/utilities/reservationChanges.js +88 -0
  85. package/dist/utilities/reservationChanges.js.map +1 -0
  86. package/dist/utilities/scheduleUtils.d.ts +14 -8
  87. package/dist/utilities/scheduleUtils.js +26 -19
  88. package/dist/utilities/scheduleUtils.js.map +1 -1
  89. package/dist/utilities/tenantTimezone.d.ts +41 -0
  90. package/dist/utilities/tenantTimezone.js +77 -0
  91. package/dist/utilities/tenantTimezone.js.map +1 -0
  92. package/dist/utilities/timezoneUtils.d.ts +44 -0
  93. package/dist/utilities/timezoneUtils.js +146 -0
  94. package/dist/utilities/timezoneUtils.js.map +1 -0
  95. 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,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
- end.setMonth(end.getMonth() + 1, 0);
170
- end.setDate(end.getDate() + (6 - end.getDay()));
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: '500',
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
- setReservations(result.docs ?? []);
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
- setLoading(false);
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
- limit: '0',
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: '500',
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 = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
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 dayStr = `${day.getFullYear()}-${day.getMonth()}-${day.getDate()}`;
685
- const isToday = dayStr === todayStr;
686
- const isOtherMonth = day.getMonth() !== currentDate.getMonth();
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 rDate = new Date(r.startTime);
689
- return rDate.getFullYear() === day.getFullYear() && rDate.getMonth() === day.getMonth() && rDate.getDate() === day.getDate();
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: 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
- ;
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 = localDayKey(day);
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.getFullYear() === day.getFullYear() && rDate.getMonth() === day.getMonth() && rDate.getDate() === day.getDate() && rDate.getHours() === hour;
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: 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
- ;
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 = localDayKey(currentDate);
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.getFullYear() === currentDate.getFullYear() && rDate.getMonth() === currentDate.getMonth() && rDate.getDate() === currentDate.getDate() && rDate.getHours() === hour;
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 isoDay = localDayKey(currentDate);
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' && /*#__PURE__*/ _jsx(LaneTimelineView, {
1300
- apiBase: apiBase,
1301
- day: currentDate,
1302
- onBook: handleLaneBook,
1303
- resources: selectedResourceId ? resources.filter((r)=>r.id === selectedResourceId) : resources
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(),