@ticketboothapp/booking 1.2.166 → 1.2.168

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.
Files changed (65) hide show
  1. package/package.json +1 -1
  2. package/src/components/booking/AdminChangeBookingContent.tsx +15 -1
  3. package/src/components/booking/AdminChangeBookingFlow.tsx +274 -94
  4. package/src/components/booking/AdminChangeCheckoutPanel.tsx +49 -13
  5. package/src/components/booking/AdminChangePricingPanel.tsx +400 -134
  6. package/src/components/booking/AdminChangePromoEditor.tsx +97 -0
  7. package/src/components/booking/AdminChangeReceiptComparison.tsx +172 -147
  8. package/src/components/booking/ChangeBookingFlow.tsx +6 -3
  9. package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +7 -2
  10. package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +6 -0
  11. package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +80 -0
  12. package/src/components/booking/NewBookingFlow.tsx +14 -2
  13. package/src/components/booking/PickupLocationSelector.module.css +22 -0
  14. package/src/components/booking/PickupLocationSelector.tsx +3 -3
  15. package/src/components/booking/PriceBreakdown.tsx +66 -15
  16. package/src/components/booking/PriceSummary.tsx +16 -0
  17. package/src/components/booking/PrivateShuttleBookingFlow.tsx +52 -14
  18. package/src/components/booking/PrivateShuttleCheckoutSection.tsx +95 -54
  19. package/src/components/booking/admin-adjustment-components.ts +44 -0
  20. package/src/components/booking/admin-adjustment-tax-behavior.ts +50 -0
  21. package/src/components/booking/admin-change-flow-state-helpers.ts +10 -0
  22. package/src/components/booking/admin-change-provider-payload.ts +12 -1
  23. package/src/components/booking/admin-change-quote-request-key.ts +41 -1
  24. package/src/components/booking/admin-change-receipt-lines.ts +49 -0
  25. package/src/components/booking/admin-change-v2-quote-request.ts +109 -0
  26. package/src/components/booking/admin-refund-decision-context.ts +122 -0
  27. package/src/components/booking/admin-refund-disposition.ts +71 -2
  28. package/src/components/booking/availability-date-selection.ts +16 -0
  29. package/src/components/booking/booking-flow-types.ts +4 -0
  30. package/src/components/booking/booking-flow.css +5 -0
  31. package/src/components/booking/change-booking-error-message.ts +137 -0
  32. package/src/components/booking/change-booking-flow-helpers.ts +77 -2
  33. package/src/components/booking/change-booking-quote-guards.ts +4 -1
  34. package/src/components/booking/change-booking-quote-state.ts +44 -1
  35. package/src/components/booking/incompatible-add-on-selections.ts +19 -0
  36. package/src/components/booking/private-shuttle-availability.ts +14 -0
  37. package/src/components/booking/private-shuttle-cancellation-policy.ts +9 -0
  38. package/src/components/booking/private-shuttle-checkout-controller.ts +5 -0
  39. package/src/components/booking/private-shuttle-provider-change-runner.ts +21 -0
  40. package/src/components/booking/private-shuttle-reservation-runner.ts +5 -3
  41. package/src/components/booking/provider-dashboard-change-booking.ts +18 -2
  42. package/src/components/booking/use-standard-booking-availability.ts +5 -2
  43. package/src/components/booking/useAdminChangeCheckoutController.ts +56 -1
  44. package/src/components/booking/useAdminChangeProductReset.ts +6 -0
  45. package/src/components/booking/useAdminChangeProtectedPricing.ts +4 -0
  46. package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +8 -6
  47. package/src/components/booking/useAdminChangeQuotePreview.ts +146 -61
  48. package/src/components/booking/useAdminCustomReceiptLines.ts +171 -10
  49. package/src/components/booking/useAdminProviderPricingAdjustments.ts +50 -19
  50. package/src/components/booking/useBookingAvailabilityAddOns.ts +26 -4
  51. package/src/components/booking/useBookingPromoAndQuantityController.ts +16 -2
  52. package/src/components/booking/useChangeBookingAddOnFloorController.ts +15 -9
  53. package/src/components/booking/useChangeBookingAutoSelections.ts +60 -17
  54. package/src/components/booking/useChangeBookingInitialHydration.ts +10 -7
  55. package/src/components/booking/useChangeBookingQuotePreview.ts +3 -1
  56. package/src/components/booking/useChangeBookingSelectionDetails.ts +23 -17
  57. package/src/components/booking/useStandardBookingAutoSelections.ts +55 -7
  58. package/src/lib/booking/booking-cutoffs.ts +22 -0
  59. package/src/lib/booking/change-booking-server-preview.ts +92 -5
  60. package/src/lib/booking/change-flow-pricing.ts +12 -0
  61. package/src/lib/booking/i18n/messages/en.json +1 -0
  62. package/src/lib/booking/i18n/messages/fr.json +1 -0
  63. package/src/lib/booking-api.ts +76 -1
  64. package/test/change-booking-helpers.test.ts +1319 -7
  65. package/src/components/booking/useAdminChangePricingDebugPanel.tsx +0 -178
@@ -1,6 +1,10 @@
1
1
  import type { AddOn, Availability, ReturnOption } from '../../lib/booking-api';
2
2
  import { AddOnsSection, type AddOnSelection } from './AddOnsSection';
3
3
  import type { Currency } from './CurrencySwitcher';
4
+ import {
5
+ incompatibleAddOnSelections,
6
+ removeAddOnSelections,
7
+ } from './incompatible-add-on-selections';
4
8
  import { TicketSelector, type TicketRate } from './TicketSelector';
5
9
 
6
10
  type TranslationFn = (key: string, params?: Record<string, string | number>) => string;
@@ -29,6 +33,8 @@ export interface ChangeBookingTicketsAndAddOnsPanelProps {
29
33
  applyReceiptPaidFloors: boolean;
30
34
  ticketUnitFloorByCategory?: Map<string, number>;
31
35
  addOns: AddOn[];
36
+ knownAddOns?: AddOn[];
37
+ addOnCatalogLoaded?: boolean;
32
38
  addOnSelections: AddOnSelection[];
33
39
  onAddOnSelectionsChange: AddOnSelectionsChangeHandler;
34
40
  minimumTotalByAddOnId?: Map<string, number>;
@@ -56,12 +62,23 @@ export function ChangeBookingTicketsAndAddOnsPanel({
56
62
  applyReceiptPaidFloors,
57
63
  ticketUnitFloorByCategory,
58
64
  addOns,
65
+ knownAddOns = addOns,
66
+ addOnCatalogLoaded = false,
59
67
  addOnSelections,
60
68
  onAddOnSelectionsChange,
61
69
  minimumTotalByAddOnId,
62
70
  suppressUnitPrices = false,
63
71
  suppressAddOnPrices = false,
64
72
  }: ChangeBookingTicketsAndAddOnsPanelProps) {
73
+ const incompatibleSelections = incompatibleAddOnSelections(
74
+ addOnSelections,
75
+ addOns,
76
+ addOnCatalogLoaded,
77
+ );
78
+ const incompatibleAddOnIds = Array.from(
79
+ new Set(incompatibleSelections.map((selection) => selection.addOnId)),
80
+ );
81
+
65
82
  return (
66
83
  <>
67
84
  <TicketSelector
@@ -97,6 +114,69 @@ export function ChangeBookingTicketsAndAddOnsPanel({
97
114
  suppressPrices={suppressAddOnPrices}
98
115
  />
99
116
  ) : null}
117
+
118
+ {incompatibleAddOnIds.length > 0 ? (
119
+ <div
120
+ className="space-y-3 rounded-lg border border-red-300 bg-red-50 p-4"
121
+ data-testid="incompatible-add-ons"
122
+ >
123
+ <div>
124
+ <h3 className="text-sm font-semibold !text-red-700">
125
+ Add-on unavailable with selected tour
126
+ </h3>
127
+ <p className="mt-1 text-sm text-red-800">
128
+ This booking still contains an add-on that the selected tour does not offer.
129
+ Remove it explicitly before recalculating the amendment.
130
+ </p>
131
+ </div>
132
+ {incompatibleAddOnIds.map((addOnId) => {
133
+ const addOn = knownAddOns.find((candidate) => candidate.addOnId === addOnId);
134
+ const selections = incompatibleSelections.filter(
135
+ (selection) => selection.addOnId === addOnId,
136
+ );
137
+ const quantity = selections.reduce(
138
+ (total, selection) => total + Math.max(1, Number(selection.quantity) || 1),
139
+ 0,
140
+ );
141
+ const variants = selections
142
+ .map((selection) => (
143
+ addOn?.variants.find((variant) => variant.id === selection.variantId)?.label ??
144
+ (selection.variantId ? 'Selected option' : null)
145
+ ))
146
+ .filter((label): label is string => Boolean(label));
147
+ return (
148
+ <div
149
+ key={addOnId}
150
+ className="flex flex-col gap-3 rounded-md border border-red-200 bg-white p-3 sm:flex-row sm:items-center sm:justify-between"
151
+ >
152
+ <div className="text-sm">
153
+ <div className="font-medium text-stone-900">{addOn?.name ?? 'Selected add-on'}</div>
154
+ <div className="text-stone-600">
155
+ {variants.length > 0 ? variants.join(', ') : `Quantity ${quantity}`}
156
+ </div>
157
+ </div>
158
+ {isAdmin ? (
159
+ <button
160
+ type="button"
161
+ className="rounded-md border border-red-500 bg-white px-3 py-2 text-sm font-semibold text-red-800 hover:bg-red-100"
162
+ onClick={() => {
163
+ onAddOnSelectionsChange((current) =>
164
+ removeAddOnSelections(current, addOnId),
165
+ );
166
+ }}
167
+ >
168
+ Remove add-on
169
+ </button>
170
+ ) : (
171
+ <p className="text-sm text-red-800">
172
+ Choose another tour to keep this add-on.
173
+ </p>
174
+ )}
175
+ </div>
176
+ );
177
+ })}
178
+ </div>
179
+ ) : null}
100
180
  </>
101
181
  );
102
182
  }
@@ -149,10 +149,9 @@ export function NewBookingFlow({
149
149
  }, [activeOptions]);
150
150
 
151
151
  useEffect(() => {
152
- if (isAdmin) return;
153
152
  const interval = window.setInterval(() => setBookingCutoffNowMs(Date.now()), 60 * 1000);
154
153
  return () => window.clearInterval(interval);
155
- }, [isAdmin]);
154
+ }, []);
156
155
 
157
156
  const bookingCutoffNow = useMemo(() => new Date(bookingCutoffNowMs), [bookingCutoffNowMs]);
158
157
  const bookingCutoffMinutes = product.bookingCutoffMinutes ?? null;
@@ -213,6 +212,19 @@ export function NewBookingFlow({
213
212
  onProductOptionChange: clearAddOnSelections,
214
213
  });
215
214
 
215
+ const previousAddOnIdsRef = useRef<Set<string>>(new Set());
216
+ useEffect(() => {
217
+ const availableIds = new Set(addOns.map((a) => a.addOnId));
218
+ const previousIds = previousAddOnIdsRef.current;
219
+ if (previousIds.size > 0) {
220
+ setAddOnSelections((prev) => {
221
+ const next = prev.filter((s) => !previousIds.has(s.addOnId) || availableIds.has(s.addOnId));
222
+ return next.length === prev.length ? prev : next;
223
+ });
224
+ }
225
+ previousAddOnIdsRef.current = availableIds;
226
+ }, [addOns]);
227
+
216
228
  const hidePickupSkipOption = useMemo(
217
229
  () =>
218
230
  !isAdmin &&
@@ -3,6 +3,28 @@
3
3
  * Uses CSS module to avoid Tailwind purging/override issues in booking-flow.
4
4
  */
5
5
 
6
+ .searchInputWrapper {
7
+ position: relative;
8
+ }
9
+
10
+ .searchIcon {
11
+ position: absolute;
12
+ top: 50%;
13
+ left: 1rem;
14
+ z-index: 1;
15
+ width: 1.25rem;
16
+ height: 1.25rem;
17
+ color: #a8a29e;
18
+ pointer-events: none;
19
+ transform: translateY(-50%);
20
+ }
21
+
22
+ .searchIcon svg {
23
+ display: block;
24
+ width: 100%;
25
+ height: 100%;
26
+ }
27
+
6
28
  /* Filter pills - horizontal scroll on mobile, scrollbar hidden until scroll (overlay-style) */
7
29
  .filterPillsScroll {
8
30
  display: flex;
@@ -916,9 +916,9 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
916
916
  )}
917
917
 
918
918
  {/* Input with search icon inside - padding ensures text stays to the right of icon */}
919
- <div className="relative">
920
- <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-stone-400">
921
- <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
919
+ <div className={styles.searchInputWrapper}>
920
+ <div className={styles.searchIcon}>
921
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
922
922
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
923
923
  </svg>
924
924
  </div>
@@ -13,6 +13,14 @@ export interface PriceBreakdownProps {
13
13
  qty: number;
14
14
  itemTotal: number;
15
15
  breakdown: PriceBreakdownType | null;
16
+ unitPriceComparison?: {
17
+ previousLabel: string;
18
+ updatedLabel: string;
19
+ differenceLabel: string;
20
+ previousUnitAmount: number;
21
+ updatedUnitAmount: number;
22
+ differenceUnitAmount: number;
23
+ };
16
24
  currency: Currency;
17
25
  locale: Locale;
18
26
  editable?: boolean;
@@ -60,6 +68,7 @@ export function PriceBreakdown({
60
68
  qty,
61
69
  itemTotal,
62
70
  breakdown,
71
+ unitPriceComparison,
63
72
  currency,
64
73
  locale,
65
74
  editable = false,
@@ -214,17 +223,22 @@ export function PriceBreakdown({
214
223
  Price Breakdown ({category})
215
224
  </div>
216
225
  <div className="space-y-1.5">
217
- {breakdown.lineItems.map((line) => (
218
- <div key={line.id ?? line.name} className="flex justify-between">
219
- <span className="text-stone-400">
220
- {line.name}{line.type === 'base' ? ` (${currency})` : ''}{formatAdjustmentDetail(line.adjustmentType, line.adjustmentValue, line.amountInDisplayCurrency < 0)}
221
- </span>
222
- <span className={line.type === 'adjustment' && line.amountInDisplayCurrency < 0 ? 'text-red-300' : ''}>
223
- {line.type === 'adjustment' && line.amountInDisplayCurrency >= 0 ? '+' : ''}
224
- {formatCurrencyAmount(line.amountInDisplayCurrency, currency, locale)}
225
- </span>
226
- </div>
227
- ))}
226
+ {unitPriceComparison && breakdown.lineItems.length > 0 ? (
227
+ <div className="font-medium text-stone-300">New-date price calculation</div>
228
+ ) : null}
229
+ {(!unitPriceComparison || breakdown.lineItems.length > 0) ? (
230
+ breakdown.lineItems.map((line) => (
231
+ <div key={line.id ?? line.name} className="flex justify-between">
232
+ <span className="text-stone-400">
233
+ {line.name}{line.type === 'base' ? ` (${currency})` : ''}{formatAdjustmentDetail(line.adjustmentType, line.adjustmentValue, line.amountInDisplayCurrency < 0)}
234
+ </span>
235
+ <span className={line.type === 'adjustment' && line.amountInDisplayCurrency < 0 ? 'text-red-300' : ''}>
236
+ {line.type === 'adjustment' && line.amountInDisplayCurrency >= 0 ? '+' : ''}
237
+ {formatCurrencyAmount(line.amountInDisplayCurrency, currency, locale)}
238
+ </span>
239
+ </div>
240
+ ))
241
+ ) : null}
228
242
  {/* Only show fees and tax in breakdown when rolled up (tax-inclusive); for CAD/USD they're already separate line items. */}
229
243
  {breakdown.isTaxIncluded && (
230
244
  <>
@@ -244,10 +258,47 @@ export function PriceBreakdown({
244
258
  </div>
245
259
  </>
246
260
  )}
247
- <div className="flex justify-between pt-2 mt-2 border-t border-stone-700 font-semibold">
248
- <span>Final price ({currency}):</span>
249
- <span>{formatCurrencyAmount(breakdown.finalPrice, currency, locale)}</span>
250
- </div>
261
+ {unitPriceComparison && breakdown.lineItems.length > 0 ? (
262
+ <div className="flex justify-between pt-2 mt-2 border-t border-stone-700 font-semibold">
263
+ <span>New-date final price:</span>
264
+ <span>{formatCurrencyAmount(breakdown.finalPrice, currency, locale)}</span>
265
+ </div>
266
+ ) : null}
267
+ {unitPriceComparison ? (
268
+ <>
269
+ {breakdown.lineItems.length > 0 ? (
270
+ <div className="pt-2 mt-2 border-t border-stone-700 font-medium text-stone-300">
271
+ Price difference
272
+ </div>
273
+ ) : null}
274
+ <div className="flex justify-between gap-3">
275
+ <span className="text-stone-400">{unitPriceComparison.previousLabel}</span>
276
+ <span>{formatCurrencyAmount(unitPriceComparison.previousUnitAmount, currency, locale)}</span>
277
+ </div>
278
+ <div className="flex justify-between gap-3">
279
+ <span className="text-stone-400">{unitPriceComparison.updatedLabel}</span>
280
+ <span>{formatCurrencyAmount(unitPriceComparison.updatedUnitAmount, currency, locale)}</span>
281
+ </div>
282
+ <div className="flex justify-between gap-3 border-t border-stone-700 pt-1.5">
283
+ <span className="text-stone-400">{unitPriceComparison.differenceLabel}</span>
284
+ <span className={unitPriceComparison.differenceUnitAmount < 0 ? 'text-red-300' : ''}>
285
+ {unitPriceComparison.differenceUnitAmount > 0 ? '+' : ''}
286
+ {formatCurrencyAmount(unitPriceComparison.differenceUnitAmount, currency, locale)}
287
+ </span>
288
+ </div>
289
+ </>
290
+ ) : (
291
+ <div className="flex justify-between pt-2 mt-2 border-t border-stone-700 font-semibold">
292
+ <span>Final price ({currency}):</span>
293
+ <span>{formatCurrencyAmount(breakdown.finalPrice, currency, locale)}</span>
294
+ </div>
295
+ )}
296
+ {unitPriceComparison && qty > 1 ? (
297
+ <div className="flex justify-between pt-2 mt-2 border-t border-stone-700 font-semibold">
298
+ <span>Total difference (× {qty}):</span>
299
+ <span>{formatCurrencyAmount(itemTotal, currency, locale)}</span>
300
+ </div>
301
+ ) : null}
251
302
  </div>
252
303
  </div>
253
304
  )}
@@ -14,11 +14,22 @@ export type PriceSummaryLine =
14
14
  | {
15
15
  kind: 'ticket';
16
16
  lineKey?: string;
17
+ /** Pricing V2 amendment operation that produced this row, when applicable. */
18
+ operationId?: string;
17
19
  editable?: boolean;
18
20
  category: string;
19
21
  qty: number;
20
22
  itemTotal: number;
21
23
  breakdown?: PriceBreakdownType | null;
24
+ /** Optional server-value comparison for amendment delta rows. */
25
+ unitPriceComparison?: {
26
+ previousLabel: string;
27
+ updatedLabel: string;
28
+ differenceLabel: string;
29
+ previousUnitAmount: number;
30
+ updatedUnitAmount: number;
31
+ differenceUnitAmount: number;
32
+ };
22
33
  }
23
34
  | {
24
35
  kind: 'line';
@@ -69,6 +80,8 @@ export interface PriceSummaryProps {
69
80
  hideSubtotal?: boolean;
70
81
  /** Overrides the final "Total" row label (e.g. change booking: amount due for the change) */
71
82
  totalLabel?: string;
83
+ /** Prefix a positive total with + when the row represents a signed change rather than an absolute total. */
84
+ showPositiveTotalSign?: boolean;
72
85
  /** Render after the total row (e.g. provider price delta + keep-original-price) */
73
86
  extraAfterTotal?: ReactNode;
74
87
  /** Optional map of editable line input values by line key. */
@@ -171,6 +184,7 @@ export function PriceSummary({
171
184
  depositMode,
172
185
  hideSubtotal = false,
173
186
  totalLabel,
187
+ showPositiveTotalSign = false,
174
188
  extraAfterTotal,
175
189
  lineAmountInputs,
176
190
  lineLabelInputs,
@@ -211,6 +225,7 @@ export function PriceSummary({
211
225
  qty={row.qty}
212
226
  itemTotal={row.itemTotal}
213
227
  breakdown={row.breakdown ?? null}
228
+ unitPriceComparison={row.unitPriceComparison}
214
229
  currency={currency}
215
230
  locale={locale}
216
231
  editable={Boolean(row.editable && isBeforeSubtotalBoundary)}
@@ -370,6 +385,7 @@ export function PriceSummary({
370
385
  (t('common.total') && t('common.total') !== 'common.total' ? t('common.total') : 'Total')}
371
386
  </span>
372
387
  <span className="flex-shrink-0 whitespace-nowrap font-semibold text-stone-900">
388
+ {showPositiveTotalSign && total > 0.005 ? '+' : ''}
373
389
  {formatCurrencyAmount(total, currency, locale)}
374
390
  </span>
375
391
  </div>
@@ -32,9 +32,12 @@ import { PrivateShuttleStartTimeSection } from './PrivateShuttleStartTimeSection
32
32
  import {
33
33
  getPrivateShuttleAvailabilityOptionId,
34
34
  privateShuttleBookingCutoffMessage,
35
+ privateShuttleInitialDateKey,
36
+ privateShuttleStartDateTime,
35
37
  privateShuttleStartTimeAllowed,
36
38
  resolveHydratedPrivateShuttleAvailability,
37
39
  } from './private-shuttle-availability';
40
+ import { formatInTimeZone } from 'date-fns-tz';
38
41
  import { usePrivateShuttleAvailability } from './use-private-shuttle-availability';
39
42
  import type {
40
43
  ProviderDashboardChangeBookingPayload,
@@ -49,6 +52,7 @@ import {
49
52
  } from './usePrivateShuttlePriceSummary';
50
53
  import { usePrivateShuttlePromoController } from './usePrivateShuttlePromoController';
51
54
  import { useBookingViewItemAnalytics } from './useBookingViewItemAnalytics';
55
+ import { resolvePrivateShuttleCancellationPolicyId } from './private-shuttle-cancellation-policy';
52
56
 
53
57
  interface PrivateShuttleBookingFlowProps {
54
58
  product: Product;
@@ -129,7 +133,12 @@ export function PrivateShuttleBookingFlow({
129
133
  const isAdmin = permissions.viewerRole === 'admin';
130
134
  const isProviderChangeMode = Boolean(initialBooking && onChangeBooking);
131
135
 
132
- const [selectedDate, setSelectedDate] = useState<string>('');
136
+ const [selectedDate, setSelectedDate] = useState<string>(() =>
137
+ privateShuttleInitialDateKey(
138
+ initialBooking?.dateTime ?? initialValues?.dateTime,
139
+ companyTimezone,
140
+ )
141
+ );
133
142
  const [selectedAvailability, setSelectedAvailability] = useState<Availability | null>(null);
134
143
  const [selectedOption, setSelectedOption] = useState<string>('');
135
144
  const [selectedStartTime, setSelectedStartTime] = useState<string>('');
@@ -137,10 +146,16 @@ export function PrivateShuttleBookingFlow({
137
146
  const [passengerCount, setPassengerCount] = useState<number>(
138
147
  () => initialBooking?.privateShuttleDetails?.passengerCount ?? initialValues?.passengers ?? 1
139
148
  );
140
- const [email, setEmail] = useState('');
141
- const [firstName, setFirstName] = useState('');
142
- const [lastName, setLastName] = useState('');
143
- const [phoneNumber, setPhoneNumber] = useState('');
149
+ const [email, setEmail] = useState(() => initialBooking?.customer?.email?.trim() ?? '');
150
+ const [firstName, setFirstName] = useState(
151
+ () => initialBooking?.customer?.firstName?.trim() ?? ''
152
+ );
153
+ const [lastName, setLastName] = useState(
154
+ () => initialBooking?.customer?.lastName?.trim() ?? ''
155
+ );
156
+ const [phoneNumber, setPhoneNumber] = useState(
157
+ () => initialBooking?.customer?.phoneNumber?.trim() ?? ''
158
+ );
144
159
  const [pickupLocationId, setPickupLocationId] = useState<string | null>(
145
160
  () => initialBooking?.pickupLocationId ?? initialValues?.pickupLocationId ?? null
146
161
  );
@@ -285,13 +300,35 @@ export function PrivateShuttleBookingFlow({
285
300
  setAddOns([]);
286
301
  return;
287
302
  }
303
+ const dateTimeForAddOns =
304
+ selectedDate && selectedStartTime
305
+ ? formatInTimeZone(
306
+ privateShuttleStartDateTime(selectedDate, selectedStartTime, companyTimezone),
307
+ companyTimezone,
308
+ "yyyy-MM-dd'T'HH:mm:ssXXX",
309
+ )
310
+ : selectedDate || undefined;
288
311
  getAddOns(product.companyId!, {
289
312
  productOptionId: selectedOption,
290
313
  preCheckout: true,
314
+ dateTime: dateTimeForAddOns,
291
315
  })
292
316
  .then(setAddOns)
293
317
  .catch(() => setAddOns([]));
294
- }, [selectedOption, product.companyId]);
318
+ }, [selectedOption, selectedDate, selectedStartTime, product.companyId, companyTimezone]);
319
+
320
+ const previousAddOnIdsRef = useRef<Set<string>>(new Set());
321
+ useEffect(() => {
322
+ const availableIds = new Set(addOns.map((a) => a.addOnId));
323
+ const previousIds = previousAddOnIdsRef.current;
324
+ if (previousIds.size > 0) {
325
+ setAddOnSelections((prev) => {
326
+ const next = prev.filter((s) => !previousIds.has(s.addOnId) || availableIds.has(s.addOnId));
327
+ return next.length === prev.length ? prev : next;
328
+ });
329
+ }
330
+ previousAddOnIdsRef.current = availableIds;
331
+ }, [addOns]);
295
332
 
296
333
  useEffect(() => {
297
334
  if (!draftItineraryDestinations.includes('emerald_lake')) {
@@ -520,14 +557,14 @@ export function PrivateShuttleBookingFlow({
520
557
  }, [isAdmin, isCustomTimeMode, selectedStartTime, suggestedStartTimes]);
521
558
 
522
559
  useEffect(() => {
523
- if (
524
- pricingConfig?.cancellationPolicies &&
525
- pricingConfig.cancellationPolicies.length > 0 &&
526
- !cancellationPolicyId
527
- ) {
528
- setCancellationPolicyId(pricingConfig.cancellationPolicies[0].id);
529
- }
530
- }, [pricingConfig?.cancellationPolicies, cancellationPolicyId]);
560
+ const policies = pricingConfig?.cancellationPolicies;
561
+ if (!policies?.length || forcedCancellationPolicy) return;
562
+ const resolved = resolvePrivateShuttleCancellationPolicyId(
563
+ cancellationPolicyId,
564
+ policies.map((policy) => policy.id),
565
+ );
566
+ if (resolved !== cancellationPolicyId) setCancellationPolicyId(resolved);
567
+ }, [pricingConfig?.cancellationPolicies, cancellationPolicyId, forcedCancellationPolicy]);
531
568
 
532
569
  const handleOptionSelect = (optionId: string) => {
533
570
  setSelectedOption(optionId);
@@ -784,6 +821,7 @@ export function PrivateShuttleBookingFlow({
784
821
  isLoading={loadingAvailabilities || isFetchingMoreAvailabilities}
785
822
  onDateSelect={handleDateSelect}
786
823
  timezone={companyTimezone}
824
+ syncVisibleWeekToSelectedDate={isProviderChangeMode || Boolean(initialValues?.dateTime)}
787
825
  displayMode="status"
788
826
  earliestDate={earliestAvailabilityDate}
789
827
  onVisibleRangeChange={handleVisibleRangeChange}