@ticketboothapp/booking 1.2.96 → 1.2.97
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/CHANGE_BOOKING_BE_HANDOFF.md +11 -0
- package/package.json +1 -1
- package/src/components/booking/AdminChangeBookingFlow.tsx +183 -6
- package/src/components/booking/booking-flow-types.ts +2 -0
- package/src/components/booking/provider-dashboard-change-booking.ts +3 -0
- package/src/lib/booking-api.ts +3 -0
|
@@ -126,6 +126,17 @@ Same selection fields as `ChangeBookingQuoteRequest`, plus:
|
|
|
126
126
|
- `lineItems: Array<{ label?: string; amount?: number; type?: string; quantity?: number }>`
|
|
127
127
|
- `feAmountDueMajorUnits?` (optional signed delta the FE displays; `newTotal - previousTotal`)
|
|
128
128
|
|
|
129
|
+
For admin/provider cross-product changes, FE also sends:
|
|
130
|
+
|
|
131
|
+
- `newParentProductId`: destination parent catalog product id (`p_...`)
|
|
132
|
+
- `newProductId`: destination product option id selected from availability (`po_...` or equivalent), preserved for existing quote/apply compatibility
|
|
133
|
+
|
|
134
|
+
BE should persist both the destination parent product and selected option when applying the change. Capacity checks,
|
|
135
|
+
receipt pricing, add-ons, pickup locations, and itinerary display should resolve against `newParentProductId` +
|
|
136
|
+
`newProductId`, not the booking’s original product. Same-parent receipt-floor rules continue only when the booking
|
|
137
|
+
parent product equals `newParentProductId`; cross-parent changes should price from the destination catalog / FE
|
|
138
|
+
authoritative admin receipt path.
|
|
139
|
+
|
|
129
140
|
### Backend expectations
|
|
130
141
|
|
|
131
142
|
- Validate admin auth + booking/availability eligibility only.
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import { parseISO, addWeeks, format, isBefore, isAfter, startOfDay, endOfDay } f
|
|
|
5
5
|
import { formatInTimeZone, fromZonedTime } from 'date-fns-tz';
|
|
6
6
|
import {
|
|
7
7
|
getAvailabilities,
|
|
8
|
+
fetchProducts,
|
|
8
9
|
cancelReservation,
|
|
9
10
|
cancelReservationBestEffort,
|
|
10
11
|
createPaymentIntent,
|
|
@@ -687,12 +688,29 @@ function resolveInitialAvailabilityFromBooking(
|
|
|
687
688
|
return { selection: fallback, defer: false };
|
|
688
689
|
}
|
|
689
690
|
|
|
691
|
+
function isPrivateShuttleProduct(product: Pick<Product, 'productType'> | null | undefined): boolean {
|
|
692
|
+
return product?.productType === 'PRIVATE_SHUTTLE';
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function getAdminChangeEligibleProducts(products: Product[], currentProduct: Product): Product[] {
|
|
696
|
+
if (isPrivateShuttleProduct(currentProduct)) return [currentProduct];
|
|
697
|
+
const activeStandardProducts = products.filter(
|
|
698
|
+
(p) =>
|
|
699
|
+
p.status === 'ACTIVE' &&
|
|
700
|
+
!isPrivateShuttleProduct(p) &&
|
|
701
|
+
p.options?.some((o) => o.status === 'ACTIVE'),
|
|
702
|
+
);
|
|
703
|
+
return activeStandardProducts.some((p) => p.productId === currentProduct.productId)
|
|
704
|
+
? activeStandardProducts
|
|
705
|
+
: [currentProduct, ...activeStandardProducts];
|
|
706
|
+
}
|
|
707
|
+
|
|
690
708
|
/**
|
|
691
709
|
* Admin / provider-dashboard **change booking** — literal duplicate of {@link ChangeBookingFlow} for now
|
|
692
710
|
* so ticketbooth can diverge without affecting the public site flow.
|
|
693
711
|
*/
|
|
694
712
|
export function AdminChangeBookingFlow({
|
|
695
|
-
product,
|
|
713
|
+
product: initialProduct,
|
|
696
714
|
productId,
|
|
697
715
|
onBack,
|
|
698
716
|
currency: currencyFromParent,
|
|
@@ -714,6 +732,7 @@ export function AdminChangeBookingFlow({
|
|
|
714
732
|
availabilityPricingProfileId,
|
|
715
733
|
availabilityCancellationPolicyProfileId,
|
|
716
734
|
onChangeBooking,
|
|
735
|
+
changeProductOptions,
|
|
717
736
|
}: ChangeBookingFlowProps) {
|
|
718
737
|
/** Always the booking’s sold currency — not the site currency switcher / parent default. */
|
|
719
738
|
const currency = useMemo((): Currency => {
|
|
@@ -736,6 +755,19 @@ export function AdminChangeBookingFlow({
|
|
|
736
755
|
return !(isPromoLikeType || isPromoLikeLabel);
|
|
737
756
|
};
|
|
738
757
|
const { env, analytics } = useBookingHost();
|
|
758
|
+
const isInitialPrivateShuttleBooking = isPrivateShuttleProduct(initialProduct);
|
|
759
|
+
const [availableChangeProducts, setAvailableChangeProducts] = useState<Product[]>(
|
|
760
|
+
() => getAdminChangeEligibleProducts(changeProductOptions ?? [initialProduct], initialProduct),
|
|
761
|
+
);
|
|
762
|
+
const [selectedChangeProductId, setSelectedChangeProductId] = useState(initialProduct.productId);
|
|
763
|
+
const [changeProductsLoading, setChangeProductsLoading] = useState(false);
|
|
764
|
+
const [changeProductsError, setChangeProductsError] = useState<string | null>(null);
|
|
765
|
+
const product = useMemo(
|
|
766
|
+
() =>
|
|
767
|
+
availableChangeProducts.find((p) => p.productId === selectedChangeProductId) ??
|
|
768
|
+
initialProduct,
|
|
769
|
+
[availableChangeProducts, selectedChangeProductId, initialProduct],
|
|
770
|
+
);
|
|
739
771
|
const { t } = useTranslations();
|
|
740
772
|
const { locale } = useLocale();
|
|
741
773
|
const companyTimezone = useCompanyTimezone(); // Get timezone from context
|
|
@@ -752,6 +784,12 @@ export function AdminChangeBookingFlow({
|
|
|
752
784
|
} = useBookingApp();
|
|
753
785
|
const availabilitiesCache = useAvailabilitiesCache();
|
|
754
786
|
const isAdmin = permissions.viewerRole === 'admin';
|
|
787
|
+
const originalParentProductId = useMemo(() => {
|
|
788
|
+
const pid = initialValues?.productId?.trim();
|
|
789
|
+
if (pid && isParentProductId(pid)) return pid;
|
|
790
|
+
return initialProduct.productId;
|
|
791
|
+
}, [initialValues?.productId, initialProduct.productId]);
|
|
792
|
+
const selectedParentProductChanged = selectedChangeProductId !== originalParentProductId;
|
|
755
793
|
const [availabilities, setAvailabilities] = useState<Availability[]>([]);
|
|
756
794
|
const [selectedAvailability, setSelectedAvailability] = useState<Availability | null>(null);
|
|
757
795
|
const [selectedReturnOption, setSelectedReturnOption] = useState<ReturnOption | null>(null);
|
|
@@ -783,6 +821,83 @@ export function AdminChangeBookingFlow({
|
|
|
783
821
|
Array<{ id: string; label: string; amountInput: string; amountSign: 1 | -1 }>
|
|
784
822
|
>([]);
|
|
785
823
|
const adminCustomLineIdRef = useRef(0);
|
|
824
|
+
const previousSelectedChangeProductIdRef = useRef(selectedChangeProductId);
|
|
825
|
+
|
|
826
|
+
useEffect(() => {
|
|
827
|
+
if (changeProductOptions?.length) {
|
|
828
|
+
const nextProducts = getAdminChangeEligibleProducts(changeProductOptions, initialProduct);
|
|
829
|
+
setAvailableChangeProducts(nextProducts);
|
|
830
|
+
setSelectedChangeProductId((current) =>
|
|
831
|
+
nextProducts.some((p) => p.productId === current) ? current : initialProduct.productId,
|
|
832
|
+
);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
setAvailableChangeProducts((prev) => {
|
|
836
|
+
const hasInitial = prev.some((p) => p.productId === initialProduct.productId);
|
|
837
|
+
return hasInitial ? prev : [initialProduct, ...prev];
|
|
838
|
+
});
|
|
839
|
+
setSelectedChangeProductId((current) => current || initialProduct.productId);
|
|
840
|
+
}, [initialProduct, changeProductOptions]);
|
|
841
|
+
|
|
842
|
+
useEffect(() => {
|
|
843
|
+
if (changeProductOptions?.length) return;
|
|
844
|
+
if (!isAdmin || !env.COMPANY_ID) return;
|
|
845
|
+
let cancelled = false;
|
|
846
|
+
setChangeProductsLoading(true);
|
|
847
|
+
setChangeProductsError(null);
|
|
848
|
+
fetchProducts(env.COMPANY_ID)
|
|
849
|
+
.then((products) => {
|
|
850
|
+
if (cancelled) return;
|
|
851
|
+
const nextProducts = getAdminChangeEligibleProducts(products, initialProduct);
|
|
852
|
+
setAvailableChangeProducts(nextProducts);
|
|
853
|
+
setSelectedChangeProductId((current) =>
|
|
854
|
+
nextProducts.some((p) => p.productId === current) ? current : initialProduct.productId,
|
|
855
|
+
);
|
|
856
|
+
})
|
|
857
|
+
.catch((err) => {
|
|
858
|
+
if (cancelled) return;
|
|
859
|
+
setChangeProductsError(err instanceof Error ? err.message : 'Failed to load products');
|
|
860
|
+
})
|
|
861
|
+
.finally(() => {
|
|
862
|
+
if (!cancelled) setChangeProductsLoading(false);
|
|
863
|
+
});
|
|
864
|
+
return () => {
|
|
865
|
+
cancelled = true;
|
|
866
|
+
};
|
|
867
|
+
}, [changeProductOptions, isAdmin, env.COMPANY_ID, initialProduct]);
|
|
868
|
+
|
|
869
|
+
useEffect(() => {
|
|
870
|
+
if (previousSelectedChangeProductIdRef.current === selectedChangeProductId) return;
|
|
871
|
+
previousSelectedChangeProductIdRef.current = selectedChangeProductId;
|
|
872
|
+
setAvailabilities([]);
|
|
873
|
+
setSelectedAvailability(null);
|
|
874
|
+
setSelectedReturnOption(null);
|
|
875
|
+
setAddOns([]);
|
|
876
|
+
setPrecomputedPricesByOption(null);
|
|
877
|
+
setPricingConfig(null);
|
|
878
|
+
pricingConfigSetRef.current = false;
|
|
879
|
+
fetchingRef.current = false;
|
|
880
|
+
hasLoadedAvailabilitiesRef.current = false;
|
|
881
|
+
inFlightRangeRef.current = null;
|
|
882
|
+
fetchedRangesRef.current = [];
|
|
883
|
+
pendingRangeRef.current = null;
|
|
884
|
+
earliestAvailabilityDateRef.current = null;
|
|
885
|
+
lastCompletedQuoteInputsKeyRef.current = null;
|
|
886
|
+
inFlightQuoteInputsKeyRef.current = null;
|
|
887
|
+
setLatestChangeQuote(null);
|
|
888
|
+
setChangeQuoteFetchError(null);
|
|
889
|
+
setChangeQuoteLoading(false);
|
|
890
|
+
setError('');
|
|
891
|
+
}, [selectedChangeProductId]);
|
|
892
|
+
|
|
893
|
+
useEffect(() => {
|
|
894
|
+
if (!pickupLocationId) return;
|
|
895
|
+
const pickupLocations = product.pickupLocations ?? [];
|
|
896
|
+
if (pickupLocations.length === 0) return;
|
|
897
|
+
if (pickupLocations.some((loc) => loc.id === pickupLocationId)) return;
|
|
898
|
+
setPickupLocationId(null);
|
|
899
|
+
setPickupLocationSkipped(false);
|
|
900
|
+
}, [pickupLocationId, product.pickupLocations]);
|
|
786
901
|
|
|
787
902
|
// Auto-apply promo code when parent page passes one (e.g. partner pages).
|
|
788
903
|
// Seed input only; validate/apply runs after date/time + tickets exist (debounced + handleApplyPromo).
|
|
@@ -3574,6 +3689,7 @@ export function AdminChangeBookingFlow({
|
|
|
3574
3689
|
hasOperationalChangesFromInitial: false,
|
|
3575
3690
|
dateChanged: false,
|
|
3576
3691
|
ticketsChanged: false,
|
|
3692
|
+
productChanged: false,
|
|
3577
3693
|
optionChanged: false,
|
|
3578
3694
|
pickupChanged: false,
|
|
3579
3695
|
countsChanged: false,
|
|
@@ -3587,6 +3703,7 @@ export function AdminChangeBookingFlow({
|
|
|
3587
3703
|
hasOperationalChangesFromInitial: false,
|
|
3588
3704
|
dateChanged: false,
|
|
3589
3705
|
ticketsChanged: false,
|
|
3706
|
+
productChanged: false,
|
|
3590
3707
|
optionChanged: false,
|
|
3591
3708
|
pickupChanged: false,
|
|
3592
3709
|
countsChanged: false,
|
|
@@ -3605,6 +3722,7 @@ export function AdminChangeBookingFlow({
|
|
|
3605
3722
|
const optionChanged = Boolean(
|
|
3606
3723
|
selectedOpt != null && initialOpt != null && initialOpt !== selectedOpt
|
|
3607
3724
|
);
|
|
3725
|
+
const productChanged = selectedParentProductChanged;
|
|
3608
3726
|
const normalizePickupId = (value: string | null | undefined) => {
|
|
3609
3727
|
const trimmed = value?.trim();
|
|
3610
3728
|
return trimmed ? trimmed : null;
|
|
@@ -3678,6 +3796,7 @@ export function AdminChangeBookingFlow({
|
|
|
3678
3796
|
return {
|
|
3679
3797
|
hasChangesFromInitial:
|
|
3680
3798
|
dateChanged ||
|
|
3799
|
+
productChanged ||
|
|
3681
3800
|
optionChanged ||
|
|
3682
3801
|
pickupChanged ||
|
|
3683
3802
|
countsChanged ||
|
|
@@ -3687,13 +3806,15 @@ export function AdminChangeBookingFlow({
|
|
|
3687
3806
|
// ignore option-id noise and only consider user-visible booking deltas.
|
|
3688
3807
|
hasOperationalChangesFromInitial:
|
|
3689
3808
|
dateChanged ||
|
|
3809
|
+
productChanged ||
|
|
3690
3810
|
pickupChanged ||
|
|
3691
3811
|
countsChanged ||
|
|
3692
3812
|
addOnsChanged ||
|
|
3693
3813
|
returnChanged,
|
|
3694
3814
|
dateChanged,
|
|
3695
3815
|
// Tickets line corresponds to "option + ticket counts"; add-ons and pickup changes affect itinerary but not the ticket label.
|
|
3696
|
-
ticketsChanged: Boolean(optionChanged || countsChanged),
|
|
3816
|
+
ticketsChanged: Boolean(productChanged || optionChanged || countsChanged),
|
|
3817
|
+
productChanged,
|
|
3697
3818
|
optionChanged,
|
|
3698
3819
|
pickupChanged,
|
|
3699
3820
|
countsChanged,
|
|
@@ -3702,6 +3823,7 @@ export function AdminChangeBookingFlow({
|
|
|
3702
3823
|
};
|
|
3703
3824
|
}, [
|
|
3704
3825
|
initialValues,
|
|
3826
|
+
selectedParentProductChanged,
|
|
3705
3827
|
changeFlowResolvedInitialProductOptionId,
|
|
3706
3828
|
selectedAvailability,
|
|
3707
3829
|
selectedReturnOption,
|
|
@@ -3726,6 +3848,7 @@ export function AdminChangeBookingFlow({
|
|
|
3726
3848
|
const changeQuoteInputsKey = useMemo(() => JSON.stringify({
|
|
3727
3849
|
bookingReference: initialValues?.bookingReference?.trim() ?? '',
|
|
3728
3850
|
lastName: lastName.trim().toLowerCase(),
|
|
3851
|
+
parentProductId: product.productId,
|
|
3729
3852
|
optionId: selectedAvailability?.productOptionId?.trim() || activeOptions[0]?.optionId || '',
|
|
3730
3853
|
dateTime: selectedAvailability?.dateTime ?? '',
|
|
3731
3854
|
availabilityId: selectedAvailability?.availabilityId ?? null,
|
|
@@ -3740,6 +3863,7 @@ export function AdminChangeBookingFlow({
|
|
|
3740
3863
|
}), [
|
|
3741
3864
|
initialValues?.bookingReference,
|
|
3742
3865
|
lastName,
|
|
3866
|
+
product.productId,
|
|
3743
3867
|
selectedAvailability?.productOptionId,
|
|
3744
3868
|
selectedAvailability?.dateTime,
|
|
3745
3869
|
selectedAvailability?.availabilityId,
|
|
@@ -3753,8 +3877,8 @@ export function AdminChangeBookingFlow({
|
|
|
3753
3877
|
editableSummaryLineLabelInputs,
|
|
3754
3878
|
useAdminFeAuthoritativeQuote,
|
|
3755
3879
|
]);
|
|
3756
|
-
const
|
|
3757
|
-
const missingRequiredReturnSelection =
|
|
3880
|
+
const destinationRequiresReturnSelection = Boolean(selectedAvailability?.returnOptions?.length);
|
|
3881
|
+
const missingRequiredReturnSelection = destinationRequiresReturnSelection && !selectedReturnOption;
|
|
3758
3882
|
|
|
3759
3883
|
const changeFlowSubmitDisabled =
|
|
3760
3884
|
missingRequiredReturnSelection ||
|
|
@@ -4104,7 +4228,7 @@ export function AdminChangeBookingFlow({
|
|
|
4104
4228
|
|
|
4105
4229
|
const checkoutFormError =
|
|
4106
4230
|
(error || '') ||
|
|
4107
|
-
(missingRequiredReturnSelection ? '
|
|
4231
|
+
(missingRequiredReturnSelection ? 'Please select a return time for this product.' : '') ||
|
|
4108
4232
|
(isCustomerSelfServeChange && isChangeQuoteBlocked ? (latestChangeQuote?.reasonIfBlocked ?? '') : '') ||
|
|
4109
4233
|
(isCustomerSelfServeChange ? changeQuoteFetchError ?? '' : '');
|
|
4110
4234
|
|
|
@@ -4317,6 +4441,7 @@ export function AdminChangeBookingFlow({
|
|
|
4317
4441
|
bookingReference: bookingReferenceForQuote,
|
|
4318
4442
|
lastName: lastName.trim(),
|
|
4319
4443
|
newProductId: optionId,
|
|
4444
|
+
newParentProductId: product.productId,
|
|
4320
4445
|
newDateTime: selectedAvailability.dateTime,
|
|
4321
4446
|
newAvailabilityId: selectedAvailability.availabilityId || null,
|
|
4322
4447
|
newPickupLocationId: pickupLocationId || null,
|
|
@@ -4588,6 +4713,21 @@ export function AdminChangeBookingFlow({
|
|
|
4588
4713
|
initialValues?.returnDateTime,
|
|
4589
4714
|
]);
|
|
4590
4715
|
|
|
4716
|
+
useEffect(() => {
|
|
4717
|
+
if (!selectedReturnOption) return;
|
|
4718
|
+
const returnOptions = selectedAvailability?.returnOptions ?? [];
|
|
4719
|
+
if (returnOptions.length === 0) {
|
|
4720
|
+
setSelectedReturnOption(null);
|
|
4721
|
+
return;
|
|
4722
|
+
}
|
|
4723
|
+
const updatedReturnOption = returnOptions.find(
|
|
4724
|
+
(opt) => opt.returnAvailabilityId === selectedReturnOption.returnAvailabilityId,
|
|
4725
|
+
);
|
|
4726
|
+
if (!updatedReturnOption) {
|
|
4727
|
+
setSelectedReturnOption(null);
|
|
4728
|
+
}
|
|
4729
|
+
}, [selectedAvailability?.returnOptions, selectedReturnOption]);
|
|
4730
|
+
|
|
4591
4731
|
// Fetch add-ons when availability (product option) is selected; clear selections when option changes
|
|
4592
4732
|
const availabilityProductOptionId = selectedAvailability?.productOptionId ?? null;
|
|
4593
4733
|
const prevAvailabilityProductOptionIdRef = useRef<string | null>(null);
|
|
@@ -4856,7 +4996,7 @@ export function AdminChangeBookingFlow({
|
|
|
4856
4996
|
return;
|
|
4857
4997
|
}
|
|
4858
4998
|
if (missingRequiredReturnSelection) {
|
|
4859
|
-
setError('
|
|
4999
|
+
setError('Please select a return time for this product.');
|
|
4860
5000
|
return;
|
|
4861
5001
|
}
|
|
4862
5002
|
|
|
@@ -4939,6 +5079,7 @@ export function AdminChangeBookingFlow({
|
|
|
4939
5079
|
bookingReference: changeBookingReference,
|
|
4940
5080
|
lastName: changeLastName,
|
|
4941
5081
|
newProductId: availabilityProductOptionId,
|
|
5082
|
+
newParentProductId: product.productId,
|
|
4942
5083
|
newDateTime: selectedAvailability.dateTime,
|
|
4943
5084
|
newAvailabilityId: selectedAvailability.availabilityId || null,
|
|
4944
5085
|
newPickupLocationId: pickupLocationId || null,
|
|
@@ -5053,6 +5194,7 @@ export function AdminChangeBookingFlow({
|
|
|
5053
5194
|
: null;
|
|
5054
5195
|
await onChangeBooking({
|
|
5055
5196
|
productId: availabilityProductOptionId,
|
|
5197
|
+
parentProductId: product.productId,
|
|
5056
5198
|
dateTime: selectedAvailability.dateTime,
|
|
5057
5199
|
bookingItems,
|
|
5058
5200
|
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
|
|
@@ -5448,6 +5590,7 @@ export function AdminChangeBookingFlow({
|
|
|
5448
5590
|
: null;
|
|
5449
5591
|
await onChangeBooking({
|
|
5450
5592
|
productId: availabilityProductOptionId,
|
|
5593
|
+
parentProductId: product.productId,
|
|
5451
5594
|
dateTime: selectedAvailability.dateTime,
|
|
5452
5595
|
bookingItems,
|
|
5453
5596
|
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
|
|
@@ -5645,6 +5788,40 @@ export function AdminChangeBookingFlow({
|
|
|
5645
5788
|
)}
|
|
5646
5789
|
{isPartialLaunch ? null : (
|
|
5647
5790
|
<div className="booking-calendar-section">
|
|
5791
|
+
{isAdmin && !isInitialPrivateShuttleBooking && availableChangeProducts.length > 1 ? (
|
|
5792
|
+
<div className="mb-6 rounded-lg border border-stone-200 bg-stone-50 p-4">
|
|
5793
|
+
<label
|
|
5794
|
+
className="mb-2 block text-sm font-medium text-stone-800"
|
|
5795
|
+
htmlFor="admin-change-product-select"
|
|
5796
|
+
>
|
|
5797
|
+
Product
|
|
5798
|
+
</label>
|
|
5799
|
+
<select
|
|
5800
|
+
id="admin-change-product-select"
|
|
5801
|
+
className="w-full rounded-md border border-stone-300 bg-white px-3 py-2 text-sm text-stone-900 focus:outline-none focus:ring-2 focus:ring-stone-400/80"
|
|
5802
|
+
value={selectedChangeProductId}
|
|
5803
|
+
disabled={changeProductsLoading || loading || showCheckoutModal || showAdminPaymentChoice}
|
|
5804
|
+
onChange={(e) => {
|
|
5805
|
+
setSelectedChangeProductId(e.target.value);
|
|
5806
|
+
setSelectedDate('');
|
|
5807
|
+
setImplicitReturnBaselineId(null);
|
|
5808
|
+
changeFlowOutboundAnchorRef.current = null;
|
|
5809
|
+
changeFlowReturnAnchorRef.current = null;
|
|
5810
|
+
}}
|
|
5811
|
+
>
|
|
5812
|
+
{availableChangeProducts.map((p) => (
|
|
5813
|
+
<option key={p.productId} value={p.productId}>
|
|
5814
|
+
{p.name}
|
|
5815
|
+
</option>
|
|
5816
|
+
))}
|
|
5817
|
+
</select>
|
|
5818
|
+
{changeProductsError ? (
|
|
5819
|
+
<div className="mt-2 text-xs text-amber-800">
|
|
5820
|
+
Product list could not be refreshed. Showing the current product only.
|
|
5821
|
+
</div>
|
|
5822
|
+
) : null}
|
|
5823
|
+
</div>
|
|
5824
|
+
) : null}
|
|
5648
5825
|
{loadingAvailabilities && availabilities.length === 0 ? (
|
|
5649
5826
|
<div className="flex flex-col items-center justify-center py-12 gap-4">
|
|
5650
5827
|
<div className="booking-loading-spinner" aria-hidden />
|
|
@@ -104,6 +104,8 @@ export interface BookingFlowBaseProps {
|
|
|
104
104
|
availabilityPricingProfileId?: string | null;
|
|
105
105
|
/** When set (e.g. partner portal), get-availabilities filters cancellation policies by this profile. */
|
|
106
106
|
availabilityCancellationPolicyProfileId?: string | null;
|
|
107
|
+
/** Admin change-booking: available destination products for switching the booking to another product. */
|
|
108
|
+
changeProductOptions?: Product[];
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
/** Standard (new) reservation flow — no change-booking receipt or callbacks. */
|
|
@@ -2,6 +2,9 @@ import type { Currency } from './CurrencySwitcher';
|
|
|
2
2
|
|
|
3
3
|
/** Payload passed to `onChangeBooking` when applying a dashboard-managed booking change. */
|
|
4
4
|
export type ProviderDashboardChangeBookingPayload = {
|
|
5
|
+
/** Parent catalog product id. Present when admin changes the booking to a different product. */
|
|
6
|
+
parentProductId?: string;
|
|
7
|
+
/** Product option id selected for the new booking. */
|
|
5
8
|
productId: string;
|
|
6
9
|
dateTime: string;
|
|
7
10
|
bookingItems: Array<{ category: string; count: number }>;
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -845,6 +845,9 @@ export interface CreatePaymentIntentResponse {
|
|
|
845
845
|
export interface ChangeBookingQuoteRequest {
|
|
846
846
|
bookingReference: string;
|
|
847
847
|
lastName: string;
|
|
848
|
+
/** Parent catalog product id for cross-product admin/provider changes. */
|
|
849
|
+
newParentProductId?: string | null;
|
|
850
|
+
/** Product option id selected for the new booking; retained as `newProductId` for existing BE compatibility. */
|
|
848
851
|
newProductId: string;
|
|
849
852
|
newDateTime: string;
|
|
850
853
|
/** Outbound availability id for the new selection (must match option + datetime server-side). */
|