@ticketboothapp/booking 1.2.55 → 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.
@@ -15,6 +15,11 @@ export interface PriceBreakdownProps {
15
15
  breakdown: PriceBreakdownType | null;
16
16
  currency: Currency;
17
17
  locale: Locale;
18
+ editable?: boolean;
19
+ editableValue?: string;
20
+ onEditableChange?: (value: string) => void;
21
+ onEditableBlur?: () => void;
22
+ onEditableReset?: () => void;
18
23
  }
19
24
 
20
25
  /**
@@ -39,6 +44,10 @@ function formatAdjustmentDetail(
39
44
  return '';
40
45
  }
41
46
 
47
+ function getCurrencySymbol(currency: Currency, locale: Locale): string {
48
+ return formatCurrencyAmount(0, currency, locale).replace(/[0-9\s,.-]/g, '') || currency;
49
+ }
50
+
42
51
  /**
43
52
  * Renders a single price line (e.g. "Adult × 2 — $X"). When the host app grants
44
53
  * permission, shows a hover tooltip with the full price breakdown (base, adjustments,
@@ -51,11 +60,17 @@ export function PriceBreakdown({
51
60
  breakdown,
52
61
  currency,
53
62
  locale,
63
+ editable = false,
64
+ editableValue,
65
+ onEditableChange,
66
+ onEditableBlur,
67
+ onEditableReset,
54
68
  }: PriceBreakdownProps) {
55
69
  const [showTooltip, setShowTooltip] = useState(false);
56
70
  const { t } = useTranslations();
57
71
  const { permissions } = useBookingApp();
58
72
  const canShowBreakdown = breakdown != null && permissions.canViewPriceBreakdown;
73
+ const currencySymbol = getCurrencySymbol(currency, locale);
59
74
 
60
75
  // Calculate base price for discount detection
61
76
  // In public mode, the base price in breakdown already has dynamic increases rolled in
@@ -69,9 +84,34 @@ export function PriceBreakdown({
69
84
  <span className="text-sm text-stone-600">
70
85
  {category} {qty > 1 ? `× ${qty}` : ''}
71
86
  </span>
72
- <span className="text-sm font-medium text-stone-700">
73
- {formatCurrencyAmount(itemTotal, currency, locale)}
74
- </span>
87
+ {editable && onEditableChange ? (
88
+ <div className="flex items-center gap-1">
89
+ {onEditableReset ? (
90
+ <button
91
+ type="button"
92
+ className="rounded p-0.5 text-stone-400 hover:bg-stone-100 hover:text-stone-700"
93
+ onClick={onEditableReset}
94
+ aria-label={`Reset ${category} amount`}
95
+ title="Reset to quoted value"
96
+ >
97
+ <span className="inline-block h-3.5 w-3.5 text-center text-xs leading-[14px]">↺</span>
98
+ </button>
99
+ ) : null}
100
+ <span className="text-sm text-stone-500">{currencySymbol}</span>
101
+ <input
102
+ type="text"
103
+ inputMode="decimal"
104
+ 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"
105
+ value={editableValue ?? String(itemTotal)}
106
+ onChange={(e) => onEditableChange(e.target.value)}
107
+ onBlur={onEditableBlur}
108
+ />
109
+ </div>
110
+ ) : (
111
+ <span className="text-sm font-medium text-stone-700">
112
+ {formatCurrencyAmount(itemTotal, currency, locale)}
113
+ </span>
114
+ )}
75
115
  </div>
76
116
  );
77
117
  }
@@ -82,30 +122,55 @@ export function PriceBreakdown({
82
122
  {category} {qty > 1 ? `× ${qty}` : ''}
83
123
  </span>
84
124
  <div className="relative flex-shrink-0 whitespace-nowrap">
85
- <span
86
- className={
87
- canShowBreakdown
88
- ? 'text-sm font-medium text-stone-700 cursor-help underline decoration-dotted'
89
- : 'text-sm font-medium text-stone-700'
90
- }
91
- onMouseEnter={canShowBreakdown ? () => setShowTooltip(true) : undefined}
92
- onMouseLeave={canShowBreakdown ? () => setShowTooltip(false) : undefined}
93
- >
94
- {hasDiscount ? (
95
- <>
96
- <span className="line-through text-stone-400">
97
- {formatCurrencyAmount(baseTotal, currency, locale)}
98
- </span>
99
- {' '}
100
- <span className="text-emerald-600">
101
- {formatCurrencyAmount(itemTotal, currency, locale)}
102
- </span>
103
- </>
104
- ) : (
105
- formatCurrencyAmount(itemTotal, currency, locale)
106
- )}
107
- </span>
108
- {canShowBreakdown && showTooltip && (
125
+ {editable && onEditableChange ? (
126
+ <div className="flex items-center gap-1">
127
+ {onEditableReset ? (
128
+ <button
129
+ type="button"
130
+ className="rounded p-0.5 text-stone-400 hover:bg-stone-100 hover:text-stone-700"
131
+ onClick={onEditableReset}
132
+ aria-label={`Reset ${category} amount`}
133
+ title="Reset to quoted value"
134
+ >
135
+ <span className="inline-block h-3.5 w-3.5 text-center text-xs leading-[14px]">↺</span>
136
+ </button>
137
+ ) : null}
138
+ <span className="text-sm text-stone-500">{currencySymbol}</span>
139
+ <input
140
+ type="text"
141
+ inputMode="decimal"
142
+ 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"
143
+ value={editableValue ?? String(itemTotal)}
144
+ onChange={(e) => onEditableChange(e.target.value)}
145
+ onBlur={onEditableBlur}
146
+ />
147
+ </div>
148
+ ) : (
149
+ <span
150
+ className={
151
+ canShowBreakdown
152
+ ? 'text-sm font-medium text-stone-700 cursor-help underline decoration-dotted'
153
+ : 'text-sm font-medium text-stone-700'
154
+ }
155
+ onMouseEnter={canShowBreakdown ? () => setShowTooltip(true) : undefined}
156
+ onMouseLeave={canShowBreakdown ? () => setShowTooltip(false) : undefined}
157
+ >
158
+ {hasDiscount ? (
159
+ <>
160
+ <span className="line-through text-stone-400">
161
+ {formatCurrencyAmount(baseTotal, currency, locale)}
162
+ </span>
163
+ {' '}
164
+ <span className="text-emerald-600">
165
+ {formatCurrencyAmount(itemTotal, currency, locale)}
166
+ </span>
167
+ </>
168
+ ) : (
169
+ formatCurrencyAmount(itemTotal, currency, locale)
170
+ )}
171
+ </span>
172
+ )}
173
+ {!editable && canShowBreakdown && showTooltip && (
109
174
  <div className="absolute right-0 top-full mt-2 w-72 p-3 bg-stone-900 text-white text-xs rounded-lg shadow-xl z-50">
110
175
  <div className="font-semibold mb-2 pb-2 border-b border-stone-700">
111
176
  Price Breakdown ({category})
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { ReactNode } from 'react';
3
4
  import { formatCurrencyAmount } from '../../lib/currency';
4
5
  import type { PriceBreakdown as PriceBreakdownType } from '../../lib/booking/pricing';
5
6
  import { PriceBreakdown } from './PriceBreakdown';
@@ -12,6 +13,8 @@ import styles from './PriceSummary.module.css';
12
13
  export type PriceSummaryLine =
13
14
  | {
14
15
  kind: 'ticket';
16
+ lineKey?: string;
17
+ editable?: boolean;
15
18
  category: string;
16
19
  qty: number;
17
20
  itemTotal: number;
@@ -19,6 +22,7 @@ export type PriceSummaryLine =
19
22
  }
20
23
  | {
21
24
  kind: 'line';
25
+ lineKey?: string;
22
26
  label: string;
23
27
  amount: number;
24
28
  /** Drives styling: discount (red, -), return add-on (green, +), default (stone). Receipt types: TICKET, FEE, RETURN_OPTION, PROMO_CODE, CANCELLATION_UPGRADE, TAX, etc. */
@@ -26,6 +30,8 @@ export type PriceSummaryLine =
26
30
  quantity?: number | null;
27
31
  /** Optional tooltip text - when set, shows info icon next to label (e.g. for Moraine Lake Road Access Fee) */
28
32
  tooltip?: string;
33
+ /** Optional: render amount as inline editable input when handler is provided. */
34
+ editable?: boolean;
29
35
  };
30
36
 
31
37
  export interface PriceSummaryProps {
@@ -53,6 +59,8 @@ export interface PriceSummaryProps {
53
59
  className?: string;
54
60
  /** Optional: render between tax row and total row (e.g. promo code input in BookingFlow) */
55
61
  extraBetweenTaxAndTotal?: React.ReactNode;
62
+ /** Optional: render inside receipt before subtotal/tax rows. */
63
+ extraBeforeSubtotal?: React.ReactNode;
56
64
  /** Subtotal row spacing: 'compact' for booking flow/Stripe modal, 'relaxed' for /manage (equal top/bottom) */
57
65
  subtotalSpacing?: 'compact' | 'relaxed';
58
66
  /** Deposit mode: show Total (full), Deposit (amount due today), Remaining Balance */
@@ -61,6 +69,16 @@ export interface PriceSummaryProps {
61
69
  hideSubtotal?: boolean;
62
70
  /** Overrides the final "Total" row label (e.g. change booking: amount due for the change) */
63
71
  totalLabel?: string;
72
+ /** Render after the total row (e.g. provider price delta + keep-original-price) */
73
+ extraAfterTotal?: ReactNode;
74
+ /** Optional map of editable line input values by line key. */
75
+ lineAmountInputs?: Record<string, string>;
76
+ /** Optional change handler for inline editable line amounts. */
77
+ onLineAmountInputChange?: (lineKey: string, value: string) => void;
78
+ /** Optional blur handler for enforcing display format (e.g. 2 decimals). */
79
+ onLineAmountInputBlur?: (lineKey: string) => void;
80
+ /** Optional reset handler for inline editable line amounts. */
81
+ onLineAmountReset?: (lineKey: string) => void;
64
82
  }
65
83
 
66
84
  function getLineAmountClass(type: string | undefined, amount: number): string {
@@ -84,6 +102,10 @@ function formatLineAmount(
84
102
  return formatted;
85
103
  }
86
104
 
105
+ function getCurrencySymbol(currency: Currency, locale: Locale): string {
106
+ return formatCurrencyAmount(0, currency, locale).replace(/[0-9\s,.-]/g, '') || currency;
107
+ }
108
+
87
109
  /**
88
110
  * Reusable price breakdown/summary used in:
89
111
  * - BookingFlow sidebar (order summary)
@@ -105,13 +127,20 @@ export function PriceSummary({
105
127
  t = (k) => k,
106
128
  className = '',
107
129
  extraBetweenTaxAndTotal,
130
+ extraBeforeSubtotal,
108
131
  subtotalSpacing = 'compact',
109
132
  depositMode,
110
133
  hideSubtotal = false,
111
134
  totalLabel,
135
+ extraAfterTotal,
136
+ lineAmountInputs,
137
+ onLineAmountInputChange,
138
+ onLineAmountInputBlur,
139
+ onLineAmountReset,
112
140
  }: PriceSummaryProps) {
113
141
  const textSize = size === 'sm' ? 'text-sm' : 'text-base';
114
142
  const totalSize = size === 'sm' ? 'text-xl' : 'text-2xl';
143
+ const currencySymbol = getCurrencySymbol(currency, locale);
115
144
  const subtotalRowClass = subtotalSpacing === 'relaxed'
116
145
  ? `pt-3 pb-3 mt-2 ${styles.ruleAbove}`
117
146
  : `mt-2 pt-1.5 ${styles.ruleAbove}`;
@@ -131,10 +160,27 @@ export function PriceSummary({
131
160
  breakdown={row.breakdown ?? null}
132
161
  currency={currency}
133
162
  locale={locale}
163
+ editable={row.editable}
164
+ editableValue={row.lineKey ? lineAmountInputs?.[row.lineKey] : undefined}
165
+ onEditableChange={
166
+ row.editable && row.lineKey && onLineAmountInputChange
167
+ ? (value) => onLineAmountInputChange(row.lineKey!, value)
168
+ : undefined
169
+ }
170
+ onEditableBlur={
171
+ row.editable && row.lineKey && onLineAmountInputBlur
172
+ ? () => onLineAmountInputBlur(row.lineKey!)
173
+ : undefined
174
+ }
175
+ onEditableReset={
176
+ row.editable && row.lineKey && onLineAmountReset
177
+ ? () => onLineAmountReset(row.lineKey!)
178
+ : undefined
179
+ }
134
180
  />
135
181
  );
136
182
  }
137
- const { label, amount, type, quantity, tooltip } = row;
183
+ const { lineKey, label, amount, type, quantity, tooltip, editable } = row;
138
184
  // Receipt mode: insert Subtotal row before first TAX line
139
185
  const isTaxLine = type === 'TAX';
140
186
  const showSubtotalBeforeTax = isTaxLine && subtotal != null && subtotal > 0 && !subtotalShown;
@@ -155,13 +201,39 @@ export function PriceSummary({
155
201
  </span>
156
202
  {tooltip && <InfoTooltip text={tooltip} />}
157
203
  </span>
158
- <span className={`flex-shrink-0 whitespace-nowrap font-medium ${getLineAmountClass(type, amount)}`}>
159
- {formatLineAmount('line', amount, type, currency, locale)}
160
- </span>
204
+ {editable && lineKey && onLineAmountInputChange ? (
205
+ <div className="flex items-center gap-1">
206
+ {onLineAmountReset ? (
207
+ <button
208
+ type="button"
209
+ className="rounded p-0.5 text-stone-400 hover:bg-stone-100 hover:text-stone-700"
210
+ onClick={() => onLineAmountReset(lineKey)}
211
+ aria-label={`Reset ${label} amount`}
212
+ title="Reset to quoted value"
213
+ >
214
+ <span className="inline-block h-3.5 w-3.5 text-center text-xs leading-[14px]">↺</span>
215
+ </button>
216
+ ) : null}
217
+ <span className="text-sm text-stone-500">{currencySymbol}</span>
218
+ <input
219
+ type="text"
220
+ inputMode="decimal"
221
+ 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"
222
+ value={lineAmountInputs?.[lineKey] ?? String(amount)}
223
+ onChange={(e) => onLineAmountInputChange(lineKey, e.target.value)}
224
+ onBlur={() => onLineAmountInputBlur?.(lineKey)}
225
+ />
226
+ </div>
227
+ ) : (
228
+ <span className={`flex-shrink-0 whitespace-nowrap font-medium ${getLineAmountClass(type, amount)}`}>
229
+ {formatLineAmount('line', amount, type, currency, locale)}
230
+ </span>
231
+ )}
161
232
  </div>
162
233
  </div>
163
234
  );
164
235
  })}
236
+ {extraBeforeSubtotal}
165
237
 
166
238
  {/* Checkout mode: subtotal/tax/discount not in lines (e.g. Stripe Review & pay modal) */}
167
239
  {subtotal != null && !subtotalShown && !hideSubtotal && (subtotal !== total || discountAmount > 0) && (
@@ -230,6 +302,7 @@ export function PriceSummary({
230
302
  </div>
231
303
  )}
232
304
  </div>
305
+ {extraAfterTotal}
233
306
  </div>
234
307
  );
235
308
  }
@@ -58,6 +58,10 @@ import {
58
58
  } from '../../lib/booking/source-metadata';
59
59
  import type { VideoSources } from '../../constants/products';
60
60
  import type { BookingFlowUiOptions } from './booking-flow-ui';
61
+ import type {
62
+ ProviderDashboardChangeBookingPayload,
63
+ ProviderDashboardInitialBooking,
64
+ } from './provider-dashboard-change-booking';
61
65
  import { BOOKING_FLOW_ABANDON_EVENT } from '../../providers/booking-dialog-provider';
62
66
 
63
67
  interface PrivateShuttleBookingFlowProps {
@@ -99,6 +103,9 @@ interface PrivateShuttleBookingFlowProps {
99
103
  specialRequest?: string | null;
100
104
  notes?: string | null;
101
105
  };
106
+ /** Provider dashboard change-booking (TicketBooth `BookingWidget` initialBooking + onChangeBooking). */
107
+ initialBooking?: ProviderDashboardInitialBooking;
108
+ onChangeBooking?: (data: ProviderDashboardChangeBookingPayload) => Promise<void>;
102
109
  }
103
110
 
104
111
  const RESOURCE_CAPACITY = 13;
@@ -151,6 +158,8 @@ export function PrivateShuttleBookingFlow({
151
158
  availabilityPricingProfileId,
152
159
  availabilityCancellationPolicyProfileId,
153
160
  initialValues,
161
+ initialBooking,
162
+ onChangeBooking,
154
163
  }: PrivateShuttleBookingFlowProps) {
155
164
  const { strings: defaultStrings, analytics, catalog } = useBookingHost();
156
165
  const { t } = useTranslations();
@@ -168,6 +177,7 @@ export function PrivateShuttleBookingFlow({
168
177
  } = useBookingApp();
169
178
  const availabilitiesCache = useAvailabilitiesCache();
170
179
  const isAdmin = permissions.viewerRole === 'admin';
180
+ const isProviderChangeMode = Boolean(initialBooking && onChangeBooking);
171
181
 
172
182
  const [availabilities, setAvailabilities] = useState<Availability[]>([]);
173
183
  const [selectedDate, setSelectedDate] = useState<string>('');
@@ -175,11 +185,15 @@ export function PrivateShuttleBookingFlow({
175
185
  const [selectedOption, setSelectedOption] = useState<string>('');
176
186
  const [selectedStartTime, setSelectedStartTime] = useState<string>('');
177
187
  const [isCustomTimeMode, setIsCustomTimeMode] = useState(false);
178
- const [passengerCount, setPassengerCount] = useState<number>(1);
188
+ const [passengerCount, setPassengerCount] = useState<number>(
189
+ () => initialBooking?.privateShuttleDetails?.passengerCount ?? initialValues?.passengers ?? 1
190
+ );
179
191
  const [email, setEmail] = useState('');
180
192
  const [firstName, setFirstName] = useState('');
181
193
  const [lastName, setLastName] = useState('');
182
- const [pickupLocationId, setPickupLocationId] = useState<string | null>(null);
194
+ const [pickupLocationId, setPickupLocationId] = useState<string | null>(
195
+ () => initialBooking?.pickupLocationId ?? initialValues?.pickupLocationId ?? null
196
+ );
183
197
  const hasAutoSelectedPartnerDateRef = useRef(false);
184
198
  const hasAutoSelectedPartnerPickupRef = useRef(false);
185
199
  const handleDateSelectRef = useRef<(date: string) => void>(() => {});
@@ -191,7 +205,15 @@ export function PrivateShuttleBookingFlow({
191
205
  const [foodRestrictions, setFoodRestrictions] = useState('');
192
206
  const [addOnSelections, setAddOnSelections] = useState<
193
207
  Array<{ addOnId: string; variantId?: string; quantity?: number }>
194
- >([]);
208
+ >(() =>
209
+ initialBooking?.addOnSelections?.length
210
+ ? initialBooking.addOnSelections.map((s) => ({
211
+ addOnId: s.addOnId,
212
+ variantId: s.variantId,
213
+ quantity: s.quantity ?? 1,
214
+ }))
215
+ : []
216
+ );
195
217
  const [addOns, setAddOns] = useState<AddOn[]>([]);
196
218
  const [loading, setLoading] = useState(false);
197
219
  const [loadingAvailabilities, setLoadingAvailabilities] = useState(true);
@@ -230,14 +252,18 @@ export function PrivateShuttleBookingFlow({
230
252
  const [skipConfirmationCommunications, setSkipConfirmationCommunications] = useState(false);
231
253
  const [disableAutoCommunications, setDisableAutoCommunications] = useState(false);
232
254
  const [showAdminPaymentChoice, setShowAdminPaymentChoice] = useState(false);
233
- const [promoCodeInput, setPromoCodeInput] = useState('');
234
- const [appliedPromoCode, setAppliedPromoCode] = useState<string | null>(null);
255
+ const [promoCodeInput, setPromoCodeInput] = useState(() => initialBooking?.promoCode?.trim() ?? '');
256
+ const [appliedPromoCode, setAppliedPromoCode] = useState<string | null>(
257
+ () => initialBooking?.promoCode?.trim() || null
258
+ );
235
259
  const [promoCodeError, setPromoCodeError] = useState('');
236
260
  const [promoCodeValidating, setPromoCodeValidating] = useState(false);
237
261
  const [promoDiscountAmount, setPromoDiscountAmount] = useState(0);
238
262
  const [isGiftCard, setIsGiftCard] = useState(false);
239
263
  const [isVoucher, setIsVoucher] = useState(false);
240
- const [additionalHoursCount, setAdditionalHoursCount] = useState(0);
264
+ const [additionalHoursCount, setAdditionalHoursCount] = useState(
265
+ () => initialBooking?.additionalHoursCount ?? initialValues?.additionalHoursCount ?? 0
266
+ );
241
267
  const [isSpecialRequestMode, setIsSpecialRequestMode] = useState(false);
242
268
  const [specialRequestInputValue, setSpecialRequestInputValue] = useState('');
243
269
  const [adminChoiceData, setAdminChoiceData] = useState<{
@@ -267,7 +293,9 @@ export function PrivateShuttleBookingFlow({
267
293
  discountLabel?: string | null;
268
294
  } | null>(null);
269
295
  const [pricingConfig, setPricingConfig] = useState<PricingConfig | null>(null);
270
- const [cancellationPolicyId, setCancellationPolicyId] = useState<string | null>(null);
296
+ const [cancellationPolicyId, setCancellationPolicyId] = useState<string | null>(
297
+ () => initialBooking?.cancellationPolicyId ?? null
298
+ );
271
299
  const [precomputedPrices, setPrecomputedPrices] = useState<PrecomputedPricesByCategory | null>(null);
272
300
  const [resourcePriceByCurrency, setResourcePriceByCurrency] = useState<Record<string, number> | null>(null);
273
301
  const [resourcePriceByOption, setResourcePriceByOption] = useState<
@@ -1199,21 +1227,23 @@ export function PrivateShuttleBookingFlow({
1199
1227
  setError(t('booking.selectPickupLocation'));
1200
1228
  return;
1201
1229
  }
1202
- if (!email) {
1203
- setError(t('booking.enterEmail') || 'Please enter your email address');
1204
- return;
1205
- }
1206
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
1207
- setError(t('booking.invalidEmail') || 'Please enter a valid email address');
1208
- return;
1209
- }
1210
- if (!firstName?.trim()) {
1211
- setError(t('booking.enterFirstName') || 'Please enter your first name');
1212
- return;
1213
- }
1214
- if (!lastName?.trim()) {
1215
- setError(t('booking.enterLastName') || 'Please enter your last name');
1216
- return;
1230
+ if (!isProviderChangeMode) {
1231
+ if (!email) {
1232
+ setError(t('booking.enterEmail') || 'Please enter your email address');
1233
+ return;
1234
+ }
1235
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
1236
+ setError(t('booking.invalidEmail') || 'Please enter a valid email address');
1237
+ return;
1238
+ }
1239
+ if (!firstName?.trim()) {
1240
+ setError(t('booking.enterFirstName') || 'Please enter your first name');
1241
+ return;
1242
+ }
1243
+ if (!lastName?.trim()) {
1244
+ setError(t('booking.enterLastName') || 'Please enter your last name');
1245
+ return;
1246
+ }
1217
1247
  }
1218
1248
 
1219
1249
  setLoading(true);
@@ -1226,6 +1256,31 @@ export function PrivateShuttleBookingFlow({
1226
1256
  setLoading(false);
1227
1257
  return;
1228
1258
  }
1259
+ if (isProviderChangeMode && onChangeBooking) {
1260
+ const bookingItems = [{ category: 'RESOURCE' as const, count: billableResourceCount }];
1261
+ const [hours, minutes] = selectedStartTime.split(':').map(Number);
1262
+ const startTimeISO = `${selectedDate}T${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00-06:00`;
1263
+ await onChangeBooking({
1264
+ productId: selectedOption,
1265
+ dateTime: selectedDate,
1266
+ bookingItems,
1267
+ returnAvailabilityId: null,
1268
+ pickupLocationId: pickupLocationId ?? null,
1269
+ travelerHotel:
1270
+ selectedPickupLocation?.name ?? customPickupAddress ?? initialBooking?.travelerHotel ?? null,
1271
+ startTime: startTimeISO,
1272
+ passengerCount,
1273
+ childSafetySeatsCount: childSafetySeatsCount > 0 ? childSafetySeatsCount : null,
1274
+ foodRestrictions: foodRestrictions.trim() || null,
1275
+ addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
1276
+ cancellationPolicyId: cancellationPolicyId ?? initialBooking?.cancellationPolicyId ?? null,
1277
+ promoCode: appliedPromoCode ?? null,
1278
+ newTotalAmount: totalPrice,
1279
+ additionalHoursCount: isAdmin ? additionalHoursCount : null,
1280
+ });
1281
+ setLoading(false);
1282
+ return;
1283
+ }
1229
1284
  const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
1230
1285
  clientChannelSource: inferClientBookingSourceFromProductIds(product.productId, selectedOption),
1231
1286
  forcePartnerPortalChannel: partnerPortalBooking,
@@ -2649,11 +2704,12 @@ export function PrivateShuttleBookingFlow({
2649
2704
  className={styles.submitBtn}
2650
2705
  >
2651
2706
  {loading
2652
- ? (t('booking.creatingReservation') || 'Creating reservation...')
2653
- : (flowUi?.partnerDeferredInvoiceSubmitLabel ||
2654
- t('booking.continueToPayment') ||
2655
- 'Continue to Payment')}{' '}
2656
- ({formatCurrencyAmount(depositInfo ? depositInfo.depositAmount : totalPrice, currency, locale as 'en' | 'fr')})
2707
+ ? isProviderChangeMode
2708
+ ? 'Processing…'
2709
+ : (t('booking.creatingReservation') || 'Creating reservation...')
2710
+ : isProviderChangeMode
2711
+ ? `Change booking (${formatCurrencyAmount(depositInfo ? depositInfo.depositAmount : totalPrice, currency, locale as 'en' | 'fr')})`
2712
+ : `${flowUi?.partnerDeferredInvoiceSubmitLabel || t('booking.continueToPayment') || 'Continue to Payment'} (${formatCurrencyAmount(depositInfo ? depositInfo.depositAmount : totalPrice, currency, locale as 'en' | 'fr')})`}
2657
2713
  </button>
2658
2714
  <p className={styles.secureNote}>
2659
2715
  {t('booking.securePayment') || 'Secure payment powered by Stripe'}
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState } from 'react';
4
4
  import { useBookingHost } from '../../runtime';
5
+ import { DefaultTermsContent } from './DefaultTermsContent';
5
6
 
6
7
  export interface TermsAcceptanceProps {
7
8
  checked: boolean;
@@ -25,7 +26,7 @@ export function TermsAcceptance({
25
26
  termsContent,
26
27
  }: TermsAcceptanceProps) {
27
28
  const { slots } = useBookingHost();
28
- const TermsContent = slots.TermsContent;
29
+ const TermsContent = slots.TermsContent ?? DefaultTermsContent;
29
30
  const [modalOpen, setModalOpen] = useState(false);
30
31
  const content =
31
32
  termsContent ?? (
@@ -1,3 +1,43 @@
1
+ export type ProviderDashboardPricingLine = {
2
+ lineKey: string;
3
+ label: string;
4
+ amount: number;
5
+ editable: boolean;
6
+ type?: string;
7
+ };
8
+
9
+ export type ProviderDashboardChangePricingUi = {
10
+ loading?: boolean;
11
+ error?: string | null;
12
+ helperText?: string;
13
+ quotedLines?: ProviderDashboardPricingLine[];
14
+ lineAmountInputs?: Record<string, string>;
15
+ onLineAmountInputChange?: (lineKey: string, value: string) => void;
16
+ onLineAmountInputBlur?: (lineKey: string) => void;
17
+ onLineAmountReset?: (lineKey: string) => void;
18
+ additionalAdjustments?: Array<{
19
+ id: string;
20
+ label: string;
21
+ amountInput: string;
22
+ mode: 'DISCOUNT' | 'CHARGE';
23
+ originalLabel?: string;
24
+ originalAmountInput?: string;
25
+ originalMode?: 'DISCOUNT' | 'CHARGE';
26
+ }>;
27
+ onAddAdditionalAdjustment?: () => void;
28
+ onUpdateAdditionalAdjustment?: (
29
+ id: string,
30
+ patch: Partial<{ label: string; amountInput: string; mode: 'DISCOUNT' | 'CHARGE' }>
31
+ ) => void;
32
+ onRemoveAdditionalAdjustment?: (id: string) => void;
33
+ totalsPreview?: {
34
+ subtotalBeforeTax: number;
35
+ taxAmount: number;
36
+ totalAmount: number;
37
+ currency: string;
38
+ } | null;
39
+ };
40
+
1
41
  /**
2
42
  * Optional UI/behavior overrides for booking flows.
3
43
  * Omitted fields keep main-site / change-booking defaults.
@@ -23,6 +63,8 @@ export interface BookingFlowUiOptions {
23
63
  * when a host page has its own sticky chrome (e.g. partner portal header + tabs).
24
64
  */
25
65
  itineraryStickyTopOffsetPx?: number;
66
+ /** Provider dashboard change flow: quote/override panel shown immediately before confirm CTA. */
67
+ providerDashboardChangePricingUi?: ProviderDashboardChangePricingUi;
26
68
  }
27
69
 
28
70
  /**
@@ -301,13 +301,16 @@
301
301
  color: #b91c1c !important;
302
302
  }
303
303
 
304
- /* Clickable "your pickup location" etc. - style as link (blue, underlined) like ticketbooth */
304
+ /* Clickable "your pickup location" etc. should stay booking green */
305
305
  .booking-flow-preflight button.text-stone-400.underline,
306
306
  .booking-flow-preflight button.text-stone-400.underline:hover {
307
- color: var(--booking-primary) !important;
307
+ color: var(--booking-emerald-600, #059669) !important;
308
308
  background: none;
309
309
  border: none;
310
310
  }
311
+ .booking-flow-preflight button.text-stone-400.underline:hover {
312
+ color: var(--booking-emerald-700, #047857) !important;
313
+ }
311
314
 
312
315
  /* ========== Itinerary section - orange bg, Poppins, lowercase (match TourDescription) ========== */
313
316
  .booking-flow-preflight [class*="ItineraryBox_box"],
@@ -907,6 +910,38 @@
907
910
  border-bottom-width: 1px !important;
908
911
  }
909
912
 
913
+ /* Change-booking compare: single vertical / horizontal rule between current vs new (preflight-safe) */
914
+ /* Same as column total row: `border-t border-stone-200/80` in change-booking-compare.tsx */
915
+ .booking-flow-preflight .booking-change-compare-divider,
916
+ .booking-flow-root .booking-change-compare-divider {
917
+ flex-shrink: 0 !important;
918
+ border: none !important;
919
+ background-color: color-mix(
920
+ in srgb,
921
+ var(--booking-stone-200, #e7e5e4) 80%,
922
+ transparent
923
+ ) !important;
924
+ }
925
+ @media (max-width: 767px) {
926
+ .booking-flow-preflight .booking-change-compare-divider,
927
+ .booking-flow-root .booking-change-compare-divider {
928
+ height: 1px !important;
929
+ width: 100% !important;
930
+ min-height: 1px !important;
931
+ }
932
+ }
933
+ @media (min-width: 768px) {
934
+ .booking-flow-preflight .booking-change-compare-divider,
935
+ .booking-flow-root .booking-change-compare-divider {
936
+ width: 1px !important;
937
+ align-self: stretch !important;
938
+ height: auto !important;
939
+ min-height: 3rem !important;
940
+ margin-top: 0.875rem !important;
941
+ margin-bottom: 0.875rem !important;
942
+ }
943
+ }
944
+
910
945
  /* PriceSummary.ruleAbove — explicit module border beats preflight `* { border-width: 0 }` */
911
946
  .booking-flow-preflight [class*="PriceSummary_ruleAbove"],
912
947
  .booking-flow-root [class*="PriceSummary_ruleAbove"] {