@ticketboothapp/booking 1.2.112 → 1.2.113
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/package.json
CHANGED
|
@@ -76,8 +76,10 @@
|
|
|
76
76
|
--cal-discount-tag-bg: rgba(239, 68, 68, 0.9);
|
|
77
77
|
--cal-discount-tag-text: #fff;
|
|
78
78
|
|
|
79
|
-
/* Today
|
|
80
|
-
--cal-today-ring: var(--booking-
|
|
79
|
+
/* Today marker */
|
|
80
|
+
--cal-today-ring: var(--booking-orange-600, #ff4d00);
|
|
81
|
+
--cal-today-text: var(--booking-orange-600, #ff4d00);
|
|
82
|
+
--cal-today-dot: var(--booking-orange-600, #ff4d00);
|
|
81
83
|
|
|
82
84
|
/* Grid borders */
|
|
83
85
|
--cal-grid-border: var(--booking-stone-300, #d6d3d1);
|
|
@@ -201,7 +203,8 @@
|
|
|
201
203
|
--cal-dropdown-day-selected-bg-hover: var(--booking-emerald-700, #047857);
|
|
202
204
|
--cal-dropdown-day-soldout-bg: #f87171;
|
|
203
205
|
--cal-dropdown-day-soldout-bg-hover: #ef4444;
|
|
204
|
-
--cal-dropdown-day-today-ring: var(--booking-
|
|
206
|
+
--cal-dropdown-day-today-ring: var(--booking-orange-600, #ff4d00);
|
|
207
|
+
--cal-dropdown-day-today-text: var(--booking-orange-600, #ff4d00);
|
|
205
208
|
|
|
206
209
|
position: fixed;
|
|
207
210
|
background-color: var(--cal-dropdown-bg);
|
|
@@ -305,6 +308,7 @@
|
|
|
305
308
|
transition: background-color 0.2s, color 0.2s;
|
|
306
309
|
border: none;
|
|
307
310
|
cursor: pointer;
|
|
311
|
+
position: relative;
|
|
308
312
|
}
|
|
309
313
|
|
|
310
314
|
.calendarDropdownDayMuted {
|
|
@@ -312,6 +316,12 @@
|
|
|
312
316
|
cursor: not-allowed;
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
.calendarDropdownDayPast {
|
|
320
|
+
background-color: var(--booking-stone-100, #f5f5f4);
|
|
321
|
+
color: var(--booking-stone-400, #a8a29e);
|
|
322
|
+
cursor: not-allowed;
|
|
323
|
+
}
|
|
324
|
+
|
|
315
325
|
.calendarDropdownDayAvailable {
|
|
316
326
|
color: var(--cal-dropdown-day-text);
|
|
317
327
|
background-color: var(--cal-dropdown-day-available-bg);
|
|
@@ -348,6 +358,20 @@
|
|
|
348
358
|
|
|
349
359
|
.calendarDropdownDayToday {
|
|
350
360
|
box-shadow: inset 0 0 0 1px var(--cal-dropdown-day-today-ring);
|
|
361
|
+
color: var(--cal-dropdown-day-today-text);
|
|
362
|
+
font-weight: 700;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.calendarDropdownDayToday::after {
|
|
366
|
+
content: "";
|
|
367
|
+
position: absolute;
|
|
368
|
+
left: 50%;
|
|
369
|
+
bottom: 0.1875rem;
|
|
370
|
+
width: 0.25rem;
|
|
371
|
+
height: 0.25rem;
|
|
372
|
+
border-radius: 999px;
|
|
373
|
+
background-color: var(--cal-dropdown-day-today-ring);
|
|
374
|
+
transform: translateX(-50%);
|
|
351
375
|
}
|
|
352
376
|
|
|
353
377
|
.calendarGrid {
|
|
@@ -402,10 +426,10 @@
|
|
|
402
426
|
}
|
|
403
427
|
}
|
|
404
428
|
|
|
405
|
-
/* Desktop:
|
|
429
|
+
/* Desktop: stretch each day to the row height so grid borders stay aligned across empty and available days. */
|
|
406
430
|
@media (min-width: 640px) {
|
|
407
431
|
.calendarDaysGrid {
|
|
408
|
-
align-items:
|
|
432
|
+
align-items: stretch;
|
|
409
433
|
}
|
|
410
434
|
|
|
411
435
|
/* Admin capacity mode: keep all days in a row equal height (including empty days like leading/trailing month dates). */
|
|
@@ -456,15 +480,9 @@
|
|
|
456
480
|
width: 100%;
|
|
457
481
|
}
|
|
458
482
|
|
|
459
|
-
/* Desktop:
|
|
483
|
+
/* Desktop: center availability content while row stretching keeps grid borders aligned. */
|
|
460
484
|
@media (min-width: 640px) {
|
|
461
485
|
.calendarDayCellInner:not(.calendarDayCellInnerMobile) {
|
|
462
|
-
justify-content: flex-end;
|
|
463
|
-
padding-bottom: 0.125rem;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/* Admin capacity mode: with stretched row heights, bottom-pinning creates excessive top whitespace. */
|
|
467
|
-
.calendar.calendarAdminMode .calendarDayCellInner:not(.calendarDayCellInnerMobile) {
|
|
468
486
|
justify-content: center;
|
|
469
487
|
padding-bottom: 0;
|
|
470
488
|
}
|
|
@@ -517,8 +535,19 @@
|
|
|
517
535
|
cursor: not-allowed;
|
|
518
536
|
}
|
|
519
537
|
|
|
538
|
+
.calendar button.calendarDayCellPast,
|
|
539
|
+
.calendar .calendarDayCellPast {
|
|
540
|
+
background-color: var(--booking-stone-100, #f5f5f4);
|
|
541
|
+
color: var(--booking-stone-400, #a8a29e);
|
|
542
|
+
cursor: not-allowed;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.calendarDayCellPast .calendarDayNumber {
|
|
546
|
+
color: var(--booking-stone-400, #a8a29e);
|
|
547
|
+
}
|
|
548
|
+
|
|
520
549
|
.calendarDayCellToday {
|
|
521
|
-
box-shadow:
|
|
550
|
+
box-shadow: none;
|
|
522
551
|
}
|
|
523
552
|
|
|
524
553
|
/* Day number */
|
|
@@ -534,6 +563,22 @@
|
|
|
534
563
|
color: var(--cal-day-number-selected);
|
|
535
564
|
}
|
|
536
565
|
|
|
566
|
+
.calendarDayCellToday .calendarDayNumber {
|
|
567
|
+
color: var(--cal-today-text);
|
|
568
|
+
font-weight: 700;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.calendarDayCellToday .calendarDayNumber::after {
|
|
572
|
+
content: "";
|
|
573
|
+
display: inline-block;
|
|
574
|
+
width: 0.25rem;
|
|
575
|
+
height: 0.25rem;
|
|
576
|
+
margin-left: 0.1875rem;
|
|
577
|
+
border-radius: 999px;
|
|
578
|
+
background-color: var(--cal-today-dot);
|
|
579
|
+
vertical-align: 0.125rem;
|
|
580
|
+
}
|
|
581
|
+
|
|
537
582
|
/* ============ BADGES & PILLS ============ */
|
|
538
583
|
.calendarDiscountTag {
|
|
539
584
|
position: absolute;
|
|
@@ -178,6 +178,7 @@ interface DateCellProps {
|
|
|
178
178
|
availability: DateAvailability | null;
|
|
179
179
|
isSelected: boolean;
|
|
180
180
|
isToday: boolean;
|
|
181
|
+
isPast: boolean;
|
|
181
182
|
showCapacity?: boolean;
|
|
182
183
|
timezone: string;
|
|
183
184
|
displayMode: 'times' | 'status';
|
|
@@ -192,6 +193,7 @@ const DateCell = memo(function DateCell({
|
|
|
192
193
|
availability,
|
|
193
194
|
isSelected,
|
|
194
195
|
isToday,
|
|
196
|
+
isPast,
|
|
195
197
|
showCapacity,
|
|
196
198
|
timezone,
|
|
197
199
|
displayMode,
|
|
@@ -209,7 +211,7 @@ const DateCell = memo(function DateCell({
|
|
|
209
211
|
// In change flow, also allow the original booked sold-out date to stay selectable.
|
|
210
212
|
const hasAvailability =
|
|
211
213
|
hasAvailabilityData && (!availability.isSoldOut || showCapacity || allowPinnedSoldOutDate);
|
|
212
|
-
const isDisabled = !hasAvailability;
|
|
214
|
+
const isDisabled = isPast || !hasAvailability;
|
|
213
215
|
|
|
214
216
|
// Format time for display. timeStr (e.g. "09:00") is already in company timezone (MDT).
|
|
215
217
|
// Create a Date with those components, interpret as company timezone, then format.
|
|
@@ -246,21 +248,24 @@ const DateCell = memo(function DateCell({
|
|
|
246
248
|
? 'min-w-0 min-h-[5.75rem]'
|
|
247
249
|
: 'min-h-[5rem]'
|
|
248
250
|
),
|
|
249
|
-
|
|
251
|
+
isPast
|
|
252
|
+
? styles.calendarDayCellPast
|
|
253
|
+
: isDisabled
|
|
250
254
|
? styles.calendarDayCellDisabled
|
|
251
255
|
: isSelected
|
|
252
256
|
? styles.calendarDayCellSelected
|
|
253
257
|
: styles.calendarDayCellAvailable,
|
|
254
|
-
isToday &&
|
|
255
|
-
), [isDisabled, isSelected, isToday,
|
|
258
|
+
isToday && !isPast && !isSelected && styles.calendarDayCellToday
|
|
259
|
+
), [isPast, isDisabled, isSelected, isToday, needsTallerCell, isMobile, mobileNeedsTaller, adminCapacityTallCells]);
|
|
256
260
|
|
|
257
261
|
const ariaLabel = useMemo(() => {
|
|
258
262
|
const parts = [dayNumber];
|
|
263
|
+
if (isPast) parts.push(' - Past date');
|
|
259
264
|
if (availability?.isSoldOut) parts.push(' - Sold out');
|
|
260
265
|
const pct = availability?.totalDiscountPercent;
|
|
261
266
|
if (pct != null && pct > 0) parts.push(` - ${pct}% off`);
|
|
262
267
|
return parts.join('');
|
|
263
|
-
}, [dayNumber, availability?.isSoldOut, availability?.totalDiscountPercent]);
|
|
268
|
+
}, [dayNumber, isPast, availability?.isSoldOut, availability?.totalDiscountPercent]);
|
|
264
269
|
|
|
265
270
|
return (
|
|
266
271
|
<button
|
|
@@ -996,6 +1001,8 @@ export function Calendar({
|
|
|
996
1001
|
getEffectiveVacancies,
|
|
997
1002
|
]);
|
|
998
1003
|
|
|
1004
|
+
const todayDateStr = formatInTimeZone(new Date(), timezone, 'yyyy-MM-dd');
|
|
1005
|
+
|
|
999
1006
|
// Get display range for header
|
|
1000
1007
|
const displayRange = useMemo(() => {
|
|
1001
1008
|
const firstDay = calendarDays[0];
|
|
@@ -1022,6 +1029,8 @@ export function Calendar({
|
|
|
1022
1029
|
|
|
1023
1030
|
const handleDateClick = (date: Date) => {
|
|
1024
1031
|
const dateStr = formatInTimeZone(date, timezone, 'yyyy-MM-dd');
|
|
1032
|
+
if (dateStr < todayDateStr) return;
|
|
1033
|
+
|
|
1025
1034
|
const availability = dateAvailabilityMap[dateStr] || null;
|
|
1026
1035
|
|
|
1027
1036
|
const allowPinnedSoldOutDate = Boolean(
|
|
@@ -1165,6 +1174,7 @@ export function Calendar({
|
|
|
1165
1174
|
const availability = dateAvailabilityMap[dateStr] || null;
|
|
1166
1175
|
const isSelected = selectedDate === dateStr;
|
|
1167
1176
|
const isCurrentMonth = day >= miniCalendarData.monthStart && day <= miniCalendarData.monthEnd;
|
|
1177
|
+
const isPast = dateStr < todayDateStr;
|
|
1168
1178
|
// Check if day is within allowed range (June 1 - October 12, 2026) in company timezone
|
|
1169
1179
|
const isInAllowedRange = day >= miniCalendarBoundsInTz.start && day <= miniCalendarBoundsInTz.end;
|
|
1170
1180
|
|
|
@@ -1178,19 +1188,21 @@ export function Calendar({
|
|
|
1178
1188
|
const isAvailable =
|
|
1179
1189
|
hasAvailabilityData &&
|
|
1180
1190
|
isInAllowedRange &&
|
|
1191
|
+
!isPast &&
|
|
1181
1192
|
(!isSoldOut || showCapacity || allowPinnedSoldOutDate);
|
|
1182
1193
|
const isToday = isSameDay(day, new Date());
|
|
1183
1194
|
|
|
1184
1195
|
const dayClass = cn(
|
|
1185
1196
|
styles.calendarDropdownDay,
|
|
1186
1197
|
!isCurrentMonth && styles.calendarDropdownDayMuted,
|
|
1198
|
+
isPast && styles.calendarDropdownDayPast,
|
|
1187
1199
|
isSoldOut && !showCapacity && styles.calendarDropdownDaySoldOut,
|
|
1188
1200
|
isSoldOut && showCapacity && styles.calendarDropdownDaySoldOutAdmin,
|
|
1189
1201
|
!isSoldOut && !isInAllowedRange && styles.calendarDropdownDayMuted,
|
|
1190
1202
|
!hasAvailabilityData && isInAllowedRange && styles.calendarDropdownDayMuted,
|
|
1191
1203
|
isSelected && isAvailable && styles.calendarDropdownDaySelected,
|
|
1192
1204
|
isAvailable && !isSelected && styles.calendarDropdownDayAvailable,
|
|
1193
|
-
isToday &&
|
|
1205
|
+
isToday && !isPast && !isSelected && styles.calendarDropdownDayToday
|
|
1194
1206
|
);
|
|
1195
1207
|
|
|
1196
1208
|
return (
|
|
@@ -1276,6 +1288,7 @@ export function Calendar({
|
|
|
1276
1288
|
const availability = dateAvailabilityMap[dateStr] || null;
|
|
1277
1289
|
const isSelected = selectedDate === dateStr;
|
|
1278
1290
|
const isToday = isSameDay(date, new Date());
|
|
1291
|
+
const isPast = dateStr < todayDateStr;
|
|
1279
1292
|
|
|
1280
1293
|
// Include availability data in key to force re-render when availability changes
|
|
1281
1294
|
const availabilityKey = availability
|
|
@@ -1289,6 +1302,7 @@ export function Calendar({
|
|
|
1289
1302
|
availability={availability}
|
|
1290
1303
|
isSelected={isSelected}
|
|
1291
1304
|
isToday={isToday}
|
|
1305
|
+
isPast={isPast}
|
|
1292
1306
|
showCapacity={showCapacity}
|
|
1293
1307
|
timezone={timezone}
|
|
1294
1308
|
displayMode={displayMode}
|
|
@@ -33,6 +33,10 @@ import {
|
|
|
33
33
|
import { formatCurrencyAmount } from '../../lib/currency';
|
|
34
34
|
import { formatBookingRefForDisplay } from '../../lib/booking-ref';
|
|
35
35
|
import { buildCheckoutBreakdown } from '../../lib/booking/checkout-breakdown';
|
|
36
|
+
import {
|
|
37
|
+
isDateAfterPublicBookingCutoff,
|
|
38
|
+
resolvePublicBookingCutoffMinutes,
|
|
39
|
+
} from '../../lib/booking/booking-cutoffs';
|
|
36
40
|
import type { PricingConfig, PrecomputedPricesByCategory } from '../../lib/booking-api';
|
|
37
41
|
import { Calendar } from './Calendar';
|
|
38
42
|
import { PickupLocationSelector } from './PickupLocationSelector';
|
|
@@ -122,6 +126,32 @@ function parseAvailabilityDateTime(value: string): Date {
|
|
|
122
126
|
return parseISO(hasExplicitOffset ? value : `${value}Z`);
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
function privateShuttleStartDateTime(dateStr: string, timeStr: string, timezone: string): Date {
|
|
130
|
+
return fromZonedTime(parseISO(`${dateStr}T${timeStr}:00`), timezone);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function privateShuttleStartTimeAllowed(
|
|
134
|
+
dateStr: string,
|
|
135
|
+
timeStr: string,
|
|
136
|
+
timezone: string,
|
|
137
|
+
now: Date,
|
|
138
|
+
cutoffMinutes?: number | null,
|
|
139
|
+
): boolean {
|
|
140
|
+
return isDateAfterPublicBookingCutoff(
|
|
141
|
+
privateShuttleStartDateTime(dateStr, timeStr, timezone),
|
|
142
|
+
now,
|
|
143
|
+
cutoffMinutes,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function privateShuttleBookingCutoffMessage(cutoffMinutes?: number | null): string {
|
|
148
|
+
const resolvedCutoffMinutes = resolvePublicBookingCutoffMinutes(cutoffMinutes);
|
|
149
|
+
if (resolvedCutoffMinutes % 60 === 0) {
|
|
150
|
+
return `Private shuttles must be booked at least ${resolvedCutoffMinutes / 60} hours before departure.`;
|
|
151
|
+
}
|
|
152
|
+
return `Private shuttles must be booked at least ${resolvedCutoffMinutes} minutes before departure.`;
|
|
153
|
+
}
|
|
154
|
+
|
|
125
155
|
function findMergedPrivateShuttleAvailability(
|
|
126
156
|
merged: Availability[],
|
|
127
157
|
sel: Availability | null,
|
|
@@ -257,6 +287,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
257
287
|
const [loading, setLoading] = useState(false);
|
|
258
288
|
const [loadingAvailabilities, setLoadingAvailabilities] = useState(true);
|
|
259
289
|
const [isFetchingMoreAvailabilities, setIsFetchingMoreAvailabilities] = useState(false);
|
|
290
|
+
const [bookingCutoffNowMs, setBookingCutoffNowMs] = useState(() => Date.now());
|
|
260
291
|
const [error, setError] = useState('');
|
|
261
292
|
const [showCheckoutModal, setShowCheckoutModal] = useState(false);
|
|
262
293
|
/** Pending reservation while user is in checkout (RESERVED until confirmed/cancelled). */
|
|
@@ -355,6 +386,14 @@ export function PrivateShuttleBookingFlow({
|
|
|
355
386
|
() => product.options?.filter((opt) => opt.status === 'ACTIVE') || [],
|
|
356
387
|
[product.options]
|
|
357
388
|
);
|
|
389
|
+
const bookingCutoffNow = useMemo(() => new Date(bookingCutoffNowMs), [bookingCutoffNowMs]);
|
|
390
|
+
const bookingCutoffMinutes = product.bookingCutoffMinutes ?? null;
|
|
391
|
+
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
if (isAdmin) return;
|
|
394
|
+
const interval = window.setInterval(() => setBookingCutoffNowMs(Date.now()), 60 * 1000);
|
|
395
|
+
return () => window.clearInterval(interval);
|
|
396
|
+
}, [isAdmin]);
|
|
358
397
|
|
|
359
398
|
// Fire view_item when product is first displayed
|
|
360
399
|
const hasFiredViewItem = useRef(false);
|
|
@@ -780,9 +819,43 @@ export function PrivateShuttleBookingFlow({
|
|
|
780
819
|
availabilitiesCache,
|
|
781
820
|
]);
|
|
782
821
|
|
|
822
|
+
const isPrivateShuttleAvailabilityBookable = useCallback(
|
|
823
|
+
(availability: Availability): boolean => {
|
|
824
|
+
if (isAdmin) return true;
|
|
825
|
+
const dateInCompanyTz = DATE_ONLY_REGEX.test(availability.dateTime)
|
|
826
|
+
? availability.dateTime
|
|
827
|
+
: formatInTimeZone(parseAvailabilityDateTime(availability.dateTime), companyTimezone, 'yyyy-MM-dd');
|
|
828
|
+
const optionId = getPrivateShuttleAvailabilityOptionId(availability);
|
|
829
|
+
const option = activeOptions.find((opt) => opt.optionId === optionId);
|
|
830
|
+
const startTimes = availability.suggestedStartTimes || option?.privateShuttleConfig?.suggestedStartTimes || [];
|
|
831
|
+
if (startTimes.length === 0) {
|
|
832
|
+
return isDateAfterPublicBookingCutoff(
|
|
833
|
+
fromZonedTime(parseISO(`${dateInCompanyTz}T23:59:59`), companyTimezone),
|
|
834
|
+
bookingCutoffNow,
|
|
835
|
+
bookingCutoffMinutes,
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
return startTimes.some((time) =>
|
|
839
|
+
privateShuttleStartTimeAllowed(
|
|
840
|
+
dateInCompanyTz,
|
|
841
|
+
time,
|
|
842
|
+
companyTimezone,
|
|
843
|
+
bookingCutoffNow,
|
|
844
|
+
bookingCutoffMinutes,
|
|
845
|
+
),
|
|
846
|
+
);
|
|
847
|
+
},
|
|
848
|
+
[activeOptions, bookingCutoffMinutes, bookingCutoffNow, companyTimezone, isAdmin],
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
const publicVisibleAvailabilities = useMemo(
|
|
852
|
+
() => (isAdmin ? availabilities : availabilities.filter(isPrivateShuttleAvailabilityBookable)),
|
|
853
|
+
[availabilities, isAdmin, isPrivateShuttleAvailabilityBookable],
|
|
854
|
+
);
|
|
855
|
+
|
|
783
856
|
// Group availabilities by date (in company timezone - MDT). Availabilities from API are in UTC.
|
|
784
857
|
const availabilitiesByDate = useMemo(() => {
|
|
785
|
-
const grouped =
|
|
858
|
+
const grouped = publicVisibleAvailabilities.reduce(
|
|
786
859
|
(acc, avail) => {
|
|
787
860
|
const dateInCompanyTz = DATE_ONLY_REGEX.test(avail.dateTime)
|
|
788
861
|
? avail.dateTime
|
|
@@ -803,7 +876,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
803
876
|
});
|
|
804
877
|
});
|
|
805
878
|
return sorted;
|
|
806
|
-
}, [
|
|
879
|
+
}, [publicVisibleAvailabilities, companyTimezone]);
|
|
807
880
|
|
|
808
881
|
const optionsAvailableForSelectedDate = useMemo(() => {
|
|
809
882
|
if (!selectedDate) return [];
|
|
@@ -914,8 +987,27 @@ export function PrivateShuttleBookingFlow({
|
|
|
914
987
|
return fromZonedTime(parseISO(`${dates[0]}T12:00:00`), companyTimezone);
|
|
915
988
|
}, [dates, companyTimezone]);
|
|
916
989
|
|
|
917
|
-
const suggestedStartTimes =
|
|
918
|
-
selectedAvailability?.suggestedStartTimes || privateShuttleConfig?.suggestedStartTimes || [];
|
|
990
|
+
const suggestedStartTimes = useMemo(() => {
|
|
991
|
+
const times = selectedAvailability?.suggestedStartTimes || privateShuttleConfig?.suggestedStartTimes || [];
|
|
992
|
+
if (isAdmin || !selectedDate) return times;
|
|
993
|
+
return times.filter((time) =>
|
|
994
|
+
privateShuttleStartTimeAllowed(
|
|
995
|
+
selectedDate,
|
|
996
|
+
time,
|
|
997
|
+
companyTimezone,
|
|
998
|
+
bookingCutoffNow,
|
|
999
|
+
bookingCutoffMinutes,
|
|
1000
|
+
),
|
|
1001
|
+
);
|
|
1002
|
+
}, [
|
|
1003
|
+
bookingCutoffMinutes,
|
|
1004
|
+
bookingCutoffNow,
|
|
1005
|
+
companyTimezone,
|
|
1006
|
+
isAdmin,
|
|
1007
|
+
privateShuttleConfig?.suggestedStartTimes,
|
|
1008
|
+
selectedAvailability?.suggestedStartTimes,
|
|
1009
|
+
selectedDate,
|
|
1010
|
+
]);
|
|
919
1011
|
const resourceCount = Math.ceil(passengerCount / RESOURCE_CAPACITY);
|
|
920
1012
|
const maxVacancies = selectedAvailability?.vacancies || 0;
|
|
921
1013
|
const isSpecialRequest = passengerCount > maxVacancies;
|
|
@@ -1255,6 +1347,13 @@ export function PrivateShuttleBookingFlow({
|
|
|
1255
1347
|
}
|
|
1256
1348
|
}, [suggestedStartTimes, isCustomTimeMode, selectedStartTime]);
|
|
1257
1349
|
|
|
1350
|
+
useEffect(() => {
|
|
1351
|
+
if (isAdmin || isCustomTimeMode || !selectedStartTime) return;
|
|
1352
|
+
if (!suggestedStartTimes.includes(selectedStartTime)) {
|
|
1353
|
+
setSelectedStartTime('');
|
|
1354
|
+
}
|
|
1355
|
+
}, [isAdmin, isCustomTimeMode, selectedStartTime, suggestedStartTimes]);
|
|
1356
|
+
|
|
1258
1357
|
useEffect(() => {
|
|
1259
1358
|
if (
|
|
1260
1359
|
pricingConfig?.cancellationPolicies &&
|
|
@@ -1282,6 +1381,14 @@ export function PrivateShuttleBookingFlow({
|
|
|
1282
1381
|
};
|
|
1283
1382
|
|
|
1284
1383
|
const handleStartTimeSelect = (time: string) => {
|
|
1384
|
+
if (
|
|
1385
|
+
!isAdmin &&
|
|
1386
|
+
selectedDate &&
|
|
1387
|
+
!privateShuttleStartTimeAllowed(selectedDate, time, companyTimezone, bookingCutoffNow, bookingCutoffMinutes)
|
|
1388
|
+
) {
|
|
1389
|
+
setError(privateShuttleBookingCutoffMessage(bookingCutoffMinutes));
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1285
1392
|
setSelectedStartTime(time);
|
|
1286
1393
|
setIsCustomTimeMode(false);
|
|
1287
1394
|
setError('');
|
|
@@ -1465,6 +1572,19 @@ export function PrivateShuttleBookingFlow({
|
|
|
1465
1572
|
setError('Please select a date, start time, and passenger count');
|
|
1466
1573
|
return;
|
|
1467
1574
|
}
|
|
1575
|
+
if (
|
|
1576
|
+
!isAdmin &&
|
|
1577
|
+
!privateShuttleStartTimeAllowed(
|
|
1578
|
+
selectedDate,
|
|
1579
|
+
selectedStartTime,
|
|
1580
|
+
companyTimezone,
|
|
1581
|
+
bookingCutoffNow,
|
|
1582
|
+
bookingCutoffMinutes,
|
|
1583
|
+
)
|
|
1584
|
+
) {
|
|
1585
|
+
setError(privateShuttleBookingCutoffMessage(bookingCutoffMinutes));
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1468
1588
|
if (
|
|
1469
1589
|
product.pickupLocations &&
|
|
1470
1590
|
product.pickupLocations.length > 0 &&
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
"productId": "p_urmbvZepRegd",
|
|
114
114
|
"optionIds": ["po_uO9Ra6NHzwof", "po_44EQlZn24gXg", "po_vuzqpvGk6wVS", "po_nIToR7Qwau3E"],
|
|
115
115
|
"productType": "PRIVATE_SHUTTLE",
|
|
116
|
+
"bookingCutoffMinutes": 2880,
|
|
116
117
|
"display": {
|
|
117
118
|
"path": "/private-shuttle",
|
|
118
119
|
"slug": "private-tour",
|