@ticketboothapp/booking 1.2.104 → 1.2.106
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 +1 -1
- package/src/components/booking/AdminChangeBookingFlow.tsx +65 -44
- package/src/components/booking/ChangeBookingFlow.tsx +62 -40
- package/src/components/booking/NewBookingFlow.tsx +52 -33
- package/src/components/booking/PickupLocationSelector.tsx +47 -18
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +78 -38
- package/src/lib/booking/pricing.ts +5 -1
package/package.json
CHANGED
|
@@ -333,18 +333,30 @@ function findMergedAvailabilityForSelection(
|
|
|
333
333
|
selected: Availability | null
|
|
334
334
|
): Availability | undefined {
|
|
335
335
|
if (!selected) return undefined;
|
|
336
|
-
const optId = selected
|
|
336
|
+
const optId = getAvailabilityOptionId(selected) || undefined;
|
|
337
337
|
const dt = selected.dateTime;
|
|
338
338
|
const availId = selected.availabilityId?.trim();
|
|
339
339
|
if (availId && optId) {
|
|
340
340
|
const exact = merged.find(
|
|
341
341
|
(a) =>
|
|
342
342
|
a.availabilityId === availId &&
|
|
343
|
-
(a
|
|
343
|
+
getAvailabilityOptionId(a) === optId
|
|
344
344
|
);
|
|
345
345
|
if (exact) return exact;
|
|
346
346
|
}
|
|
347
|
-
return merged.find((a) => a.dateTime === dt && a
|
|
347
|
+
return merged.find((a) => a.dateTime === dt && getAvailabilityOptionId(a) === optId);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function getAvailabilityOptionId(availability: Availability, fallbackOptionId?: string): string {
|
|
351
|
+
const productOptionId = availability.productOptionId?.trim();
|
|
352
|
+
if (productOptionId) return productOptionId;
|
|
353
|
+
const productId = availability.productId?.trim();
|
|
354
|
+
if (productId?.startsWith('po_')) return productId;
|
|
355
|
+
return fallbackOptionId?.trim() ?? '';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function getAvailabilityCacheKey(availability: Availability): string {
|
|
359
|
+
return `${availability.dateTime}-${getAvailabilityOptionId(availability)}`;
|
|
348
360
|
}
|
|
349
361
|
|
|
350
362
|
/** Ticket rates for one availability row — mirrors main cart [pricing] useMemo so baseline slots match BE. */
|
|
@@ -358,7 +370,7 @@ function buildPricingFromAvailability(
|
|
|
358
370
|
isSimplifiedPricingView: boolean,
|
|
359
371
|
) {
|
|
360
372
|
if (!selectedAvailability || !pricingConfig) return [];
|
|
361
|
-
const optionId = selectedAvailability
|
|
373
|
+
const optionId = getAvailabilityOptionId(selectedAvailability);
|
|
362
374
|
const selectedOption = activeOptions.find((opt) => opt.optionId === optionId);
|
|
363
375
|
const precomputed = optionId ? precomputedPricesByOption?.[optionId] : undefined;
|
|
364
376
|
const rateToDisplayPrice = (backendInDisplayCurrency: number) =>
|
|
@@ -376,11 +388,17 @@ function buildPricingFromAvailability(
|
|
|
376
388
|
appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }>,
|
|
377
389
|
) => {
|
|
378
390
|
const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
|
|
391
|
+
const hasOptionScopedPrice = baseInDisplayCurrency > 0;
|
|
392
|
+
const priceCADForBreakdown =
|
|
393
|
+
hasOptionScopedPrice && currency === 'CAD' ? baseInDisplayCurrency : backendPriceCAD;
|
|
394
|
+
const displayCurrencyPriceForFallback = hasOptionScopedPrice
|
|
395
|
+
? baseInDisplayCurrency
|
|
396
|
+
: backendInDisplayCurrency;
|
|
379
397
|
const isPublicMode = isSimplifiedPricingView;
|
|
380
398
|
const breakdown = computePriceBreakdown(
|
|
381
399
|
pricingConfig,
|
|
382
400
|
currency,
|
|
383
|
-
|
|
401
|
+
priceCADForBreakdown,
|
|
384
402
|
basePriceCAD,
|
|
385
403
|
hasFees,
|
|
386
404
|
appliedAdjustments,
|
|
@@ -388,8 +406,8 @@ function buildPricingFromAvailability(
|
|
|
388
406
|
baseInDisplayCurrency,
|
|
389
407
|
isPublicMode,
|
|
390
408
|
);
|
|
391
|
-
const price = breakdown?.finalPrice ?? rateToDisplayPrice(
|
|
392
|
-
return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD:
|
|
409
|
+
const price = breakdown?.finalPrice ?? rateToDisplayPrice(displayCurrencyPriceForFallback);
|
|
410
|
+
return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD: priceCADForBreakdown };
|
|
393
411
|
};
|
|
394
412
|
return (
|
|
395
413
|
selectedAvailability.rates?.map((rate) => {
|
|
@@ -1281,10 +1299,10 @@ export function AdminChangeBookingFlow({
|
|
|
1281
1299
|
let mergedOut: Availability[] = [];
|
|
1282
1300
|
setAvailabilities((prev) => {
|
|
1283
1301
|
const existingMap = new Map(
|
|
1284
|
-
prev.map((avail) => [
|
|
1302
|
+
prev.map((avail) => [getAvailabilityCacheKey(avail), avail])
|
|
1285
1303
|
);
|
|
1286
1304
|
allFetchedAvailabilities.forEach((avail) => {
|
|
1287
|
-
existingMap.set(
|
|
1305
|
+
existingMap.set(getAvailabilityCacheKey(avail), avail);
|
|
1288
1306
|
});
|
|
1289
1307
|
mergedOut = Array.from(existingMap.values());
|
|
1290
1308
|
return mergedOut;
|
|
@@ -1318,10 +1336,10 @@ export function AdminChangeBookingFlow({
|
|
|
1318
1336
|
const existingCache = availabilitiesCache.get(cacheKey);
|
|
1319
1337
|
const existingAvailabilities = existingCache?.availabilities ?? [];
|
|
1320
1338
|
const mergedAvailabilitiesMap = new Map(
|
|
1321
|
-
existingAvailabilities.map((a) => [
|
|
1339
|
+
existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
|
|
1322
1340
|
);
|
|
1323
1341
|
allFetchedAvailabilities.forEach((a) => {
|
|
1324
|
-
mergedAvailabilitiesMap.set(
|
|
1342
|
+
mergedAvailabilitiesMap.set(getAvailabilityCacheKey(a), a);
|
|
1325
1343
|
});
|
|
1326
1344
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
1327
1345
|
results.forEach((r) => {
|
|
@@ -1598,12 +1616,12 @@ export function AdminChangeBookingFlow({
|
|
|
1598
1616
|
// Merge with existing availabilities (avoid duplicates by dateTime + productOptionId)
|
|
1599
1617
|
setAvailabilities(prev => {
|
|
1600
1618
|
const existingMap = new Map(
|
|
1601
|
-
prev.map(avail => [
|
|
1619
|
+
prev.map(avail => [getAvailabilityCacheKey(avail), avail])
|
|
1602
1620
|
);
|
|
1603
1621
|
|
|
1604
1622
|
// Merge new availabilities - update existing ones or add new ones
|
|
1605
1623
|
allFetchedAvailabilities.forEach(avail => {
|
|
1606
|
-
const key =
|
|
1624
|
+
const key = getAvailabilityCacheKey(avail);
|
|
1607
1625
|
// Always update to get latest data (vacancies, prices, etc.)
|
|
1608
1626
|
existingMap.set(key, avail);
|
|
1609
1627
|
});
|
|
@@ -1632,10 +1650,10 @@ export function AdminChangeBookingFlow({
|
|
|
1632
1650
|
const existingCache = availabilitiesCache.get(cacheKey);
|
|
1633
1651
|
const existingAvailabilities = existingCache?.availabilities ?? [];
|
|
1634
1652
|
const mergedAvailabilitiesMap = new Map(
|
|
1635
|
-
existingAvailabilities.map((a) => [
|
|
1653
|
+
existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
|
|
1636
1654
|
);
|
|
1637
1655
|
allFetchedAvailabilities.forEach((a) => {
|
|
1638
|
-
mergedAvailabilitiesMap.set(
|
|
1656
|
+
mergedAvailabilitiesMap.set(getAvailabilityCacheKey(a), a);
|
|
1639
1657
|
});
|
|
1640
1658
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
1641
1659
|
results.forEach((r) => {
|
|
@@ -1881,7 +1899,7 @@ export function AdminChangeBookingFlow({
|
|
|
1881
1899
|
timesForSelectedDate
|
|
1882
1900
|
.map(
|
|
1883
1901
|
(avail) =>
|
|
1884
|
-
`${avail.dateTime}|${avail
|
|
1902
|
+
`${avail.dateTime}|${getAvailabilityOptionId(avail)}|${avail.vacancies ?? 0}`,
|
|
1885
1903
|
)
|
|
1886
1904
|
.join('||'),
|
|
1887
1905
|
[timesForSelectedDate],
|
|
@@ -2315,7 +2333,9 @@ export function AdminChangeBookingFlow({
|
|
|
2315
2333
|
const calculateStaySummary = useCallback((returnDateTime: Date): string | null => {
|
|
2316
2334
|
if (!selectedAvailability) return null;
|
|
2317
2335
|
|
|
2318
|
-
const availabilityProductOptionId =
|
|
2336
|
+
const availabilityProductOptionId =
|
|
2337
|
+
getAvailabilityOptionId(selectedAvailability) ||
|
|
2338
|
+
(activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
|
|
2319
2339
|
const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
|
|
2320
2340
|
if (!selectedOption) return null;
|
|
2321
2341
|
|
|
@@ -2391,7 +2411,9 @@ export function AdminChangeBookingFlow({
|
|
|
2391
2411
|
// Helper function to compute itinerary display for storage (returns same shape as "Your Itinerary" box)
|
|
2392
2412
|
const computeItineraryDisplay = useCallback((): ItineraryDisplayStep[] | null => {
|
|
2393
2413
|
if (!selectedAvailability) return null;
|
|
2394
|
-
const availabilityProductOptionId =
|
|
2414
|
+
const availabilityProductOptionId =
|
|
2415
|
+
getAvailabilityOptionId(selectedAvailability) ||
|
|
2416
|
+
(activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
|
|
2395
2417
|
const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
|
|
2396
2418
|
if (!selectedOption) return null;
|
|
2397
2419
|
const tourStartTime = parseISO(selectedAvailability.dateTime);
|
|
@@ -2630,13 +2652,13 @@ export function AdminChangeBookingFlow({
|
|
|
2630
2652
|
const selectedMs = parseAvailabilityDateTime(selectedAvailability.dateTime).getTime();
|
|
2631
2653
|
const initialMs = parseAvailabilityDateTime(initialValues.dateTime).getTime();
|
|
2632
2654
|
if (selectedMs !== initialMs) return false;
|
|
2633
|
-
const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability
|
|
2655
|
+
const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
|
|
2634
2656
|
const initialOpt = changeFlowResolvedInitialProductOptionId;
|
|
2635
2657
|
if (selectedOpt != null && initialOpt != null) {
|
|
2636
2658
|
return selectedOpt === initialOpt;
|
|
2637
2659
|
}
|
|
2638
2660
|
const legacyInitial = initialValues.productOptionId?.trim() || null;
|
|
2639
|
-
const legacySelected = selectedAvailability
|
|
2661
|
+
const legacySelected = getAvailabilityOptionId(selectedAvailability) || null;
|
|
2640
2662
|
return !legacySelected || !legacyInitial || legacySelected === legacyInitial;
|
|
2641
2663
|
}, [
|
|
2642
2664
|
selectedAvailability,
|
|
@@ -2663,13 +2685,13 @@ export function AdminChangeBookingFlow({
|
|
|
2663
2685
|
return false;
|
|
2664
2686
|
}
|
|
2665
2687
|
if (!selectedDate || selectedDate !== initialDay) return false;
|
|
2666
|
-
const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability
|
|
2688
|
+
const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
|
|
2667
2689
|
const initialOpt = changeFlowResolvedInitialProductOptionId;
|
|
2668
2690
|
if (selectedOpt != null && initialOpt != null) {
|
|
2669
2691
|
return selectedOpt === initialOpt;
|
|
2670
2692
|
}
|
|
2671
2693
|
const legacyInitial = initialValues.productOptionId?.trim() || null;
|
|
2672
|
-
const legacySelected = selectedAvailability
|
|
2694
|
+
const legacySelected = getAvailabilityOptionId(selectedAvailability) || null;
|
|
2673
2695
|
return Boolean(legacySelected && legacyInitial && legacySelected === legacyInitial);
|
|
2674
2696
|
}, [
|
|
2675
2697
|
selectedDate,
|
|
@@ -2866,7 +2888,9 @@ export function AdminChangeBookingFlow({
|
|
|
2866
2888
|
// Price breakdown: mid-layer returns line items (base + one per rule/deal). UI renders each line; rate in brackets when used.
|
|
2867
2889
|
const getPriceBreakdown = useCallback((category: string, priceCAD: number, baseInDisplayCurrency: number | undefined, appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }> = []): PriceBreakdownData | null => {
|
|
2868
2890
|
if (!pricingConfig) return null;
|
|
2869
|
-
const selectedOption = activeOptions.find(
|
|
2891
|
+
const selectedOption = activeOptions.find(
|
|
2892
|
+
opt => opt.optionId === (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '')
|
|
2893
|
+
);
|
|
2870
2894
|
const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
|
|
2871
2895
|
const isPublicMode = isSimplifiedPricingView;
|
|
2872
2896
|
return computePriceBreakdown(
|
|
@@ -3342,7 +3366,7 @@ export function AdminChangeBookingFlow({
|
|
|
3342
3366
|
if (!appliedPromoCode || !selectedAvailability || totalQuantity === 0) return '';
|
|
3343
3367
|
const companyId = product.companyId ?? env.COMPANY_ID;
|
|
3344
3368
|
if (!companyId) return '';
|
|
3345
|
-
const optionId = selectedAvailability
|
|
3369
|
+
const optionId = getAvailabilityOptionId(selectedAvailability);
|
|
3346
3370
|
if (!optionId || !quantitiesSignature) return '';
|
|
3347
3371
|
return [
|
|
3348
3372
|
appliedPromoCode,
|
|
@@ -3359,7 +3383,7 @@ export function AdminChangeBookingFlow({
|
|
|
3359
3383
|
}, [
|
|
3360
3384
|
appliedPromoCode,
|
|
3361
3385
|
selectedAvailability?.dateTime,
|
|
3362
|
-
selectedAvailability
|
|
3386
|
+
selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
|
|
3363
3387
|
selectedAvailability?.availabilityId,
|
|
3364
3388
|
totalQuantity,
|
|
3365
3389
|
product.companyId,
|
|
@@ -3402,7 +3426,7 @@ export function AdminChangeBookingFlow({
|
|
|
3402
3426
|
} = promoDiscountParamsRef.current;
|
|
3403
3427
|
if (!code || !sel) return;
|
|
3404
3428
|
const companyId = product.companyId ?? env.COMPANY_ID;
|
|
3405
|
-
const optionId = sel
|
|
3429
|
+
const optionId = getAvailabilityOptionId(sel);
|
|
3406
3430
|
if (!companyId || !optionId) return;
|
|
3407
3431
|
const items = lines.map((l) => ({ category: l.category, qty: l.qty }));
|
|
3408
3432
|
if (items.length === 0) return;
|
|
@@ -3802,7 +3826,7 @@ export function AdminChangeBookingFlow({
|
|
|
3802
3826
|
const initialOpt =
|
|
3803
3827
|
changeFlowResolvedInitialProductOptionId ??
|
|
3804
3828
|
(initialValues.productOptionId?.trim() || null);
|
|
3805
|
-
const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability
|
|
3829
|
+
const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
|
|
3806
3830
|
const optionChanged = Boolean(
|
|
3807
3831
|
selectedOpt != null && initialOpt != null && initialOpt !== selectedOpt
|
|
3808
3832
|
);
|
|
@@ -3933,7 +3957,7 @@ export function AdminChangeBookingFlow({
|
|
|
3933
3957
|
bookingReference: initialValues?.bookingReference?.trim() ?? '',
|
|
3934
3958
|
lastName: lastName.trim().toLowerCase(),
|
|
3935
3959
|
parentProductId: product.productId,
|
|
3936
|
-
optionId: selectedAvailability
|
|
3960
|
+
optionId: selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
|
|
3937
3961
|
dateTime: selectedAvailability?.dateTime ?? '',
|
|
3938
3962
|
availabilityId: selectedAvailability?.availabilityId ?? null,
|
|
3939
3963
|
pickupLocationId: pickupLocationId ?? null,
|
|
@@ -3948,7 +3972,7 @@ export function AdminChangeBookingFlow({
|
|
|
3948
3972
|
initialValues?.bookingReference,
|
|
3949
3973
|
lastName,
|
|
3950
3974
|
product.productId,
|
|
3951
|
-
selectedAvailability
|
|
3975
|
+
selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
|
|
3952
3976
|
selectedAvailability?.dateTime,
|
|
3953
3977
|
selectedAvailability?.availabilityId,
|
|
3954
3978
|
activeOptions,
|
|
@@ -4506,8 +4530,7 @@ export function AdminChangeBookingFlow({
|
|
|
4506
4530
|
return;
|
|
4507
4531
|
}
|
|
4508
4532
|
|
|
4509
|
-
const optionId =
|
|
4510
|
-
selectedAvailability.productOptionId?.trim() || activeOptions[0]?.optionId;
|
|
4533
|
+
const optionId = getAvailabilityOptionId(selectedAvailability);
|
|
4511
4534
|
if (!optionId) {
|
|
4512
4535
|
setLatestChangeQuote(null);
|
|
4513
4536
|
setChangeQuoteLoading(false);
|
|
@@ -4605,7 +4628,7 @@ export function AdminChangeBookingFlow({
|
|
|
4605
4628
|
hasChangeSelection,
|
|
4606
4629
|
selectedAvailability,
|
|
4607
4630
|
selectedAvailability?.dateTime,
|
|
4608
|
-
selectedAvailability
|
|
4631
|
+
selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
|
|
4609
4632
|
initialValues?.bookingReference,
|
|
4610
4633
|
lastName,
|
|
4611
4634
|
missingRequiredReturnSelection,
|
|
@@ -4671,7 +4694,7 @@ export function AdminChangeBookingFlow({
|
|
|
4671
4694
|
|
|
4672
4695
|
const mostPopularOption = activeOptions.find(opt => opt.mostPopular);
|
|
4673
4696
|
const candidate = mostPopularOption
|
|
4674
|
-
? timesForSelectedDate.find(avail => avail
|
|
4697
|
+
? timesForSelectedDate.find(avail => getAvailabilityOptionId(avail) === mostPopularOption.optionId && avail.vacancies > 0)
|
|
4675
4698
|
: null;
|
|
4676
4699
|
const fallback = timesForSelectedDate.find(avail => avail.vacancies > 0);
|
|
4677
4700
|
const toSelect = candidate ?? fallback;
|
|
@@ -4697,9 +4720,9 @@ export function AdminChangeBookingFlow({
|
|
|
4697
4720
|
useEffect(() => {
|
|
4698
4721
|
if (selectedAvailability && availabilities.length > 0) {
|
|
4699
4722
|
const updatedAvailability = availabilities.find(
|
|
4700
|
-
|
|
4723
|
+
avail =>
|
|
4701
4724
|
avail.dateTime === selectedAvailability.dateTime &&
|
|
4702
|
-
avail
|
|
4725
|
+
getAvailabilityOptionId(avail) === getAvailabilityOptionId(selectedAvailability)
|
|
4703
4726
|
);
|
|
4704
4727
|
if (updatedAvailability) {
|
|
4705
4728
|
setSelectedAvailability(updatedAvailability);
|
|
@@ -4730,7 +4753,7 @@ export function AdminChangeBookingFlow({
|
|
|
4730
4753
|
setImplicitReturnBaselineId(null);
|
|
4731
4754
|
}, [
|
|
4732
4755
|
selectedAvailability?.dateTime,
|
|
4733
|
-
selectedAvailability
|
|
4756
|
+
selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
|
|
4734
4757
|
initialValues?.returnAvailabilityId,
|
|
4735
4758
|
initialValues?.returnDateTime,
|
|
4736
4759
|
]);
|
|
@@ -4818,7 +4841,7 @@ export function AdminChangeBookingFlow({
|
|
|
4818
4841
|
}, [selectedAvailability?.returnOptions, selectedReturnOption]);
|
|
4819
4842
|
|
|
4820
4843
|
// Fetch add-ons when availability (product option) is selected; clear selections when option changes
|
|
4821
|
-
const availabilityProductOptionId = selectedAvailability
|
|
4844
|
+
const availabilityProductOptionId = selectedAvailability ? getAvailabilityOptionId(selectedAvailability) || null : null;
|
|
4822
4845
|
const prevAvailabilityProductOptionIdRef = useRef<string | null>(null);
|
|
4823
4846
|
useEffect(() => {
|
|
4824
4847
|
if (!availabilityProductOptionId || !product.companyId) {
|
|
@@ -4838,7 +4861,7 @@ export function AdminChangeBookingFlow({
|
|
|
4838
4861
|
if (date === selectedDate) return;
|
|
4839
4862
|
if (selectedAvailability) {
|
|
4840
4863
|
changeFlowOutboundAnchorRef.current = {
|
|
4841
|
-
productOptionId: selectedAvailability
|
|
4864
|
+
productOptionId: getAvailabilityOptionId(selectedAvailability) || null,
|
|
4842
4865
|
minutesFromMidnight: getMinutesFromMidnightInTimezone(
|
|
4843
4866
|
selectedAvailability.dateTime,
|
|
4844
4867
|
companyTimezone
|
|
@@ -4975,8 +4998,8 @@ export function AdminChangeBookingFlow({
|
|
|
4975
4998
|
[selectedAvailability]
|
|
4976
4999
|
);
|
|
4977
5000
|
const selectedAvailabilityKey = useMemo(
|
|
4978
|
-
() => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability
|
|
4979
|
-
[selectedAvailability
|
|
5001
|
+
() => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''}`,
|
|
5002
|
+
[selectedAvailability]
|
|
4980
5003
|
);
|
|
4981
5004
|
// Remember where promo was successfully applied to avoid self-clearing on same selection.
|
|
4982
5005
|
const promoAppliedSelectionKeyRef = useRef<string | null>(null);
|
|
@@ -5131,8 +5154,7 @@ export function AdminChangeBookingFlow({
|
|
|
5131
5154
|
.map(([category, count]) => ({ category, count }));
|
|
5132
5155
|
|
|
5133
5156
|
// Get the productOptionId from the selected availability (we tagged it when fetching)
|
|
5134
|
-
const availabilityProductOptionId = selectedAvailability
|
|
5135
|
-
|| activeOptions[0]?.optionId;
|
|
5157
|
+
const availabilityProductOptionId = getAvailabilityOptionId(selectedAvailability);
|
|
5136
5158
|
|
|
5137
5159
|
if (!availabilityProductOptionId) {
|
|
5138
5160
|
setError('No product option selected');
|
|
@@ -5667,8 +5689,7 @@ export function AdminChangeBookingFlow({
|
|
|
5667
5689
|
.map(([category, count]) => ({ category, count }));
|
|
5668
5690
|
const availabilityProductOptionId =
|
|
5669
5691
|
adminChoiceData.availabilityProductOptionId ||
|
|
5670
|
-
selectedAvailability
|
|
5671
|
-
activeOptions[0]?.optionId;
|
|
5692
|
+
getAvailabilityOptionId(selectedAvailability);
|
|
5672
5693
|
if (!availabilityProductOptionId) {
|
|
5673
5694
|
setError('No product option selected');
|
|
5674
5695
|
setLoading(false);
|
|
@@ -263,18 +263,30 @@ function findMergedAvailabilityForSelection(
|
|
|
263
263
|
selected: Availability | null
|
|
264
264
|
): Availability | undefined {
|
|
265
265
|
if (!selected) return undefined;
|
|
266
|
-
const optId = selected
|
|
266
|
+
const optId = getAvailabilityOptionId(selected) || undefined;
|
|
267
267
|
const dt = selected.dateTime;
|
|
268
268
|
const availId = selected.availabilityId?.trim();
|
|
269
269
|
if (availId && optId) {
|
|
270
270
|
const exact = merged.find(
|
|
271
271
|
(a) =>
|
|
272
272
|
a.availabilityId === availId &&
|
|
273
|
-
(a
|
|
273
|
+
getAvailabilityOptionId(a) === optId
|
|
274
274
|
);
|
|
275
275
|
if (exact) return exact;
|
|
276
276
|
}
|
|
277
|
-
return merged.find((a) => a.dateTime === dt && a
|
|
277
|
+
return merged.find((a) => a.dateTime === dt && getAvailabilityOptionId(a) === optId);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function getAvailabilityOptionId(availability: Availability, fallbackOptionId?: string): string {
|
|
281
|
+
const productOptionId = availability.productOptionId?.trim();
|
|
282
|
+
if (productOptionId) return productOptionId;
|
|
283
|
+
const productId = availability.productId?.trim();
|
|
284
|
+
if (productId?.startsWith('po_')) return productId;
|
|
285
|
+
return fallbackOptionId?.trim() ?? '';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function getAvailabilityCacheKey(availability: Availability): string {
|
|
289
|
+
return `${availability.dateTime}-${getAvailabilityOptionId(availability)}`;
|
|
278
290
|
}
|
|
279
291
|
|
|
280
292
|
/** Ticket rates for one availability row — mirrors main cart [pricing] useMemo so baseline slots match BE. */
|
|
@@ -288,7 +300,7 @@ function buildPricingFromAvailability(
|
|
|
288
300
|
isSimplifiedPricingView: boolean,
|
|
289
301
|
) {
|
|
290
302
|
if (!selectedAvailability || !pricingConfig) return [];
|
|
291
|
-
const optionId = selectedAvailability
|
|
303
|
+
const optionId = getAvailabilityOptionId(selectedAvailability);
|
|
292
304
|
const selectedOption = activeOptions.find((opt) => opt.optionId === optionId);
|
|
293
305
|
const precomputed = optionId ? precomputedPricesByOption?.[optionId] : undefined;
|
|
294
306
|
const rateToDisplayPrice = (backendInDisplayCurrency: number) =>
|
|
@@ -306,11 +318,17 @@ function buildPricingFromAvailability(
|
|
|
306
318
|
appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }>,
|
|
307
319
|
) => {
|
|
308
320
|
const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
|
|
321
|
+
const hasOptionScopedPrice = baseInDisplayCurrency > 0;
|
|
322
|
+
const priceCADForBreakdown =
|
|
323
|
+
hasOptionScopedPrice && currency === 'CAD' ? baseInDisplayCurrency : backendPriceCAD;
|
|
324
|
+
const displayCurrencyPriceForFallback = hasOptionScopedPrice
|
|
325
|
+
? baseInDisplayCurrency
|
|
326
|
+
: backendInDisplayCurrency;
|
|
309
327
|
const isPublicMode = isSimplifiedPricingView;
|
|
310
328
|
const breakdown = computePriceBreakdown(
|
|
311
329
|
pricingConfig,
|
|
312
330
|
currency,
|
|
313
|
-
|
|
331
|
+
priceCADForBreakdown,
|
|
314
332
|
basePriceCAD,
|
|
315
333
|
hasFees,
|
|
316
334
|
appliedAdjustments,
|
|
@@ -318,8 +336,8 @@ function buildPricingFromAvailability(
|
|
|
318
336
|
baseInDisplayCurrency,
|
|
319
337
|
isPublicMode,
|
|
320
338
|
);
|
|
321
|
-
const price = breakdown?.finalPrice ?? rateToDisplayPrice(
|
|
322
|
-
return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD:
|
|
339
|
+
const price = breakdown?.finalPrice ?? rateToDisplayPrice(displayCurrencyPriceForFallback);
|
|
340
|
+
return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD: priceCADForBreakdown };
|
|
323
341
|
};
|
|
324
342
|
return (
|
|
325
343
|
selectedAvailability.rates?.map((rate) => {
|
|
@@ -1033,10 +1051,10 @@ export function ChangeBookingFlow({
|
|
|
1033
1051
|
let mergedOut: Availability[] = [];
|
|
1034
1052
|
setAvailabilities((prev) => {
|
|
1035
1053
|
const existingMap = new Map(
|
|
1036
|
-
prev.map((avail) => [
|
|
1054
|
+
prev.map((avail) => [getAvailabilityCacheKey(avail), avail])
|
|
1037
1055
|
);
|
|
1038
1056
|
allFetchedAvailabilities.forEach((avail) => {
|
|
1039
|
-
existingMap.set(
|
|
1057
|
+
existingMap.set(getAvailabilityCacheKey(avail), avail);
|
|
1040
1058
|
});
|
|
1041
1059
|
mergedOut = Array.from(existingMap.values());
|
|
1042
1060
|
return mergedOut;
|
|
@@ -1070,10 +1088,10 @@ export function ChangeBookingFlow({
|
|
|
1070
1088
|
const existingCache = availabilitiesCache.get(cacheKey);
|
|
1071
1089
|
const existingAvailabilities = existingCache?.availabilities ?? [];
|
|
1072
1090
|
const mergedAvailabilitiesMap = new Map(
|
|
1073
|
-
existingAvailabilities.map((a) => [
|
|
1091
|
+
existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
|
|
1074
1092
|
);
|
|
1075
1093
|
allFetchedAvailabilities.forEach((a) => {
|
|
1076
|
-
mergedAvailabilitiesMap.set(
|
|
1094
|
+
mergedAvailabilitiesMap.set(getAvailabilityCacheKey(a), a);
|
|
1077
1095
|
});
|
|
1078
1096
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
1079
1097
|
results.forEach((r) => {
|
|
@@ -1351,12 +1369,12 @@ export function ChangeBookingFlow({
|
|
|
1351
1369
|
// Merge with existing availabilities (avoid duplicates by dateTime + productOptionId)
|
|
1352
1370
|
setAvailabilities(prev => {
|
|
1353
1371
|
const existingMap = new Map(
|
|
1354
|
-
prev.map(avail => [
|
|
1372
|
+
prev.map(avail => [getAvailabilityCacheKey(avail), avail])
|
|
1355
1373
|
);
|
|
1356
1374
|
|
|
1357
1375
|
// Merge new availabilities - update existing ones or add new ones
|
|
1358
1376
|
allFetchedAvailabilities.forEach(avail => {
|
|
1359
|
-
const key =
|
|
1377
|
+
const key = getAvailabilityCacheKey(avail);
|
|
1360
1378
|
// Always update to get latest data (vacancies, prices, etc.)
|
|
1361
1379
|
existingMap.set(key, avail);
|
|
1362
1380
|
});
|
|
@@ -1385,10 +1403,10 @@ export function ChangeBookingFlow({
|
|
|
1385
1403
|
const existingCache = availabilitiesCache.get(cacheKey);
|
|
1386
1404
|
const existingAvailabilities = existingCache?.availabilities ?? [];
|
|
1387
1405
|
const mergedAvailabilitiesMap = new Map(
|
|
1388
|
-
existingAvailabilities.map((a) => [
|
|
1406
|
+
existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
|
|
1389
1407
|
);
|
|
1390
1408
|
allFetchedAvailabilities.forEach((a) => {
|
|
1391
|
-
mergedAvailabilitiesMap.set(
|
|
1409
|
+
mergedAvailabilitiesMap.set(getAvailabilityCacheKey(a), a);
|
|
1392
1410
|
});
|
|
1393
1411
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
1394
1412
|
results.forEach((r) => {
|
|
@@ -1634,7 +1652,7 @@ export function ChangeBookingFlow({
|
|
|
1634
1652
|
timesForSelectedDate
|
|
1635
1653
|
.map(
|
|
1636
1654
|
(avail) =>
|
|
1637
|
-
`${avail.dateTime}|${avail
|
|
1655
|
+
`${avail.dateTime}|${getAvailabilityOptionId(avail)}|${avail.vacancies ?? 0}`,
|
|
1638
1656
|
)
|
|
1639
1657
|
.join('||'),
|
|
1640
1658
|
[timesForSelectedDate],
|
|
@@ -2066,7 +2084,9 @@ export function ChangeBookingFlow({
|
|
|
2066
2084
|
const calculateStaySummary = useCallback((returnDateTime: Date): string | null => {
|
|
2067
2085
|
if (!selectedAvailability) return null;
|
|
2068
2086
|
|
|
2069
|
-
const availabilityProductOptionId =
|
|
2087
|
+
const availabilityProductOptionId =
|
|
2088
|
+
getAvailabilityOptionId(selectedAvailability) ||
|
|
2089
|
+
(activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
|
|
2070
2090
|
const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
|
|
2071
2091
|
if (!selectedOption) return null;
|
|
2072
2092
|
|
|
@@ -2142,7 +2162,9 @@ export function ChangeBookingFlow({
|
|
|
2142
2162
|
// Helper function to compute itinerary display for storage (returns same shape as "Your Itinerary" box)
|
|
2143
2163
|
const computeItineraryDisplay = useCallback((): ItineraryDisplayStep[] | null => {
|
|
2144
2164
|
if (!selectedAvailability) return null;
|
|
2145
|
-
const availabilityProductOptionId =
|
|
2165
|
+
const availabilityProductOptionId =
|
|
2166
|
+
getAvailabilityOptionId(selectedAvailability) ||
|
|
2167
|
+
(activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
|
|
2146
2168
|
const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
|
|
2147
2169
|
if (!selectedOption) return null;
|
|
2148
2170
|
const tourStartTime = parseISO(selectedAvailability.dateTime);
|
|
@@ -2381,13 +2403,13 @@ export function ChangeBookingFlow({
|
|
|
2381
2403
|
const selectedMs = parseAvailabilityDateTime(selectedAvailability.dateTime).getTime();
|
|
2382
2404
|
const initialMs = parseAvailabilityDateTime(initialValues.dateTime).getTime();
|
|
2383
2405
|
if (selectedMs !== initialMs) return false;
|
|
2384
|
-
const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability
|
|
2406
|
+
const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
|
|
2385
2407
|
const initialOpt = changeFlowResolvedInitialProductOptionId;
|
|
2386
2408
|
if (selectedOpt != null && initialOpt != null) {
|
|
2387
2409
|
return selectedOpt === initialOpt;
|
|
2388
2410
|
}
|
|
2389
2411
|
const legacyInitial = initialValues.productOptionId?.trim() || null;
|
|
2390
|
-
const legacySelected = selectedAvailability
|
|
2412
|
+
const legacySelected = getAvailabilityOptionId(selectedAvailability) || null;
|
|
2391
2413
|
return !legacySelected || !legacyInitial || legacySelected === legacyInitial;
|
|
2392
2414
|
}, [
|
|
2393
2415
|
selectedAvailability,
|
|
@@ -2414,13 +2436,13 @@ export function ChangeBookingFlow({
|
|
|
2414
2436
|
return false;
|
|
2415
2437
|
}
|
|
2416
2438
|
if (!selectedDate || selectedDate !== initialDay) return false;
|
|
2417
|
-
const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability
|
|
2439
|
+
const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
|
|
2418
2440
|
const initialOpt = changeFlowResolvedInitialProductOptionId;
|
|
2419
2441
|
if (selectedOpt != null && initialOpt != null) {
|
|
2420
2442
|
return selectedOpt === initialOpt;
|
|
2421
2443
|
}
|
|
2422
2444
|
const legacyInitial = initialValues.productOptionId?.trim() || null;
|
|
2423
|
-
const legacySelected = selectedAvailability
|
|
2445
|
+
const legacySelected = getAvailabilityOptionId(selectedAvailability) || null;
|
|
2424
2446
|
return Boolean(legacySelected && legacyInitial && legacySelected === legacyInitial);
|
|
2425
2447
|
}, [
|
|
2426
2448
|
selectedDate,
|
|
@@ -2617,7 +2639,9 @@ export function ChangeBookingFlow({
|
|
|
2617
2639
|
// Price breakdown: mid-layer returns line items (base + one per rule/deal). UI renders each line; rate in brackets when used.
|
|
2618
2640
|
const getPriceBreakdown = useCallback((category: string, priceCAD: number, baseInDisplayCurrency: number | undefined, appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }> = []): PriceBreakdownData | null => {
|
|
2619
2641
|
if (!pricingConfig) return null;
|
|
2620
|
-
const selectedOption = activeOptions.find(
|
|
2642
|
+
const selectedOption = activeOptions.find(
|
|
2643
|
+
opt => opt.optionId === (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '')
|
|
2644
|
+
);
|
|
2621
2645
|
const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
|
|
2622
2646
|
const isPublicMode = isSimplifiedPricingView;
|
|
2623
2647
|
return computePriceBreakdown(
|
|
@@ -3049,7 +3073,7 @@ export function ChangeBookingFlow({
|
|
|
3049
3073
|
if (!appliedPromoCode || !selectedAvailability || totalQuantity === 0) return '';
|
|
3050
3074
|
const companyId = product.companyId ?? env.COMPANY_ID;
|
|
3051
3075
|
if (!companyId) return '';
|
|
3052
|
-
const optionId = selectedAvailability
|
|
3076
|
+
const optionId = getAvailabilityOptionId(selectedAvailability);
|
|
3053
3077
|
if (!optionId || !quantitiesSignature) return '';
|
|
3054
3078
|
return [
|
|
3055
3079
|
appliedPromoCode,
|
|
@@ -3066,7 +3090,7 @@ export function ChangeBookingFlow({
|
|
|
3066
3090
|
}, [
|
|
3067
3091
|
appliedPromoCode,
|
|
3068
3092
|
selectedAvailability?.dateTime,
|
|
3069
|
-
selectedAvailability
|
|
3093
|
+
selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
|
|
3070
3094
|
selectedAvailability?.availabilityId,
|
|
3071
3095
|
totalQuantity,
|
|
3072
3096
|
product.companyId,
|
|
@@ -3109,7 +3133,7 @@ export function ChangeBookingFlow({
|
|
|
3109
3133
|
} = promoDiscountParamsRef.current;
|
|
3110
3134
|
if (!code || !sel) return;
|
|
3111
3135
|
const companyId = product.companyId ?? env.COMPANY_ID;
|
|
3112
|
-
const optionId = sel
|
|
3136
|
+
const optionId = getAvailabilityOptionId(sel);
|
|
3113
3137
|
if (!companyId || !optionId) return;
|
|
3114
3138
|
const items = lines.map((l) => ({ category: l.category, qty: l.qty }));
|
|
3115
3139
|
if (items.length === 0) return;
|
|
@@ -3252,7 +3276,7 @@ export function ChangeBookingFlow({
|
|
|
3252
3276
|
const initialOpt =
|
|
3253
3277
|
changeFlowResolvedInitialProductOptionId ??
|
|
3254
3278
|
(initialValues.productOptionId?.trim() || null);
|
|
3255
|
-
const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability
|
|
3279
|
+
const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
|
|
3256
3280
|
const optionChanged = Boolean(
|
|
3257
3281
|
selectedOpt != null && initialOpt != null && initialOpt !== selectedOpt
|
|
3258
3282
|
);
|
|
@@ -3620,8 +3644,7 @@ export function ChangeBookingFlow({
|
|
|
3620
3644
|
return;
|
|
3621
3645
|
}
|
|
3622
3646
|
|
|
3623
|
-
const optionId =
|
|
3624
|
-
selectedAvailability.productOptionId?.trim() || activeOptions[0]?.optionId;
|
|
3647
|
+
const optionId = getAvailabilityOptionId(selectedAvailability);
|
|
3625
3648
|
if (!optionId) {
|
|
3626
3649
|
setLatestChangeQuote(null);
|
|
3627
3650
|
setChangeQuoteLoading(false);
|
|
@@ -3695,7 +3718,7 @@ export function ChangeBookingFlow({
|
|
|
3695
3718
|
hasChangeSelection,
|
|
3696
3719
|
selectedAvailability,
|
|
3697
3720
|
selectedAvailability?.dateTime,
|
|
3698
|
-
selectedAvailability
|
|
3721
|
+
selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
|
|
3699
3722
|
initialValues?.bookingReference,
|
|
3700
3723
|
lastName,
|
|
3701
3724
|
missingRequiredReturnSelection,
|
|
@@ -3754,7 +3777,7 @@ export function ChangeBookingFlow({
|
|
|
3754
3777
|
|
|
3755
3778
|
const mostPopularOption = activeOptions.find(opt => opt.mostPopular);
|
|
3756
3779
|
const candidate = mostPopularOption
|
|
3757
|
-
? timesForSelectedDate.find(avail => avail
|
|
3780
|
+
? timesForSelectedDate.find(avail => getAvailabilityOptionId(avail) === mostPopularOption.optionId && avail.vacancies > 0)
|
|
3758
3781
|
: null;
|
|
3759
3782
|
const fallback = timesForSelectedDate.find(avail => avail.vacancies > 0);
|
|
3760
3783
|
const toSelect = candidate ?? fallback;
|
|
@@ -3780,9 +3803,9 @@ export function ChangeBookingFlow({
|
|
|
3780
3803
|
useEffect(() => {
|
|
3781
3804
|
if (selectedAvailability && availabilities.length > 0) {
|
|
3782
3805
|
const updatedAvailability = availabilities.find(
|
|
3783
|
-
|
|
3806
|
+
avail =>
|
|
3784
3807
|
avail.dateTime === selectedAvailability.dateTime &&
|
|
3785
|
-
avail
|
|
3808
|
+
getAvailabilityOptionId(avail) === getAvailabilityOptionId(selectedAvailability)
|
|
3786
3809
|
);
|
|
3787
3810
|
if (updatedAvailability) {
|
|
3788
3811
|
setSelectedAvailability(updatedAvailability);
|
|
@@ -3813,7 +3836,7 @@ export function ChangeBookingFlow({
|
|
|
3813
3836
|
setImplicitReturnBaselineId(null);
|
|
3814
3837
|
}, [
|
|
3815
3838
|
selectedAvailability?.dateTime,
|
|
3816
|
-
selectedAvailability
|
|
3839
|
+
selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
|
|
3817
3840
|
initialValues?.returnAvailabilityId,
|
|
3818
3841
|
initialValues?.returnDateTime,
|
|
3819
3842
|
]);
|
|
@@ -3885,7 +3908,7 @@ export function ChangeBookingFlow({
|
|
|
3885
3908
|
]);
|
|
3886
3909
|
|
|
3887
3910
|
// Fetch add-ons when availability (product option) is selected; clear selections when option changes
|
|
3888
|
-
const availabilityProductOptionId = selectedAvailability
|
|
3911
|
+
const availabilityProductOptionId = selectedAvailability ? getAvailabilityOptionId(selectedAvailability) || null : null;
|
|
3889
3912
|
const prevAvailabilityProductOptionIdRef = useRef<string | null>(null);
|
|
3890
3913
|
useEffect(() => {
|
|
3891
3914
|
if (!availabilityProductOptionId || !product.companyId) {
|
|
@@ -3905,7 +3928,7 @@ export function ChangeBookingFlow({
|
|
|
3905
3928
|
if (date === selectedDate) return;
|
|
3906
3929
|
if (selectedAvailability) {
|
|
3907
3930
|
changeFlowOutboundAnchorRef.current = {
|
|
3908
|
-
productOptionId: selectedAvailability
|
|
3931
|
+
productOptionId: getAvailabilityOptionId(selectedAvailability) || null,
|
|
3909
3932
|
minutesFromMidnight: getMinutesFromMidnightInTimezone(
|
|
3910
3933
|
selectedAvailability.dateTime,
|
|
3911
3934
|
companyTimezone
|
|
@@ -4042,8 +4065,8 @@ export function ChangeBookingFlow({
|
|
|
4042
4065
|
[selectedAvailability]
|
|
4043
4066
|
);
|
|
4044
4067
|
const selectedAvailabilityKey = useMemo(
|
|
4045
|
-
() => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability
|
|
4046
|
-
[selectedAvailability
|
|
4068
|
+
() => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''}`,
|
|
4069
|
+
[selectedAvailability]
|
|
4047
4070
|
);
|
|
4048
4071
|
// Remember where promo was successfully applied to avoid self-clearing on same selection.
|
|
4049
4072
|
const promoAppliedSelectionKeyRef = useRef<string | null>(null);
|
|
@@ -4195,8 +4218,7 @@ export function ChangeBookingFlow({
|
|
|
4195
4218
|
.map(([category, count]) => ({ category, count }));
|
|
4196
4219
|
|
|
4197
4220
|
// Get the productOptionId from the selected availability (we tagged it when fetching)
|
|
4198
|
-
const availabilityProductOptionId = selectedAvailability
|
|
4199
|
-
|| activeOptions[0]?.optionId;
|
|
4221
|
+
const availabilityProductOptionId = getAvailabilityOptionId(selectedAvailability);
|
|
4200
4222
|
|
|
4201
4223
|
if (!availabilityProductOptionId) {
|
|
4202
4224
|
setError('No product option selected');
|
|
@@ -158,22 +158,30 @@ function findMergedAvailabilityForSelection(
|
|
|
158
158
|
selected: Availability | null
|
|
159
159
|
): Availability | undefined {
|
|
160
160
|
if (!selected) return undefined;
|
|
161
|
-
const optId = selected
|
|
161
|
+
const optId = getAvailabilityOptionId(selected) || undefined;
|
|
162
162
|
const dt = selected.dateTime;
|
|
163
163
|
const availId = selected.availabilityId?.trim();
|
|
164
164
|
if (availId && optId) {
|
|
165
165
|
const exact = merged.find(
|
|
166
166
|
(a) =>
|
|
167
167
|
a.availabilityId === availId &&
|
|
168
|
-
(a
|
|
168
|
+
getAvailabilityOptionId(a) === optId
|
|
169
169
|
);
|
|
170
170
|
if (exact) return exact;
|
|
171
171
|
}
|
|
172
|
-
return merged.find((a) => a.dateTime === dt && a
|
|
172
|
+
return merged.find((a) => a.dateTime === dt && getAvailabilityOptionId(a) === optId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getAvailabilityOptionId(availability: Availability, fallbackOptionId?: string): string {
|
|
176
|
+
const productOptionId = availability.productOptionId?.trim();
|
|
177
|
+
if (productOptionId) return productOptionId;
|
|
178
|
+
const productId = availability.productId?.trim();
|
|
179
|
+
if (productId?.startsWith('po_')) return productId;
|
|
180
|
+
return fallbackOptionId?.trim() ?? '';
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
function availabilityWithOptionId(availability: Availability, fallbackOptionId?: string): Availability {
|
|
176
|
-
const productOptionId = availability
|
|
184
|
+
const productOptionId = getAvailabilityOptionId(availability, fallbackOptionId);
|
|
177
185
|
return productOptionId ? { ...availability, productOptionId } : availability;
|
|
178
186
|
}
|
|
179
187
|
|
|
@@ -195,11 +203,15 @@ function mergeAvailabilitiesIntoMap(
|
|
|
195
203
|
availabilities: Availability[],
|
|
196
204
|
): void {
|
|
197
205
|
for (const availability of availabilities) {
|
|
198
|
-
const key =
|
|
206
|
+
const key = getAvailabilityCacheKey(availability);
|
|
199
207
|
map.set(key, mergeAvailabilityRecord(map.get(key), availability));
|
|
200
208
|
}
|
|
201
209
|
}
|
|
202
210
|
|
|
211
|
+
function getAvailabilityCacheKey(availability: Availability): string {
|
|
212
|
+
return `${availability.dateTime}-${getAvailabilityOptionId(availability)}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
203
215
|
function pickDefaultAvailabilityForTimes(
|
|
204
216
|
times: Availability[],
|
|
205
217
|
activeOptions: Product['options'],
|
|
@@ -208,7 +220,7 @@ function pickDefaultAvailabilityForTimes(
|
|
|
208
220
|
const mostPopularOption = activeOptions.find((opt) => opt.mostPopular);
|
|
209
221
|
const candidate = mostPopularOption
|
|
210
222
|
? times.find(
|
|
211
|
-
(avail) => avail
|
|
223
|
+
(avail) => getAvailabilityOptionId(avail) === mostPopularOption.optionId && avail.vacancies > 0,
|
|
212
224
|
)
|
|
213
225
|
: null;
|
|
214
226
|
const fallback = times.find((avail) => avail.vacancies > 0);
|
|
@@ -271,7 +283,7 @@ function buildPricingFromAvailability(
|
|
|
271
283
|
isSimplifiedPricingView: boolean,
|
|
272
284
|
) {
|
|
273
285
|
if (!selectedAvailability || !pricingConfig) return [];
|
|
274
|
-
const optionId = selectedAvailability
|
|
286
|
+
const optionId = getAvailabilityOptionId(selectedAvailability);
|
|
275
287
|
const selectedOption = activeOptions.find((opt) => opt.optionId === optionId);
|
|
276
288
|
const precomputed = optionId ? precomputedPricesByOption?.[optionId] : undefined;
|
|
277
289
|
const rateToDisplayPrice = (backendInDisplayCurrency: number) =>
|
|
@@ -289,11 +301,17 @@ function buildPricingFromAvailability(
|
|
|
289
301
|
appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }>,
|
|
290
302
|
) => {
|
|
291
303
|
const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
|
|
304
|
+
const hasOptionScopedPrice = baseInDisplayCurrency > 0;
|
|
305
|
+
const priceCADForBreakdown =
|
|
306
|
+
hasOptionScopedPrice && currency === 'CAD' ? baseInDisplayCurrency : backendPriceCAD;
|
|
307
|
+
const displayCurrencyPriceForFallback = hasOptionScopedPrice
|
|
308
|
+
? baseInDisplayCurrency
|
|
309
|
+
: backendInDisplayCurrency;
|
|
292
310
|
const isPublicMode = isSimplifiedPricingView;
|
|
293
311
|
const breakdown = computePriceBreakdown(
|
|
294
312
|
pricingConfig,
|
|
295
313
|
currency,
|
|
296
|
-
|
|
314
|
+
priceCADForBreakdown,
|
|
297
315
|
basePriceCAD,
|
|
298
316
|
hasFees,
|
|
299
317
|
appliedAdjustments,
|
|
@@ -301,8 +319,8 @@ function buildPricingFromAvailability(
|
|
|
301
319
|
baseInDisplayCurrency,
|
|
302
320
|
isPublicMode,
|
|
303
321
|
);
|
|
304
|
-
const price = breakdown?.finalPrice ?? rateToDisplayPrice(
|
|
305
|
-
return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD:
|
|
322
|
+
const price = breakdown?.finalPrice ?? rateToDisplayPrice(displayCurrencyPriceForFallback);
|
|
323
|
+
return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD: priceCADForBreakdown };
|
|
306
324
|
};
|
|
307
325
|
return (
|
|
308
326
|
selectedAvailability.rates?.map((rate) => {
|
|
@@ -708,9 +726,7 @@ export function NewBookingFlow({
|
|
|
708
726
|
|
|
709
727
|
let mergedOut: Availability[] = [];
|
|
710
728
|
setAvailabilities((prev) => {
|
|
711
|
-
const existingMap = new Map(
|
|
712
|
-
prev.map((avail) => [`${avail.dateTime}-${avail.productOptionId}`, avail])
|
|
713
|
-
);
|
|
729
|
+
const existingMap = new Map(prev.map((avail) => [getAvailabilityCacheKey(avail), avail]));
|
|
714
730
|
mergeAvailabilitiesIntoMap(existingMap, allFetchedAvailabilities);
|
|
715
731
|
mergedOut = Array.from(existingMap.values());
|
|
716
732
|
return mergedOut;
|
|
@@ -744,7 +760,7 @@ export function NewBookingFlow({
|
|
|
744
760
|
const existingCache = availabilitiesCache.get(cacheKey);
|
|
745
761
|
const existingAvailabilities = existingCache?.availabilities ?? [];
|
|
746
762
|
const mergedAvailabilitiesMap = new Map(
|
|
747
|
-
existingAvailabilities.map((a) => [
|
|
763
|
+
existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
|
|
748
764
|
);
|
|
749
765
|
mergeAvailabilitiesIntoMap(mergedAvailabilitiesMap, allFetchedAvailabilities);
|
|
750
766
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
@@ -828,7 +844,7 @@ export function NewBookingFlow({
|
|
|
828
844
|
|
|
829
845
|
let mergedOut: Availability[] = [];
|
|
830
846
|
setAvailabilities((prev) => {
|
|
831
|
-
const merged = new Map(prev.map((availability) => [
|
|
847
|
+
const merged = new Map(prev.map((availability) => [getAvailabilityCacheKey(availability), availability]));
|
|
832
848
|
mergeAvailabilitiesIntoMap(merged, detailedAvailabilities);
|
|
833
849
|
mergedOut = Array.from(merged.values());
|
|
834
850
|
return mergedOut;
|
|
@@ -847,7 +863,7 @@ export function NewBookingFlow({
|
|
|
847
863
|
const existingCache = availabilitiesCache.get(cacheKey);
|
|
848
864
|
const mergedAvailabilities = new Map(
|
|
849
865
|
(existingCache?.availabilities ?? []).map((availability) => [
|
|
850
|
-
|
|
866
|
+
getAvailabilityCacheKey(availability),
|
|
851
867
|
availability,
|
|
852
868
|
])
|
|
853
869
|
);
|
|
@@ -1083,7 +1099,7 @@ export function NewBookingFlow({
|
|
|
1083
1099
|
// Merge with existing availabilities (avoid duplicates by dateTime + productOptionId)
|
|
1084
1100
|
setAvailabilities(prev => {
|
|
1085
1101
|
const existingMap = new Map(
|
|
1086
|
-
prev.map(avail => [
|
|
1102
|
+
prev.map(avail => [getAvailabilityCacheKey(avail), avail])
|
|
1087
1103
|
);
|
|
1088
1104
|
|
|
1089
1105
|
// Merge new availabilities - update existing ones or add new ones
|
|
@@ -1112,7 +1128,7 @@ export function NewBookingFlow({
|
|
|
1112
1128
|
const existingCache = availabilitiesCache.get(cacheKey);
|
|
1113
1129
|
const existingAvailabilities = existingCache?.availabilities ?? [];
|
|
1114
1130
|
const mergedAvailabilitiesMap = new Map(
|
|
1115
|
-
existingAvailabilities.map((a) => [
|
|
1131
|
+
existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
|
|
1116
1132
|
);
|
|
1117
1133
|
mergeAvailabilitiesIntoMap(mergedAvailabilitiesMap, allFetchedAvailabilities);
|
|
1118
1134
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
@@ -1294,7 +1310,7 @@ export function NewBookingFlow({
|
|
|
1294
1310
|
});
|
|
1295
1311
|
|
|
1296
1312
|
setAvailabilities((prev) => {
|
|
1297
|
-
const merged = new Map(prev.map((availability) => [
|
|
1313
|
+
const merged = new Map(prev.map((availability) => [getAvailabilityCacheKey(availability), availability]));
|
|
1298
1314
|
mergeAvailabilitiesIntoMap(merged, detailedAvailabilities);
|
|
1299
1315
|
return Array.from(merged.values());
|
|
1300
1316
|
});
|
|
@@ -1322,7 +1338,7 @@ export function NewBookingFlow({
|
|
|
1322
1338
|
const existingCache = availabilitiesCache.get(cacheKey);
|
|
1323
1339
|
const mergedAvailabilities = new Map(
|
|
1324
1340
|
(existingCache?.availabilities ?? []).map((availability) => [
|
|
1325
|
-
|
|
1341
|
+
getAvailabilityCacheKey(availability),
|
|
1326
1342
|
availability,
|
|
1327
1343
|
])
|
|
1328
1344
|
);
|
|
@@ -1507,7 +1523,7 @@ export function NewBookingFlow({
|
|
|
1507
1523
|
timesForSelectedDate
|
|
1508
1524
|
.map(
|
|
1509
1525
|
(avail) =>
|
|
1510
|
-
`${avail.dateTime}|${avail
|
|
1526
|
+
`${avail.dateTime}|${getAvailabilityOptionId(avail)}|${avail.vacancies ?? 0}`,
|
|
1511
1527
|
)
|
|
1512
1528
|
.join('||'),
|
|
1513
1529
|
[timesForSelectedDate],
|
|
@@ -1694,7 +1710,9 @@ export function NewBookingFlow({
|
|
|
1694
1710
|
const calculateStaySummary = useCallback((returnDateTime: Date): string | null => {
|
|
1695
1711
|
if (!selectedAvailability) return null;
|
|
1696
1712
|
|
|
1697
|
-
const availabilityProductOptionId =
|
|
1713
|
+
const availabilityProductOptionId =
|
|
1714
|
+
getAvailabilityOptionId(selectedAvailability) ||
|
|
1715
|
+
(activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
|
|
1698
1716
|
const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
|
|
1699
1717
|
if (!selectedOption) return null;
|
|
1700
1718
|
|
|
@@ -1770,7 +1788,9 @@ export function NewBookingFlow({
|
|
|
1770
1788
|
// Helper function to compute itinerary display for storage (returns same shape as "Your Itinerary" box)
|
|
1771
1789
|
const computeItineraryDisplay = useCallback((): ItineraryDisplayStep[] | null => {
|
|
1772
1790
|
if (!selectedAvailability) return null;
|
|
1773
|
-
const availabilityProductOptionId =
|
|
1791
|
+
const availabilityProductOptionId =
|
|
1792
|
+
getAvailabilityOptionId(selectedAvailability) ||
|
|
1793
|
+
(activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
|
|
1774
1794
|
const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
|
|
1775
1795
|
if (!selectedOption) return null;
|
|
1776
1796
|
const tourStartTime = parseISO(selectedAvailability.dateTime);
|
|
@@ -1940,7 +1960,7 @@ export function NewBookingFlow({
|
|
|
1940
1960
|
// Price breakdown: mid-layer returns line items (base + one per rule/deal). UI renders each line; rate in brackets when used.
|
|
1941
1961
|
const getPriceBreakdown = useCallback((category: string, priceCAD: number, baseInDisplayCurrency: number | undefined, appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }> = []): PriceBreakdownData | null => {
|
|
1942
1962
|
if (!pricingConfig) return null;
|
|
1943
|
-
const selectedOption = activeOptions.find(opt => opt.optionId === selectedAvailability
|
|
1963
|
+
const selectedOption = activeOptions.find(opt => opt.optionId === (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''));
|
|
1944
1964
|
const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
|
|
1945
1965
|
const isPublicMode = isSimplifiedPricingView;
|
|
1946
1966
|
return computePriceBreakdown(
|
|
@@ -2163,7 +2183,7 @@ export function NewBookingFlow({
|
|
|
2163
2183
|
if (!appliedPromoCode || !selectedAvailability || totalQuantity === 0) return '';
|
|
2164
2184
|
const companyId = product.companyId ?? env.COMPANY_ID;
|
|
2165
2185
|
if (!companyId) return '';
|
|
2166
|
-
const optionId = selectedAvailability
|
|
2186
|
+
const optionId = getAvailabilityOptionId(selectedAvailability);
|
|
2167
2187
|
if (!optionId || !quantitiesSignature) return '';
|
|
2168
2188
|
return [
|
|
2169
2189
|
appliedPromoCode,
|
|
@@ -2179,7 +2199,7 @@ export function NewBookingFlow({
|
|
|
2179
2199
|
}, [
|
|
2180
2200
|
appliedPromoCode,
|
|
2181
2201
|
selectedAvailability?.dateTime,
|
|
2182
|
-
selectedAvailability
|
|
2202
|
+
selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
|
|
2183
2203
|
selectedAvailability?.availabilityId,
|
|
2184
2204
|
totalQuantity,
|
|
2185
2205
|
product.companyId,
|
|
@@ -2375,9 +2395,9 @@ export function NewBookingFlow({
|
|
|
2375
2395
|
useEffect(() => {
|
|
2376
2396
|
if (selectedAvailability && availabilities.length > 0) {
|
|
2377
2397
|
const updatedAvailability = availabilities.find(
|
|
2378
|
-
|
|
2398
|
+
avail =>
|
|
2379
2399
|
avail.dateTime === selectedAvailability.dateTime &&
|
|
2380
|
-
avail
|
|
2400
|
+
getAvailabilityOptionId(avail) === getAvailabilityOptionId(selectedAvailability)
|
|
2381
2401
|
);
|
|
2382
2402
|
if (updatedAvailability && shouldSyncSelectedAvailability(selectedAvailability, updatedAvailability)) {
|
|
2383
2403
|
setSelectedAvailability(updatedAvailability);
|
|
@@ -2435,7 +2455,7 @@ export function NewBookingFlow({
|
|
|
2435
2455
|
]);
|
|
2436
2456
|
|
|
2437
2457
|
// Fetch add-ons when availability (product option) is selected; clear selections when option changes
|
|
2438
|
-
const availabilityProductOptionId = selectedAvailability
|
|
2458
|
+
const availabilityProductOptionId = selectedAvailability ? getAvailabilityOptionId(selectedAvailability) || null : null;
|
|
2439
2459
|
const prevAvailabilityProductOptionIdRef = useRef<string | null>(null);
|
|
2440
2460
|
useEffect(() => {
|
|
2441
2461
|
if (!availabilityProductOptionId || !product.companyId) {
|
|
@@ -2564,8 +2584,8 @@ export function NewBookingFlow({
|
|
|
2564
2584
|
[selectedAvailability]
|
|
2565
2585
|
);
|
|
2566
2586
|
const selectedAvailabilityKey = useMemo(
|
|
2567
|
-
() => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability
|
|
2568
|
-
[selectedAvailability
|
|
2587
|
+
() => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''}`,
|
|
2588
|
+
[selectedAvailability]
|
|
2569
2589
|
);
|
|
2570
2590
|
// Remember where promo was successfully applied to avoid self-clearing on same selection.
|
|
2571
2591
|
const promoAppliedSelectionKeyRef = useRef<string | null>(null);
|
|
@@ -2756,8 +2776,7 @@ export function NewBookingFlow({
|
|
|
2756
2776
|
.map(([category, count]) => ({ category, count }));
|
|
2757
2777
|
|
|
2758
2778
|
// Get the productOptionId from the selected availability (we tagged it when fetching)
|
|
2759
|
-
const availabilityProductOptionId = selectedAvailability
|
|
2760
|
-
|| activeOptions[0]?.optionId;
|
|
2779
|
+
const availabilityProductOptionId = getAvailabilityOptionId(selectedAvailability);
|
|
2761
2780
|
|
|
2762
2781
|
if (!availabilityProductOptionId) {
|
|
2763
2782
|
setError('No product option selected');
|
|
@@ -251,6 +251,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
251
251
|
const [mapZoom, setMapZoom] = useState(DEFAULT_MAP_ZOOM);
|
|
252
252
|
const [showSkipWarning, setShowSkipWarning] = useState(false);
|
|
253
253
|
const [dontKnowFilterActive, setDontKnowFilterActive] = useState(false);
|
|
254
|
+
const [mapsAuthError, setMapsAuthError] = useState(false);
|
|
254
255
|
const mapRef = useRef<google.maps.Map | null>(null);
|
|
255
256
|
const highlightedPickupLocationIdSet = useMemo(
|
|
256
257
|
() => new Set(highlightIds),
|
|
@@ -285,8 +286,26 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
285
286
|
googleMapsApiKey,
|
|
286
287
|
libraries,
|
|
287
288
|
});
|
|
289
|
+
const canUseMap = isLoaded && !loadError && !mapsAuthError;
|
|
288
290
|
|
|
289
291
|
// ============ Effects ============
|
|
292
|
+
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
const mapsWindow = window as typeof window & { gm_authFailure?: () => void };
|
|
295
|
+
const previousAuthFailure = mapsWindow.gm_authFailure;
|
|
296
|
+
|
|
297
|
+
const handleAuthFailure = () => {
|
|
298
|
+
setMapsAuthError(true);
|
|
299
|
+
previousAuthFailure?.();
|
|
300
|
+
};
|
|
301
|
+
mapsWindow.gm_authFailure = handleAuthFailure;
|
|
302
|
+
|
|
303
|
+
return () => {
|
|
304
|
+
if (mapsWindow.gm_authFailure === handleAuthFailure) {
|
|
305
|
+
mapsWindow.gm_authFailure = previousAuthFailure;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}, []);
|
|
290
309
|
|
|
291
310
|
// Apply filter pills to pickup locations
|
|
292
311
|
const filteredPickupLocations = useMemo(() => {
|
|
@@ -868,16 +887,20 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
868
887
|
|
|
869
888
|
// ============ Render ============
|
|
870
889
|
|
|
871
|
-
if (loadError) {
|
|
872
|
-
return (
|
|
873
|
-
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
|
874
|
-
Error loading Google Maps. Please check your API key.
|
|
875
|
-
</div>
|
|
876
|
-
);
|
|
877
|
-
}
|
|
878
|
-
|
|
879
890
|
return (
|
|
880
891
|
<div className="space-y-3">
|
|
892
|
+
{loadError && (
|
|
893
|
+
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-800 text-sm">
|
|
894
|
+
Google Maps could not load. You can still select a pickup location from the list below.
|
|
895
|
+
</div>
|
|
896
|
+
)}
|
|
897
|
+
|
|
898
|
+
{mapsAuthError && (
|
|
899
|
+
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-800 text-sm">
|
|
900
|
+
Google Maps is not authorized for this website. You can still select a pickup location from the list below.
|
|
901
|
+
</div>
|
|
902
|
+
)}
|
|
903
|
+
|
|
881
904
|
{!hideTitle && (
|
|
882
905
|
<div className="space-y-0.5">
|
|
883
906
|
<h2 className="text-2xl font-bold text-stone-900">
|
|
@@ -1038,7 +1061,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
1038
1061
|
</div>
|
|
1039
1062
|
|
|
1040
1063
|
{/* Filter Pills + I don't know pill - horizontal scroll on mobile */}
|
|
1041
|
-
{
|
|
1064
|
+
{pickupLocations.length > 0 && (
|
|
1042
1065
|
<div className={styles.filterPillsScroll}>
|
|
1043
1066
|
<FilterPills
|
|
1044
1067
|
pickupLocations={pickupLocations}
|
|
@@ -1112,7 +1135,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
1112
1135
|
)}
|
|
1113
1136
|
|
|
1114
1137
|
{/* Two-column layout: list left, map right - list height matches map */}
|
|
1115
|
-
{
|
|
1138
|
+
{pickupLocations.length > 0 && (
|
|
1116
1139
|
<div className={styles.twoColLayout}>
|
|
1117
1140
|
{/* Left column: scrollable list of pickup locations + I don't know */}
|
|
1118
1141
|
<div className={styles.leftColumn}>
|
|
@@ -1235,13 +1258,14 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
1235
1258
|
{/* Right column: map */}
|
|
1236
1259
|
<div className={styles.rightColumn}>
|
|
1237
1260
|
<div className={styles.mapWrapper}>
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1261
|
+
{canUseMap ? (
|
|
1262
|
+
<GoogleMap
|
|
1263
|
+
mapContainerClassName="w-full h-full rounded-lg overflow-hidden"
|
|
1264
|
+
center={mapCenter}
|
|
1265
|
+
zoom={mapZoom}
|
|
1266
|
+
options={mapOptions}
|
|
1267
|
+
onLoad={onMapLoad}
|
|
1268
|
+
>
|
|
1245
1269
|
{/* Markers for filtered locations */}
|
|
1246
1270
|
{nearbyLocations.map((location) => {
|
|
1247
1271
|
if (!location.coordinates) return null;
|
|
@@ -1480,7 +1504,12 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
|
|
|
1480
1504
|
cursor="default"
|
|
1481
1505
|
/>
|
|
1482
1506
|
))}
|
|
1483
|
-
|
|
1507
|
+
</GoogleMap>
|
|
1508
|
+
) : (
|
|
1509
|
+
<div className="h-full min-h-[280px] flex items-center justify-center rounded-lg bg-stone-50 border border-stone-200 p-4 text-center text-sm text-stone-600">
|
|
1510
|
+
Google Maps is unavailable right now. The pickup list is still available.
|
|
1511
|
+
</div>
|
|
1512
|
+
)}
|
|
1484
1513
|
</div>
|
|
1485
1514
|
</div>
|
|
1486
1515
|
</div>
|
|
@@ -130,7 +130,7 @@ function findMergedPrivateShuttleAvailability(
|
|
|
130
130
|
tz: string
|
|
131
131
|
): Availability | undefined {
|
|
132
132
|
if (!sel || !optionId || !selectedDateStr) return undefined;
|
|
133
|
-
const oidMatches = (a: Availability) => (a
|
|
133
|
+
const oidMatches = (a: Availability) => getPrivateShuttleAvailabilityOptionId(a) === optionId;
|
|
134
134
|
const availId = sel.availabilityId?.trim();
|
|
135
135
|
if (availId) {
|
|
136
136
|
const hit = merged.find((a) => a.availabilityId === availId && oidMatches(a));
|
|
@@ -146,6 +146,40 @@ function findMergedPrivateShuttleAvailability(
|
|
|
146
146
|
return onDay[0];
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function getPrivateShuttleAvailabilityOptionId(availability: Availability): string {
|
|
150
|
+
const productOptionId = availability.productOptionId?.trim();
|
|
151
|
+
if (productOptionId) return productOptionId;
|
|
152
|
+
const productId = availability.productId?.trim();
|
|
153
|
+
return productId?.startsWith('po_') ? productId : '';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getPrivateShuttleAvailabilityCacheKey(availability: Availability): string {
|
|
157
|
+
return `${availability.dateTime}-${getPrivateShuttleAvailabilityOptionId(availability)}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getResourceRate(availability: Availability | undefined | null) {
|
|
161
|
+
return availability?.rates?.find((r) => r.rateId === 'RESOURCE' || r.category === 'RESOURCE');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function applyResourceAdjustments(
|
|
165
|
+
basePrice: number,
|
|
166
|
+
availability: Availability | undefined | null,
|
|
167
|
+
currency: Currency
|
|
168
|
+
): number {
|
|
169
|
+
const resourceRate = getResourceRate(availability);
|
|
170
|
+
const adjustmentTotal =
|
|
171
|
+
resourceRate?.appliedAdjustments?.reduce(
|
|
172
|
+
(sum, adjustment) => sum + (adjustment.changeByCurrency?.[currency] ?? 0),
|
|
173
|
+
0
|
|
174
|
+
) ??
|
|
175
|
+
resourceRate?.applied_adjustments?.reduce(
|
|
176
|
+
(sum, adjustment) => sum + (adjustment.changeByCurrency?.[currency] ?? 0),
|
|
177
|
+
0
|
|
178
|
+
) ??
|
|
179
|
+
0;
|
|
180
|
+
return basePrice + adjustmentTotal;
|
|
181
|
+
}
|
|
182
|
+
|
|
149
183
|
export function PrivateShuttleBookingFlow({
|
|
150
184
|
product,
|
|
151
185
|
productId,
|
|
@@ -394,11 +428,9 @@ export function PrivateShuttleBookingFlow({
|
|
|
394
428
|
|
|
395
429
|
let mergedAvailabilities: Availability[] = [];
|
|
396
430
|
setAvailabilities((prev) => {
|
|
397
|
-
const existingMap = new Map(
|
|
398
|
-
prev.map((avail) => [`${avail.dateTime}-${avail.productId || avail.productOptionId}`, avail])
|
|
399
|
-
);
|
|
431
|
+
const existingMap = new Map(prev.map((avail) => [getPrivateShuttleAvailabilityCacheKey(avail), avail]));
|
|
400
432
|
result.availabilities.forEach((avail) => {
|
|
401
|
-
existingMap.set(
|
|
433
|
+
existingMap.set(getPrivateShuttleAvailabilityCacheKey(avail), avail);
|
|
402
434
|
});
|
|
403
435
|
mergedAvailabilities = Array.from(existingMap.values());
|
|
404
436
|
return mergedAvailabilities;
|
|
@@ -476,11 +508,9 @@ export function PrivateShuttleBookingFlow({
|
|
|
476
508
|
|
|
477
509
|
let mergedAvailabilities: Availability[] = [];
|
|
478
510
|
setAvailabilities((prev) => {
|
|
479
|
-
const existingMap = new Map(
|
|
480
|
-
prev.map((avail) => [`${avail.dateTime}-${avail.productId || avail.productOptionId}`, avail])
|
|
481
|
-
);
|
|
511
|
+
const existingMap = new Map(prev.map((avail) => [getPrivateShuttleAvailabilityCacheKey(avail), avail]));
|
|
482
512
|
result.availabilities.forEach((avail) => {
|
|
483
|
-
existingMap.set(
|
|
513
|
+
existingMap.set(getPrivateShuttleAvailabilityCacheKey(avail), avail);
|
|
484
514
|
});
|
|
485
515
|
mergedAvailabilities = Array.from(existingMap.values());
|
|
486
516
|
return mergedAvailabilities;
|
|
@@ -498,12 +528,12 @@ export function PrivateShuttleBookingFlow({
|
|
|
498
528
|
const existingCache = availabilitiesCache.get(cacheKey);
|
|
499
529
|
const mergedAvailabilitiesMap = new Map(
|
|
500
530
|
(existingCache?.availabilities ?? []).map((availability) => [
|
|
501
|
-
|
|
531
|
+
getPrivateShuttleAvailabilityCacheKey(availability),
|
|
502
532
|
availability,
|
|
503
533
|
])
|
|
504
534
|
);
|
|
505
535
|
result.availabilities.forEach((availability) => {
|
|
506
|
-
mergedAvailabilitiesMap.set(
|
|
536
|
+
mergedAvailabilitiesMap.set(getPrivateShuttleAvailabilityCacheKey(availability), availability);
|
|
507
537
|
});
|
|
508
538
|
availabilitiesCache.merge(cacheKey, {
|
|
509
539
|
fetchedRanges: existingCache?.fetchedRanges ?? fetchedRangesRef.current,
|
|
@@ -687,13 +717,12 @@ export function PrivateShuttleBookingFlow({
|
|
|
687
717
|
if (existing.length === 0) return result.availabilities;
|
|
688
718
|
const existingMap = new Map(
|
|
689
719
|
existing.map((avail) => [
|
|
690
|
-
|
|
720
|
+
getPrivateShuttleAvailabilityCacheKey(avail),
|
|
691
721
|
avail,
|
|
692
722
|
])
|
|
693
723
|
);
|
|
694
724
|
result.availabilities.forEach((avail) => {
|
|
695
|
-
|
|
696
|
-
existingMap.set(key, avail);
|
|
725
|
+
existingMap.set(getPrivateShuttleAvailabilityCacheKey(avail), avail);
|
|
697
726
|
});
|
|
698
727
|
return Array.from(existingMap.values());
|
|
699
728
|
})();
|
|
@@ -779,9 +808,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
779
808
|
const optionsAvailableForSelectedDate = useMemo(() => {
|
|
780
809
|
if (!selectedDate) return [];
|
|
781
810
|
const dateAvailabilities = availabilitiesByDate[selectedDate] || [];
|
|
782
|
-
const optionIds = new Set(
|
|
783
|
-
dateAvailabilities.map((a) => a.productId || a.productOptionId).filter(Boolean)
|
|
784
|
-
);
|
|
811
|
+
const optionIds = new Set(dateAvailabilities.map(getPrivateShuttleAvailabilityOptionId).filter(Boolean));
|
|
785
812
|
return activeOptions.filter((opt) => optionIds.has(opt.optionId));
|
|
786
813
|
}, [selectedDate, availabilitiesByDate, activeOptions]);
|
|
787
814
|
|
|
@@ -819,12 +846,12 @@ export function PrivateShuttleBookingFlow({
|
|
|
819
846
|
setAvailabilities((prev) => {
|
|
820
847
|
const merged = new Map(
|
|
821
848
|
prev.map((availability) => [
|
|
822
|
-
|
|
849
|
+
getPrivateShuttleAvailabilityCacheKey(availability),
|
|
823
850
|
availability,
|
|
824
851
|
])
|
|
825
852
|
);
|
|
826
853
|
result.availabilities.forEach((availability) => {
|
|
827
|
-
merged.set(
|
|
854
|
+
merged.set(getPrivateShuttleAvailabilityCacheKey(availability), availability);
|
|
828
855
|
});
|
|
829
856
|
return Array.from(merged.values());
|
|
830
857
|
});
|
|
@@ -841,12 +868,12 @@ export function PrivateShuttleBookingFlow({
|
|
|
841
868
|
const existingCache = availabilitiesCache.get(cacheKey);
|
|
842
869
|
const mergedAvailabilities = new Map(
|
|
843
870
|
(existingCache?.availabilities ?? []).map((availability) => [
|
|
844
|
-
|
|
871
|
+
getPrivateShuttleAvailabilityCacheKey(availability),
|
|
845
872
|
availability,
|
|
846
873
|
])
|
|
847
874
|
);
|
|
848
875
|
result.availabilities.forEach((availability) => {
|
|
849
|
-
mergedAvailabilities.set(
|
|
876
|
+
mergedAvailabilities.set(getPrivateShuttleAvailabilityCacheKey(availability), availability);
|
|
850
877
|
});
|
|
851
878
|
availabilitiesCache.merge(cacheKey, {
|
|
852
879
|
fetchedRanges: existingCache?.fetchedRanges ?? fetchedRangesRef.current,
|
|
@@ -898,22 +925,25 @@ export function PrivateShuttleBookingFlow({
|
|
|
898
925
|
|
|
899
926
|
const getOptionPrice = useCallback(
|
|
900
927
|
(optionId: string) => {
|
|
928
|
+
const opt = activeOptions.find((o) => o.optionId === optionId);
|
|
901
929
|
const dateAvailabilities = selectedDate ? availabilitiesByDate[selectedDate] || [] : [];
|
|
902
930
|
const avail = dateAvailabilities.find(
|
|
903
|
-
(a) => (a
|
|
931
|
+
(a) => getPrivateShuttleAvailabilityOptionId(a) === optionId
|
|
904
932
|
);
|
|
933
|
+
const profileOptionPrice = resourcePriceByOption?.[optionId]?.[currency];
|
|
934
|
+
if (profileOptionPrice != null) {
|
|
935
|
+
return applyResourceAdjustments(profileOptionPrice, avail, currency);
|
|
936
|
+
}
|
|
937
|
+
if (currency === 'CAD' && opt?.pricing?.['RESOURCE'] != null) {
|
|
938
|
+
return applyResourceAdjustments(opt.pricing['RESOURCE'], avail, currency);
|
|
939
|
+
}
|
|
905
940
|
if (avail?.rates) {
|
|
906
|
-
const resourceRate = avail
|
|
907
|
-
(r) => r.rateId === 'RESOURCE' || r.category === 'RESOURCE'
|
|
908
|
-
);
|
|
941
|
+
const resourceRate = getResourceRate(avail);
|
|
909
942
|
if (resourceRate?.priceByCurrency?.[currency] != null)
|
|
910
943
|
return resourceRate.priceByCurrency[currency];
|
|
911
944
|
if (currency === 'CAD' && resourceRate?.price != null) return resourceRate.price;
|
|
912
945
|
}
|
|
913
|
-
if (
|
|
914
|
-
return resourcePriceByOption[optionId][currency];
|
|
915
|
-
if (resourcePriceByCurrency?.[currency] != null) return resourcePriceByCurrency[currency];
|
|
916
|
-
const opt = activeOptions.find((o) => o.optionId === optionId);
|
|
946
|
+
if (currency === 'CAD' && resourcePriceByCurrency?.[currency] != null) return resourcePriceByCurrency[currency];
|
|
917
947
|
return opt?.pricing?.['RESOURCE'] ?? 0;
|
|
918
948
|
},
|
|
919
949
|
[
|
|
@@ -927,10 +957,20 @@ export function PrivateShuttleBookingFlow({
|
|
|
927
957
|
);
|
|
928
958
|
|
|
929
959
|
const resourcePrice = useMemo(() => {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
960
|
+
const selectedProfileOptionPrice = selectedOption
|
|
961
|
+
? resourcePriceByOption?.[selectedOption]?.[currency]
|
|
962
|
+
: undefined;
|
|
963
|
+
if (selectedProfileOptionPrice != null) {
|
|
964
|
+
return applyResourceAdjustments(selectedProfileOptionPrice, selectedAvailability, currency);
|
|
965
|
+
}
|
|
966
|
+
if (currency === 'CAD' && selectedOptionConfig?.pricing?.['RESOURCE'] != null)
|
|
967
|
+
return applyResourceAdjustments(
|
|
968
|
+
selectedOptionConfig.pricing['RESOURCE'],
|
|
969
|
+
selectedAvailability,
|
|
970
|
+
currency
|
|
933
971
|
);
|
|
972
|
+
if (selectedAvailability?.rates) {
|
|
973
|
+
const resourceRate = getResourceRate(selectedAvailability);
|
|
934
974
|
if (resourceRate) {
|
|
935
975
|
const fromRate =
|
|
936
976
|
resourceRate.priceByCurrency?.[currency] ??
|
|
@@ -938,12 +978,12 @@ export function PrivateShuttleBookingFlow({
|
|
|
938
978
|
if (fromRate != null) return fromRate;
|
|
939
979
|
}
|
|
940
980
|
}
|
|
941
|
-
if (
|
|
942
|
-
return resourcePriceByOption[selectedOption][currency];
|
|
943
|
-
if (resourcePriceByCurrency?.[currency] != null) return resourcePriceByCurrency[currency];
|
|
981
|
+
if (currency === 'CAD' && resourcePriceByCurrency?.[currency] != null) return resourcePriceByCurrency[currency];
|
|
944
982
|
if (precomputedPrices?.['RESOURCE']?.[currency] != null)
|
|
945
983
|
return precomputedPrices['RESOURCE'][currency];
|
|
946
|
-
|
|
984
|
+
if (selectedOptionConfig?.pricing?.['RESOURCE'] != null)
|
|
985
|
+
return selectedOptionConfig.pricing['RESOURCE'];
|
|
986
|
+
return 0;
|
|
947
987
|
}, [
|
|
948
988
|
selectedAvailability,
|
|
949
989
|
resourcePriceByOption,
|
|
@@ -1173,7 +1213,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
1173
1213
|
setSelectedOption(opt.optionId);
|
|
1174
1214
|
const dateAvailabilities = availabilitiesByDate[selectedDate] || [];
|
|
1175
1215
|
const avail = dateAvailabilities.find(
|
|
1176
|
-
(a) => (a
|
|
1216
|
+
(a) => getPrivateShuttleAvailabilityOptionId(a) === opt.optionId
|
|
1177
1217
|
);
|
|
1178
1218
|
setSelectedAvailability(avail || null);
|
|
1179
1219
|
}
|
|
@@ -1231,7 +1271,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
1231
1271
|
if (selectedDate) {
|
|
1232
1272
|
const dateAvailabilities = availabilitiesByDate[selectedDate] || [];
|
|
1233
1273
|
const avail = dateAvailabilities.find(
|
|
1234
|
-
(a) => (a
|
|
1274
|
+
(a) => getPrivateShuttleAvailabilityOptionId(a) === optionId
|
|
1235
1275
|
);
|
|
1236
1276
|
setSelectedAvailability(avail || null);
|
|
1237
1277
|
} else {
|
|
@@ -331,10 +331,14 @@ export function computePriceBreakdown(
|
|
|
331
331
|
|
|
332
332
|
// Subtotal in display currency = base + adjustments; pass so final price is (subtotal + fees) * (1+tax) and breakdown adds up
|
|
333
333
|
const subtotalInDisplayCurrency = baseAmount + adjustmentSum;
|
|
334
|
+
const backendPriceForDetails =
|
|
335
|
+
currency === BASE_CURRENCY && subtotalInDisplayCurrency > 0
|
|
336
|
+
? subtotalInDisplayCurrency
|
|
337
|
+
: backendPriceCAD;
|
|
334
338
|
const details = computeTicketPriceDetails(
|
|
335
339
|
pricingConfig,
|
|
336
340
|
currency,
|
|
337
|
-
|
|
341
|
+
backendPriceForDetails,
|
|
338
342
|
hasFees,
|
|
339
343
|
subtotalInDisplayCurrency > 0 ? subtotalInDisplayCurrency : baseInDisplayCurrency
|
|
340
344
|
);
|