@ticketboothapp/booking 1.2.56 → 1.2.58
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/BookingFlow.tsx +664 -168
- package/src/components/booking/Calendar.module.css +6 -1
- package/src/components/booking/ChangeBookingDialog.tsx +41 -178
- package/src/components/booking/CheckoutForm.tsx +29 -4
- package/src/components/booking/DefaultTermsContent.tsx +178 -0
- package/src/components/booking/PriceBreakdown.tsx +92 -27
- package/src/components/booking/PriceSummary.tsx +77 -4
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +83 -27
- package/src/components/booking/TermsAcceptance.tsx +2 -1
- package/src/components/booking/booking-flow-ui.ts +42 -0
- package/src/components/booking/booking-flow.css +32 -0
- package/src/components/booking/change-booking-compare.module.css +97 -0
- package/src/components/booking/change-booking-compare.tsx +228 -0
- package/src/components/booking/provider-dashboard-change-booking.ts +47 -0
- package/src/index.ts +19 -0
- package/src/runtime/types.ts +2 -1
|
@@ -76,6 +76,7 @@ import { getItineraryStepLabel } from '../../lib/booking/itinerary-display';
|
|
|
76
76
|
import { MANAGE_BOOKING_FROM_CHANGE_PAYMENT, MANAGE_BOOKING_QUERY_FROM } from '../../lib/manage-booking-post-checkout';
|
|
77
77
|
import { useBookingHost } from '../../runtime';
|
|
78
78
|
import type { BookingFlowUiOptions } from './booking-flow-ui';
|
|
79
|
+
import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
|
|
79
80
|
import { BOOKING_FLOW_ABANDON_EVENT } from '../../providers/booking-dialog-provider';
|
|
80
81
|
|
|
81
82
|
/** Live selection snapshot for change-booking compare UI (parent dialog). */
|
|
@@ -134,6 +135,14 @@ function extractTrailingQty(label: string): { baseLabel: string; qty: number } {
|
|
|
134
135
|
return { baseLabel, qty };
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
function normalizeLineLabelForCompare(label: string): string {
|
|
139
|
+
return label
|
|
140
|
+
.toLowerCase()
|
|
141
|
+
.replace(/\([^)]*\)/g, '')
|
|
142
|
+
.replace(/[^a-z0-9]+/g, '')
|
|
143
|
+
.trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
137
146
|
function deriveAddOnSelectionsFromReceiptLines(
|
|
138
147
|
addOns: AddOn[],
|
|
139
148
|
lines: Array<{ type?: string; label?: string; amount?: number; quantity?: number }>
|
|
@@ -308,6 +317,11 @@ interface BookingFlowProps {
|
|
|
308
317
|
availabilityPricingProfileId?: string | null;
|
|
309
318
|
/** When set (e.g. partner portal), get-availabilities filters cancellation policies by this profile. */
|
|
310
319
|
availabilityCancellationPolicyProfileId?: string | null;
|
|
320
|
+
/**
|
|
321
|
+
* Provider dashboard: with `mode="change"`, submit via this callback instead of customer self-serve
|
|
322
|
+
* quote and payment (`quoteChangeBooking`, Stripe).
|
|
323
|
+
*/
|
|
324
|
+
onChangeBooking?: (data: ProviderDashboardChangeBookingPayload) => Promise<void>;
|
|
311
325
|
}
|
|
312
326
|
|
|
313
327
|
function parseAvailabilityDateTime(value: string): Date {
|
|
@@ -602,7 +616,18 @@ export function BookingFlow({
|
|
|
602
616
|
partnerPortalBooking = false,
|
|
603
617
|
availabilityPricingProfileId,
|
|
604
618
|
availabilityCancellationPolicyProfileId,
|
|
619
|
+
onChangeBooking,
|
|
605
620
|
}: BookingFlowProps) {
|
|
621
|
+
const isManualOverrideEligibleLine = (line: { editable: boolean; type?: string; label?: string }): boolean => {
|
|
622
|
+
if (!line.editable) return false;
|
|
623
|
+
const type = (line.type ?? '').toUpperCase();
|
|
624
|
+
const label = (line.label ?? '').toLowerCase();
|
|
625
|
+
const isPromoLikeType =
|
|
626
|
+
type.includes('PROMO') || type.includes('DISCOUNT') || type.includes('VOUCHER') || type.includes('GIFT');
|
|
627
|
+
const isPromoLikeLabel =
|
|
628
|
+
label.includes('promo') || label.includes('discount') || label.includes('voucher') || label.includes('gift');
|
|
629
|
+
return !(isPromoLikeType || isPromoLikeLabel);
|
|
630
|
+
};
|
|
606
631
|
const { env, strings: defaultStrings, analytics, catalog } = useBookingHost();
|
|
607
632
|
const { t } = useTranslations();
|
|
608
633
|
const { locale } = useLocale();
|
|
@@ -638,6 +663,8 @@ export function BookingFlow({
|
|
|
638
663
|
changeWindowHoursBefore?: number | null;
|
|
639
664
|
} | null>(null);
|
|
640
665
|
const cancellationPolicyRef = useRef<HTMLDivElement>(null);
|
|
666
|
+
/** Dedupe parent updates from `onChangeFlowSelectionPreview` when serialized preview is unchanged. */
|
|
667
|
+
const lastChangeFlowPreviewKeyRef = useRef<string | null>(null);
|
|
641
668
|
const [promoCodeValidating, setPromoCodeValidating] = useState(false);
|
|
642
669
|
const [pickupLocationId, setPickupLocationId] = useState<string | null>(null);
|
|
643
670
|
const [pickupLocationSkipped, setPickupLocationSkipped] = useState(false);
|
|
@@ -739,6 +766,8 @@ export function BookingFlow({
|
|
|
739
766
|
const hasAutoSelectedPartnerPickupRef = useRef(false);
|
|
740
767
|
const handleDateSelectRef = useRef<(date: string) => void>(() => {});
|
|
741
768
|
const isChangeFlow = mode === 'change';
|
|
769
|
+
const isProviderDashboardChange = Boolean(onChangeBooking);
|
|
770
|
+
const isCustomerSelfServeChange = isChangeFlow && !isProviderDashboardChange;
|
|
742
771
|
|
|
743
772
|
useEffect(() => {
|
|
744
773
|
setPartnerAttributionConfirmed(false);
|
|
@@ -748,12 +777,14 @@ export function BookingFlow({
|
|
|
748
777
|
* user picks a different return time — baseline is the first auto-selected return for this outbound.
|
|
749
778
|
*/
|
|
750
779
|
const [implicitReturnBaselineId, setImplicitReturnBaselineId] = useState<string | null>(null);
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
780
|
+
/** Any change flow (self-serve or provider): promo from booking is fixed — show read-only, never add new. */
|
|
781
|
+
const lockedPromoCode =
|
|
782
|
+
isChangeFlow && initialValues?.promoCode?.trim()
|
|
783
|
+
? initialValues.promoCode.trim().toUpperCase()
|
|
784
|
+
: null;
|
|
785
|
+
/** Public self-serve only: cannot reduce tickets below original counts. */
|
|
755
786
|
const changeBookingMinimumQuantities = useMemo(() => {
|
|
756
|
-
if (!
|
|
787
|
+
if (!isCustomerSelfServeChange || !initialValues?.bookingItems?.length) return undefined;
|
|
757
788
|
const m: Record<string, number> = {};
|
|
758
789
|
for (const item of initialValues.bookingItems) {
|
|
759
790
|
const key = item.category?.trim();
|
|
@@ -761,7 +792,7 @@ export function BookingFlow({
|
|
|
761
792
|
m[key] = Math.max(0, Number(item.count) || 0);
|
|
762
793
|
}
|
|
763
794
|
return m;
|
|
764
|
-
}, [
|
|
795
|
+
}, [isCustomerSelfServeChange, initialValues?.bookingItems]);
|
|
765
796
|
const [adminChoiceData, setAdminChoiceData] = useState<{
|
|
766
797
|
reservationReference: string;
|
|
767
798
|
reservationExpiration?: string;
|
|
@@ -1671,27 +1702,27 @@ export function BookingFlow({
|
|
|
1671
1702
|
|
|
1672
1703
|
const initialAddOnMinQtyByKey = useMemo(() => {
|
|
1673
1704
|
const map = new Map<string, number>();
|
|
1674
|
-
if (!
|
|
1705
|
+
if (!isCustomerSelfServeChange) return map;
|
|
1675
1706
|
for (const sel of initialAddOnBaselineSelections) {
|
|
1676
1707
|
const key = `${sel.addOnId.trim()}::${sel.variantId?.trim() || ''}`;
|
|
1677
1708
|
map.set(key, (map.get(key) ?? 0) + Math.max(1, Number(sel.quantity) || 1));
|
|
1678
1709
|
}
|
|
1679
1710
|
return map;
|
|
1680
|
-
}, [
|
|
1711
|
+
}, [isCustomerSelfServeChange, initialAddOnBaselineSelections]);
|
|
1681
1712
|
|
|
1682
1713
|
const initialAddOnMinTotalByAddOnId = useMemo(() => {
|
|
1683
1714
|
const map = new Map<string, number>();
|
|
1684
|
-
if (!
|
|
1715
|
+
if (!isCustomerSelfServeChange) return map;
|
|
1685
1716
|
for (const sel of initialAddOnBaselineSelections) {
|
|
1686
1717
|
const addOnId = sel.addOnId.trim();
|
|
1687
1718
|
map.set(addOnId, (map.get(addOnId) ?? 0) + Math.max(1, Number(sel.quantity) || 1));
|
|
1688
1719
|
}
|
|
1689
1720
|
return map;
|
|
1690
|
-
}, [
|
|
1721
|
+
}, [isCustomerSelfServeChange, initialAddOnBaselineSelections]);
|
|
1691
1722
|
|
|
1692
1723
|
const applyChangeFlowAddOnFloor = useCallback(
|
|
1693
1724
|
(nextSelections: Array<{ addOnId: string; variantId?: string; quantity?: number }>) => {
|
|
1694
|
-
if (!
|
|
1725
|
+
if (!isCustomerSelfServeChange || initialAddOnMinQtyByKey.size === 0) return nextSelections;
|
|
1695
1726
|
const qtyByKey = new Map<string, number>();
|
|
1696
1727
|
for (const sel of nextSelections) {
|
|
1697
1728
|
const key = `${sel.addOnId.trim()}::${sel.variantId?.trim() || ''}`;
|
|
@@ -1718,7 +1749,7 @@ export function BookingFlow({
|
|
|
1718
1749
|
return { addOnId, variantId, quantity: qty };
|
|
1719
1750
|
});
|
|
1720
1751
|
},
|
|
1721
|
-
[
|
|
1752
|
+
[isCustomerSelfServeChange, initialAddOnMinQtyByKey, initialAddOnMinTotalByAddOnId]
|
|
1722
1753
|
);
|
|
1723
1754
|
|
|
1724
1755
|
const updateAddOnSelections = useCallback(
|
|
@@ -2042,7 +2073,7 @@ export function BookingFlow({
|
|
|
2042
2073
|
[pricingConfig?.fees]
|
|
2043
2074
|
);
|
|
2044
2075
|
|
|
2045
|
-
const
|
|
2076
|
+
const changeFlowTicketBookedUnitPriceByCategory = useMemo(() => {
|
|
2046
2077
|
if (!isChangeFlow || !originalReceipt?.lineItems?.length) return new Map<string, number>();
|
|
2047
2078
|
const amountByCategory = new Map<string, number>();
|
|
2048
2079
|
const qtyByCategory = new Map<string, number>();
|
|
@@ -2083,6 +2114,25 @@ export function BookingFlow({
|
|
|
2083
2114
|
if (totalAmount <= 0 || totalQty <= 0) return null;
|
|
2084
2115
|
return totalAmount / totalQty;
|
|
2085
2116
|
}, [isChangeFlow, originalReceipt?.lineItems, initialValues?.bookingItems]);
|
|
2117
|
+
const changeFlowBookedFeeUnitByNormalizedLabel = useMemo(() => {
|
|
2118
|
+
const feeUnitByLabel = new Map<string, number>();
|
|
2119
|
+
if (!isChangeFlow || !originalReceipt?.lineItems?.length) return feeUnitByLabel;
|
|
2120
|
+
const fallbackBookedQty =
|
|
2121
|
+
(initialValues?.bookingItems ?? []).reduce((sum, item) => sum + Math.max(0, Number(item.count) || 0), 0);
|
|
2122
|
+
for (const line of originalReceipt.lineItems) {
|
|
2123
|
+
const type = (line.type || '').trim().toUpperCase();
|
|
2124
|
+
if (!line.label || type === 'TICKET' || type === 'RETURN_OPTION' || type === 'TAX') continue;
|
|
2125
|
+
if (type.includes('PROMO') || type.includes('VOUCHER') || type.includes('GIFT')) continue;
|
|
2126
|
+
const amount = Number(line.amount ?? 0);
|
|
2127
|
+
const qtyRaw = Number(line.quantity ?? 0);
|
|
2128
|
+
const qty = qtyRaw > 0 ? qtyRaw : fallbackBookedQty;
|
|
2129
|
+
if (!Number.isFinite(amount) || !Number.isFinite(qty) || amount <= 0 || qty <= 0) continue;
|
|
2130
|
+
const key = normalizeLineLabelForCompare(line.label);
|
|
2131
|
+
if (!key) continue;
|
|
2132
|
+
feeUnitByLabel.set(key, amount / qty);
|
|
2133
|
+
}
|
|
2134
|
+
return feeUnitByLabel;
|
|
2135
|
+
}, [isChangeFlow, originalReceipt?.lineItems, initialValues?.bookingItems]);
|
|
2086
2136
|
|
|
2087
2137
|
const returnOptionsWithFloor = useMemo(() => {
|
|
2088
2138
|
const options = selectedAvailability?.returnOptions ?? [];
|
|
@@ -2156,13 +2206,11 @@ export function BookingFlow({
|
|
|
2156
2206
|
const backendInDisplayCurrency = rate.priceByCurrency?.[currency] ?? (currency === 'CAD' ? backendPriceCAD : 0);
|
|
2157
2207
|
const baseInDisplayCurrency = getBaseInDisplayCurrency(rate.category);
|
|
2158
2208
|
const built = buildRate(rate.category, backendPriceCAD, backendInDisplayCurrency, baseInDisplayCurrency, rate.appliedAdjustments ?? rate.applied_adjustments ?? []);
|
|
2159
|
-
const floorUnitPrice = changeFlowTicketPriceFloorByCategory.get(rate.category.toUpperCase());
|
|
2160
|
-
const price = floorUnitPrice != null ? Math.max(built.price, floorUnitPrice) : built.price;
|
|
2161
2209
|
return {
|
|
2162
2210
|
category: rate.category,
|
|
2163
2211
|
rateId: rate.rateId || rate.category,
|
|
2164
2212
|
available: rate.available,
|
|
2165
|
-
price,
|
|
2213
|
+
price: built.price,
|
|
2166
2214
|
priceCAD: built.priceCAD,
|
|
2167
2215
|
baseInDisplayCurrency: built.baseInDisplayCurrency,
|
|
2168
2216
|
appliedAdjustments: built.appliedAdjustments,
|
|
@@ -2172,19 +2220,17 @@ export function BookingFlow({
|
|
|
2172
2220
|
const baseInDisplayCurrency = getBaseInDisplayCurrency(p.category);
|
|
2173
2221
|
const backendInDisplayCurrency = currency === 'CAD' ? priceCADFromApi : 0;
|
|
2174
2222
|
const built = buildRate(p.category, priceCADFromApi, backendInDisplayCurrency, baseInDisplayCurrency, []);
|
|
2175
|
-
const floorUnitPrice = changeFlowTicketPriceFloorByCategory.get(p.category.toUpperCase());
|
|
2176
|
-
const price = floorUnitPrice != null ? Math.max(built.price, floorUnitPrice) : built.price;
|
|
2177
2223
|
return {
|
|
2178
2224
|
category: p.category,
|
|
2179
2225
|
rateId: p.category,
|
|
2180
2226
|
available: selectedAvailability.vacancies,
|
|
2181
|
-
price,
|
|
2227
|
+
price: built.price,
|
|
2182
2228
|
priceCAD: built.priceCAD,
|
|
2183
2229
|
baseInDisplayCurrency: built.baseInDisplayCurrency,
|
|
2184
2230
|
appliedAdjustments: built.appliedAdjustments,
|
|
2185
2231
|
};
|
|
2186
2232
|
}) || [];
|
|
2187
|
-
}, [selectedAvailability, currency, hasFees, pricingConfig, precomputedPricesByOption, activeOptions, isSimplifiedPricingView
|
|
2233
|
+
}, [selectedAvailability, currency, hasFees, pricingConfig, precomputedPricesByOption, activeOptions, isSimplifiedPricingView]);
|
|
2188
2234
|
|
|
2189
2235
|
// Price breakdown: mid-layer returns line items (base + one per rule/deal). UI renders each line; rate in brackets when used.
|
|
2190
2236
|
const getPriceBreakdown = useCallback((category: string, priceCAD: number, baseInDisplayCurrency: number | undefined, appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }> = []): PriceBreakdownData | null => {
|
|
@@ -2220,6 +2266,51 @@ export function BookingFlow({
|
|
|
2220
2266
|
[quantities, pricing, selectedReturnOptionWithFloor, pricingConfig, currency, hasFees, cancellationPolicyId]
|
|
2221
2267
|
);
|
|
2222
2268
|
const { totalQuantity, subtotal, tax, total: totalFromSummary, feeLineItems, returnPriceAdjustment, cancellationPolicyFee, isTaxIncludedInPrice, ticketLineItems } = orderSummary;
|
|
2269
|
+
const changeFlowInitialTicketQtyByCategory = useMemo(() => {
|
|
2270
|
+
const qtyByCategory = new Map<string, number>();
|
|
2271
|
+
if (!isChangeFlow || !initialValues?.bookingItems?.length) return qtyByCategory;
|
|
2272
|
+
for (const item of initialValues.bookingItems) {
|
|
2273
|
+
const category = item.category?.trim().toUpperCase();
|
|
2274
|
+
if (!category) continue;
|
|
2275
|
+
qtyByCategory.set(category, Math.max(0, Number(item.count) || 0));
|
|
2276
|
+
}
|
|
2277
|
+
return qtyByCategory;
|
|
2278
|
+
}, [isChangeFlow, initialValues?.bookingItems]);
|
|
2279
|
+
const changeFlowPreserveBookedTicketPricing = useMemo(() => {
|
|
2280
|
+
if (!isChangeFlow || !selectedAvailability || !initialValues?.dateTime) return false;
|
|
2281
|
+
const selectedMs = parseAvailabilityDateTime(selectedAvailability.dateTime).getTime();
|
|
2282
|
+
const initialMs = parseAvailabilityDateTime(initialValues.dateTime).getTime();
|
|
2283
|
+
if (selectedMs !== initialMs) return false;
|
|
2284
|
+
const selectedOptionId = selectedAvailability.productOptionId?.trim() || null;
|
|
2285
|
+
const initialOptionId = initialValues.productOptionId?.trim() || null;
|
|
2286
|
+
// If option IDs are missing, still protect pricing when date/time is unchanged.
|
|
2287
|
+
if (!selectedOptionId || !initialOptionId) return true;
|
|
2288
|
+
return selectedOptionId === initialOptionId;
|
|
2289
|
+
}, [isChangeFlow, selectedAvailability, initialValues?.dateTime, initialValues?.productOptionId]);
|
|
2290
|
+
const changeFlowProtectedTicketSubtotal = useMemo(() => {
|
|
2291
|
+
const currentTicketSubtotal = ticketLineItems.reduce(
|
|
2292
|
+
(sum, line) => sum + Math.max(0, Number(line.itemTotal) || 0),
|
|
2293
|
+
0,
|
|
2294
|
+
);
|
|
2295
|
+
if (!changeFlowPreserveBookedTicketPricing) return currentTicketSubtotal;
|
|
2296
|
+
return ticketLineItems.reduce((sum, line) => {
|
|
2297
|
+
const category = line.category?.trim().toUpperCase();
|
|
2298
|
+
const qty = Math.max(0, Number(line.qty) || 0);
|
|
2299
|
+
const liveUnitPrice = qty > 0 ? line.itemTotal / qty : 0;
|
|
2300
|
+
if (!category || qty <= 0) return sum;
|
|
2301
|
+
const bookedUnitPrice = changeFlowTicketBookedUnitPriceByCategory.get(category);
|
|
2302
|
+
if (bookedUnitPrice == null) return sum + line.itemTotal;
|
|
2303
|
+
const baselineQty = Math.max(0, changeFlowInitialTicketQtyByCategory.get(category) ?? 0);
|
|
2304
|
+
const protectedQty = Math.min(qty, baselineQty);
|
|
2305
|
+
const incrementalQty = Math.max(0, qty - baselineQty);
|
|
2306
|
+
return sum + protectedQty * bookedUnitPrice + incrementalQty * liveUnitPrice;
|
|
2307
|
+
}, 0);
|
|
2308
|
+
}, [
|
|
2309
|
+
changeFlowPreserveBookedTicketPricing,
|
|
2310
|
+
ticketLineItems,
|
|
2311
|
+
changeFlowTicketBookedUnitPriceByCategory,
|
|
2312
|
+
changeFlowInitialTicketQtyByCategory,
|
|
2313
|
+
]);
|
|
2223
2314
|
/** Round-trip party limit: both legs must fit — use the tighter of outbound vs return vacancies. */
|
|
2224
2315
|
const effectivePartySizeCap = useMemo(() => {
|
|
2225
2316
|
if (!selectedAvailability) return 0;
|
|
@@ -2283,7 +2374,63 @@ export function BookingFlow({
|
|
|
2283
2374
|
}, [addOnSelections, addOns]);
|
|
2284
2375
|
|
|
2285
2376
|
// Effective subtotal includes add-ons (for promo discount and total)
|
|
2286
|
-
const
|
|
2377
|
+
const currentTicketSubtotal = useMemo(
|
|
2378
|
+
() => ticketLineItems.reduce((sum, line) => sum + Math.max(0, Number(line.itemTotal) || 0), 0),
|
|
2379
|
+
[ticketLineItems],
|
|
2380
|
+
);
|
|
2381
|
+
const changeFlowInitialTotalTicketQty = useMemo(() => {
|
|
2382
|
+
let sum = 0;
|
|
2383
|
+
for (const qty of changeFlowInitialTicketQtyByCategory.values()) sum += qty;
|
|
2384
|
+
return sum;
|
|
2385
|
+
}, [changeFlowInitialTicketQtyByCategory]);
|
|
2386
|
+
const currentFeeSubtotal = useMemo(
|
|
2387
|
+
() => feeLineItems.reduce((sum, line) => sum + Math.max(0, Number(line.totalAmount) || 0), 0),
|
|
2388
|
+
[feeLineItems],
|
|
2389
|
+
);
|
|
2390
|
+
const changeFlowProtectedFeeSubtotal = useMemo(() => {
|
|
2391
|
+
if (!changeFlowPreserveBookedTicketPricing || totalQuantity <= 0) return currentFeeSubtotal;
|
|
2392
|
+
return feeLineItems.reduce((sum, line) => {
|
|
2393
|
+
const key = normalizeLineLabelForCompare(line.name || '');
|
|
2394
|
+
const currentUnitPrice = line.totalAmount / totalQuantity;
|
|
2395
|
+
const bookedUnitPrice = key ? changeFlowBookedFeeUnitByNormalizedLabel.get(key) : null;
|
|
2396
|
+
if (bookedUnitPrice == null) return sum + line.totalAmount;
|
|
2397
|
+
const protectedQty = Math.min(totalQuantity, changeFlowInitialTotalTicketQty);
|
|
2398
|
+
const incrementalQty = Math.max(0, totalQuantity - changeFlowInitialTotalTicketQty);
|
|
2399
|
+
return sum + protectedQty * bookedUnitPrice + incrementalQty * currentUnitPrice;
|
|
2400
|
+
}, 0);
|
|
2401
|
+
}, [
|
|
2402
|
+
changeFlowPreserveBookedTicketPricing,
|
|
2403
|
+
totalQuantity,
|
|
2404
|
+
currentFeeSubtotal,
|
|
2405
|
+
feeLineItems,
|
|
2406
|
+
changeFlowBookedFeeUnitByNormalizedLabel,
|
|
2407
|
+
changeFlowInitialTotalTicketQty,
|
|
2408
|
+
]);
|
|
2409
|
+
const changeFlowProtectedReturnAdjustment = useMemo(() => {
|
|
2410
|
+
if (!changeFlowPreserveBookedTicketPricing || totalQuantity <= 0) return returnPriceAdjustment;
|
|
2411
|
+
if (changeFlowReturnUnitFloorPerPerson == null) return returnPriceAdjustment;
|
|
2412
|
+
const currentUnitPrice = returnPriceAdjustment / totalQuantity;
|
|
2413
|
+
const protectedQty = Math.min(totalQuantity, changeFlowInitialTotalTicketQty);
|
|
2414
|
+
const incrementalQty = Math.max(0, totalQuantity - changeFlowInitialTotalTicketQty);
|
|
2415
|
+
return protectedQty * changeFlowReturnUnitFloorPerPerson + incrementalQty * currentUnitPrice;
|
|
2416
|
+
}, [
|
|
2417
|
+
changeFlowPreserveBookedTicketPricing,
|
|
2418
|
+
totalQuantity,
|
|
2419
|
+
returnPriceAdjustment,
|
|
2420
|
+
changeFlowReturnUnitFloorPerPerson,
|
|
2421
|
+
changeFlowInitialTotalTicketQty,
|
|
2422
|
+
]);
|
|
2423
|
+
const effectiveSubtotalBeforeAddOns =
|
|
2424
|
+
isChangeFlow && changeFlowPreserveBookedTicketPricing
|
|
2425
|
+
? subtotal -
|
|
2426
|
+
currentTicketSubtotal -
|
|
2427
|
+
currentFeeSubtotal -
|
|
2428
|
+
returnPriceAdjustment +
|
|
2429
|
+
changeFlowProtectedTicketSubtotal +
|
|
2430
|
+
changeFlowProtectedFeeSubtotal +
|
|
2431
|
+
changeFlowProtectedReturnAdjustment
|
|
2432
|
+
: subtotal;
|
|
2433
|
+
const effectiveSubtotal = effectiveSubtotalBeforeAddOns + addOnTotal;
|
|
2287
2434
|
|
|
2288
2435
|
/** Stable signature for promo discount API (avoid effect re-fire on object identity churn). */
|
|
2289
2436
|
const quantitiesSignature = useMemo(
|
|
@@ -2315,6 +2462,30 @@ export function BookingFlow({
|
|
|
2315
2462
|
return [...feeLineItems, ...addOnLines];
|
|
2316
2463
|
}, [feeLineItems, addOnSelections, addOns]);
|
|
2317
2464
|
|
|
2465
|
+
const providerPricingUi = flowUi?.providerDashboardChangePricingUi;
|
|
2466
|
+
const providerQuotedLines = providerPricingUi?.quotedLines ?? [];
|
|
2467
|
+
const providerEditableLines = providerQuotedLines.filter((line) => isManualOverrideEligibleLine(line));
|
|
2468
|
+
const showProviderPricingInlineEditor =
|
|
2469
|
+
isProviderDashboardChange && isAdmin && isChangeFlow && (
|
|
2470
|
+
providerPricingUi?.loading ||
|
|
2471
|
+
providerPricingUi?.error != null ||
|
|
2472
|
+
providerQuotedLines.length > 0
|
|
2473
|
+
);
|
|
2474
|
+
const normalizeReceiptLabel = (value: string): string =>
|
|
2475
|
+
value
|
|
2476
|
+
.toLowerCase()
|
|
2477
|
+
.replace(/\([^)]*\)/g, '')
|
|
2478
|
+
.replace(/[^a-z0-9]+/g, '')
|
|
2479
|
+
.trim();
|
|
2480
|
+
const providerEditableLineByNormalizedLabel = useMemo(() => {
|
|
2481
|
+
const m = new Map<string, (typeof providerEditableLines)[number]>();
|
|
2482
|
+
for (const line of providerEditableLines) {
|
|
2483
|
+
const key = normalizeReceiptLabel(line.label ?? '');
|
|
2484
|
+
if (key) m.set(key, line);
|
|
2485
|
+
}
|
|
2486
|
+
return m;
|
|
2487
|
+
}, [providerEditableLines]);
|
|
2488
|
+
|
|
2318
2489
|
const checkoutPriceSummaryLines = useMemo((): PriceSummaryLine[] => {
|
|
2319
2490
|
if (!selectedAvailability) return [];
|
|
2320
2491
|
return [
|
|
@@ -2328,6 +2499,13 @@ export function BookingFlow({
|
|
|
2328
2499
|
);
|
|
2329
2500
|
return {
|
|
2330
2501
|
kind: 'ticket',
|
|
2502
|
+
lineKey:
|
|
2503
|
+
showProviderPricingInlineEditor
|
|
2504
|
+
? providerEditableLineByNormalizedLabel.get(normalizeReceiptLabel(line.category))?.lineKey
|
|
2505
|
+
: undefined,
|
|
2506
|
+
editable:
|
|
2507
|
+
showProviderPricingInlineEditor &&
|
|
2508
|
+
providerEditableLineByNormalizedLabel.has(normalizeReceiptLabel(line.category)),
|
|
2331
2509
|
category: line.category,
|
|
2332
2510
|
qty: line.qty,
|
|
2333
2511
|
itemTotal: line.itemTotal,
|
|
@@ -2362,6 +2540,25 @@ export function BookingFlow({
|
|
|
2362
2540
|
fee.name.toLowerCase().includes('license'));
|
|
2363
2541
|
return {
|
|
2364
2542
|
kind: 'line' as const,
|
|
2543
|
+
lineKey:
|
|
2544
|
+
showProviderPricingInlineEditor
|
|
2545
|
+
? providerEditableLineByNormalizedLabel.get(
|
|
2546
|
+
normalizeReceiptLabel(
|
|
2547
|
+
feeLineItems.some((f) => f.name === fee.name)
|
|
2548
|
+
? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
|
|
2549
|
+
: fee.name
|
|
2550
|
+
)
|
|
2551
|
+
)?.lineKey
|
|
2552
|
+
: undefined,
|
|
2553
|
+
editable:
|
|
2554
|
+
showProviderPricingInlineEditor &&
|
|
2555
|
+
providerEditableLineByNormalizedLabel.has(
|
|
2556
|
+
normalizeReceiptLabel(
|
|
2557
|
+
feeLineItems.some((f) => f.name === fee.name)
|
|
2558
|
+
? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
|
|
2559
|
+
: fee.name
|
|
2560
|
+
)
|
|
2561
|
+
),
|
|
2365
2562
|
label: feeLineItems.some((f) => f.name === fee.name)
|
|
2366
2563
|
? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
|
|
2367
2564
|
: fee.name,
|
|
@@ -2388,6 +2585,8 @@ export function BookingFlow({
|
|
|
2388
2585
|
effectiveCancellationPolicyLabel,
|
|
2389
2586
|
feeLineItemsWithAddOns,
|
|
2390
2587
|
feeLineItems,
|
|
2588
|
+
showProviderPricingInlineEditor,
|
|
2589
|
+
providerEditableLineByNormalizedLabel,
|
|
2391
2590
|
]);
|
|
2392
2591
|
|
|
2393
2592
|
// Promo discount from backend (order-level only; rates are pre-promo)
|
|
@@ -2519,10 +2718,30 @@ export function BookingFlow({
|
|
|
2519
2718
|
: totalPrice;
|
|
2520
2719
|
const changeSelectionDetails = useMemo(() => {
|
|
2521
2720
|
if (!isChangeFlow || !initialValues) {
|
|
2522
|
-
return {
|
|
2721
|
+
return {
|
|
2722
|
+
hasChangesFromInitial: true,
|
|
2723
|
+
hasOperationalChangesFromInitial: true,
|
|
2724
|
+
dateChanged: false,
|
|
2725
|
+
ticketsChanged: false,
|
|
2726
|
+
optionChanged: false,
|
|
2727
|
+
pickupChanged: false,
|
|
2728
|
+
countsChanged: false,
|
|
2729
|
+
addOnsChanged: false,
|
|
2730
|
+
returnChanged: false,
|
|
2731
|
+
};
|
|
2523
2732
|
}
|
|
2524
2733
|
if (!selectedAvailability) {
|
|
2525
|
-
return {
|
|
2734
|
+
return {
|
|
2735
|
+
hasChangesFromInitial: false,
|
|
2736
|
+
hasOperationalChangesFromInitial: false,
|
|
2737
|
+
dateChanged: false,
|
|
2738
|
+
ticketsChanged: false,
|
|
2739
|
+
optionChanged: false,
|
|
2740
|
+
pickupChanged: false,
|
|
2741
|
+
countsChanged: false,
|
|
2742
|
+
addOnsChanged: false,
|
|
2743
|
+
returnChanged: false,
|
|
2744
|
+
};
|
|
2526
2745
|
}
|
|
2527
2746
|
const initialMs = initialValues.dateTime ? parseAvailabilityDateTime(initialValues.dateTime).getTime() : null;
|
|
2528
2747
|
const selectedMs = parseAvailabilityDateTime(selectedAvailability.dateTime).getTime();
|
|
@@ -2534,7 +2753,13 @@ export function BookingFlow({
|
|
|
2534
2753
|
const optionChanged = Boolean(
|
|
2535
2754
|
initialOpt && selectedOpt && initialOpt !== selectedOpt
|
|
2536
2755
|
);
|
|
2537
|
-
const
|
|
2756
|
+
const normalizePickupId = (value: string | null | undefined) => {
|
|
2757
|
+
const trimmed = value?.trim();
|
|
2758
|
+
return trimmed ? trimmed : null;
|
|
2759
|
+
};
|
|
2760
|
+
const pickupChanged =
|
|
2761
|
+
normalizePickupId(initialValues.pickupLocationId ?? null) !==
|
|
2762
|
+
normalizePickupId(pickupLocationId ?? null);
|
|
2538
2763
|
const normalizeCounts = (items: Array<{ category: string; count: number }> | null | undefined) => {
|
|
2539
2764
|
const map = new Map<string, number>();
|
|
2540
2765
|
for (const item of items ?? []) {
|
|
@@ -2580,7 +2805,16 @@ export function BookingFlow({
|
|
|
2580
2805
|
let returnChanged = false;
|
|
2581
2806
|
if (hasReturnOptions && selectedReturnOption) {
|
|
2582
2807
|
if (initialReturnId && selectedReturnId) {
|
|
2583
|
-
|
|
2808
|
+
if (initialReturnId === selectedReturnId) {
|
|
2809
|
+
returnChanged = false;
|
|
2810
|
+
} else if (initialReturnDt && selectedReturnDt) {
|
|
2811
|
+
// Some refreshes can rotate return availability IDs for the same departure time.
|
|
2812
|
+
returnChanged =
|
|
2813
|
+
parseAvailabilityDateTime(initialReturnDt).getTime() !==
|
|
2814
|
+
parseAvailabilityDateTime(selectedReturnDt).getTime();
|
|
2815
|
+
} else {
|
|
2816
|
+
returnChanged = true;
|
|
2817
|
+
}
|
|
2584
2818
|
} else if (initialReturnDt && selectedReturnDt) {
|
|
2585
2819
|
returnChanged =
|
|
2586
2820
|
parseAvailabilityDateTime(initialReturnDt).getTime() !==
|
|
@@ -2597,9 +2831,22 @@ export function BookingFlow({
|
|
|
2597
2831
|
countsChanged ||
|
|
2598
2832
|
addOnsChanged ||
|
|
2599
2833
|
returnChanged,
|
|
2834
|
+
// Authoritative for "real user change" gating in provider dashboard:
|
|
2835
|
+
// ignore option-id noise and only consider user-visible booking deltas.
|
|
2836
|
+
hasOperationalChangesFromInitial:
|
|
2837
|
+
dateChanged ||
|
|
2838
|
+
pickupChanged ||
|
|
2839
|
+
countsChanged ||
|
|
2840
|
+
addOnsChanged ||
|
|
2841
|
+
returnChanged,
|
|
2600
2842
|
dateChanged,
|
|
2601
2843
|
// Tickets line corresponds to "option + ticket counts"; add-ons and pickup changes affect itinerary but not the ticket label.
|
|
2602
2844
|
ticketsChanged: Boolean(optionChanged || countsChanged),
|
|
2845
|
+
optionChanged,
|
|
2846
|
+
pickupChanged,
|
|
2847
|
+
countsChanged,
|
|
2848
|
+
addOnsChanged,
|
|
2849
|
+
returnChanged,
|
|
2603
2850
|
};
|
|
2604
2851
|
}, [
|
|
2605
2852
|
isChangeFlow,
|
|
@@ -2612,25 +2859,72 @@ export function BookingFlow({
|
|
|
2612
2859
|
addOnSelections,
|
|
2613
2860
|
initialAddOnMinQtyByKey,
|
|
2614
2861
|
]);
|
|
2615
|
-
const hasChangeSelection =
|
|
2862
|
+
const hasChangeSelection =
|
|
2863
|
+
isProviderDashboardChange
|
|
2864
|
+
? changeSelectionDetails.hasOperationalChangesFromInitial
|
|
2865
|
+
: changeSelectionDetails.hasChangesFromInitial;
|
|
2616
2866
|
|
|
2617
2867
|
const changeFlowNeedsServerPrice =
|
|
2618
|
-
|
|
2868
|
+
isCustomerSelfServeChange &&
|
|
2619
2869
|
hasChangeSelection &&
|
|
2620
2870
|
!!initialValues?.bookingReference?.trim() &&
|
|
2621
2871
|
!!lastName.trim();
|
|
2622
2872
|
|
|
2623
|
-
const isChangeQuoteBlocked =
|
|
2624
|
-
const requiresReturnInChangeFlow =
|
|
2873
|
+
const isChangeQuoteBlocked = isCustomerSelfServeChange && latestChangeQuote?.canProceed === false;
|
|
2874
|
+
const requiresReturnInChangeFlow = isCustomerSelfServeChange && !!initialValues?.returnAvailabilityId?.trim();
|
|
2625
2875
|
const missingRequiredReturnSelection = requiresReturnInChangeFlow && !selectedReturnOption;
|
|
2626
2876
|
|
|
2627
2877
|
const changeFlowSubmitDisabled =
|
|
2628
2878
|
isChangeFlow &&
|
|
2629
2879
|
(missingRequiredReturnSelection ||
|
|
2630
|
-
(
|
|
2880
|
+
(isCustomerSelfServeChange &&
|
|
2881
|
+
changeFlowNeedsServerPrice &&
|
|
2882
|
+
(changeQuoteLoading || (!latestChangeQuote && !changeQuoteFetchError))));
|
|
2883
|
+
|
|
2884
|
+
const providerTotalsPreview =
|
|
2885
|
+
showProviderPricingInlineEditor && providerPricingUi?.totalsPreview
|
|
2886
|
+
? providerPricingUi.totalsPreview
|
|
2887
|
+
: null;
|
|
2888
|
+
const displayChangeFlowProposedTotal = providerTotalsPreview
|
|
2889
|
+
? providerTotalsPreview.totalAmount
|
|
2890
|
+
: changeFlowProposedTotal;
|
|
2891
|
+
const displayChangeFlowSubtotal = providerTotalsPreview
|
|
2892
|
+
? providerTotalsPreview.subtotalBeforeTax
|
|
2893
|
+
: effectiveSubtotal;
|
|
2894
|
+
const displayChangeFlowTax = providerTotalsPreview
|
|
2895
|
+
? providerTotalsPreview.taxAmount
|
|
2896
|
+
: effectiveTax;
|
|
2897
|
+
const providerHasEditedLineOverrides =
|
|
2898
|
+
isProviderDashboardChange &&
|
|
2899
|
+
providerQuotedLines.some((line) => {
|
|
2900
|
+
if (!isManualOverrideEligibleLine(line)) return false;
|
|
2901
|
+
const raw = providerPricingUi?.lineAmountInputs?.[line.lineKey];
|
|
2902
|
+
const parsed = raw == null || raw.trim() === '' ? line.amount : Number(raw);
|
|
2903
|
+
if (!Number.isFinite(parsed)) return false;
|
|
2904
|
+
const rounded = Math.round(parsed * 100) / 100;
|
|
2905
|
+
return Math.abs(rounded - line.amount) > 0.0001;
|
|
2906
|
+
});
|
|
2907
|
+
const providerHasAdditionalAdjustments =
|
|
2908
|
+
isProviderDashboardChange &&
|
|
2909
|
+
(providerPricingUi?.additionalAdjustments ?? []).some((adj) => {
|
|
2910
|
+
const parsed = Number((adj.amountInput ?? '').trim());
|
|
2911
|
+
const hasAmount = Number.isFinite(parsed) && parsed > 0;
|
|
2912
|
+
const currentAmount = hasAmount ? (Math.round(parsed * 100) / 100).toFixed(2) : '';
|
|
2913
|
+
const originalAmount = adj.originalAmountInput ?? '';
|
|
2914
|
+
const currentLabel = (adj.label ?? '').trim();
|
|
2915
|
+
const originalLabel = (adj.originalLabel ?? '').trim();
|
|
2916
|
+
const currentMode = adj.mode;
|
|
2917
|
+
const originalMode = adj.originalMode;
|
|
2918
|
+
if (!originalMode) return hasAmount || currentLabel.length > 0;
|
|
2919
|
+
return currentAmount !== originalAmount || currentLabel !== originalLabel || currentMode !== originalMode;
|
|
2920
|
+
});
|
|
2921
|
+
const hasEffectiveChangeSelection =
|
|
2922
|
+
hasChangeSelection || providerHasEditedLineOverrides || providerHasAdditionalAdjustments;
|
|
2631
2923
|
|
|
2632
2924
|
const changeFlowClientEstimateDue = originalReceipt
|
|
2633
|
-
?
|
|
2925
|
+
? (isProviderDashboardChange
|
|
2926
|
+
? displayChangeFlowProposedTotal - originalReceipt.total
|
|
2927
|
+
: Math.max(displayChangeFlowProposedTotal - originalReceipt.total, 0))
|
|
2634
2928
|
: totalPrice;
|
|
2635
2929
|
|
|
2636
2930
|
/**
|
|
@@ -2638,12 +2932,24 @@ export function BookingFlow({
|
|
|
2638
2932
|
* Quote is still required before submit (session + canProceed); `clientProposedTotal` on quote keeps BE in sync.
|
|
2639
2933
|
*/
|
|
2640
2934
|
const changeFlowAmountDueRaw = isChangeFlow ? changeFlowClientEstimateDue : totalPrice;
|
|
2641
|
-
const changeFlowAmountDue =
|
|
2642
|
-
|
|
2935
|
+
const changeFlowAmountDue = isChangeFlow
|
|
2936
|
+
? (() => {
|
|
2937
|
+
const rounded = Math.round(changeFlowAmountDueRaw * 100) / 100;
|
|
2938
|
+
return Math.abs(rounded) < 0.01 ? 0 : rounded;
|
|
2939
|
+
})()
|
|
2940
|
+
: changeFlowAmountDueRaw;
|
|
2643
2941
|
|
|
2644
2942
|
const changeCheckoutButtonLabel = (() => {
|
|
2645
2943
|
if (!isChangeFlow) return undefined;
|
|
2646
|
-
if (!
|
|
2944
|
+
if (!hasEffectiveChangeSelection) return undefined;
|
|
2945
|
+
if (isProviderDashboardChange) {
|
|
2946
|
+
const est = Math.round(changeFlowClientEstimateDue * 100) / 100;
|
|
2947
|
+
return est > 0
|
|
2948
|
+
? `Change booking (${formatCurrencyAmount(est, currency, locale as 'en' | 'fr')})`
|
|
2949
|
+
: est < 0
|
|
2950
|
+
? `Change booking (${formatCurrencyAmount(est, currency, locale as 'en' | 'fr')})`
|
|
2951
|
+
: 'Change booking (no charge)';
|
|
2952
|
+
}
|
|
2647
2953
|
if (changeFlowNeedsServerPrice) {
|
|
2648
2954
|
if (changeQuoteLoading) {
|
|
2649
2955
|
const tr = t('booking.updatingPrice');
|
|
@@ -2679,8 +2985,39 @@ export function BookingFlow({
|
|
|
2679
2985
|
const checkoutFormError =
|
|
2680
2986
|
(error || '') ||
|
|
2681
2987
|
(missingRequiredReturnSelection ? 'Removing return option in self-serve is not available. Please contact support.' : '') ||
|
|
2682
|
-
(isChangeQuoteBlocked ? (latestChangeQuote?.reasonIfBlocked ?? '') : '') ||
|
|
2683
|
-
(changeQuoteFetchError ?? '');
|
|
2988
|
+
(isCustomerSelfServeChange && isChangeQuoteBlocked ? (latestChangeQuote?.reasonIfBlocked ?? '') : '') ||
|
|
2989
|
+
(isCustomerSelfServeChange ? changeQuoteFetchError ?? '' : '');
|
|
2990
|
+
|
|
2991
|
+
const providerPricingOverrides =
|
|
2992
|
+
isProviderDashboardChange && providerQuotedLines.length > 0
|
|
2993
|
+
? providerQuotedLines
|
|
2994
|
+
.filter((line) => isManualOverrideEligibleLine(line))
|
|
2995
|
+
.map((line) => {
|
|
2996
|
+
const raw = providerPricingUi?.lineAmountInputs?.[line.lineKey];
|
|
2997
|
+
const parsed = raw == null || raw.trim() === '' ? line.amount : Number(raw);
|
|
2998
|
+
if (!Number.isFinite(parsed)) return null;
|
|
2999
|
+
const rounded = Math.round(parsed * 100) / 100;
|
|
3000
|
+
return Math.abs(rounded - line.amount) > 0.0001
|
|
3001
|
+
? { lineKey: line.lineKey, amount: rounded, reason: 'Provider dashboard override' }
|
|
3002
|
+
: null;
|
|
3003
|
+
})
|
|
3004
|
+
.filter((v): v is { lineKey: string; amount: number; reason: string } => v != null)
|
|
3005
|
+
: [];
|
|
3006
|
+
const providerAdditionalAdjustments =
|
|
3007
|
+
isProviderDashboardChange
|
|
3008
|
+
? (providerPricingUi?.additionalAdjustments ?? [])
|
|
3009
|
+
.map((adj) => {
|
|
3010
|
+
const parsed = Number((adj.amountInput ?? '').trim());
|
|
3011
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
|
3012
|
+
const rounded = Math.round(parsed * 100) / 100;
|
|
3013
|
+
const signed = adj.mode === 'DISCOUNT' ? -rounded : rounded;
|
|
3014
|
+
return {
|
|
3015
|
+
label: adj.label?.trim() || (signed < 0 ? 'Provider discount' : 'Provider charge'),
|
|
3016
|
+
amount: signed,
|
|
3017
|
+
};
|
|
3018
|
+
})
|
|
3019
|
+
.filter((v): v is { label: string; amount: number } => v != null)
|
|
3020
|
+
: [];
|
|
2684
3021
|
|
|
2685
3022
|
const changeFlowSelectionPreview = useMemo((): ChangeFlowSelectionPreview | null => {
|
|
2686
3023
|
if (!isChangeFlow) return null;
|
|
@@ -2718,8 +3055,29 @@ export function BookingFlow({
|
|
|
2718
3055
|
]);
|
|
2719
3056
|
|
|
2720
3057
|
useEffect(() => {
|
|
2721
|
-
if (!isChangeFlow
|
|
2722
|
-
|
|
3058
|
+
if (!isChangeFlow) {
|
|
3059
|
+
lastChangeFlowPreviewKeyRef.current = null;
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
if (!onChangeFlowSelectionPreview) return;
|
|
3063
|
+
const next = changeFlowSelectionPreview;
|
|
3064
|
+
const key =
|
|
3065
|
+
next === null
|
|
3066
|
+
? 'null'
|
|
3067
|
+
: JSON.stringify({
|
|
3068
|
+
tourName: next.tourName,
|
|
3069
|
+
dateTime: next.dateTime,
|
|
3070
|
+
ticketsLine: next.ticketsLine,
|
|
3071
|
+
itinerarySteps: next.itinerarySteps,
|
|
3072
|
+
dateChanged: next.dateChanged,
|
|
3073
|
+
ticketsChanged: next.ticketsChanged,
|
|
3074
|
+
hasChangesFromInitial: next.hasChangesFromInitial,
|
|
3075
|
+
selectionTotal: next.selectionTotal,
|
|
3076
|
+
selectionCurrency: next.selectionCurrency,
|
|
3077
|
+
});
|
|
3078
|
+
if (key === lastChangeFlowPreviewKeyRef.current) return;
|
|
3079
|
+
lastChangeFlowPreviewKeyRef.current = key;
|
|
3080
|
+
onChangeFlowSelectionPreview(next);
|
|
2723
3081
|
}, [isChangeFlow, changeFlowSelectionPreview, onChangeFlowSelectionPreview]);
|
|
2724
3082
|
|
|
2725
3083
|
useEffect(() => {
|
|
@@ -2747,9 +3105,10 @@ export function BookingFlow({
|
|
|
2747
3105
|
|
|
2748
3106
|
/** Debounced server quote so CTA + “amount owed” match PaymentIntent; avoids free confirm when FE estimate ≠ BE. */
|
|
2749
3107
|
useEffect(() => {
|
|
2750
|
-
if (!
|
|
3108
|
+
if (!isCustomerSelfServeChange) {
|
|
2751
3109
|
setChangeQuoteLoading(false);
|
|
2752
3110
|
setChangeQuoteFetchError(null);
|
|
3111
|
+
setLatestChangeQuote(null);
|
|
2753
3112
|
return;
|
|
2754
3113
|
}
|
|
2755
3114
|
|
|
@@ -2829,7 +3188,7 @@ export function BookingFlow({
|
|
|
2829
3188
|
window.clearTimeout(timer);
|
|
2830
3189
|
};
|
|
2831
3190
|
}, [
|
|
2832
|
-
|
|
3191
|
+
isCustomerSelfServeChange,
|
|
2833
3192
|
hasChangeSelection,
|
|
2834
3193
|
selectedAvailability,
|
|
2835
3194
|
selectedAvailability?.dateTime,
|
|
@@ -3339,31 +3698,33 @@ export function BookingFlow({
|
|
|
3339
3698
|
setError('Removing return option in self-serve is not available. Please contact support.');
|
|
3340
3699
|
return;
|
|
3341
3700
|
}
|
|
3342
|
-
|
|
3343
|
-
// Validate email (required)
|
|
3344
|
-
if (!email) {
|
|
3345
|
-
setError(t('booking.enterEmail') || 'Please enter your email address');
|
|
3346
|
-
return;
|
|
3347
|
-
}
|
|
3348
|
-
|
|
3349
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
3350
|
-
setError(t('booking.invalidEmail') || 'Please enter a valid email address');
|
|
3351
|
-
return;
|
|
3352
|
-
}
|
|
3353
3701
|
|
|
3354
|
-
|
|
3355
|
-
if (!
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3702
|
+
const skipContactFields = isProviderDashboardChange && isChangeFlow;
|
|
3703
|
+
if (!skipContactFields) {
|
|
3704
|
+
// Validate email (required)
|
|
3705
|
+
if (!email) {
|
|
3706
|
+
setError(t('booking.enterEmail') || 'Please enter your email address');
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3359
3709
|
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3710
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
3711
|
+
setError(t('booking.invalidEmail') || 'Please enter a valid email address');
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
// Validate first name (required)
|
|
3716
|
+
if (!firstName?.trim()) {
|
|
3717
|
+
setError(t('booking.enterFirstName') || 'Please enter your first name');
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
// Validate last name (required for manage booking lookup)
|
|
3722
|
+
if (!lastName?.trim()) {
|
|
3723
|
+
setError(t('booking.enterLastName') || 'Please enter your last name');
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3364
3726
|
}
|
|
3365
|
-
|
|
3366
|
-
|
|
3727
|
+
|
|
3367
3728
|
// Allow checkout if pickup location is selected OR if user chose "I don't know"
|
|
3368
3729
|
if (product.pickupLocations && product.pickupLocations.length > 0 && !pickupLocationId && !pickupLocationSkipped) {
|
|
3369
3730
|
setError(t('booking.selectPickupLocation'));
|
|
@@ -3389,6 +3750,39 @@ export function BookingFlow({
|
|
|
3389
3750
|
return;
|
|
3390
3751
|
}
|
|
3391
3752
|
|
|
3753
|
+
if (onChangeBooking && isChangeFlow) {
|
|
3754
|
+
const pickupForChange = pickupLocationId
|
|
3755
|
+
? product.pickupLocations?.find((loc) => loc.id === pickupLocationId)
|
|
3756
|
+
: null;
|
|
3757
|
+
await onChangeBooking({
|
|
3758
|
+
productId: availabilityProductOptionId,
|
|
3759
|
+
dateTime: selectedAvailability.dateTime,
|
|
3760
|
+
bookingItems,
|
|
3761
|
+
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
|
|
3762
|
+
pickupLocationId: pickupLocationId ?? null,
|
|
3763
|
+
travelerHotel: pickupForChange?.name ?? null,
|
|
3764
|
+
startTime: selectedAvailability.dateTime ?? null,
|
|
3765
|
+
passengerCount: null,
|
|
3766
|
+
childSafetySeatsCount: null,
|
|
3767
|
+
foodRestrictions: null,
|
|
3768
|
+
addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
|
|
3769
|
+
cancellationPolicyId: cancellationPolicyId ?? initialValues?.cancellationPolicyId ?? null,
|
|
3770
|
+
promoCode: appliedPromoCode ?? null,
|
|
3771
|
+
newTotalAmount: displayChangeFlowProposedTotal,
|
|
3772
|
+
additionalHoursCount: null,
|
|
3773
|
+
pricingAdjustment:
|
|
3774
|
+
providerPricingOverrides.length > 0 || providerAdditionalAdjustments.length > 0
|
|
3775
|
+
? {
|
|
3776
|
+
mode: 'MANUAL_LINES',
|
|
3777
|
+
lineOverrides: providerPricingOverrides,
|
|
3778
|
+
additionalAdjustments: providerAdditionalAdjustments,
|
|
3779
|
+
}
|
|
3780
|
+
: undefined,
|
|
3781
|
+
});
|
|
3782
|
+
setLoading(false);
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3392
3786
|
const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
|
|
3393
3787
|
clientChannelSource: inferClientBookingSourceFromProductIds(
|
|
3394
3788
|
product.productId,
|
|
@@ -3406,7 +3800,7 @@ export function BookingFlow({
|
|
|
3406
3800
|
let changeIntentIdForCheckout: string | undefined;
|
|
3407
3801
|
let changeBookingReferenceForPaidFlow: string | undefined;
|
|
3408
3802
|
|
|
3409
|
-
if (
|
|
3803
|
+
if (isCustomerSelfServeChange) {
|
|
3410
3804
|
const changeBookingReference = initialValues?.bookingReference?.trim();
|
|
3411
3805
|
const changeLastName = lastName.trim();
|
|
3412
3806
|
if (!changeBookingReference || !changeLastName) {
|
|
@@ -3552,7 +3946,7 @@ export function BookingFlow({
|
|
|
3552
3946
|
// Build checkout breakdown from the exact same values we show in the UI and Stripe modal.
|
|
3553
3947
|
// Backend will charge totalAmount and store this as the receipt so /manage matches.
|
|
3554
3948
|
const taxForBreakdown = effectivePromoDiscountAmount > 0 ? effectiveTax : tax;
|
|
3555
|
-
const amountDueForCheckout =
|
|
3949
|
+
const amountDueForCheckout = isCustomerSelfServeChange
|
|
3556
3950
|
? Math.max(0, changeFlowProposedTotal - (originalReceipt?.total ?? 0))
|
|
3557
3951
|
: totalPrice;
|
|
3558
3952
|
const lines = [
|
|
@@ -3674,7 +4068,7 @@ export function BookingFlow({
|
|
|
3674
4068
|
return;
|
|
3675
4069
|
}
|
|
3676
4070
|
|
|
3677
|
-
const paymentIntent =
|
|
4071
|
+
const paymentIntent = isCustomerSelfServeChange
|
|
3678
4072
|
? await createChangeBookingPaymentIntent(
|
|
3679
4073
|
(() => {
|
|
3680
4074
|
const id = changeIntentIdForCheckout ?? latestChangeQuote?.changeIntentId;
|
|
@@ -3811,7 +4205,7 @@ export function BookingFlow({
|
|
|
3811
4205
|
// Paid change: always return to stable ref+lastName + explicit intent (not reservationRef).
|
|
3812
4206
|
// /manage-booking runs bounded refresh only when `from=change_payment` (see manage-booking page).
|
|
3813
4207
|
successUrlOverride:
|
|
3814
|
-
|
|
4208
|
+
isCustomerSelfServeChange && changeBookingReferenceForPaidFlow
|
|
3815
4209
|
? (() => {
|
|
3816
4210
|
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
3817
4211
|
const ref = encodeURIComponent(
|
|
@@ -3839,7 +4233,7 @@ export function BookingFlow({
|
|
|
3839
4233
|
promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
|
|
3840
4234
|
discountLabel: appliedPromoCode ? `Promo: ${appliedPromoCode}` : (originalReceipt?.promoLabel || undefined),
|
|
3841
4235
|
changeTotals:
|
|
3842
|
-
|
|
4236
|
+
isCustomerSelfServeChange && originalReceipt
|
|
3843
4237
|
? {
|
|
3844
4238
|
previousTotal: originalReceipt.total,
|
|
3845
4239
|
newTotal: totalPrice,
|
|
@@ -4135,7 +4529,7 @@ export function BookingFlow({
|
|
|
4135
4529
|
currency={currency}
|
|
4136
4530
|
showCapacity={isAdmin}
|
|
4137
4531
|
extraDiscountPercent={calendarDiscountPercent}
|
|
4138
|
-
capDiscountToSelectedDate={isChangeFlow &&
|
|
4532
|
+
capDiscountToSelectedDate={isChangeFlow && changeFlowTicketBookedUnitPriceByCategory.size > 0}
|
|
4139
4533
|
/>
|
|
4140
4534
|
</div>
|
|
4141
4535
|
</div>
|
|
@@ -4261,7 +4655,7 @@ export function BookingFlow({
|
|
|
4261
4655
|
t={t}
|
|
4262
4656
|
onQuantityChange={handleQuantityChange}
|
|
4263
4657
|
minimumQuantities={changeBookingMinimumQuantities}
|
|
4264
|
-
ticketUnitFloorByCategory={isChangeFlow ?
|
|
4658
|
+
ticketUnitFloorByCategory={isChangeFlow ? changeFlowTicketBookedUnitPriceByCategory : undefined}
|
|
4265
4659
|
/>
|
|
4266
4660
|
)}
|
|
4267
4661
|
|
|
@@ -4273,48 +4667,71 @@ export function BookingFlow({
|
|
|
4273
4667
|
currency={currency}
|
|
4274
4668
|
locale={locale}
|
|
4275
4669
|
onSelectionsChange={updateAddOnSelections}
|
|
4276
|
-
minimumTotalByAddOnId={
|
|
4670
|
+
minimumTotalByAddOnId={isCustomerSelfServeChange ? initialAddOnMinTotalByAddOnId : undefined}
|
|
4277
4671
|
/>
|
|
4278
4672
|
)}
|
|
4279
4673
|
|
|
4280
4674
|
{/* Total and Checkout — shared PriceSummary component */}
|
|
4281
4675
|
{selectedAvailability && (
|
|
4282
4676
|
<CheckoutForm
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4677
|
+
priceSummaryLines={checkoutPriceSummaryLines}
|
|
4678
|
+
totalPrice={changeFlowAmountDue}
|
|
4679
|
+
totalSummaryLabel={
|
|
4680
|
+
isChangeFlow
|
|
4681
|
+
? (t('booking.totalOwedForBookingChange') &&
|
|
4682
|
+
t('booking.totalOwedForBookingChange') !== 'booking.totalOwedForBookingChange'
|
|
4683
|
+
? t('booking.totalOwedForBookingChange')
|
|
4684
|
+
: 'Total owed for booking difference')
|
|
4685
|
+
: undefined
|
|
4686
|
+
}
|
|
4687
|
+
subtotal={
|
|
4688
|
+
isChangeFlow
|
|
4689
|
+
? displayChangeFlowSubtotal
|
|
4690
|
+
: (subtotal !== totalFromSummary || effectivePromoDiscountAmount > 0 || addOnTotal > 0
|
|
4691
|
+
? effectiveSubtotal
|
|
4692
|
+
: undefined)
|
|
4693
|
+
}
|
|
4694
|
+
taxAmount={
|
|
4695
|
+
!isTaxIncludedInPrice &&
|
|
4696
|
+
(isChangeFlow ? displayChangeFlowTax : (effectivePromoDiscountAmount > 0 ? effectiveTax : tax)) > 0
|
|
4697
|
+
? (isChangeFlow ? displayChangeFlowTax : (effectivePromoDiscountAmount > 0 ? effectiveTax : tax))
|
|
4698
|
+
: 0
|
|
4699
|
+
}
|
|
4700
|
+
taxRate={pricingConfig?.taxRate}
|
|
4701
|
+
currency={currency}
|
|
4702
|
+
locale={locale}
|
|
4703
|
+
t={t}
|
|
4704
|
+
extraBetweenTaxAndTotal={
|
|
4705
|
+
<>
|
|
4706
|
+
{showProviderPricingInlineEditor && providerPricingUi?.error ? (
|
|
4707
|
+
<div className="mt-2 text-sm text-red-700">{providerPricingUi.error}</div>
|
|
4708
|
+
) : null}
|
|
4709
|
+
{showProviderPricingInlineEditor &&
|
|
4710
|
+
providerPricingUi?.loading &&
|
|
4711
|
+
providerQuotedLines.length === 0 ? (
|
|
4712
|
+
<div className="mt-2 text-sm text-stone-500">Loading price lines...</div>
|
|
4713
|
+
) : null}
|
|
4714
|
+
{showProviderPricingInlineEditor &&
|
|
4715
|
+
providerPricingUi?.helperText &&
|
|
4716
|
+
!providerPricingUi.error ? (
|
|
4717
|
+
<div className="mt-2 text-xs text-stone-500">{providerPricingUi.helperText}</div>
|
|
4718
|
+
) : null}
|
|
4719
|
+
{isChangeFlow && lockedPromoCode ? (
|
|
4720
|
+
<PromoCodeInput
|
|
4721
|
+
promoCodeInput={promoCodeInput}
|
|
4722
|
+
appliedPromoCode={appliedPromoCode}
|
|
4723
|
+
promoCodeError={promoCodeError}
|
|
4724
|
+
promoCodeValidating={promoCodeValidating}
|
|
4725
|
+
promoDiscountAmount={effectivePromoDiscountAmount}
|
|
4726
|
+
currency={currency}
|
|
4727
|
+
locale={locale}
|
|
4728
|
+
t={t}
|
|
4729
|
+
onInputChange={() => {}}
|
|
4730
|
+
onApply={() => {}}
|
|
4731
|
+
onRemove={() => {}}
|
|
4732
|
+
locked
|
|
4733
|
+
/>
|
|
4734
|
+
) : !isChangeFlow ? (
|
|
4318
4735
|
<PromoCodeInput
|
|
4319
4736
|
promoCodeInput={promoCodeInput}
|
|
4320
4737
|
appliedPromoCode={appliedPromoCode}
|
|
@@ -4339,71 +4756,150 @@ export function BookingFlow({
|
|
|
4339
4756
|
fetchedRangesRef.current = [];
|
|
4340
4757
|
}}
|
|
4341
4758
|
/>
|
|
4342
|
-
) : null
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4759
|
+
) : null}
|
|
4760
|
+
</>
|
|
4761
|
+
}
|
|
4762
|
+
extraBeforeSubtotal={
|
|
4763
|
+
showProviderPricingInlineEditor && (providerPricingUi?.additionalAdjustments?.length ?? 0) > 0 ? (
|
|
4764
|
+
<div className="space-y-1">
|
|
4765
|
+
{providerPricingUi?.additionalAdjustments?.map((adj) => (
|
|
4766
|
+
<div key={adj.id} className="flex items-center justify-between gap-2 text-sm">
|
|
4767
|
+
<div className="flex min-w-0 items-center gap-1">
|
|
4768
|
+
<button
|
|
4769
|
+
type="button"
|
|
4770
|
+
className="rounded border border-stone-300 px-1 text-xs text-stone-600 hover:bg-stone-100"
|
|
4771
|
+
onClick={() => providerPricingUi?.onRemoveAdditionalAdjustment?.(adj.id)}
|
|
4772
|
+
>
|
|
4773
|
+
-
|
|
4774
|
+
</button>
|
|
4775
|
+
<input
|
|
4776
|
+
type="text"
|
|
4777
|
+
className="w-40 rounded border border-stone-300 px-2 py-0.5 text-sm"
|
|
4778
|
+
placeholder="Line description"
|
|
4779
|
+
value={adj.label}
|
|
4780
|
+
onChange={(e) =>
|
|
4781
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, { label: e.target.value })
|
|
4782
|
+
}
|
|
4783
|
+
/>
|
|
4784
|
+
</div>
|
|
4785
|
+
<div className="flex items-center gap-1">
|
|
4786
|
+
<select
|
|
4787
|
+
className="rounded border border-stone-300 px-1 py-0.5 text-xs"
|
|
4788
|
+
value={adj.mode}
|
|
4789
|
+
onChange={(e) =>
|
|
4790
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
|
|
4791
|
+
mode: e.target.value as 'DISCOUNT' | 'CHARGE',
|
|
4792
|
+
})
|
|
4793
|
+
}
|
|
4794
|
+
>
|
|
4795
|
+
<option value="DISCOUNT">-</option>
|
|
4796
|
+
<option value="CHARGE">+</option>
|
|
4797
|
+
</select>
|
|
4798
|
+
<input
|
|
4799
|
+
type="text"
|
|
4800
|
+
inputMode="decimal"
|
|
4801
|
+
className="h-6 w-24 rounded border border-stone-300 bg-white px-2 py-0.5 text-right text-sm font-medium leading-none text-stone-700"
|
|
4802
|
+
placeholder="0.00"
|
|
4803
|
+
value={adj.amountInput}
|
|
4804
|
+
onChange={(e) =>
|
|
4805
|
+
providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
|
|
4806
|
+
amountInput: e.target.value,
|
|
4807
|
+
})
|
|
4808
|
+
}
|
|
4809
|
+
/>
|
|
4810
|
+
</div>
|
|
4811
|
+
</div>
|
|
4812
|
+
))}
|
|
4813
|
+
<button
|
|
4814
|
+
type="button"
|
|
4815
|
+
className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
|
|
4816
|
+
onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
|
|
4817
|
+
>
|
|
4818
|
+
+ add line item
|
|
4819
|
+
</button>
|
|
4820
|
+
</div>
|
|
4821
|
+
) : showProviderPricingInlineEditor ? (
|
|
4822
|
+
<button
|
|
4823
|
+
type="button"
|
|
4824
|
+
className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
|
|
4825
|
+
onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
|
|
4826
|
+
>
|
|
4827
|
+
+ add line item
|
|
4828
|
+
</button>
|
|
4829
|
+
) : undefined
|
|
4830
|
+
}
|
|
4831
|
+
firstName={firstName}
|
|
4832
|
+
lastName={lastName}
|
|
4833
|
+
email={email}
|
|
4834
|
+
onFirstNameChange={(v) => { setFirstName(v); setError(''); }}
|
|
4835
|
+
onLastNameChange={(v) => { setLastName(v); setError(''); }}
|
|
4836
|
+
onEmailChange={(v) => { setEmail(v); setError(''); }}
|
|
4837
|
+
readOnlyContactFields={isChangeFlow}
|
|
4838
|
+
pickupLocations={
|
|
4839
|
+
selectedDate && product.pickupLocations && product.pickupLocations.length > 0
|
|
4840
|
+
? product.pickupLocations
|
|
4841
|
+
: undefined
|
|
4842
|
+
}
|
|
4843
|
+
destinations={product.destinations}
|
|
4844
|
+
pickupLocationId={pickupLocationId}
|
|
4845
|
+
pickupLocationSkipped={pickupLocationSkipped}
|
|
4846
|
+
selectedPickupLocation={selectedPickupLocation}
|
|
4847
|
+
highlightedPickupLocationIds={highlightedPickupLocationIds}
|
|
4848
|
+
onLocationSelect={(locationId) => {
|
|
4849
|
+
setPickupLocationId(locationId);
|
|
4850
|
+
setError('');
|
|
4851
|
+
if (locationId === null && pickupLocationSkipped) {
|
|
4852
|
+
setPickupLocationSkipped(false);
|
|
4853
|
+
} else if (locationId !== null) {
|
|
4854
|
+
setPickupLocationSkipped(false);
|
|
4855
|
+
}
|
|
4856
|
+
}}
|
|
4857
|
+
onSkip={() => {
|
|
4858
|
+
setPickupLocationSkipped(true);
|
|
4859
|
+
setPickupLocationId(null);
|
|
4860
|
+
setError('');
|
|
4861
|
+
}}
|
|
4862
|
+
onChangePickup={() => {
|
|
4863
|
+
setPickupLocationId(null);
|
|
4369
4864
|
setPickupLocationSkipped(false);
|
|
4865
|
+
}}
|
|
4866
|
+
termsAccepted={termsAccepted}
|
|
4867
|
+
onTermsChange={(checked) => {
|
|
4868
|
+
setTermsAccepted(checked);
|
|
4869
|
+
setTermsAcceptedAt(checked ? new Date().toISOString() : null);
|
|
4870
|
+
}}
|
|
4871
|
+
isAdmin={isAdmin}
|
|
4872
|
+
showCommunicationAdminSection={!isChangeFlow}
|
|
4873
|
+
skipConfirmationCommunications={skipConfirmationCommunications}
|
|
4874
|
+
disableAutoCommunications={disableAutoCommunications}
|
|
4875
|
+
onSkipConfirmationChange={setSkipConfirmationCommunications}
|
|
4876
|
+
onDisableCommunicationsChange={setDisableAutoCommunications}
|
|
4877
|
+
error={checkoutFormError}
|
|
4878
|
+
loading={loading}
|
|
4879
|
+
totalQuantity={totalQuantity}
|
|
4880
|
+
onCheckout={handleCheckout}
|
|
4881
|
+
submitLabel={changeCheckoutButtonLabel ?? deferredInvoiceSubmitLabel}
|
|
4882
|
+
hideSubmitButton={
|
|
4883
|
+
showCheckoutModal ||
|
|
4884
|
+
showAdminPaymentChoice ||
|
|
4885
|
+
(isChangeFlow && (!hasEffectiveChangeSelection || isChangeQuoteBlocked))
|
|
4370
4886
|
}
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
skipConfirmationCommunications={skipConfirmationCommunications}
|
|
4388
|
-
disableAutoCommunications={disableAutoCommunications}
|
|
4389
|
-
onSkipConfirmationChange={setSkipConfirmationCommunications}
|
|
4390
|
-
onDisableCommunicationsChange={setDisableAutoCommunications}
|
|
4391
|
-
error={checkoutFormError}
|
|
4392
|
-
loading={loading}
|
|
4393
|
-
totalQuantity={totalQuantity}
|
|
4394
|
-
onCheckout={handleCheckout}
|
|
4395
|
-
submitLabel={changeCheckoutButtonLabel ?? deferredInvoiceSubmitLabel}
|
|
4396
|
-
hideSubmitButton={
|
|
4397
|
-
showCheckoutModal ||
|
|
4398
|
-
showAdminPaymentChoice ||
|
|
4399
|
-
(isChangeFlow && (!hasChangeSelection || isChangeQuoteBlocked))
|
|
4400
|
-
}
|
|
4401
|
-
submitDisabled={changeFlowSubmitDisabled}
|
|
4402
|
-
attributionSummary={flowUi?.partnerAttributionSummary}
|
|
4403
|
-
attributionConfirmLabel={flowUi?.partnerAttributionConfirmLabel}
|
|
4404
|
-
attributionConfirmed={partnerAttributionConfirmed}
|
|
4405
|
-
onAttributionConfirmedChange={setPartnerAttributionConfirmed}
|
|
4406
|
-
/>
|
|
4887
|
+
submitDisabled={changeFlowSubmitDisabled}
|
|
4888
|
+
attributionSummary={flowUi?.partnerAttributionSummary}
|
|
4889
|
+
attributionConfirmLabel={flowUi?.partnerAttributionConfirmLabel}
|
|
4890
|
+
attributionConfirmed={partnerAttributionConfirmed}
|
|
4891
|
+
onAttributionConfirmedChange={setPartnerAttributionConfirmed}
|
|
4892
|
+
lineAmountInputs={showProviderPricingInlineEditor ? providerPricingUi?.lineAmountInputs : undefined}
|
|
4893
|
+
onLineAmountInputChange={
|
|
4894
|
+
showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountInputChange : undefined
|
|
4895
|
+
}
|
|
4896
|
+
onLineAmountInputBlur={
|
|
4897
|
+
showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountInputBlur : undefined
|
|
4898
|
+
}
|
|
4899
|
+
onLineAmountReset={
|
|
4900
|
+
showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountReset : undefined
|
|
4901
|
+
}
|
|
4902
|
+
/>
|
|
4407
4903
|
)}
|
|
4408
4904
|
</div>
|
|
4409
4905
|
</>
|