@ticketboothapp/booking 1.2.98 → 1.2.100
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 +3 -1
- package/src/components/booking/AdminChangeBookingFlow.tsx +5 -8
- package/src/components/booking/BookingDialog.tsx +29 -16
- package/src/components/booking/ChangeBookingDialog.tsx +10 -2
- package/src/components/booking/CheckoutModal.tsx +4 -1
- package/src/components/booking/NewBookingFlow.tsx +494 -134
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +208 -6
- package/src/components/booking/booking-flow-types.ts +50 -2
- package/src/components/booking/booking-flow-ui.ts +2 -0
- package/src/contexts/AvailabilitiesCacheContext.tsx +2 -2
- package/src/lib/booking/pricing.ts +1 -0
- package/src/lib/booking-api.ts +24 -1
- package/src/runtime/types.ts +2 -0
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
EARLIEST_AVAILABILITY_DATE,
|
|
28
28
|
LATEST_AVAILABILITY_DATE,
|
|
29
29
|
INITIAL_FETCH_WEEKS,
|
|
30
|
+
VISIBLE_RANGE_BUFFER_WEEKS,
|
|
30
31
|
} from '../../lib/booking-constants';
|
|
31
32
|
import { Calendar } from './Calendar';
|
|
32
33
|
import { AdminPaymentChoiceModal } from './AdminPaymentChoiceModal';
|
|
@@ -44,7 +45,7 @@ import { type Currency } from './CurrencySwitcher';
|
|
|
44
45
|
import { formatBookingRefForDisplay } from '../../lib/booking-ref';
|
|
45
46
|
import { formatCurrencyAmount } from '../../lib/currency';
|
|
46
47
|
import { buildCheckoutBreakdown } from '../../lib/booking/checkout-breakdown';
|
|
47
|
-
import type { PricingConfig, PrecomputedPricesByCategory, ItineraryDisplayStep } from '../../lib/booking-api';
|
|
48
|
+
import type { PricingConfig, PrecomputedPricesByCategory, ItineraryDisplayStep, GetAvailabilitiesResponse } from '../../lib/booking-api';
|
|
48
49
|
import { ItineraryStepType as StepType } from '../../lib/booking-api';
|
|
49
50
|
import {
|
|
50
51
|
getDisplayPriceFromBaseInDisplayCurrency,
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
computeOrderSummary,
|
|
53
54
|
type PriceBreakdown as PriceBreakdownData,
|
|
54
55
|
type OrderSummary,
|
|
56
|
+
type OrderSummaryFeeLine,
|
|
55
57
|
} from '../../lib/booking/pricing';
|
|
56
58
|
import { useCompanyTimezone } from '../../contexts/CompanyContext';
|
|
57
59
|
import { useBookingApp } from '../../contexts/BookingAppContext';
|
|
@@ -169,6 +171,26 @@ function findMergedAvailabilityForSelection(
|
|
|
169
171
|
return merged.find((a) => a.dateTime === dt && a.productOptionId === optId);
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
function availabilityWithOptionId(availability: Availability, fallbackOptionId?: string): Availability {
|
|
175
|
+
const productOptionId = availability.productOptionId || availability.productId || fallbackOptionId;
|
|
176
|
+
return productOptionId ? { ...availability, productOptionId } : availability;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function mergePrecomputedPricesFromAvailabilityResult(
|
|
180
|
+
next: Record<string, PrecomputedPricesByCategory>,
|
|
181
|
+
result: GetAvailabilitiesResponse,
|
|
182
|
+
fallbackOptionId?: string,
|
|
183
|
+
) {
|
|
184
|
+
if (result.precomputedPricesByOption) {
|
|
185
|
+
Object.entries(result.precomputedPricesByOption).forEach(([optionId, prices]) => {
|
|
186
|
+
if (prices && Object.keys(prices).length > 0) next[optionId] = prices;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (fallbackOptionId && result.precomputedPrices && Object.keys(result.precomputedPrices).length > 0) {
|
|
190
|
+
next[fallbackOptionId] = result.precomputedPrices;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
172
194
|
/** Ticket rates for one availability row — mirrors main cart [pricing] useMemo so baseline slots match BE. */
|
|
173
195
|
function buildPricingFromAvailability(
|
|
174
196
|
selectedAvailability: Availability | null,
|
|
@@ -320,6 +342,11 @@ export function NewBookingFlow({
|
|
|
320
342
|
initialValues,
|
|
321
343
|
hideItineraryBox = false,
|
|
322
344
|
flowUi,
|
|
345
|
+
afterItinerary,
|
|
346
|
+
augmentItineraryItems,
|
|
347
|
+
augmentPriceSummary,
|
|
348
|
+
dependentAddOnSelection,
|
|
349
|
+
validateBeforeCheckout,
|
|
323
350
|
bookingSourceAttribution,
|
|
324
351
|
partnerPortalBooking = false,
|
|
325
352
|
availabilityPricingProfileId,
|
|
@@ -563,45 +590,39 @@ export function NewBookingFlow({
|
|
|
563
590
|
endDateStr = formatInTimeZone(endMoment, 'UTC', "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
|
|
564
591
|
}
|
|
565
592
|
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
});
|
|
576
|
-
if (result.pricingConfig && !pricingConfigSetRef.current) {
|
|
577
|
-
setPricingConfig((prev) => {
|
|
578
|
-
if (!prev) {
|
|
579
|
-
pricingConfigSetRef.current = true;
|
|
580
|
-
return result.pricingConfig!;
|
|
581
|
-
}
|
|
582
|
-
return prev;
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
return {
|
|
586
|
-
optionId: option.optionId,
|
|
587
|
-
availabilities: result.availabilities.map((avail) => ({
|
|
588
|
-
...avail,
|
|
589
|
-
productOptionId: option.optionId,
|
|
590
|
-
})),
|
|
591
|
-
precomputedPrices: result.precomputedPrices,
|
|
592
|
-
pricingConfig: result.pricingConfig,
|
|
593
|
-
};
|
|
593
|
+
const result = await getAvailabilities(product.productId, startDateStr, endDateStr, {
|
|
594
|
+
allOptions: true,
|
|
595
|
+
promoCode: appliedPromoCode || undefined,
|
|
596
|
+
...(pricingProfileIdForAvailabilities
|
|
597
|
+
? { pricingProfileId: pricingProfileIdForAvailabilities }
|
|
598
|
+
: {}),
|
|
599
|
+
...(cancellationPolicyProfileIdForAvailabilities
|
|
600
|
+
? { cancellationPolicyProfileId: cancellationPolicyProfileIdForAvailabilities }
|
|
601
|
+
: {}),
|
|
594
602
|
});
|
|
603
|
+
if (result.pricingConfig && !pricingConfigSetRef.current) {
|
|
604
|
+
setPricingConfig((prev) => {
|
|
605
|
+
if (!prev) {
|
|
606
|
+
pricingConfigSetRef.current = true;
|
|
607
|
+
return result.pricingConfig!;
|
|
608
|
+
}
|
|
609
|
+
return prev;
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const results = [{
|
|
613
|
+
optionId: product.productId,
|
|
614
|
+
availabilities: result.availabilities.map((avail) => availabilityWithOptionId(avail)),
|
|
615
|
+
precomputedPrices: result.precomputedPrices,
|
|
616
|
+
precomputedPricesByOption: result.precomputedPricesByOption,
|
|
617
|
+
pricingConfig: result.pricingConfig,
|
|
618
|
+
}];
|
|
595
619
|
|
|
596
|
-
const results = await Promise.all(availabilityPromises);
|
|
597
620
|
const allFetchedAvailabilities = results.flatMap((r) => r.availabilities);
|
|
598
621
|
|
|
599
622
|
setPrecomputedPricesByOption((prev) => {
|
|
600
623
|
const next = { ...(prev || {}) };
|
|
601
624
|
results.forEach((r) => {
|
|
602
|
-
|
|
603
|
-
next[r.optionId] = r.precomputedPrices;
|
|
604
|
-
}
|
|
625
|
+
mergePrecomputedPricesFromAvailabilityResult(next, r);
|
|
605
626
|
});
|
|
606
627
|
return Object.keys(next).length > 0 ? next : prev;
|
|
607
628
|
});
|
|
@@ -652,9 +673,7 @@ export function NewBookingFlow({
|
|
|
652
673
|
});
|
|
653
674
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
654
675
|
results.forEach((r) => {
|
|
655
|
-
|
|
656
|
-
mergedPrecomputed[r.optionId] = r.precomputedPrices;
|
|
657
|
-
}
|
|
676
|
+
mergePrecomputedPricesFromAvailabilityResult(mergedPrecomputed, r);
|
|
658
677
|
});
|
|
659
678
|
const firstPricingConfig =
|
|
660
679
|
(results[0] as { pricingConfig?: PricingConfig } | undefined)?.pricingConfig ??
|
|
@@ -684,10 +703,110 @@ export function NewBookingFlow({
|
|
|
684
703
|
availabilitiesCache,
|
|
685
704
|
]);
|
|
686
705
|
|
|
706
|
+
/** Fresh selected-day check before reserve so stale calendar vacancies are corrected before payment. */
|
|
707
|
+
const refreshSelectedDateDetailsForCheckout = useCallback(async (): Promise<Availability[]> => {
|
|
708
|
+
if (!selectedDate || activeOptions.length === 0) return [];
|
|
709
|
+
|
|
710
|
+
let startDateStr: string;
|
|
711
|
+
let endDateStr: string;
|
|
712
|
+
if (isPrivateShuttle) {
|
|
713
|
+
startDateStr = selectedDate;
|
|
714
|
+
endDateStr = selectedDate;
|
|
715
|
+
} else {
|
|
716
|
+
const startMoment = fromZonedTime(parseISO(`${selectedDate}T00:00:00.000`), companyTimezone);
|
|
717
|
+
const endMoment = fromZonedTime(parseISO(`${selectedDate}T23:59:59.999`), companyTimezone);
|
|
718
|
+
startDateStr = formatInTimeZone(startMoment, 'UTC', "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
|
|
719
|
+
endDateStr = formatInTimeZone(endMoment, 'UTC', "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const result = await getAvailabilities(product.productId, startDateStr, endDateStr, {
|
|
723
|
+
allOptions: true,
|
|
724
|
+
promoCode: appliedPromoCode || undefined,
|
|
725
|
+
...(pricingProfileIdForAvailabilities
|
|
726
|
+
? { pricingProfileId: pricingProfileIdForAvailabilities }
|
|
727
|
+
: {}),
|
|
728
|
+
...(cancellationPolicyProfileIdForAvailabilities
|
|
729
|
+
? { cancellationPolicyProfileId: cancellationPolicyProfileIdForAvailabilities }
|
|
730
|
+
: {}),
|
|
731
|
+
});
|
|
732
|
+
const results = [{
|
|
733
|
+
optionId: product.productId,
|
|
734
|
+
availabilities: result.availabilities.map((availability) => availabilityWithOptionId(availability)),
|
|
735
|
+
precomputedPrices: result.precomputedPrices,
|
|
736
|
+
precomputedPricesByOption: result.precomputedPricesByOption,
|
|
737
|
+
pricingConfig: result.pricingConfig,
|
|
738
|
+
}];
|
|
739
|
+
const detailedAvailabilities = results.flatMap((r) => r.availabilities);
|
|
740
|
+
if (detailedAvailabilities.length === 0) return [];
|
|
741
|
+
|
|
742
|
+
if (result.pricingConfig && !pricingConfigSetRef.current) {
|
|
743
|
+
setPricingConfig(result.pricingConfig);
|
|
744
|
+
pricingConfigSetRef.current = true;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
setPrecomputedPricesByOption((prev) => {
|
|
748
|
+
const next = { ...(prev || {}) };
|
|
749
|
+
results.forEach((r) => mergePrecomputedPricesFromAvailabilityResult(next, r));
|
|
750
|
+
return Object.keys(next).length > 0 ? next : prev;
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
let mergedOut: Availability[] = [];
|
|
754
|
+
setAvailabilities((prev) => {
|
|
755
|
+
const merged = new Map(prev.map((availability) => [`${availability.dateTime}-${availability.productOptionId}`, availability]));
|
|
756
|
+
detailedAvailabilities.forEach((availability) => {
|
|
757
|
+
merged.set(`${availability.dateTime}-${availability.productOptionId}`, availability);
|
|
758
|
+
});
|
|
759
|
+
mergedOut = Array.from(merged.values());
|
|
760
|
+
return mergedOut;
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
const cacheKey = availabilitiesCache
|
|
764
|
+
? buildAvailabilitiesCacheKey(
|
|
765
|
+
product.productId,
|
|
766
|
+
activeOptionIdsKey,
|
|
767
|
+
appliedPromoCode,
|
|
768
|
+
pricingProfileIdForAvailabilities,
|
|
769
|
+
)
|
|
770
|
+
: null;
|
|
771
|
+
if (cacheKey && availabilitiesCache) {
|
|
772
|
+
const existingCache = availabilitiesCache.get(cacheKey);
|
|
773
|
+
const mergedAvailabilities = new Map(
|
|
774
|
+
(existingCache?.availabilities ?? []).map((availability) => [
|
|
775
|
+
`${availability.dateTime}-${availability.productOptionId}`,
|
|
776
|
+
availability,
|
|
777
|
+
])
|
|
778
|
+
);
|
|
779
|
+
detailedAvailabilities.forEach((availability) => {
|
|
780
|
+
mergedAvailabilities.set(`${availability.dateTime}-${availability.productOptionId}`, availability);
|
|
781
|
+
});
|
|
782
|
+
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
783
|
+
results.forEach((r) => mergePrecomputedPricesFromAvailabilityResult(mergedPrecomputed, r));
|
|
784
|
+
availabilitiesCache.merge(cacheKey, {
|
|
785
|
+
fetchedRanges: existingCache?.fetchedRanges ?? fetchedRangesRef.current,
|
|
786
|
+
availabilities: Array.from(mergedAvailabilities.values()),
|
|
787
|
+
pricingConfig: result.pricingConfig ?? existingCache?.pricingConfig ?? null,
|
|
788
|
+
precomputedPricesByOption: Object.keys(mergedPrecomputed).length > 0 ? mergedPrecomputed : null,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return detailedAvailabilities;
|
|
793
|
+
}, [
|
|
794
|
+
selectedDate,
|
|
795
|
+
activeOptions.length,
|
|
796
|
+
isPrivateShuttle,
|
|
797
|
+
companyTimezone,
|
|
798
|
+
product.productId,
|
|
799
|
+
appliedPromoCode,
|
|
800
|
+
pricingProfileIdForAvailabilities,
|
|
801
|
+
cancellationPolicyProfileIdForAvailabilities,
|
|
802
|
+
availabilitiesCache,
|
|
803
|
+
activeOptionIdsKey,
|
|
804
|
+
]);
|
|
805
|
+
|
|
687
806
|
// Initialize visible range when unset (anchor at season open for fast first paint).
|
|
688
807
|
useEffect(() => {
|
|
689
808
|
if (!visibleRange) {
|
|
690
|
-
const initialEnd = addWeeks(EARLIEST_AVAILABILITY_DATE, INITIAL_FETCH_WEEKS);
|
|
809
|
+
const initialEnd = addWeeks(EARLIEST_AVAILABILITY_DATE, INITIAL_FETCH_WEEKS + VISIBLE_RANGE_BUFFER_WEEKS);
|
|
691
810
|
setVisibleRange({ start: EARLIEST_AVAILABILITY_DATE, end: initialEnd });
|
|
692
811
|
}
|
|
693
812
|
}, [visibleRange]);
|
|
@@ -760,6 +879,7 @@ export function NewBookingFlow({
|
|
|
760
879
|
)
|
|
761
880
|
: null;
|
|
762
881
|
const cached = cacheKey ? availabilitiesCache!.get(cacheKey) : undefined;
|
|
882
|
+
let shouldRevalidateCachedRange = false;
|
|
763
883
|
if (cached && cached.availabilities.length > 0) {
|
|
764
884
|
const cachedInitialQuantities = deriveDefaultQuantitiesFromAvailabilities(cached.availabilities);
|
|
765
885
|
if (cachedInitialQuantities) {
|
|
@@ -770,6 +890,7 @@ export function NewBookingFlow({
|
|
|
770
890
|
);
|
|
771
891
|
const isStale = availabilitiesCache?.isStale(cached) ?? false;
|
|
772
892
|
if (cacheCoversRange) {
|
|
893
|
+
shouldRevalidateCachedRange = true;
|
|
773
894
|
setAvailabilities(cached.availabilities);
|
|
774
895
|
if (cached.availabilities.length > 0) {
|
|
775
896
|
hasLoadedAvailabilitiesRef.current = true;
|
|
@@ -785,10 +906,9 @@ export function NewBookingFlow({
|
|
|
785
906
|
setIsFetchingMoreAvailabilities(false);
|
|
786
907
|
if (!isStale) {
|
|
787
908
|
fetchedRangesRef.current = [...cached.fetchedRanges];
|
|
788
|
-
|
|
789
|
-
return;
|
|
909
|
+
// Show cached availability instantly, then quietly revalidate the visible range.
|
|
790
910
|
}
|
|
791
|
-
// Stale-while-revalidate: show cached data, fetch in background
|
|
911
|
+
// Stale-while-revalidate: show cached data, fetch in background.
|
|
792
912
|
}
|
|
793
913
|
// Partial cache: show cached data immediately, then fetch missing range below
|
|
794
914
|
setAvailabilities(cached.availabilities);
|
|
@@ -808,7 +928,7 @@ export function NewBookingFlow({
|
|
|
808
928
|
|
|
809
929
|
// Check if we need to fetch this range
|
|
810
930
|
// Backend always returns CAD prices without fee/tax, so no need to refetch on currency change
|
|
811
|
-
const shouldFetch = needsFetch(clampedStart, clampedEnd);
|
|
931
|
+
const shouldFetch = shouldRevalidateCachedRange || needsFetch(clampedStart, clampedEnd);
|
|
812
932
|
if (!shouldFetch) {
|
|
813
933
|
// Range already fetched - ensure loading state is cleared
|
|
814
934
|
setLoadingAvailabilities(false);
|
|
@@ -826,7 +946,7 @@ export function NewBookingFlow({
|
|
|
826
946
|
end: new Date(clampedEnd),
|
|
827
947
|
};
|
|
828
948
|
if (shouldUsePrimaryLoader) setLoadingAvailabilities(true);
|
|
829
|
-
else setIsFetchingMoreAvailabilities(true);
|
|
949
|
+
else if (!shouldRevalidateCachedRange) setIsFetchingMoreAvailabilities(true);
|
|
830
950
|
|
|
831
951
|
try {
|
|
832
952
|
let startDateStr: string;
|
|
@@ -847,40 +967,33 @@ export function NewBookingFlow({
|
|
|
847
967
|
endDateStr = formatInTimeZone(endMoment, 'UTC', "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
|
|
848
968
|
}
|
|
849
969
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
});
|
|
861
|
-
// Store pricing config from first response only (all responses have same config)
|
|
862
|
-
if (result.pricingConfig && !pricingConfigSetRef.current) {
|
|
863
|
-
setPricingConfig(prev => {
|
|
864
|
-
if (!prev) {
|
|
865
|
-
pricingConfigSetRef.current = true;
|
|
866
|
-
return result.pricingConfig!;
|
|
867
|
-
}
|
|
868
|
-
return prev;
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
// Tag each availability with its productOptionId and carry precomputedPrices for this option
|
|
872
|
-
return {
|
|
873
|
-
optionId: option.optionId,
|
|
874
|
-
availabilities: result.availabilities.map(avail => ({
|
|
875
|
-
...avail,
|
|
876
|
-
productOptionId: option.optionId
|
|
877
|
-
})),
|
|
878
|
-
precomputedPrices: result.precomputedPrices,
|
|
879
|
-
pricingConfig: result.pricingConfig,
|
|
880
|
-
};
|
|
970
|
+
const result = await getAvailabilities(product.productId, startDateStr, endDateStr, {
|
|
971
|
+
allOptions: true,
|
|
972
|
+
summary: true,
|
|
973
|
+
promoCode: appliedPromoCode || undefined,
|
|
974
|
+
...(pricingProfileIdForAvailabilities
|
|
975
|
+
? { pricingProfileId: pricingProfileIdForAvailabilities }
|
|
976
|
+
: {}),
|
|
977
|
+
...(cancellationPolicyProfileIdForAvailabilities
|
|
978
|
+
? { cancellationPolicyProfileId: cancellationPolicyProfileIdForAvailabilities }
|
|
979
|
+
: {}),
|
|
881
980
|
});
|
|
882
|
-
|
|
883
|
-
|
|
981
|
+
if (result.pricingConfig && !pricingConfigSetRef.current) {
|
|
982
|
+
setPricingConfig(prev => {
|
|
983
|
+
if (!prev) {
|
|
984
|
+
pricingConfigSetRef.current = true;
|
|
985
|
+
return result.pricingConfig!;
|
|
986
|
+
}
|
|
987
|
+
return prev;
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
const results = [{
|
|
991
|
+
optionId: product.productId,
|
|
992
|
+
availabilities: result.availabilities.map((avail) => availabilityWithOptionId(avail)),
|
|
993
|
+
precomputedPrices: result.precomputedPrices,
|
|
994
|
+
precomputedPricesByOption: result.precomputedPricesByOption,
|
|
995
|
+
pricingConfig: result.pricingConfig,
|
|
996
|
+
}];
|
|
884
997
|
const allFetchedAvailabilities = results.flatMap(r => r.availabilities);
|
|
885
998
|
if (allFetchedAvailabilities.length > 0) {
|
|
886
999
|
hasLoadedAvailabilitiesRef.current = true;
|
|
@@ -888,9 +1001,7 @@ export function NewBookingFlow({
|
|
|
888
1001
|
setPrecomputedPricesByOption(prev => {
|
|
889
1002
|
const next = { ...(prev || {}) };
|
|
890
1003
|
results.forEach(r => {
|
|
891
|
-
|
|
892
|
-
next[r.optionId] = r.precomputedPrices;
|
|
893
|
-
}
|
|
1004
|
+
mergePrecomputedPricesFromAvailabilityResult(next, r);
|
|
894
1005
|
});
|
|
895
1006
|
return Object.keys(next).length > 0 ? next : prev;
|
|
896
1007
|
});
|
|
@@ -939,9 +1050,7 @@ export function NewBookingFlow({
|
|
|
939
1050
|
});
|
|
940
1051
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
941
1052
|
results.forEach((r) => {
|
|
942
|
-
|
|
943
|
-
mergedPrecomputed[r.optionId] = r.precomputedPrices;
|
|
944
|
-
}
|
|
1053
|
+
mergePrecomputedPricesFromAvailabilityResult(mergedPrecomputed, r);
|
|
945
1054
|
});
|
|
946
1055
|
const firstPricingConfig = (results[0] as { pricingConfig?: PricingConfig } | undefined)?.pricingConfig ?? existingCache?.pricingConfig ?? null;
|
|
947
1056
|
availabilitiesCache.merge(cacheKey, {
|
|
@@ -959,8 +1068,12 @@ export function NewBookingFlow({
|
|
|
959
1068
|
setQuantities((prev) => (Object.keys(prev).length > 0 ? prev : fetchedInitialQuantities));
|
|
960
1069
|
}
|
|
961
1070
|
} catch (err) {
|
|
962
|
-
|
|
963
|
-
|
|
1071
|
+
if (shouldRevalidateCachedRange && cached?.availabilities.length) {
|
|
1072
|
+
console.warn('Background availability refresh failed; keeping cached availability visible.', err);
|
|
1073
|
+
} else {
|
|
1074
|
+
setError(err instanceof Error ? err.message : 'Failed to load availabilities');
|
|
1075
|
+
console.error('Error fetching availabilities:', err);
|
|
1076
|
+
}
|
|
964
1077
|
} finally {
|
|
965
1078
|
setLoadingAvailabilities(false);
|
|
966
1079
|
setIsFetchingMoreAvailabilities(false);
|
|
@@ -1043,6 +1156,129 @@ export function NewBookingFlow({
|
|
|
1043
1156
|
|
|
1044
1157
|
const dates = useMemo(() => Object.keys(availabilitiesByDate).sort(), [availabilitiesByDate]);
|
|
1045
1158
|
|
|
1159
|
+
useEffect(() => {
|
|
1160
|
+
if (!selectedDate || activeOptions.length === 0) return;
|
|
1161
|
+
const selectedDateAvailabilities = availabilitiesByDate[selectedDate] ?? [];
|
|
1162
|
+
if (!selectedDateAvailabilities.some((availability) => availability.isSummary)) return;
|
|
1163
|
+
|
|
1164
|
+
let cancelled = false;
|
|
1165
|
+
async function hydrateSelectedDateDetails() {
|
|
1166
|
+
setIsFetchingMoreAvailabilities(true);
|
|
1167
|
+
try {
|
|
1168
|
+
let startDateStr: string;
|
|
1169
|
+
let endDateStr: string;
|
|
1170
|
+
|
|
1171
|
+
if (isPrivateShuttle) {
|
|
1172
|
+
startDateStr = selectedDate;
|
|
1173
|
+
endDateStr = selectedDate;
|
|
1174
|
+
} else {
|
|
1175
|
+
const startMoment = fromZonedTime(parseISO(`${selectedDate}T00:00:00.000`), companyTimezone);
|
|
1176
|
+
const endMoment = fromZonedTime(parseISO(`${selectedDate}T23:59:59.999`), companyTimezone);
|
|
1177
|
+
startDateStr = formatInTimeZone(startMoment, 'UTC', "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
|
|
1178
|
+
endDateStr = formatInTimeZone(endMoment, 'UTC', "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const result = await getAvailabilities(product.productId, startDateStr, endDateStr, {
|
|
1182
|
+
allOptions: true,
|
|
1183
|
+
promoCode: appliedPromoCode || undefined,
|
|
1184
|
+
...(pricingProfileIdForAvailabilities
|
|
1185
|
+
? { pricingProfileId: pricingProfileIdForAvailabilities }
|
|
1186
|
+
: {}),
|
|
1187
|
+
...(cancellationPolicyProfileIdForAvailabilities
|
|
1188
|
+
? { cancellationPolicyProfileId: cancellationPolicyProfileIdForAvailabilities }
|
|
1189
|
+
: {}),
|
|
1190
|
+
});
|
|
1191
|
+
const results = [{
|
|
1192
|
+
optionId: product.productId,
|
|
1193
|
+
availabilities: result.availabilities.map((availability) => availabilityWithOptionId(availability)),
|
|
1194
|
+
precomputedPrices: result.precomputedPrices,
|
|
1195
|
+
precomputedPricesByOption: result.precomputedPricesByOption,
|
|
1196
|
+
pricingConfig: result.pricingConfig,
|
|
1197
|
+
}];
|
|
1198
|
+
if (cancelled) return;
|
|
1199
|
+
|
|
1200
|
+
const detailedAvailabilities = results.flatMap((result) => result.availabilities);
|
|
1201
|
+
if (detailedAvailabilities.length === 0) return;
|
|
1202
|
+
|
|
1203
|
+
const firstPricingConfig = results.find((result) => result.pricingConfig)?.pricingConfig;
|
|
1204
|
+
if (firstPricingConfig && !pricingConfigSetRef.current) {
|
|
1205
|
+
setPricingConfig(firstPricingConfig);
|
|
1206
|
+
pricingConfigSetRef.current = true;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
setPrecomputedPricesByOption((prev) => {
|
|
1210
|
+
const next = { ...(prev || {}) };
|
|
1211
|
+
results.forEach((result) => {
|
|
1212
|
+
mergePrecomputedPricesFromAvailabilityResult(next, result);
|
|
1213
|
+
});
|
|
1214
|
+
return Object.keys(next).length > 0 ? next : prev;
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
setAvailabilities((prev) => {
|
|
1218
|
+
const merged = new Map(prev.map((availability) => [`${availability.dateTime}-${availability.productOptionId}`, availability]));
|
|
1219
|
+
detailedAvailabilities.forEach((availability) => {
|
|
1220
|
+
merged.set(`${availability.dateTime}-${availability.productOptionId}`, availability);
|
|
1221
|
+
});
|
|
1222
|
+
return Array.from(merged.values());
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
const cacheKey = availabilitiesCache
|
|
1226
|
+
? buildAvailabilitiesCacheKey(
|
|
1227
|
+
product.productId,
|
|
1228
|
+
activeOptionIdsKey,
|
|
1229
|
+
appliedPromoCode,
|
|
1230
|
+
pricingProfileIdForAvailabilities,
|
|
1231
|
+
)
|
|
1232
|
+
: null;
|
|
1233
|
+
if (cacheKey && availabilitiesCache) {
|
|
1234
|
+
const existingCache = availabilitiesCache.get(cacheKey);
|
|
1235
|
+
const mergedAvailabilities = new Map(
|
|
1236
|
+
(existingCache?.availabilities ?? []).map((availability) => [
|
|
1237
|
+
`${availability.dateTime}-${availability.productOptionId}`,
|
|
1238
|
+
availability,
|
|
1239
|
+
])
|
|
1240
|
+
);
|
|
1241
|
+
detailedAvailabilities.forEach((availability) => {
|
|
1242
|
+
mergedAvailabilities.set(`${availability.dateTime}-${availability.productOptionId}`, availability);
|
|
1243
|
+
});
|
|
1244
|
+
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
1245
|
+
results.forEach((result) => {
|
|
1246
|
+
mergePrecomputedPricesFromAvailabilityResult(mergedPrecomputed, result);
|
|
1247
|
+
});
|
|
1248
|
+
availabilitiesCache.merge(cacheKey, {
|
|
1249
|
+
fetchedRanges: existingCache?.fetchedRanges ?? fetchedRangesRef.current,
|
|
1250
|
+
availabilities: Array.from(mergedAvailabilities.values()),
|
|
1251
|
+
pricingConfig: firstPricingConfig ?? existingCache?.pricingConfig ?? null,
|
|
1252
|
+
precomputedPricesByOption: Object.keys(mergedPrecomputed).length > 0 ? mergedPrecomputed : null,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
if (!cancelled) {
|
|
1257
|
+
console.error('Error hydrating availability details:', err);
|
|
1258
|
+
}
|
|
1259
|
+
} finally {
|
|
1260
|
+
if (!cancelled) setIsFetchingMoreAvailabilities(false);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
hydrateSelectedDateDetails();
|
|
1265
|
+
return () => {
|
|
1266
|
+
cancelled = true;
|
|
1267
|
+
};
|
|
1268
|
+
}, [
|
|
1269
|
+
selectedDate,
|
|
1270
|
+
availabilitiesByDate,
|
|
1271
|
+
activeOptions,
|
|
1272
|
+
activeOptionIdsKey,
|
|
1273
|
+
isPrivateShuttle,
|
|
1274
|
+
companyTimezone,
|
|
1275
|
+
appliedPromoCode,
|
|
1276
|
+
pricingProfileIdForAvailabilities,
|
|
1277
|
+
cancellationPolicyProfileIdForAvailabilities,
|
|
1278
|
+
availabilitiesCache,
|
|
1279
|
+
product.productId,
|
|
1280
|
+
]);
|
|
1281
|
+
|
|
1046
1282
|
// Track mobile state
|
|
1047
1283
|
useEffect(() => {
|
|
1048
1284
|
const checkMobile = () => {
|
|
@@ -1930,6 +2166,56 @@ export function NewBookingFlow({
|
|
|
1930
2166
|
? (effectiveSubtotal - effectivePromoDiscountAmount) * (pricingConfig?.taxRate ?? 0)
|
|
1931
2167
|
: taxOnSubtotal;
|
|
1932
2168
|
const totalPrice = effectiveSubtotal + effectiveTax - effectivePromoDiscountAmount;
|
|
2169
|
+
const priceSummaryAugmentation = useMemo(
|
|
2170
|
+
() =>
|
|
2171
|
+
augmentPriceSummary
|
|
2172
|
+
? augmentPriceSummary({
|
|
2173
|
+
selectedDate: selectedDate || null,
|
|
2174
|
+
selectedAvailability,
|
|
2175
|
+
itineraryItems: computeItineraryDisplay(),
|
|
2176
|
+
priceSummaryLines: checkoutPriceSummaryLines,
|
|
2177
|
+
subtotal: effectiveSubtotal,
|
|
2178
|
+
tax: effectivePromoDiscountAmount > 0 ? effectiveTax : tax,
|
|
2179
|
+
total: totalPrice,
|
|
2180
|
+
currency,
|
|
2181
|
+
})
|
|
2182
|
+
: null,
|
|
2183
|
+
[
|
|
2184
|
+
augmentPriceSummary,
|
|
2185
|
+
selectedDate,
|
|
2186
|
+
selectedAvailability,
|
|
2187
|
+
checkoutPriceSummaryLines,
|
|
2188
|
+
effectiveSubtotal,
|
|
2189
|
+
effectivePromoDiscountAmount,
|
|
2190
|
+
effectiveTax,
|
|
2191
|
+
tax,
|
|
2192
|
+
totalPrice,
|
|
2193
|
+
currency,
|
|
2194
|
+
]
|
|
2195
|
+
);
|
|
2196
|
+
const displayCheckoutPriceSummaryLines =
|
|
2197
|
+
priceSummaryAugmentation?.lines ?? checkoutPriceSummaryLines;
|
|
2198
|
+
const displaySubtotal =
|
|
2199
|
+
effectiveSubtotal + (priceSummaryAugmentation?.subtotalAdjustment ?? 0);
|
|
2200
|
+
const displayTotalPrice =
|
|
2201
|
+
totalPrice + (priceSummaryAugmentation?.totalAdjustment ?? 0);
|
|
2202
|
+
const augmentedModalFeeLineItems = useMemo((): OrderSummaryFeeLine[] => {
|
|
2203
|
+
const externalLines =
|
|
2204
|
+
priceSummaryAugmentation?.lines
|
|
2205
|
+
?.slice(checkoutPriceSummaryLines.length)
|
|
2206
|
+
.flatMap((line) =>
|
|
2207
|
+
line.kind === 'line'
|
|
2208
|
+
? [
|
|
2209
|
+
{
|
|
2210
|
+
name: line.label,
|
|
2211
|
+
totalAmount: line.amount,
|
|
2212
|
+
showQuantityLabel: false,
|
|
2213
|
+
},
|
|
2214
|
+
]
|
|
2215
|
+
: []
|
|
2216
|
+
) ?? [];
|
|
2217
|
+
return [...feeLineItemsWithAddOns, ...externalLines];
|
|
2218
|
+
}, [checkoutPriceSummaryLines.length, feeLineItemsWithAddOns, priceSummaryAugmentation?.lines]);
|
|
1933
2219
|
|
|
1934
2220
|
const changeCheckoutButtonLabel = undefined;
|
|
1935
2221
|
|
|
@@ -2352,6 +2638,15 @@ export function NewBookingFlow({
|
|
|
2352
2638
|
setError(t('booking.selectPickupLocation'));
|
|
2353
2639
|
return;
|
|
2354
2640
|
}
|
|
2641
|
+
const externalValidationError = validateBeforeCheckout?.({
|
|
2642
|
+
selectedDate: selectedDate || null,
|
|
2643
|
+
selectedAvailability,
|
|
2644
|
+
itineraryItems: computeItineraryDisplay(),
|
|
2645
|
+
});
|
|
2646
|
+
if (externalValidationError) {
|
|
2647
|
+
setError(externalValidationError);
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
2355
2650
|
|
|
2356
2651
|
setLoading(true);
|
|
2357
2652
|
setError('');
|
|
@@ -2385,6 +2680,31 @@ export function NewBookingFlow({
|
|
|
2385
2680
|
const selectedPickupLocation = pickupLocationId
|
|
2386
2681
|
? product.pickupLocations?.find(loc => loc.id === pickupLocationId)
|
|
2387
2682
|
: null;
|
|
2683
|
+
|
|
2684
|
+
try {
|
|
2685
|
+
const refreshed = await refreshSelectedDateDetailsForCheckout();
|
|
2686
|
+
const outbound = findMergedAvailabilityForSelection(refreshed, selectedAvailability);
|
|
2687
|
+
const outboundVacancies = outbound?.vacancies ?? null;
|
|
2688
|
+
const returnVacancies = findMergedReturnVacancies(outbound, selectedReturnOption);
|
|
2689
|
+
if (
|
|
2690
|
+
(outboundVacancies != null && outboundVacancies < totalQuantity) ||
|
|
2691
|
+
(selectedReturnOption && returnVacancies != null && returnVacancies < totalQuantity)
|
|
2692
|
+
) {
|
|
2693
|
+
setError(
|
|
2694
|
+
describeStandardTourCapacityConflictMessage({
|
|
2695
|
+
partySize: totalQuantity,
|
|
2696
|
+
outboundVacancies,
|
|
2697
|
+
returnVacancies,
|
|
2698
|
+
hasReturnSelection: !!selectedReturnOption,
|
|
2699
|
+
})
|
|
2700
|
+
);
|
|
2701
|
+
setLoading(false);
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
} catch (refreshErr) {
|
|
2705
|
+
console.warn('Fresh availability check failed before reserve; falling back to reserve validation.', refreshErr);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2388
2708
|
const reservation = await createReservation({
|
|
2389
2709
|
productId: availabilityProductOptionId, // GetYourGuide passes productOptionId values in productId field
|
|
2390
2710
|
dateTime: selectedAvailability.dateTime,
|
|
@@ -2448,7 +2768,22 @@ export function NewBookingFlow({
|
|
|
2448
2768
|
// Build checkout breakdown from the exact same values we show in the UI and Stripe modal.
|
|
2449
2769
|
// Backend will charge totalAmount and store this as the receipt so /manage matches.
|
|
2450
2770
|
const taxForBreakdown = effectivePromoDiscountAmount > 0 ? effectiveTax : tax;
|
|
2451
|
-
const amountDueForCheckout =
|
|
2771
|
+
const amountDueForCheckout = displayTotalPrice;
|
|
2772
|
+
const augmentedCheckoutLines =
|
|
2773
|
+
priceSummaryAugmentation?.lines
|
|
2774
|
+
?.slice(checkoutPriceSummaryLines.length)
|
|
2775
|
+
.flatMap((line) =>
|
|
2776
|
+
line.kind === 'line'
|
|
2777
|
+
? [
|
|
2778
|
+
{
|
|
2779
|
+
label: line.label,
|
|
2780
|
+
amount: line.amount,
|
|
2781
|
+
type: line.type ?? 'FEE',
|
|
2782
|
+
quantity: line.quantity ?? undefined,
|
|
2783
|
+
},
|
|
2784
|
+
]
|
|
2785
|
+
: []
|
|
2786
|
+
) ?? [];
|
|
2452
2787
|
const lines = [
|
|
2453
2788
|
...ticketLineItems.map((line) => ({
|
|
2454
2789
|
label: line.category,
|
|
@@ -2493,6 +2828,7 @@ export function NewBookingFlow({
|
|
|
2493
2828
|
};
|
|
2494
2829
|
})
|
|
2495
2830
|
.filter((x): x is NonNullable<typeof x> => x != null),
|
|
2831
|
+
...augmentedCheckoutLines,
|
|
2496
2832
|
...feeLineItems.map((fee) => ({
|
|
2497
2833
|
label: `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? (t('booking.person') || 'person') : (t('booking.people') || 'people')})`,
|
|
2498
2834
|
amount: fee.totalAmount,
|
|
@@ -2546,6 +2882,7 @@ export function NewBookingFlow({
|
|
|
2546
2882
|
skipConfirmationCommunications: isAdmin && skipConfirmationCommunications ? true : undefined,
|
|
2547
2883
|
disableAutoCommunications: isAdmin && disableAutoCommunications ? true : undefined,
|
|
2548
2884
|
checkoutBreakdown,
|
|
2885
|
+
dependentAddOnSelection: dependentAddOnSelection || undefined,
|
|
2549
2886
|
depositAmount: 0,
|
|
2550
2887
|
balanceAmount: amountDueForCheckout,
|
|
2551
2888
|
totalAmount: amountDueForCheckout,
|
|
@@ -2587,6 +2924,7 @@ export function NewBookingFlow({
|
|
|
2587
2924
|
cancellationPolicyId: cancellationPolicyId || undefined,
|
|
2588
2925
|
termsAcceptedAt: termsAcceptedAt ?? undefined,
|
|
2589
2926
|
checkoutBreakdown,
|
|
2927
|
+
dependentAddOnSelection: dependentAddOnSelection || undefined,
|
|
2590
2928
|
skipConfirmationCommunications: isAdmin && skipConfirmationCommunications ? true : undefined,
|
|
2591
2929
|
disableAutoCommunications: isAdmin && disableAutoCommunications ? true : undefined,
|
|
2592
2930
|
...bookingSourceContext,
|
|
@@ -2614,6 +2952,7 @@ export function NewBookingFlow({
|
|
|
2614
2952
|
termsAcceptedAt: termsAcceptedAt ?? undefined,
|
|
2615
2953
|
skipConfirmationCommunications: isAdmin && skipConfirmationCommunications ? true : undefined,
|
|
2616
2954
|
disableAutoCommunications: isAdmin && disableAutoCommunications ? true : undefined,
|
|
2955
|
+
dependentAddOnSelection: dependentAddOnSelection || undefined,
|
|
2617
2956
|
...bookingSourceContext,
|
|
2618
2957
|
});
|
|
2619
2958
|
pendingReservationRef.current = null;
|
|
@@ -2658,11 +2997,11 @@ export function NewBookingFlow({
|
|
|
2658
2997
|
);
|
|
2659
2998
|
return { line, breakdown };
|
|
2660
2999
|
}),
|
|
2661
|
-
feeLineItems:
|
|
3000
|
+
feeLineItems: augmentedModalFeeLineItems,
|
|
2662
3001
|
returnPriceAdjustment: checkoutReturnLineAmount,
|
|
2663
3002
|
cancellationPolicyFee,
|
|
2664
3003
|
cancellationPolicyLabel: effectiveCancellationPolicyLabel,
|
|
2665
|
-
subtotal:
|
|
3004
|
+
subtotal: displaySubtotal,
|
|
2666
3005
|
tax: effectivePromoDiscountAmount > 0 ? effectiveTax : tax,
|
|
2667
3006
|
totalQuantity,
|
|
2668
3007
|
isTaxIncludedInPrice,
|
|
@@ -2694,11 +3033,11 @@ export function NewBookingFlow({
|
|
|
2694
3033
|
bookingDate: datePart,
|
|
2695
3034
|
successUrlOverride: undefined,
|
|
2696
3035
|
ticketLines: ticketLinesForModal,
|
|
2697
|
-
feeLineItems:
|
|
3036
|
+
feeLineItems: augmentedModalFeeLineItems,
|
|
2698
3037
|
returnPriceAdjustment: checkoutReturnLineAmount,
|
|
2699
3038
|
cancellationPolicyFee,
|
|
2700
3039
|
cancellationPolicyLabel: effectiveCancellationPolicyLabel,
|
|
2701
|
-
subtotal:
|
|
3040
|
+
subtotal: displaySubtotal,
|
|
2702
3041
|
tax: effectivePromoDiscountAmount > 0 ? effectiveTax : tax,
|
|
2703
3042
|
total: amountDueForCheckout,
|
|
2704
3043
|
totalQuantity,
|
|
@@ -2959,48 +3298,50 @@ export function NewBookingFlow({
|
|
|
2959
3298
|
</div>
|
|
2960
3299
|
) : (
|
|
2961
3300
|
<>
|
|
2962
|
-
{
|
|
2963
|
-
|
|
2964
|
-
<div
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
<div className="
|
|
2968
|
-
<div className="
|
|
2969
|
-
|
|
3301
|
+
{flowUi?.showCalendar !== false && (
|
|
3302
|
+
/* Date Selection */
|
|
3303
|
+
<div>
|
|
3304
|
+
<div className="relative">
|
|
3305
|
+
{loadingAvailabilities && (
|
|
3306
|
+
<div className="absolute inset-0 bg-white/80 backdrop-blur-sm flex items-center justify-center z-10 rounded-lg">
|
|
3307
|
+
<div className="flex flex-col items-center gap-3">
|
|
3308
|
+
<div className="booking-loading-spinner" aria-hidden />
|
|
3309
|
+
<div className="text-stone-600">{t('booking.loadingTimes')}</div>
|
|
3310
|
+
</div>
|
|
2970
3311
|
</div>
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
}
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3312
|
+
)}
|
|
3313
|
+
<Calendar
|
|
3314
|
+
availabilitiesByDate={availabilitiesByDate}
|
|
3315
|
+
selectedDate={selectedDate}
|
|
3316
|
+
syncVisibleWeekToSelectedDate={false}
|
|
3317
|
+
selectableSoldOutDate={null}
|
|
3318
|
+
isLoading={loadingAvailabilities || isFetchingMoreAvailabilities}
|
|
3319
|
+
onDateSelect={(date) => {
|
|
3320
|
+
setSelectedDate(date);
|
|
3321
|
+
handleDateSelect(date);
|
|
3322
|
+
if (suppressCalendarDateScroll) return;
|
|
3323
|
+
// Scroll so calendar is almost at top and user sees the rest of the booking flow.
|
|
3324
|
+
// Dialog: scroll inside contentRef. Full-page: fall back to window scroll.
|
|
3325
|
+
setTimeout(() => {
|
|
3326
|
+
const container = contentRef?.current;
|
|
3327
|
+
if (!useWindowScroll && container && container.scrollHeight > container.clientHeight + 16) {
|
|
3328
|
+
container.scrollBy({ top: 400, behavior: 'smooth' });
|
|
3329
|
+
} else if (typeof window !== 'undefined') {
|
|
3330
|
+
window.scrollBy({ top: 400, behavior: 'smooth' });
|
|
3331
|
+
}
|
|
3332
|
+
}, 100);
|
|
3333
|
+
}}
|
|
3334
|
+
timezone={companyTimezone}
|
|
3335
|
+
earliestDate={earliestAvailabilityDate}
|
|
3336
|
+
onVisibleRangeChange={handleVisibleRangeChange}
|
|
3337
|
+
currency={currency}
|
|
3338
|
+
showCapacity={isAdmin}
|
|
3339
|
+
extraDiscountPercent={calendarDiscountPercent}
|
|
3340
|
+
capDiscountBadgesToBookingDate={null}
|
|
3341
|
+
/>
|
|
3342
|
+
</div>
|
|
3002
3343
|
</div>
|
|
3003
|
-
|
|
3344
|
+
)}
|
|
3004
3345
|
|
|
3005
3346
|
{/* Form sections - equal spacing between each */}
|
|
3006
3347
|
<div className="mt-6 space-y-6">
|
|
@@ -3012,7 +3353,14 @@ export function NewBookingFlow({
|
|
|
3012
3353
|
if (!selectedAvailability) {
|
|
3013
3354
|
return <ItineraryPlaceholder formattedDate={formattedDate} t={t} />;
|
|
3014
3355
|
}
|
|
3015
|
-
const
|
|
3356
|
+
const baseItineraryItems = computeItineraryDisplay();
|
|
3357
|
+
const itineraryItems = augmentItineraryItems
|
|
3358
|
+
? augmentItineraryItems({
|
|
3359
|
+
selectedDate,
|
|
3360
|
+
selectedAvailability,
|
|
3361
|
+
itineraryItems: baseItineraryItems,
|
|
3362
|
+
})
|
|
3363
|
+
: baseItineraryItems;
|
|
3016
3364
|
if (!itineraryItems || itineraryItems.length === 0) return null;
|
|
3017
3365
|
const isBookingComplete = Boolean(selectedAvailability &&
|
|
3018
3366
|
selectedReturnOption &&
|
|
@@ -3079,6 +3427,16 @@ export function NewBookingFlow({
|
|
|
3079
3427
|
/>
|
|
3080
3428
|
)}
|
|
3081
3429
|
|
|
3430
|
+
{selectedDate && selectedAvailability && afterItinerary
|
|
3431
|
+
? typeof afterItinerary === 'function'
|
|
3432
|
+
? afterItinerary({
|
|
3433
|
+
selectedDate,
|
|
3434
|
+
selectedAvailability,
|
|
3435
|
+
itineraryItems: computeItineraryDisplay(),
|
|
3436
|
+
})
|
|
3437
|
+
: afterItinerary
|
|
3438
|
+
: null}
|
|
3439
|
+
|
|
3082
3440
|
{/* Cancellation policy selection - all options from config, sorted by cheapest first. Also show when forced by promo. */}
|
|
3083
3441
|
{selectedAvailability && ((pricingConfig?.cancellationPolicies?.length ?? 0) > 0 || forcedCancellationPolicy) && (() => {
|
|
3084
3442
|
const sortedPolicies = [...(pricingConfig?.cancellationPolicies ?? [])].sort((a, b) => {
|
|
@@ -3141,12 +3499,15 @@ export function NewBookingFlow({
|
|
|
3141
3499
|
{selectedAvailability && (
|
|
3142
3500
|
<>
|
|
3143
3501
|
<CheckoutForm
|
|
3144
|
-
priceSummaryLines={
|
|
3145
|
-
totalPrice={
|
|
3502
|
+
priceSummaryLines={displayCheckoutPriceSummaryLines}
|
|
3503
|
+
totalPrice={displayTotalPrice}
|
|
3146
3504
|
totalSummaryLabel={undefined}
|
|
3147
3505
|
subtotal={
|
|
3148
|
-
subtotal !== totalFromSummary ||
|
|
3149
|
-
|
|
3506
|
+
subtotal !== totalFromSummary ||
|
|
3507
|
+
effectivePromoDiscountAmount > 0 ||
|
|
3508
|
+
addOnTotal > 0 ||
|
|
3509
|
+
(priceSummaryAugmentation?.subtotalAdjustment ?? 0) !== 0
|
|
3510
|
+
? displaySubtotal
|
|
3150
3511
|
: undefined
|
|
3151
3512
|
}
|
|
3152
3513
|
taxAmount={
|
|
@@ -3253,4 +3614,3 @@ export function NewBookingFlow({
|
|
|
3253
3614
|
</div>
|
|
3254
3615
|
);
|
|
3255
3616
|
}
|
|
3256
|
-
|