@ticketboothapp/booking 1.2.65 → 1.2.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.65",
3
+ "version": "1.2.67",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -7,9 +7,11 @@ import {
7
7
  getAvailabilities,
8
8
  cancelReservation,
9
9
  cancelReservationBestEffort,
10
+ createPaymentIntent,
10
11
  quoteChangeBooking,
11
12
  confirmFreeChangeBooking,
12
13
  createChangeBookingPaymentIntent,
14
+ confirmBookingWithoutPayment,
13
15
  getAddOns,
14
16
  validatePromoCode,
15
17
  getPromoDiscount,
@@ -31,6 +33,7 @@ import {
31
33
  } from '../../lib/booking-constants';
32
34
  import { getSundayOfWeek } from '../../lib/booking/sunday-week';
33
35
  import { Calendar } from './Calendar';
36
+ import { AdminPaymentChoiceModal } from './AdminPaymentChoiceModal';
34
37
  import { ItineraryBox } from './ItineraryBox';
35
38
  import { ItineraryPlaceholder } from './ItineraryPlaceholder';
36
39
  import { PickupTimeSelector } from './PickupTimeSelector';
@@ -96,6 +99,7 @@ import { getItineraryStepLabel } from '../../lib/booking/itinerary-display';
96
99
  import { MANAGE_BOOKING_FROM_CHANGE_PAYMENT, MANAGE_BOOKING_QUERY_FROM } from '../../lib/manage-booking-post-checkout';
97
100
  import { useBookingHost } from '../../runtime';
98
101
  import type { BookingFlowUiOptions } from './booking-flow-ui';
102
+ import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
99
103
  import type { ChangeBookingFlowProps, ChangeFlowSelectionPreview } from './booking-flow-types';
100
104
  import { BOOKING_FLOW_ABANDON_EVENT } from '../../providers/booking-dialog-provider';
101
105
 
@@ -107,6 +111,8 @@ import { BOOKING_FLOW_ABANDON_EVENT } from '../../providers/booking-dialog-provi
107
111
  * `serverPreview.priceSummaryLines` when the quote includes rows; otherwise falls back to FE-built lines after totals confirm.
108
112
  *
109
113
  * Until the first successful quote, `selfServeCheckoutPlaceholder` still avoids showing unchecked totals.
114
+ *
115
+ * **Provider dashboard** (`onChangeBooking`) — unchanged; manual pricing via `flowUi.providerDashboardChangePricingUi`.
110
116
  */
111
117
  function mergeQuoteSliceWithServerPreview(
112
118
  slice: ReturnType<typeof sliceChangeQuoteForUi>,
@@ -403,8 +409,9 @@ function pickOutboundMatchingPreviousSelection(
403
409
  anchor: { productOptionId: string | null; minutesFromMidnight: number },
404
410
  companyTimezone: string,
405
411
  optionsMap: Map<string, { mostPopular?: boolean }>,
412
+ isAdmin: boolean
406
413
  ): Availability | null {
407
- const selectable = timesForSelectedDate.filter((a) => (a.vacancies ?? 0) > 0);
414
+ const selectable = timesForSelectedDate.filter((a) => (a.vacancies ?? 0) > 0 || isAdmin);
408
415
  if (selectable.length === 0) return null;
409
416
 
410
417
  const withOption =
@@ -438,8 +445,9 @@ function pickReturnMatchingPreviousSelection(
438
445
  sortedReturnOptions: ReturnOption[],
439
446
  anchor: { returnLocation: string; minutesFromMidnight: number },
440
447
  companyTimezone: string,
448
+ isAdmin: boolean
441
449
  ): ReturnOption | null {
442
- const eligible = sortedReturnOptions.filter((o) => (o.vacancies ?? 0) > 0);
450
+ const eligible = sortedReturnOptions.filter((o) => (o.vacancies ?? 0) > 0 || isAdmin);
443
451
  if (eligible.length === 0) return null;
444
452
 
445
453
  const sameLoc = eligible.filter((o) => o.returnLocation === anchor.returnLocation);
@@ -628,7 +636,8 @@ function resolveInitialAvailabilityFromBooking(
628
636
  }
629
637
 
630
638
  /**
631
- * Embed fork (ticketbooth provider dashboard): same self-serve **change booking** as {@link ChangeBookingFlow}.
639
+ * Admin / provider-dashboard **change booking** literal duplicate of {@link ChangeBookingFlow} for now
640
+ * so ticketbooth can diverge without affecting the public site flow.
632
641
  */
633
642
  export function AdminChangeBookingFlow({
634
643
  product,
@@ -652,6 +661,7 @@ export function AdminChangeBookingFlow({
652
661
  partnerPortalBooking = false,
653
662
  availabilityPricingProfileId,
654
663
  availabilityCancellationPolicyProfileId,
664
+ onChangeBooking,
655
665
  }: ChangeBookingFlowProps) {
656
666
  /** Always the booking’s sold currency — not the site currency switcher / parent default. */
657
667
  const currency = useMemo((): Currency => {
@@ -663,6 +673,16 @@ export function AdminChangeBookingFlow({
663
673
  return DEFAULT_CURRENCY;
664
674
  }, [originalReceipt?.currency, initialValues?.currency, currencyFromParent]);
665
675
 
676
+ const isManualOverrideEligibleLine = (line: { editable: boolean; type?: string; label?: string }): boolean => {
677
+ if (!line.editable) return false;
678
+ const type = (line.type ?? '').toUpperCase();
679
+ const label = (line.label ?? '').toLowerCase();
680
+ const isPromoLikeType =
681
+ type.includes('PROMO') || type.includes('DISCOUNT') || type.includes('VOUCHER') || type.includes('GIFT');
682
+ const isPromoLikeLabel =
683
+ label.includes('promo') || label.includes('discount') || label.includes('voucher') || label.includes('gift');
684
+ return !(isPromoLikeType || isPromoLikeLabel);
685
+ };
666
686
  const { env, analytics } = useBookingHost();
667
687
  const { t } = useTranslations();
668
688
  const { locale } = useLocale();
@@ -671,6 +691,7 @@ export function AdminChangeBookingFlow({
671
691
  const cancellationPolicyProfileIdForAvailabilities =
672
692
  (availabilityCancellationPolicyProfileId ?? '').trim() || null;
673
693
  const {
694
+ permissions,
674
695
  isSimplifiedPricingView,
675
696
  onShowManage,
676
697
  getSuccessUrl,
@@ -678,6 +699,7 @@ export function AdminChangeBookingFlow({
678
699
  mode: bookingAppMode,
679
700
  } = useBookingApp();
680
701
  const availabilitiesCache = useAvailabilitiesCache();
702
+ const isAdmin = permissions.viewerRole === 'admin';
681
703
  const [availabilities, setAvailabilities] = useState<Availability[]>([]);
682
704
  const [selectedAvailability, setSelectedAvailability] = useState<Availability | null>(null);
683
705
  const [selectedReturnOption, setSelectedReturnOption] = useState<ReturnOption | null>(null);
@@ -776,6 +798,12 @@ export function AdminChangeBookingFlow({
776
798
  differenceTotal: number;
777
799
  };
778
800
  } | null>(null);
801
+ /** Admin only: skip sending confirmation at creation (provider dashboard). */
802
+ const [skipConfirmationCommunications, setSkipConfirmationCommunications] = useState(false);
803
+ /** Admin only: disable all auto communications for this booking (provider dashboard). */
804
+ const [disableAutoCommunications, setDisableAutoCommunications] = useState(false);
805
+ /** Admin only: show choice to pay now or confirm without payment (full balance owed). */
806
+ const [showAdminPaymentChoice, setShowAdminPaymentChoice] = useState(false);
779
807
  const hasAppliedInitialValuesRef = useRef(false);
780
808
  const hasAppliedInitialQuantitiesRef = useRef(false);
781
809
  const hasHydratedAddOnsFromReceiptRef = useRef(false);
@@ -794,11 +822,10 @@ export function AdminChangeBookingFlow({
794
822
  return null;
795
823
  }
796
824
  }, [initialValues?.dateTime, companyTimezone]);
825
+ const isProviderDashboardChange = Boolean(onChangeBooking);
826
+ const isCustomerSelfServeChange = !isProviderDashboardChange;
797
827
  /** Do not render catalog-/FE-derived dollar amounts in UI until `quoteChangeBooking` returns `serverDisplay`. */
798
- const suppressSelfServeCurrencyUi = true;
799
- /** Self-serve change flow only (no provider/admin pricing paths). */
800
- const isCustomerSelfServeChange = true;
801
- const isAdmin = false;
828
+ const suppressSelfServeCurrencyUi = isCustomerSelfServeChange;
802
829
 
803
830
  useEffect(() => {
804
831
  setPartnerAttributionConfirmed(false);
@@ -808,13 +835,13 @@ export function AdminChangeBookingFlow({
808
835
  * user picks a different return time — baseline is the first auto-selected return for this outbound.
809
836
  */
810
837
  const [implicitReturnBaselineId, setImplicitReturnBaselineId] = useState<string | null>(null);
811
- /** Promo from booking is fixed — show read-only, never add new. */
838
+ /** Any change flow (self-serve or provider): promo from booking is fixed — show read-only, never add new. */
812
839
  const lockedPromoCode = initialValues?.promoCode?.trim()
813
840
  ? initialValues.promoCode.trim().toUpperCase()
814
841
  : null;
815
842
  /** Public self-serve only: cannot reduce tickets below original counts. */
816
843
  const changeBookingMinimumQuantities = useMemo(() => {
817
- if (!initialValues?.bookingItems?.length) return undefined;
844
+ if (!isCustomerSelfServeChange || !initialValues?.bookingItems?.length) return undefined;
818
845
  const m: Record<string, number> = {};
819
846
  for (const item of initialValues.bookingItems) {
820
847
  const key = item.category?.trim();
@@ -822,7 +849,30 @@ export function AdminChangeBookingFlow({
822
849
  m[key] = Math.max(0, Number(item.count) || 0);
823
850
  }
824
851
  return m;
825
- }, [initialValues?.bookingItems]);
852
+ }, [isCustomerSelfServeChange, initialValues?.bookingItems]);
853
+ const [adminChoiceData, setAdminChoiceData] = useState<{
854
+ reservationReference: string;
855
+ reservationExpiration?: string;
856
+ checkoutBreakdown: { lineItems: Array<{ label: string; amount: number; type?: string; quantity?: number }>; totalAmount: number; currency: string };
857
+ totalAmount: number;
858
+ datePart: string;
859
+ timePart: string;
860
+ availabilityProductOptionId: string;
861
+ itineraryDisplay?: ItineraryDisplayStep[] | null;
862
+ clientSecret: string;
863
+ ticketLinesForModal: CheckoutModalLineItem[];
864
+ feeLineItems: OrderSummary['feeLineItems'];
865
+ returnPriceAdjustment: number;
866
+ cancellationPolicyFee: number;
867
+ cancellationPolicyLabel?: string;
868
+ subtotal: number;
869
+ tax: number;
870
+ totalQuantity: number;
871
+ isTaxIncludedInPrice: boolean;
872
+ taxRate: number;
873
+ promoDiscountAmount: number;
874
+ discountLabel?: string | null;
875
+ } | null>(null);
826
876
  const [latestChangeQuote, setLatestChangeQuote] = useState<{
827
877
  priceDiff: number;
828
878
  currency: Currency;
@@ -1659,12 +1709,14 @@ export function AdminChangeBookingFlow({
1659
1709
  }, [initialValues?.productId, product.productId]);
1660
1710
 
1661
1711
  /**
1662
- * Receipt pricing on protected seats/fees: Rule A (exact receipt unit when same calendar day
1663
- * + same product option) vs Rule B (`max(receipt, live)` when date or option changes).
1712
+ * Receipt pricing on protected seats/fees — **customer self-serve only**: Rule A (exact receipt unit when same calendar day
1713
+ * + same product option) vs Rule B (`max(receipt, live)` when date or option changes). Provider dashboard uses live catalog.
1664
1714
  */
1665
1715
  const changeFlowApplyReceiptPaidFloors = useMemo(
1666
- () => changeFlowBookingParentProductIdForFloors === product.productId.trim(),
1667
- [changeFlowBookingParentProductIdForFloors, product.productId],
1716
+ () =>
1717
+ !isProviderDashboardChange &&
1718
+ changeFlowBookingParentProductIdForFloors === product.productId.trim(),
1719
+ [isProviderDashboardChange, changeFlowBookingParentProductIdForFloors, product.productId],
1668
1720
  );
1669
1721
 
1670
1722
  useEffect(() => {
@@ -2774,6 +2826,7 @@ export function AdminChangeBookingFlow({
2774
2826
  */
2775
2827
  const changeFlowProtectedReturnAdjustment = useMemo(() => {
2776
2828
  if (totalQuantity <= 0) return returnPriceAdjustment;
2829
+ if (isProviderDashboardChange) return returnPriceAdjustment;
2777
2830
  if (effectiveChangeFlowReturnUnitFloorPerPerson == null) return returnPriceAdjustment;
2778
2831
  const livePerPerson =
2779
2832
  returnOptionCatalogPerPerson ?? (selectedReturnOption?.priceAdjustmentByCurrency?.[currency] ?? 0);
@@ -2787,6 +2840,7 @@ export function AdminChangeBookingFlow({
2787
2840
  }, [
2788
2841
  totalQuantity,
2789
2842
  returnPriceAdjustment,
2843
+ isProviderDashboardChange,
2790
2844
  effectiveChangeFlowReturnUnitFloorPerPerson,
2791
2845
  selectedReturnOption,
2792
2846
  returnOptionCatalogPerPerson,
@@ -2877,6 +2931,29 @@ export function AdminChangeBookingFlow({
2877
2931
  return [...feeLineItems, ...addOnLines];
2878
2932
  }, [feeLineItems, addOnSelections, addOns]);
2879
2933
 
2934
+ const providerPricingUi = flowUi?.providerDashboardChangePricingUi;
2935
+ const providerQuotedLines = providerPricingUi?.quotedLines ?? [];
2936
+ const providerEditableLines = providerQuotedLines.filter((line) => isManualOverrideEligibleLine(line));
2937
+ const showProviderPricingInlineEditor =
2938
+ isProviderDashboardChange && isAdmin && (
2939
+ providerPricingUi?.loading ||
2940
+ providerPricingUi?.error != null ||
2941
+ providerQuotedLines.length > 0
2942
+ );
2943
+ const normalizeReceiptLabel = (value: string): string =>
2944
+ value
2945
+ .toLowerCase()
2946
+ .replace(/\([^)]*\)/g, '')
2947
+ .replace(/[^a-z0-9]+/g, '')
2948
+ .trim();
2949
+ const providerEditableLineByNormalizedLabel = useMemo(() => {
2950
+ const m = new Map<string, (typeof providerEditableLines)[number]>();
2951
+ for (const line of providerEditableLines) {
2952
+ const key = normalizeReceiptLabel(line.label ?? '');
2953
+ if (key) m.set(key, line);
2954
+ }
2955
+ return m;
2956
+ }, [providerEditableLines]);
2880
2957
 
2881
2958
  const checkoutPriceSummaryLines = useMemo((): PriceSummaryLine[] => {
2882
2959
  if (!selectedAvailability) return [];
@@ -2894,6 +2971,13 @@ export function AdminChangeBookingFlow({
2894
2971
  );
2895
2972
  return {
2896
2973
  kind: 'ticket',
2974
+ lineKey:
2975
+ showProviderPricingInlineEditor
2976
+ ? providerEditableLineByNormalizedLabel.get(normalizeReceiptLabel(line.category))?.lineKey
2977
+ : undefined,
2978
+ editable:
2979
+ showProviderPricingInlineEditor &&
2980
+ providerEditableLineByNormalizedLabel.has(normalizeReceiptLabel(line.category)),
2897
2981
  category: line.category,
2898
2982
  qty: line.qty,
2899
2983
  itemTotal: line.itemTotal,
@@ -2928,6 +3012,25 @@ export function AdminChangeBookingFlow({
2928
3012
  fee.name.toLowerCase().includes('license'));
2929
3013
  return {
2930
3014
  kind: 'line' as const,
3015
+ lineKey:
3016
+ showProviderPricingInlineEditor
3017
+ ? providerEditableLineByNormalizedLabel.get(
3018
+ normalizeReceiptLabel(
3019
+ feeLineItems.some((f) => f.name === fee.name)
3020
+ ? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
3021
+ : fee.name
3022
+ )
3023
+ )?.lineKey
3024
+ : undefined,
3025
+ editable:
3026
+ showProviderPricingInlineEditor &&
3027
+ providerEditableLineByNormalizedLabel.has(
3028
+ normalizeReceiptLabel(
3029
+ feeLineItems.some((f) => f.name === fee.name)
3030
+ ? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
3031
+ : fee.name
3032
+ )
3033
+ ),
2931
3034
  label: feeLineItems.some((f) => f.name === fee.name)
2932
3035
  ? `${fee.name} (${totalQuantity} ${totalQuantity === 1 ? 'person' : 'people'})`
2933
3036
  : fee.name,
@@ -2953,6 +3056,8 @@ export function AdminChangeBookingFlow({
2953
3056
  effectiveCancellationPolicyLabel,
2954
3057
  feeLineItemsWithAddOns,
2955
3058
  feeLineItems,
3059
+ showProviderPricingInlineEditor,
3060
+ providerEditableLineByNormalizedLabel,
2956
3061
  ]);
2957
3062
 
2958
3063
  const checkoutPriceSummaryLinesForCheckout = useMemo(() => {
@@ -3456,7 +3561,8 @@ export function AdminChangeBookingFlow({
3456
3561
  countsChanged ||
3457
3562
  addOnsChanged ||
3458
3563
  returnChanged,
3459
- // Authoritative for "real user change" gating: ignore option-id noise; only user-visible deltas.
3564
+ // Authoritative for "real user change" gating in provider dashboard:
3565
+ // ignore option-id noise and only consider user-visible booking deltas.
3460
3566
  hasOperationalChangesFromInitial:
3461
3567
  dateChanged ||
3462
3568
  pickupChanged ||
@@ -3483,28 +3589,62 @@ export function AdminChangeBookingFlow({
3483
3589
  addOnSelections,
3484
3590
  initialAddOnMinQtyByKey,
3485
3591
  ]);
3486
- const hasChangeSelection = changeSelectionDetails.hasChangesFromInitial;
3592
+ const hasChangeSelection =
3593
+ isProviderDashboardChange
3594
+ ? changeSelectionDetails.hasOperationalChangesFromInitial
3595
+ : changeSelectionDetails.hasChangesFromInitial;
3487
3596
 
3488
3597
  const changeFlowNeedsServerPrice =
3598
+ isCustomerSelfServeChange &&
3489
3599
  hasChangeSelection &&
3490
3600
  !!initialValues?.bookingReference?.trim() &&
3491
3601
  !!lastName.trim();
3492
3602
 
3493
- const isChangeQuoteBlocked = latestChangeQuote?.canProceed === false;
3494
- const requiresReturnInChangeFlow = !!initialValues?.returnAvailabilityId?.trim();
3603
+ const isChangeQuoteBlocked = isCustomerSelfServeChange && latestChangeQuote?.canProceed === false;
3604
+ const requiresReturnInChangeFlow = isCustomerSelfServeChange && !!initialValues?.returnAvailabilityId?.trim();
3495
3605
  const missingRequiredReturnSelection = requiresReturnInChangeFlow && !selectedReturnOption;
3496
3606
 
3497
3607
  const changeFlowSubmitDisabled =
3498
3608
  missingRequiredReturnSelection ||
3499
- (changeFlowNeedsServerPrice &&
3609
+ (isCustomerSelfServeChange &&
3610
+ changeFlowNeedsServerPrice &&
3500
3611
  (changeQuoteLoading || (!latestChangeQuote && !changeQuoteFetchError)));
3501
3612
 
3502
- const hasEffectiveChangeSelection = hasChangeSelection;
3613
+ const providerTotalsPreview =
3614
+ showProviderPricingInlineEditor && providerPricingUi?.totalsPreview
3615
+ ? providerPricingUi.totalsPreview
3616
+ : null;
3617
+ const providerHasEditedLineOverrides =
3618
+ isProviderDashboardChange &&
3619
+ providerQuotedLines.some((line) => {
3620
+ if (!isManualOverrideEligibleLine(line)) return false;
3621
+ const raw = providerPricingUi?.lineAmountInputs?.[line.lineKey];
3622
+ const parsed = raw == null || raw.trim() === '' ? line.amount : Number(raw);
3623
+ if (!Number.isFinite(parsed)) return false;
3624
+ const rounded = Math.round(parsed * 100) / 100;
3625
+ return Math.abs(rounded - line.amount) > 0.0001;
3626
+ });
3627
+ const providerHasAdditionalAdjustments =
3628
+ isProviderDashboardChange &&
3629
+ (providerPricingUi?.additionalAdjustments ?? []).some((adj) => {
3630
+ const parsed = Number((adj.amountInput ?? '').trim());
3631
+ const hasAmount = Number.isFinite(parsed) && parsed > 0;
3632
+ const currentAmount = hasAmount ? (Math.round(parsed * 100) / 100).toFixed(2) : '';
3633
+ const originalAmount = adj.originalAmountInput ?? '';
3634
+ const currentLabel = (adj.label ?? '').trim();
3635
+ const originalLabel = (adj.originalLabel ?? '').trim();
3636
+ const currentMode = adj.mode;
3637
+ const originalMode = adj.originalMode;
3638
+ if (!originalMode) return hasAmount || currentLabel.length > 0;
3639
+ return currentAmount !== originalAmount || currentLabel !== originalLabel || currentMode !== originalMode;
3640
+ });
3641
+ const hasEffectiveChangeSelection =
3642
+ hasChangeSelection || providerHasEditedLineOverrides || providerHasAdditionalAdjustments;
3503
3643
 
3504
3644
  const displayedChangeAmounts = resolveChangeFlowDisplayedAmounts({
3505
- providerPreview: null,
3645
+ providerPreview: providerTotalsPreview,
3506
3646
  serverQuotePreview:
3507
- latestChangeQuote?.serverDisplay
3647
+ isCustomerSelfServeChange && latestChangeQuote?.serverDisplay
3508
3648
  ? latestChangeQuote.serverDisplay
3509
3649
  : null,
3510
3650
  fromCart: {
@@ -3520,13 +3660,13 @@ export function AdminChangeBookingFlow({
3520
3660
  const changeFlowClientEstimateDue = (() => {
3521
3661
  if (!originalReceipt) return totalPrice;
3522
3662
  // Customer self-serve: amount due comes from POST .../change/quote (`amountDueCents` / priceDiff), not FE delta math.
3523
- if (latestChangeQuote != null && !changeQuoteFetchError) {
3663
+ if (isCustomerSelfServeChange && latestChangeQuote != null && !changeQuoteFetchError) {
3524
3664
  return normalizeNearZeroOwed(latestChangeQuote.priceDiff);
3525
3665
  }
3526
3666
  return changeFlowBalanceVsOriginal({
3527
3667
  newTotal: displayChangeFlowProposedTotal,
3528
3668
  originalReceiptTotal: originalReceipt.total,
3529
- audience: 'customer',
3669
+ audience: isProviderDashboardChange ? 'provider' : 'customer',
3530
3670
  });
3531
3671
  })();
3532
3672
 
@@ -3535,6 +3675,14 @@ export function AdminChangeBookingFlow({
3535
3675
 
3536
3676
  const changeCheckoutButtonLabel = (() => {
3537
3677
  if (!hasEffectiveChangeSelection) return undefined;
3678
+ if (isProviderDashboardChange) {
3679
+ const est = Math.round(changeFlowClientEstimateDue * 100) / 100;
3680
+ return est > 0
3681
+ ? `Change booking (${formatCurrencyAmount(est, currency, locale as 'en' | 'fr')})`
3682
+ : est < 0
3683
+ ? `Change booking (${formatCurrencyAmount(est, currency, locale as 'en' | 'fr')})`
3684
+ : 'Change booking (no charge)';
3685
+ }
3538
3686
  if (changeFlowNeedsServerPrice) {
3539
3687
  if (changeQuoteLoading) {
3540
3688
  const tr = t('booking.updatingPrice');
@@ -3568,8 +3716,39 @@ export function AdminChangeBookingFlow({
3568
3716
  const checkoutFormError =
3569
3717
  (error || '') ||
3570
3718
  (missingRequiredReturnSelection ? 'Removing return option in self-serve is not available. Please contact support.' : '') ||
3571
- (isChangeQuoteBlocked ? (latestChangeQuote?.reasonIfBlocked ?? '') : '') ||
3572
- (changeQuoteFetchError ?? '');
3719
+ (isCustomerSelfServeChange && isChangeQuoteBlocked ? (latestChangeQuote?.reasonIfBlocked ?? '') : '') ||
3720
+ (isCustomerSelfServeChange ? changeQuoteFetchError ?? '' : '');
3721
+
3722
+ const providerPricingOverrides =
3723
+ isProviderDashboardChange && providerQuotedLines.length > 0
3724
+ ? providerQuotedLines
3725
+ .filter((line) => isManualOverrideEligibleLine(line))
3726
+ .map((line) => {
3727
+ const raw = providerPricingUi?.lineAmountInputs?.[line.lineKey];
3728
+ const parsed = raw == null || raw.trim() === '' ? line.amount : Number(raw);
3729
+ if (!Number.isFinite(parsed)) return null;
3730
+ const rounded = Math.round(parsed * 100) / 100;
3731
+ return Math.abs(rounded - line.amount) > 0.0001
3732
+ ? { lineKey: line.lineKey, amount: rounded, reason: 'Provider dashboard override' }
3733
+ : null;
3734
+ })
3735
+ .filter((v): v is { lineKey: string; amount: number; reason: string } => v != null)
3736
+ : [];
3737
+ const providerAdditionalAdjustments =
3738
+ isProviderDashboardChange
3739
+ ? (providerPricingUi?.additionalAdjustments ?? [])
3740
+ .map((adj) => {
3741
+ const parsed = Number((adj.amountInput ?? '').trim());
3742
+ if (!Number.isFinite(parsed) || parsed <= 0) return null;
3743
+ const rounded = Math.round(parsed * 100) / 100;
3744
+ const signed = adj.mode === 'DISCOUNT' ? -rounded : rounded;
3745
+ return {
3746
+ label: adj.label?.trim() || (signed < 0 ? 'Provider discount' : 'Provider charge'),
3747
+ amount: signed,
3748
+ };
3749
+ })
3750
+ .filter((v): v is { label: string; amount: number } => v != null)
3751
+ : [];
3573
3752
 
3574
3753
  const changeFlowSelectionPreview = useMemo((): ChangeFlowSelectionPreview | null => {
3575
3754
  if (!selectedAvailability || totalQuantity <= 0) return null;
@@ -3817,7 +3996,8 @@ export function AdminChangeBookingFlow({
3817
3996
  timesForSelectedDate,
3818
3997
  anchor,
3819
3998
  companyTimezone,
3820
- optionsMap
3999
+ optionsMap,
4000
+ isAdmin
3821
4001
  );
3822
4002
  changeFlowOutboundAnchorRef.current = null;
3823
4003
  if (matched) {
@@ -3908,7 +4088,8 @@ export function AdminChangeBookingFlow({
3908
4088
  const fromAnchor = pickReturnMatchingPreviousSelection(
3909
4089
  sorted,
3910
4090
  returnAnchor,
3911
- companyTimezone
4091
+ companyTimezone,
4092
+ isAdmin
3912
4093
  );
3913
4094
  changeFlowReturnAnchorRef.current = null;
3914
4095
  if (fromAnchor) {
@@ -4231,6 +4412,8 @@ export function AdminChangeBookingFlow({
4231
4412
  return;
4232
4413
  }
4233
4414
 
4415
+ const skipContactFields = isProviderDashboardChange;
4416
+ if (!skipContactFields) {
4234
4417
  // Validate email (required)
4235
4418
  if (!email) {
4236
4419
  setError(t('booking.enterEmail') || 'Please enter your email address');
@@ -4253,6 +4436,7 @@ export function AdminChangeBookingFlow({
4253
4436
  setError(t('booking.enterLastName') || 'Please enter your last name');
4254
4437
  return;
4255
4438
  }
4439
+ }
4256
4440
 
4257
4441
  // Allow checkout if pickup location is selected OR if user chose "I don't know"
4258
4442
  if (product.pickupLocations && product.pickupLocations.length > 0 && !pickupLocationId && !pickupLocationSkipped) {
@@ -4279,6 +4463,45 @@ export function AdminChangeBookingFlow({
4279
4463
  return;
4280
4464
  }
4281
4465
 
4466
+ if (onChangeBooking) {
4467
+ const pickupForChange = pickupLocationId
4468
+ ? product.pickupLocations?.find((loc) => loc.id === pickupLocationId)
4469
+ : null;
4470
+ await onChangeBooking({
4471
+ productId: availabilityProductOptionId,
4472
+ dateTime: selectedAvailability.dateTime,
4473
+ bookingItems,
4474
+ returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
4475
+ pickupLocationId: pickupLocationId ?? null,
4476
+ travelerHotel: pickupForChange?.name ?? null,
4477
+ startTime: selectedAvailability.dateTime ?? null,
4478
+ passengerCount: null,
4479
+ childSafetySeatsCount: null,
4480
+ foodRestrictions: null,
4481
+ addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
4482
+ cancellationPolicyId: cancellationPolicyId ?? initialValues?.cancellationPolicyId ?? null,
4483
+ promoCode: appliedPromoCode ?? null,
4484
+ newTotalAmount: displayChangeFlowProposedTotal,
4485
+ additionalHoursCount: null,
4486
+ pricingAdjustment:
4487
+ providerPricingOverrides.length > 0 || providerAdditionalAdjustments.length > 0
4488
+ ? {
4489
+ mode: 'MANUAL_LINES',
4490
+ lineOverrides: providerPricingOverrides,
4491
+ additionalAdjustments: providerAdditionalAdjustments,
4492
+ }
4493
+ : undefined,
4494
+ capacitySeatCredit: {
4495
+ enabled: true,
4496
+ previousPassengerCount: changeFlowInitialTicketCount,
4497
+ previousAvailabilityId: initialValues?.availabilityId ?? null,
4498
+ previousReturnAvailabilityId: initialValues?.returnAvailabilityId ?? null,
4499
+ },
4500
+ });
4501
+ setLoading(false);
4502
+ return;
4503
+ }
4504
+
4282
4505
  const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
4283
4506
  clientChannelSource: inferClientBookingSourceFromProductIds(
4284
4507
  product.productId,
@@ -4296,6 +4519,7 @@ export function AdminChangeBookingFlow({
4296
4519
  let changeIntentIdForCheckout: string | undefined;
4297
4520
  let changeBookingReferenceForPaidFlow: string | undefined;
4298
4521
 
4522
+ if (isCustomerSelfServeChange) {
4299
4523
  const changeBookingReference = initialValues?.bookingReference?.trim();
4300
4524
  const changeLastName = lastName.trim();
4301
4525
  if (!changeBookingReference || !changeLastName) {
@@ -4383,12 +4607,13 @@ export function AdminChangeBookingFlow({
4383
4607
  if (!changeIntentIdForCheckout) {
4384
4608
  throw new Error('Missing change intent for payment.');
4385
4609
  }
4610
+ }
4386
4611
 
4387
4612
  pendingReservationRef.current = null;
4388
4613
 
4389
4614
  // Note: Do NOT call onSuccess here for paid bookings — we're about to show the Stripe
4390
4615
  // CheckoutModal. onSuccess (e.g. closing the parent dialog) should only run when we're
4391
- // actually done (free booking redirect). Calling it here
4616
+ // actually done (free booking redirect, admin confirm-without-payment). Calling it here
4392
4617
  // would close the dialog before the payment modal opens.
4393
4618
 
4394
4619
  // Update stored booking data (no holds reservation — change flow keys off booking reference elsewhere).
@@ -4414,11 +4639,13 @@ export function AdminChangeBookingFlow({
4414
4639
  // Build checkout breakdown from the exact same values we show in the UI and Stripe modal.
4415
4640
  // Backend will charge totalAmount and store this as the receipt so /manage matches.
4416
4641
  const taxForBreakdown = effectivePromoDiscountAmount > 0 ? effectiveTax : tax;
4417
- const amountDueForCheckout = changeFlowBalanceVsOriginal({
4642
+ const amountDueForCheckout = isCustomerSelfServeChange
4643
+ ? changeFlowBalanceVsOriginal({
4418
4644
  newTotal: changeFlowNewBookingTotal,
4419
4645
  originalReceiptTotal: originalReceipt?.total ?? 0,
4420
4646
  audience: 'customer',
4421
- });
4647
+ })
4648
+ : totalPrice;
4422
4649
  const lines = [
4423
4650
  ...ticketLineItemsForChangeFlowDisplay.map((line) => ({
4424
4651
  label: line.category,
@@ -4495,7 +4722,8 @@ export function AdminChangeBookingFlow({
4495
4722
  roundingLabel: t('booking.rounding') || 'Rounding',
4496
4723
  });
4497
4724
 
4498
- const paymentIntent = await createChangeBookingPaymentIntent(
4725
+ const paymentIntent = isCustomerSelfServeChange
4726
+ ? await createChangeBookingPaymentIntent(
4499
4727
  (() => {
4500
4728
  const id = changeIntentIdForCheckout ?? latestChangeQuote?.changeIntentId;
4501
4729
  if (!id) {
@@ -4503,7 +4731,76 @@ export function AdminChangeBookingFlow({
4503
4731
  }
4504
4732
  return id;
4505
4733
  })()
4506
- );
4734
+ )
4735
+ : await createPaymentIntent({
4736
+ productId: product.productId,
4737
+ optionId: availabilityProductOptionId,
4738
+ date: datePart,
4739
+ time: timePart,
4740
+ quantity: totalQuantity,
4741
+ customerEmail: email,
4742
+ customerFirstName: firstName.trim() || undefined,
4743
+ customerLastName: lastName.trim() || undefined,
4744
+ currency: currency,
4745
+ travelerHotel: selectedPickupLocation?.name || undefined,
4746
+ pickupLocationId: pickupLocationId || undefined,
4747
+ itineraryDisplay: itineraryDisplay ?? undefined,
4748
+ returnAvailabilityId: selectedReturnOption?.returnAvailabilityId,
4749
+ promoCode: (lockedPromoCode || appliedPromoCode) || undefined,
4750
+ cancellationPolicyId: cancellationPolicyId || undefined,
4751
+ termsAcceptedAt: termsAcceptedAt ?? undefined,
4752
+ checkoutBreakdown,
4753
+ skipConfirmationCommunications: isAdmin && skipConfirmationCommunications ? true : undefined,
4754
+ disableAutoCommunications: isAdmin && disableAutoCommunications ? true : undefined,
4755
+ ...bookingSourceContext,
4756
+ });
4757
+
4758
+ // Admin: show choice to pay now or confirm without payment (customer owes full balance)
4759
+ if (isAdmin) {
4760
+ const adminReservationRef =
4761
+ initialValues?.bookingReference?.trim() ??
4762
+ changeBookingReferenceForPaidFlow ??
4763
+ '';
4764
+ if (!adminReservationRef) {
4765
+ throw new Error('Missing reservation reference for admin payment flow');
4766
+ }
4767
+ setError('');
4768
+ setAdminChoiceData({
4769
+ reservationReference: adminReservationRef,
4770
+ reservationExpiration: undefined,
4771
+ checkoutBreakdown,
4772
+ totalAmount: amountDueForCheckout,
4773
+ datePart,
4774
+ timePart,
4775
+ availabilityProductOptionId,
4776
+ itineraryDisplay: itineraryDisplay ?? undefined,
4777
+ clientSecret: paymentIntent.clientSecret ?? '',
4778
+ ticketLinesForModal: ticketLineItemsForChangeFlowDisplay.map((line) => {
4779
+ const rate = pricing.find((r) => r.category === line.category);
4780
+ const breakdown = getPriceBreakdown(
4781
+ line.category,
4782
+ rate?.priceCAD ?? 0,
4783
+ rate?.baseInDisplayCurrency,
4784
+ rate?.appliedAdjustments ?? []
4785
+ );
4786
+ return { line, breakdown };
4787
+ }),
4788
+ feeLineItems: feeLineItemsWithAddOns,
4789
+ returnPriceAdjustment: checkoutReturnLineAmount,
4790
+ cancellationPolicyFee,
4791
+ cancellationPolicyLabel: effectiveCancellationPolicyLabel,
4792
+ subtotal: effectiveSubtotal,
4793
+ tax: effectivePromoDiscountAmount > 0 ? effectiveTax : tax,
4794
+ totalQuantity,
4795
+ isTaxIncludedInPrice,
4796
+ taxRate: pricingConfig?.taxRate ?? 0,
4797
+ promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
4798
+ discountLabel: appliedPromoCode ? `Promo: ${appliedPromoCode}` : (originalReceipt?.promoLabel || undefined),
4799
+ });
4800
+ setShowAdminPaymentChoice(true);
4801
+ setLoading(false);
4802
+ return;
4803
+ }
4507
4804
 
4508
4805
  const ticketLinesForModal: CheckoutModalLineItem[] = ticketLineItemsForChangeFlowDisplay.map((line) => {
4509
4806
  const rate = pricing.find((r) => r.category === line.category);
@@ -4525,7 +4822,7 @@ export function AdminChangeBookingFlow({
4525
4822
  // Paid change: always return to stable ref+lastName + explicit intent (not reservationRef).
4526
4823
  // /manage-booking runs bounded refresh only when `from=change_payment` (see manage-booking page).
4527
4824
  successUrlOverride:
4528
- changeBookingReferenceForPaidFlow
4825
+ isCustomerSelfServeChange && changeBookingReferenceForPaidFlow
4529
4826
  ? (() => {
4530
4827
  const origin = typeof window !== 'undefined' ? window.location.origin : '';
4531
4828
  const ref = encodeURIComponent(
@@ -4553,7 +4850,7 @@ export function AdminChangeBookingFlow({
4553
4850
  promoDiscountAmount: effectivePromoDiscountAmount > 0 ? effectivePromoDiscountAmount : 0,
4554
4851
  discountLabel: appliedPromoCode ? `Promo: ${appliedPromoCode}` : (originalReceipt?.promoLabel || undefined),
4555
4852
  changeTotals:
4556
- originalReceipt
4853
+ isCustomerSelfServeChange && originalReceipt
4557
4854
  ? {
4558
4855
  previousTotal: originalReceipt.total,
4559
4856
  newTotal: displayChangeFlowProposedTotal,
@@ -4622,6 +4919,86 @@ export function AdminChangeBookingFlow({
4622
4919
  }
4623
4920
  };
4624
4921
 
4922
+ const handleConfirmWithoutPayment = async () => {
4923
+ if (!adminChoiceData) return;
4924
+ setLoading(true);
4925
+ setError('');
4926
+ try {
4927
+ const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
4928
+ clientChannelSource: inferClientBookingSourceFromProductIds(
4929
+ product.productId,
4930
+ adminChoiceData.availabilityProductOptionId,
4931
+ ),
4932
+ forcePartnerPortalChannel: partnerPortalBooking,
4933
+ forceDashboardSource: bookingAppMode === 'provider-dashboard',
4934
+ });
4935
+ const result = await confirmBookingWithoutPayment({
4936
+ reservationReference: adminChoiceData.reservationReference,
4937
+ productId: product.productId,
4938
+ optionId: adminChoiceData.availabilityProductOptionId,
4939
+ date: adminChoiceData.datePart,
4940
+ time: adminChoiceData.timePart,
4941
+ customerEmail: email || undefined,
4942
+ customerFirstName: firstName.trim() || undefined,
4943
+ customerLastName: lastName.trim() || undefined,
4944
+ currency: currency,
4945
+ travelerHotel: product.pickupLocations?.find(loc => loc.id === pickupLocationId)?.name || undefined,
4946
+ pickupLocationId: pickupLocationId || undefined,
4947
+ itineraryDisplay: adminChoiceData.itineraryDisplay ?? undefined,
4948
+ termsAcceptedAt: termsAcceptedAt ?? undefined,
4949
+ skipConfirmationCommunications: skipConfirmationCommunications ? true : undefined,
4950
+ disableAutoCommunications: disableAutoCommunications ? true : undefined,
4951
+ checkoutBreakdown: adminChoiceData.checkoutBreakdown,
4952
+ depositAmount: 0,
4953
+ balanceAmount: adminChoiceData.totalAmount,
4954
+ totalAmount: adminChoiceData.totalAmount,
4955
+ ...bookingSourceContext,
4956
+ });
4957
+ pendingReservationRef.current = null;
4958
+ const ref = formatBookingRefForDisplay(result.bookingReference);
4959
+ const ln = lastName.trim();
4960
+ setShowAdminPaymentChoice(false);
4961
+ setAdminChoiceData(null);
4962
+ onSuccess?.({ reservationReference: adminChoiceData.reservationReference });
4963
+ if (onShowManage) {
4964
+ onShowManage({ ref, lastName: ln });
4965
+ } else {
4966
+ const params = new URLSearchParams({ ref, lastName: ln, booking_complete: '1' });
4967
+ window.location.href = `/manage-booking?${params.toString()}`;
4968
+ }
4969
+ } catch (err) {
4970
+ setError(err instanceof Error ? err.message : 'Failed to confirm booking');
4971
+ } finally {
4972
+ setLoading(false);
4973
+ }
4974
+ };
4975
+
4976
+ const handlePayNow = () => {
4977
+ if (!adminChoiceData) return;
4978
+ setShowAdminPaymentChoice(false);
4979
+ setCheckoutClientSecret(adminChoiceData.clientSecret);
4980
+ setCheckoutModalData({
4981
+ reservationReference: adminChoiceData.reservationReference,
4982
+ reservationExpiration: adminChoiceData.reservationExpiration,
4983
+ customerLastName: lastName.trim(),
4984
+ ticketLines: adminChoiceData.ticketLinesForModal,
4985
+ feeLineItems: adminChoiceData.feeLineItems,
4986
+ returnPriceAdjustment: adminChoiceData.returnPriceAdjustment,
4987
+ cancellationPolicyFee: adminChoiceData.cancellationPolicyFee,
4988
+ cancellationPolicyLabel: adminChoiceData.cancellationPolicyLabel,
4989
+ subtotal: adminChoiceData.subtotal,
4990
+ tax: adminChoiceData.tax,
4991
+ total: adminChoiceData.totalAmount,
4992
+ totalQuantity: adminChoiceData.totalQuantity,
4993
+ isTaxIncludedInPrice: adminChoiceData.isTaxIncludedInPrice,
4994
+ taxRate: adminChoiceData.taxRate,
4995
+ promoDiscountAmount: adminChoiceData.promoDiscountAmount,
4996
+ discountLabel: adminChoiceData.discountLabel,
4997
+ });
4998
+ setShowCheckoutModal(true);
4999
+ setAdminChoiceData(null);
5000
+ };
5001
+
4625
5002
  if (activeOptions.length === 0) {
4626
5003
  return (
4627
5004
  <div className="flex items-center justify-center py-16">
@@ -4632,6 +5009,17 @@ export function AdminChangeBookingFlow({
4632
5009
 
4633
5010
  return (
4634
5011
  <div className="booking-flow-root space-y-8">
5012
+ {/* Admin: choose to pay now or confirm without payment (full balance owed) */}
5013
+ <AdminPaymentChoiceModal
5014
+ open={!!(showAdminPaymentChoice && adminChoiceData)}
5015
+ totalAmount={adminChoiceData?.totalAmount ?? 0}
5016
+ currency={currency}
5017
+ loading={loading}
5018
+ error={error}
5019
+ onPayNow={handlePayNow}
5020
+ onConfirmWithoutPayment={handleConfirmWithoutPayment}
5021
+ onCancel={() => { setShowAdminPaymentChoice(false); setAdminChoiceData(null); setError(''); }}
5022
+ />
4635
5023
  {checkoutModalData && (
4636
5024
  <CheckoutModal
4637
5025
  open={showCheckoutModal}
@@ -4886,8 +5274,92 @@ export function AdminChangeBookingFlow({
4886
5274
  currency={currency}
4887
5275
  locale={locale}
4888
5276
  t={t}
4889
- extraBetweenTaxAndTotal={<></>}
4890
- extraBeforeSubtotal={undefined}
5277
+ extraBetweenTaxAndTotal={
5278
+ <>
5279
+ {showProviderPricingInlineEditor && providerPricingUi?.error ? (
5280
+ <div className="mt-2 text-sm text-red-700">{providerPricingUi.error}</div>
5281
+ ) : null}
5282
+ {showProviderPricingInlineEditor &&
5283
+ providerPricingUi?.loading &&
5284
+ providerQuotedLines.length === 0 ? (
5285
+ <div className="mt-2 text-sm text-stone-500">Loading price lines...</div>
5286
+ ) : null}
5287
+ {showProviderPricingInlineEditor &&
5288
+ providerPricingUi?.helperText &&
5289
+ !providerPricingUi.error ? (
5290
+ <div className="mt-2 text-xs text-stone-500">{providerPricingUi.helperText}</div>
5291
+ ) : null}
5292
+ </>
5293
+ }
5294
+ extraBeforeSubtotal={
5295
+ showProviderPricingInlineEditor && (providerPricingUi?.additionalAdjustments?.length ?? 0) > 0 ? (
5296
+ <div className="space-y-1">
5297
+ {providerPricingUi?.additionalAdjustments?.map((adj) => (
5298
+ <div key={adj.id} className="flex items-center justify-between gap-2 text-sm">
5299
+ <div className="flex min-w-0 items-center gap-1">
5300
+ <button
5301
+ type="button"
5302
+ className="rounded border border-stone-300 px-1 text-xs text-stone-600 hover:bg-stone-100"
5303
+ onClick={() => providerPricingUi?.onRemoveAdditionalAdjustment?.(adj.id)}
5304
+ >
5305
+ -
5306
+ </button>
5307
+ <input
5308
+ type="text"
5309
+ className="w-40 rounded border border-stone-300 px-2 py-0.5 text-sm"
5310
+ placeholder="Line description"
5311
+ value={adj.label}
5312
+ onChange={(e) =>
5313
+ providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, { label: e.target.value })
5314
+ }
5315
+ />
5316
+ </div>
5317
+ <div className="flex items-center gap-1">
5318
+ <select
5319
+ className="rounded border border-stone-300 px-1 py-0.5 text-xs"
5320
+ value={adj.mode}
5321
+ onChange={(e) =>
5322
+ providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
5323
+ mode: e.target.value as 'DISCOUNT' | 'CHARGE',
5324
+ })
5325
+ }
5326
+ >
5327
+ <option value="DISCOUNT">-</option>
5328
+ <option value="CHARGE">+</option>
5329
+ </select>
5330
+ <input
5331
+ type="text"
5332
+ inputMode="decimal"
5333
+ 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"
5334
+ placeholder="0.00"
5335
+ value={adj.amountInput}
5336
+ onChange={(e) =>
5337
+ providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
5338
+ amountInput: e.target.value,
5339
+ })
5340
+ }
5341
+ />
5342
+ </div>
5343
+ </div>
5344
+ ))}
5345
+ <button
5346
+ type="button"
5347
+ className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
5348
+ onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
5349
+ >
5350
+ + add line item
5351
+ </button>
5352
+ </div>
5353
+ ) : showProviderPricingInlineEditor ? (
5354
+ <button
5355
+ type="button"
5356
+ className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
5357
+ onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
5358
+ >
5359
+ + add line item
5360
+ </button>
5361
+ ) : undefined
5362
+ }
4891
5363
  firstName={firstName}
4892
5364
  lastName={lastName}
4893
5365
  email={email}
@@ -4928,12 +5400,12 @@ export function AdminChangeBookingFlow({
4928
5400
  setTermsAccepted(checked);
4929
5401
  setTermsAcceptedAt(checked ? new Date().toISOString() : null);
4930
5402
  }}
4931
- isAdmin={false}
5403
+ isAdmin={isAdmin}
4932
5404
  showCommunicationAdminSection={false}
4933
- skipConfirmationCommunications={false}
4934
- disableAutoCommunications={false}
4935
- onSkipConfirmationChange={() => {}}
4936
- onDisableCommunicationsChange={() => {}}
5405
+ skipConfirmationCommunications={skipConfirmationCommunications}
5406
+ disableAutoCommunications={disableAutoCommunications}
5407
+ onSkipConfirmationChange={setSkipConfirmationCommunications}
5408
+ onDisableCommunicationsChange={setDisableAutoCommunications}
4937
5409
  error={checkoutFormError}
4938
5410
  loading={loading}
4939
5411
  totalQuantity={totalQuantity}
@@ -4941,6 +5413,7 @@ export function AdminChangeBookingFlow({
4941
5413
  submitLabel={changeCheckoutButtonLabel ?? deferredInvoiceSubmitLabel}
4942
5414
  hideSubmitButton={
4943
5415
  showCheckoutModal ||
5416
+ showAdminPaymentChoice ||
4944
5417
  !hasEffectiveChangeSelection ||
4945
5418
  isChangeQuoteBlocked
4946
5419
  }
@@ -4949,10 +5422,16 @@ export function AdminChangeBookingFlow({
4949
5422
  attributionConfirmLabel={flowUi?.partnerAttributionConfirmLabel}
4950
5423
  attributionConfirmed={partnerAttributionConfirmed}
4951
5424
  onAttributionConfirmedChange={setPartnerAttributionConfirmed}
4952
- lineAmountInputs={undefined}
4953
- onLineAmountInputChange={undefined}
4954
- onLineAmountInputBlur={undefined}
4955
- onLineAmountReset={undefined}
5425
+ lineAmountInputs={showProviderPricingInlineEditor ? providerPricingUi?.lineAmountInputs : undefined}
5426
+ onLineAmountInputChange={
5427
+ showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountInputChange : undefined
5428
+ }
5429
+ onLineAmountInputBlur={
5430
+ showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountInputBlur : undefined
5431
+ }
5432
+ onLineAmountReset={
5433
+ showProviderPricingInlineEditor ? providerPricingUi?.onLineAmountReset : undefined
5434
+ }
4956
5435
  />
4957
5436
  </>
4958
5437
  )}
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { createPortal } from 'react-dom';
4
3
  import { formatCurrencyAmount } from '../../lib/currency';
5
4
  import type { Currency } from './CurrencySwitcher';
6
5
 
@@ -17,8 +16,8 @@ interface AdminPaymentChoiceModalProps {
17
16
 
18
17
  /**
19
18
  * Provider / staff: pay now vs confirm without payment.
20
- * Uses the same overlay + card + Tailwind button pattern as {@link CheckoutModal}
21
- * so `.booking-flow-preflight button { background: transparent }` does not strip primary styles.
19
+ * Render inline (not portaled) so it remains interactive when embedded inside modal hosts
20
+ * that lock pointer events/inert siblings on `document.body`.
22
21
  */
23
22
  export function AdminPaymentChoiceModal({
24
23
  open,
@@ -34,10 +33,15 @@ export function AdminPaymentChoiceModal({
34
33
 
35
34
  const modal = (
36
35
  <div
37
- className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50"
36
+ className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50 pointer-events-auto"
38
37
  style={{ zIndex: 100_000 }}
38
+ role="dialog"
39
+ aria-modal="true"
39
40
  >
40
- <div className="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-hidden flex flex-col">
41
+ <div
42
+ className="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-hidden flex flex-col pointer-events-auto"
43
+ onClick={(e) => e.stopPropagation()}
44
+ >
41
45
  <div className="p-6 border-b border-stone-200 flex-shrink-0">
42
46
  <div className="flex justify-between items-start gap-3">
43
47
  <h3 className="text-lg font-semibold text-stone-900 pr-2">
@@ -94,5 +98,5 @@ export function AdminPaymentChoiceModal({
94
98
  </div>
95
99
  </div>
96
100
  );
97
- return typeof document !== 'undefined' ? createPortal(modal, document.body) : null;
101
+ return modal;
98
102
  }
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect, useRef, useMemo, type ReactNode } from 'react';
4
- import { createPortal } from 'react-dom';
5
4
  import { loadStripe } from '@stripe/stripe-js';
6
5
  import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
7
6
  import { useBookingHost } from '../../runtime';
@@ -238,10 +237,12 @@ export function CheckoutModal({
238
237
  if (!env.STRIPE_PUBLISHABLE_KEY) {
239
238
  const noStripe = (
240
239
  <div
241
- className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50"
240
+ className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50 pointer-events-auto"
242
241
  style={{ zIndex: 100_000 }}
242
+ role="dialog"
243
+ aria-modal="true"
243
244
  >
244
- <div className="bg-white rounded-xl shadow-xl max-w-md w-full p-6">
245
+ <div className="bg-white rounded-xl shadow-xl max-w-md w-full p-6 pointer-events-auto" onClick={(e) => e.stopPropagation()}>
245
246
  <h3 className="text-lg font-semibold text-stone-900 mb-2">
246
247
  {t('booking.checkout') || 'Checkout'}
247
248
  </h3>
@@ -258,7 +259,7 @@ export function CheckoutModal({
258
259
  </div>
259
260
  </div>
260
261
  );
261
- return typeof document !== 'undefined' ? createPortal(noStripe, document.body) : null;
262
+ return noStripe;
262
263
  }
263
264
 
264
265
  const options = {
@@ -274,10 +275,15 @@ export function CheckoutModal({
274
275
 
275
276
  const checkout = (
276
277
  <div
277
- className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50"
278
+ className="booking-flow-root booking-flow-preflight fixed inset-0 z-[10050] flex items-center justify-center p-4 bg-black/50 pointer-events-auto"
278
279
  style={{ zIndex: 100_000 }}
280
+ role="dialog"
281
+ aria-modal="true"
279
282
  >
280
- <div className="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-hidden flex flex-col">
283
+ <div
284
+ className="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-hidden flex flex-col pointer-events-auto"
285
+ onClick={(e) => e.stopPropagation()}
286
+ >
281
287
  <div className="p-6 border-b border-stone-200 flex-shrink-0">
282
288
  <div className="flex justify-between items-start">
283
289
  <h3 className="text-lg font-semibold text-stone-900">
@@ -449,5 +455,5 @@ export function CheckoutModal({
449
455
  </div>
450
456
  </div>
451
457
  );
452
- return typeof document !== 'undefined' ? createPortal(checkout, document.body) : null;
458
+ return checkout;
453
459
  }
@@ -3,6 +3,7 @@ import type { Product } from '../../lib/booking-api';
3
3
  import type { BookingSourceMetadata } from '../../lib/booking/source-metadata';
4
4
  import type { Currency } from './CurrencySwitcher';
5
5
  import type { BookingFlowUiOptions } from './booking-flow-ui';
6
+ import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
6
7
 
7
8
  /** Live selection snapshot for change-booking compare UI (parent dialog). */
8
9
  export interface ChangeFlowSelectionPreview {
@@ -126,6 +127,8 @@ export interface ChangeBookingFlowProps extends BookingFlowBaseProps {
126
127
  quantity?: number;
127
128
  }>;
128
129
  } | null;
130
+ /** Admin/provider dashboard integration hook (used by AdminChangeBookingFlow). */
131
+ onChangeBooking?: (data: ProviderDashboardChangeBookingPayload) => Promise<void>;
129
132
  /**
130
133
  * Embed compatibility (e.g. ticketbooth provider dashboard): ignored — same flow as the public site.
131
134
  * When true, {@link BookingFlow} still renders the fork {@link AdminChangeBookingFlow} for future divergence.