@ticketboothapp/booking 1.2.123 → 1.2.124

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 (112) hide show
  1. package/CHANGE_BOOKING_BE_HANDOFF.md +15 -4
  2. package/package.json +1 -1
  3. package/src/components/booking/AdminChangeBookingContent.tsx +276 -0
  4. package/src/components/booking/AdminChangeBookingFlow.tsx +651 -6202
  5. package/src/components/booking/AdminChangeCheckoutDialogs.tsx +105 -0
  6. package/src/components/booking/AdminChangeCheckoutPanel.tsx +307 -0
  7. package/src/components/booking/AdminChangePricingPanel.tsx +244 -0
  8. package/src/components/booking/AdminChangeProductSwitcher.tsx +50 -0
  9. package/src/components/booking/AdminChangeReceiptComparison.tsx +167 -0
  10. package/src/components/booking/ChangeBookingCalendarPanel.tsx +126 -0
  11. package/src/components/booking/ChangeBookingCheckoutPanel.tsx +204 -0
  12. package/src/components/booking/ChangeBookingFlow.tsx +576 -4648
  13. package/src/components/booking/ChangeBookingItineraryPanel.tsx +115 -0
  14. package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +57 -0
  15. package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +157 -0
  16. package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +102 -0
  17. package/src/components/booking/CheckoutModal.tsx +68 -49
  18. package/src/components/booking/NewBookingFlow.tsx +485 -3505
  19. package/src/components/booking/PriceBreakdown.tsx +17 -3
  20. package/src/components/booking/PriceSummary.tsx +21 -3
  21. package/src/components/booking/PrivateShuttleAddOnsSection.tsx +102 -0
  22. package/src/components/booking/PrivateShuttleBookingFlow.tsx +407 -2788
  23. package/src/components/booking/PrivateShuttleCheckoutDialogs.tsx +106 -0
  24. package/src/components/booking/PrivateShuttleCheckoutSection.tsx +333 -0
  25. package/src/components/booking/PrivateShuttleItinerarySection.tsx +95 -0
  26. package/src/components/booking/PrivateShuttleMediaIntro.tsx +62 -0
  27. package/src/components/booking/PrivateShuttleOptionSection.tsx +81 -0
  28. package/src/components/booking/PrivateShuttlePassengerSection.tsx +148 -0
  29. package/src/components/booking/PrivateShuttlePickupSection.tsx +91 -0
  30. package/src/components/booking/PrivateShuttlePreferencesSection.tsx +75 -0
  31. package/src/components/booking/PrivateShuttleStartTimeSection.tsx +88 -0
  32. package/src/components/booking/StandardBookingCheckoutDialogs.tsx +106 -0
  33. package/src/components/booking/StandardBookingCheckoutSection.tsx +185 -0
  34. package/src/components/booking/StandardBookingItineraryPanel.tsx +125 -0
  35. package/src/components/booking/StandardBookingMediaIntro.tsx +69 -0
  36. package/src/components/booking/StandardBookingSelectionControlsPanel.tsx +236 -0
  37. package/src/components/booking/admin-change-customer-quote-runner.ts +119 -0
  38. package/src/components/booking/admin-change-flow-state-helpers.ts +124 -0
  39. package/src/components/booking/admin-change-payment-choice-runner.ts +172 -0
  40. package/src/components/booking/admin-change-provider-payload.ts +101 -0
  41. package/src/components/booking/change-booking-checkout-builders.ts +185 -0
  42. package/src/components/booking/change-booking-checkout-quote-runner.ts +207 -0
  43. package/src/components/booking/change-booking-flow-helpers.ts +706 -0
  44. package/src/components/booking/change-booking-payment-modal-builders.ts +151 -0
  45. package/src/components/booking/change-booking-quote-guards.ts +223 -0
  46. package/src/components/booking/change-booking-quote-state.ts +64 -0
  47. package/src/components/booking/pricing-v2-checkout-summary.ts +144 -0
  48. package/src/components/booking/private-shuttle-availability.ts +111 -0
  49. package/src/components/booking/private-shuttle-checkout-builders.ts +264 -0
  50. package/src/components/booking/private-shuttle-checkout-controller.ts +744 -0
  51. package/src/components/booking/private-shuttle-payment-choice-runner.ts +271 -0
  52. package/src/components/booking/private-shuttle-provider-change-runner.ts +73 -0
  53. package/src/components/booking/private-shuttle-reservation-runner.ts +260 -0
  54. package/src/components/booking/standard-booking-availability.ts +305 -0
  55. package/src/components/booking/standard-booking-checkout-builders.ts +289 -0
  56. package/src/components/booking/standard-booking-checkout-controller.ts +659 -0
  57. package/src/components/booking/standard-booking-payment-choice-runner.ts +274 -0
  58. package/src/components/booking/standard-booking-reservation-runner.ts +229 -0
  59. package/src/components/booking/use-private-shuttle-availability.ts +602 -0
  60. package/src/components/booking/use-standard-booking-availability.ts +806 -0
  61. package/src/components/booking/useActiveProductOptions.ts +26 -0
  62. package/src/components/booking/useAdminChangeCheckoutController.ts +806 -0
  63. package/src/components/booking/useAdminChangeOrderSummaryModel.ts +246 -0
  64. package/src/components/booking/useAdminChangePriceSummary.ts +416 -0
  65. package/src/components/booking/useAdminChangePricingDebugPanel.tsx +178 -0
  66. package/src/components/booking/useAdminChangeProductReset.ts +60 -0
  67. package/src/components/booking/useAdminChangeProducts.ts +102 -0
  68. package/src/components/booking/useAdminChangePromoDiscount.ts +235 -0
  69. package/src/components/booking/useAdminChangeProtectedPricing.ts +280 -0
  70. package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +437 -0
  71. package/src/components/booking/useAdminChangeQuotePreview.ts +506 -0
  72. package/src/components/booking/useAdminChangeReceiptDerivation.ts +203 -0
  73. package/src/components/booking/useAdminCustomReceiptLines.ts +57 -0
  74. package/src/components/booking/useAdminProviderPricingAdjustments.ts +146 -0
  75. package/src/components/booking/useBookingAvailabilityAddOns.ts +54 -0
  76. package/src/components/booking/useBookingPromoAndQuantityController.ts +163 -0
  77. package/src/components/booking/useBookingQuantityCapTrim.ts +56 -0
  78. package/src/components/booking/useBookingViewItemAnalytics.ts +30 -0
  79. package/src/components/booking/useChangeBookingAddOnFloorController.ts +109 -0
  80. package/src/components/booking/useChangeBookingAutoSelections.ts +148 -0
  81. package/src/components/booking/useChangeBookingAvailabilities.ts +692 -0
  82. package/src/components/booking/useChangeBookingCheckoutController.ts +461 -0
  83. package/src/components/booking/useChangeBookingInitialHydration.ts +204 -0
  84. package/src/components/booking/useChangeBookingItineraryUiState.ts +133 -0
  85. package/src/components/booking/useChangeBookingPickupItinerary.ts +472 -0
  86. package/src/components/booking/useChangeBookingPriceSummary.ts +199 -0
  87. package/src/components/booking/useChangeBookingPromoDiscount.ts +218 -0
  88. package/src/components/booking/useChangeBookingProtectedPricing.ts +256 -0
  89. package/src/components/booking/useChangeBookingQuotePreview.ts +442 -0
  90. package/src/components/booking/useChangeBookingReceiptDerivation.ts +128 -0
  91. package/src/components/booking/useChangeBookingReceiptFloorController.ts +449 -0
  92. package/src/components/booking/useChangeBookingSelection.ts +362 -0
  93. package/src/components/booking/useChangeBookingSelectionDetails.ts +200 -0
  94. package/src/components/booking/useChangeBookingSummaryLines.ts +169 -0
  95. package/src/components/booking/usePrivateShuttlePriceSummary.ts +513 -0
  96. package/src/components/booking/usePrivateShuttlePromoController.ts +143 -0
  97. package/src/components/booking/usePrivateShuttlePromoDiscount.ts +124 -0
  98. package/src/components/booking/useSeedPromoCodeInput.ts +13 -0
  99. package/src/components/booking/useStandardBookingAutoSelections.ts +224 -0
  100. package/src/components/booking/useStandardBookingPickupItinerary.ts +473 -0
  101. package/src/components/booking/useStandardBookingPriceSummary.ts +438 -0
  102. package/src/components/booking/useStandardBookingPromoController.ts +258 -0
  103. package/src/components/booking/useStandardBookingPromoDiscount.ts +203 -0
  104. package/src/data/products-config.json +1 -1
  105. package/src/lib/booking/change-booking-server-preview.ts +134 -20
  106. package/src/lib/booking/change-flow-pricing.ts +31 -11
  107. package/src/lib/booking/checkout-breakdown.ts +66 -1
  108. package/src/lib/booking/pricing-config.ts +10 -0
  109. package/src/lib/booking/source-metadata.ts +58 -0
  110. package/src/lib/booking-api.ts +154 -3
  111. package/src/lib/pricing-v2-shadow.ts +272 -0
  112. package/test/change-booking-helpers.test.ts +206 -0
@@ -0,0 +1,106 @@
1
+ import { AdminPaymentChoiceModal } from './AdminPaymentChoiceModal';
2
+ import { CheckoutModal } from './CheckoutModal';
3
+ import type {
4
+ PrivateShuttleAdminChoiceData,
5
+ PrivateShuttleCheckoutModalData,
6
+ } from './private-shuttle-payment-choice-runner';
7
+ import type { ChangeBookingSuccessUrlFactory } from './change-booking-payment-modal-builders';
8
+ import type { Currency } from './CurrencySwitcher';
9
+ import type { Locale } from '../../lib/booking/i18n/config';
10
+
11
+ type TranslationFn = (key: string, params?: Record<string, string | number>) => string;
12
+
13
+ export interface PrivateShuttleCheckoutDialogsProps {
14
+ showAdminPaymentChoice: boolean;
15
+ adminChoiceData: PrivateShuttleAdminChoiceData | null;
16
+ checkoutModalData: PrivateShuttleCheckoutModalData | null;
17
+ showCheckoutModal: boolean;
18
+ checkoutClientSecret: string;
19
+ currency: Currency;
20
+ locale: Locale;
21
+ loading: boolean;
22
+ error: string;
23
+ getSuccessUrl?: ChangeBookingSuccessUrlFactory;
24
+ t: TranslationFn;
25
+ onPayNow: () => void;
26
+ onConfirmWithoutPayment: () => void;
27
+ onAdminPaymentChoiceCancel: () => void;
28
+ onCheckoutClose: () => void;
29
+ onPaymentSubmitStart: () => void;
30
+ onPaymentSubmitError: () => void;
31
+ }
32
+
33
+ export function PrivateShuttleCheckoutDialogs({
34
+ showAdminPaymentChoice,
35
+ adminChoiceData,
36
+ checkoutModalData,
37
+ showCheckoutModal,
38
+ checkoutClientSecret,
39
+ currency,
40
+ locale,
41
+ loading,
42
+ error,
43
+ getSuccessUrl,
44
+ t,
45
+ onPayNow,
46
+ onConfirmWithoutPayment,
47
+ onAdminPaymentChoiceCancel,
48
+ onCheckoutClose,
49
+ onPaymentSubmitStart,
50
+ onPaymentSubmitError,
51
+ }: PrivateShuttleCheckoutDialogsProps) {
52
+ return (
53
+ <>
54
+ <AdminPaymentChoiceModal
55
+ open={!!(showAdminPaymentChoice && adminChoiceData)}
56
+ totalAmount={adminChoiceData?.totalAmount ?? 0}
57
+ currency={currency}
58
+ loading={loading}
59
+ error={error}
60
+ onPayNow={onPayNow}
61
+ onConfirmWithoutPayment={onConfirmWithoutPayment}
62
+ onCancel={onAdminPaymentChoiceCancel}
63
+ />
64
+ {checkoutModalData && (
65
+ <CheckoutModal
66
+ open={showCheckoutModal}
67
+ onClose={onCheckoutClose}
68
+ onPaymentSubmitStart={onPaymentSubmitStart}
69
+ onPaymentSubmitError={onPaymentSubmitError}
70
+ clientSecret={checkoutClientSecret}
71
+ reservationReference={checkoutModalData.reservationReference}
72
+ reservationExpiration={checkoutModalData.reservationExpiration}
73
+ customerLastName={checkoutModalData.customerLastName}
74
+ successUrlOverride={
75
+ getSuccessUrl
76
+ ? getSuccessUrl({
77
+ reservationRef: checkoutModalData.reservationReference,
78
+ lastName: checkoutModalData.customerLastName ?? '',
79
+ focusDate: checkoutModalData.bookingDate,
80
+ })
81
+ : undefined
82
+ }
83
+ ticketLines={checkoutModalData.ticketLines}
84
+ feeLineItems={checkoutModalData.feeLineItems}
85
+ returnPriceAdjustment={checkoutModalData.returnPriceAdjustment}
86
+ cancellationPolicyFee={checkoutModalData.cancellationPolicyFee ?? 0}
87
+ cancellationPolicyLabel={checkoutModalData.cancellationPolicyLabel}
88
+ subtotal={checkoutModalData.subtotal}
89
+ tax={checkoutModalData.tax}
90
+ total={checkoutModalData.total}
91
+ promoDiscountAmount={checkoutModalData.promoDiscountAmount ?? 0}
92
+ discountLabel={checkoutModalData.discountLabel}
93
+ summaryOverride={checkoutModalData.summaryOverride}
94
+ totalQuantity={checkoutModalData.totalQuantity}
95
+ isTaxIncludedInPrice={checkoutModalData.isTaxIncludedInPrice}
96
+ taxRate={checkoutModalData.taxRate}
97
+ currency={currency}
98
+ locale={locale}
99
+ t={t}
100
+ isDepositPayment={checkoutModalData.isDepositPayment}
101
+ balanceChargeDaysBefore={checkoutModalData.balanceChargeDaysBefore}
102
+ />
103
+ )}
104
+ </>
105
+ );
106
+ }
@@ -0,0 +1,333 @@
1
+ 'use client';
2
+
3
+ import { formatCurrencyAmount } from '../../lib/currency';
4
+ import type { PricingConfig } from '../../lib/booking-api';
5
+ import type { Currency } from './CurrencySwitcher';
6
+ import type { Locale } from '../../lib/booking/i18n/config';
7
+ import { CancellationPolicySelector } from './CancellationPolicySelector';
8
+ import { CheckoutOptionalPhoneFields } from './CheckoutOptionalPhoneFields';
9
+ import { PriceSummary, type PriceSummaryLine } from './PriceSummary';
10
+ import { PromoCodeInput } from './PromoCodeInput';
11
+ import { TermsAcceptance } from './TermsAcceptance';
12
+ import type { PrivateShuttleDepositInfo } from './private-shuttle-checkout-builders';
13
+ import styles from './PrivateShuttleBookingFlow.module.css';
14
+
15
+ type TranslationFn = (key: string, params?: Record<string, string | number>) => string;
16
+
17
+ interface PrivateShuttleCheckoutSectionProps {
18
+ currency: Currency;
19
+ locale: Locale;
20
+ t: TranslationFn;
21
+ isAdmin: boolean;
22
+ isProviderChangeMode: boolean;
23
+ priceSummaryLines: PriceSummaryLine[];
24
+ depositInfo: PrivateShuttleDepositInfo | null;
25
+ balanceChargeDaysBefore?: number | null;
26
+ totalPrice: number;
27
+ effectiveTaxAmount: number;
28
+ taxRate: number;
29
+ effectivePromoDiscountAmount: number;
30
+ activePromoCode: string | null;
31
+ promoCodeInput: string;
32
+ appliedPromoCode: string | null;
33
+ promoCodeError: string;
34
+ promoCodeValidating: boolean;
35
+ promoDiscountAmount: number;
36
+ onPromoInputChange: (value: string) => void;
37
+ onPromoApply: () => void;
38
+ onPromoRemove: () => void;
39
+ firstName: string;
40
+ lastName: string;
41
+ email: string;
42
+ phoneNumber: string;
43
+ onFirstNameChange: (value: string) => void;
44
+ onLastNameChange: (value: string) => void;
45
+ onEmailChange: (value: string) => void;
46
+ onPhoneNumberChange: (value: string) => void;
47
+ cancellationPolicies?: PricingConfig['cancellationPolicies'];
48
+ cancellationPolicyId: string | null;
49
+ onCancellationPolicySelect: (policyId: string) => void;
50
+ skipConfirmationCommunications: boolean;
51
+ disableAutoCommunications: boolean;
52
+ onSkipConfirmationChange: (value: boolean) => void;
53
+ onDisableCommunicationsChange: (value: boolean) => void;
54
+ termsAccepted: boolean;
55
+ onTermsAcceptedChange: (checked: boolean) => void;
56
+ error: string;
57
+ partnerAttributionSummary?: string;
58
+ partnerAttributionConfirmLabel?: string;
59
+ partnerAttributionConfirmed: boolean;
60
+ onPartnerAttributionConfirmedChange: (checked: boolean) => void;
61
+ showCheckoutModal: boolean;
62
+ showAdminPaymentChoice: boolean;
63
+ loading: boolean;
64
+ onCheckout: () => void;
65
+ partnerDeferredInvoiceSubmitLabel?: string;
66
+ }
67
+
68
+ export function PrivateShuttleCheckoutSection({
69
+ currency,
70
+ locale,
71
+ t,
72
+ isAdmin,
73
+ isProviderChangeMode,
74
+ priceSummaryLines,
75
+ depositInfo,
76
+ balanceChargeDaysBefore,
77
+ totalPrice,
78
+ effectiveTaxAmount,
79
+ taxRate,
80
+ effectivePromoDiscountAmount,
81
+ activePromoCode,
82
+ promoCodeInput,
83
+ appliedPromoCode,
84
+ promoCodeError,
85
+ promoCodeValidating,
86
+ promoDiscountAmount,
87
+ onPromoInputChange,
88
+ onPromoApply,
89
+ onPromoRemove,
90
+ firstName,
91
+ lastName,
92
+ email,
93
+ phoneNumber,
94
+ onFirstNameChange,
95
+ onLastNameChange,
96
+ onEmailChange,
97
+ onPhoneNumberChange,
98
+ cancellationPolicies,
99
+ cancellationPolicyId,
100
+ onCancellationPolicySelect,
101
+ skipConfirmationCommunications,
102
+ disableAutoCommunications,
103
+ onSkipConfirmationChange,
104
+ onDisableCommunicationsChange,
105
+ termsAccepted,
106
+ onTermsAcceptedChange,
107
+ error,
108
+ partnerAttributionSummary,
109
+ partnerAttributionConfirmLabel,
110
+ partnerAttributionConfirmed,
111
+ onPartnerAttributionConfirmedChange,
112
+ showCheckoutModal,
113
+ showAdminPaymentChoice,
114
+ loading,
115
+ onCheckout,
116
+ partnerDeferredInvoiceSubmitLabel,
117
+ }: PrivateShuttleCheckoutSectionProps) {
118
+ const amountDueToday = depositInfo ? depositInfo.depositAmount : totalPrice;
119
+ const formattedAmountDueToday = formatCurrencyAmount(amountDueToday, currency, locale);
120
+ const hasCancellationPolicies = Boolean(cancellationPolicies?.length);
121
+
122
+ return (
123
+ <>
124
+ <div className={`${styles.section} mt-6`}>
125
+ <PriceSummary
126
+ lines={priceSummaryLines}
127
+ total={amountDueToday}
128
+ currency={currency}
129
+ locale={locale}
130
+ taxAmount={effectiveTaxAmount}
131
+ taxRate={taxRate}
132
+ discountAmount={effectivePromoDiscountAmount}
133
+ discountLabel={activePromoCode ? `Promo: ${activePromoCode}` : undefined}
134
+ extraBetweenTaxAndTotal={
135
+ isAdmin ? (
136
+ <PromoCodeInput
137
+ promoCodeInput={promoCodeInput}
138
+ appliedPromoCode={appliedPromoCode}
139
+ promoCodeError={promoCodeError}
140
+ promoCodeValidating={promoCodeValidating}
141
+ promoDiscountAmount={promoDiscountAmount}
142
+ currency={currency}
143
+ locale={locale}
144
+ t={t}
145
+ onInputChange={onPromoInputChange}
146
+ onApply={onPromoApply}
147
+ onRemove={onPromoRemove}
148
+ />
149
+ ) : null
150
+ }
151
+ size="sm"
152
+ t={t}
153
+ depositMode={
154
+ depositInfo
155
+ ? {
156
+ totalLabel: t('booking.deposit') || 'Deposit',
157
+ balanceAmount: depositInfo.balanceAmount,
158
+ fullTotalAmount: depositInfo.totalPrice,
159
+ }
160
+ : undefined
161
+ }
162
+ hideSubtotal={!!depositInfo}
163
+ />
164
+ {depositInfo && (
165
+ <div className="deposit-notice mt-3 p-3 bg-amber-50 border border-amber-200 rounded-lg text-amber-800">
166
+ <p className="text-xs font-medium">
167
+ {(t('booking.depositPaymentNotice') !== 'booking.depositPaymentNotice'
168
+ ? t('booking.depositPaymentNotice')
169
+ : null) ?? "You're paying the deposit today."}
170
+ </p>
171
+ <p className="mt-1 text-xs text-amber-700/90">
172
+ {balanceChargeDaysBefore && balanceChargeDaysBefore > 0
173
+ ? (t('booking.balanceChargeNotice', { days: balanceChargeDaysBefore }) !== 'booking.balanceChargeNotice'
174
+ ? t('booking.balanceChargeNotice', { days: balanceChargeDaysBefore })
175
+ : `The remaining balance will be charged ${balanceChargeDaysBefore} days before your booking. You can also pay it earlier from your Manage Booking page.`)
176
+ : (t('booking.balancePayEarlier') !== 'booking.balancePayEarlier'
177
+ ? t('booking.balancePayEarlier')
178
+ : 'You can pay the remaining balance anytime from your Manage Booking page.')}
179
+ </p>
180
+ </div>
181
+ )}
182
+ </div>
183
+
184
+ <div className="border-t border-stone-200 pt-6 space-y-4">
185
+ <div className="space-y-4">
186
+ <div className={styles.contactGrid}>
187
+ <div>
188
+ <label className={styles.sectionLabel}>
189
+ {t('booking.firstName')} <span className={styles.required}>*</span>
190
+ </label>
191
+ <input
192
+ type="text"
193
+ value={firstName}
194
+ onChange={(e) => onFirstNameChange(e.target.value)}
195
+ placeholder={t('booking.firstNamePlaceholder')}
196
+ className={styles.input}
197
+ />
198
+ </div>
199
+ <div>
200
+ <label className={styles.sectionLabel}>
201
+ {t('booking.lastName')} <span className={styles.required}>*</span>
202
+ </label>
203
+ <input
204
+ type="text"
205
+ value={lastName}
206
+ onChange={(e) => onLastNameChange(e.target.value)}
207
+ placeholder={t('booking.lastNamePlaceholder')}
208
+ className={styles.input}
209
+ />
210
+ </div>
211
+ </div>
212
+ <div>
213
+ <label className={styles.sectionLabel}>
214
+ {t('common.emailForConfirmation')} <span className={styles.required}>*</span>
215
+ </label>
216
+ <input
217
+ type="email"
218
+ value={email}
219
+ onChange={(e) => onEmailChange(e.target.value)}
220
+ placeholder={t('common.emailPlaceholder')}
221
+ className={styles.input}
222
+ />
223
+ </div>
224
+ <CheckoutOptionalPhoneFields
225
+ idPrefix="shuttle"
226
+ labelClassName={styles.sectionLabel}
227
+ phoneNumber={phoneNumber}
228
+ onPhoneNumberChange={onPhoneNumberChange}
229
+ t={t}
230
+ />
231
+ {hasCancellationPolicies && (
232
+ <div>
233
+ <CancellationPolicySelector
234
+ policies={[...(cancellationPolicies ?? [])].sort((a, b) => {
235
+ const feeA = a.feeByCurrency[currency] ?? 0;
236
+ const feeB = b.feeByCurrency[currency] ?? 0;
237
+ return feeA - feeB;
238
+ })}
239
+ selectedPolicyId={cancellationPolicyId}
240
+ currency={currency}
241
+ locale={locale}
242
+ t={t}
243
+ onPolicySelect={onCancellationPolicySelect}
244
+ rowSubtitle={
245
+ <>
246
+ <b>Your deposit today is 100% refundable</b> - only once we align on an itinerary
247
+ that you approve, will your deposit be locked in.
248
+ </>
249
+ }
250
+ />
251
+ </div>
252
+ )}
253
+ {isAdmin && (
254
+ <div className="space-y-2 p-4 bg-amber-50/50 rounded-lg">
255
+ <label className="flex items-center gap-2">
256
+ <input
257
+ type="checkbox"
258
+ checked={skipConfirmationCommunications}
259
+ onChange={(e) => onSkipConfirmationChange(e.target.checked)}
260
+ />
261
+ <span className="text-sm">
262
+ Don&apos;t send confirmation for this booking
263
+ </span>
264
+ </label>
265
+ <label className="flex items-center gap-2">
266
+ <input
267
+ type="checkbox"
268
+ checked={disableAutoCommunications}
269
+ onChange={(e) => onDisableCommunicationsChange(e.target.checked)}
270
+ />
271
+ <span className="text-sm">Disable all auto communications</span>
272
+ </label>
273
+ </div>
274
+ )}
275
+ <div className="mt-4 p-4 bg-stone-50 rounded-lg border-2 border-stone-300">
276
+ <TermsAcceptance
277
+ checked={termsAccepted}
278
+ onChange={onTermsAcceptedChange}
279
+ t={t}
280
+ />
281
+ </div>
282
+ </div>
283
+ {error && (
284
+ <div className={styles.errorBox}>
285
+ {error}
286
+ </div>
287
+ )}
288
+ {partnerAttributionSummary ? (
289
+ <div className="mt-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
290
+ <p className="m-0 text-sm text-blue-900">{partnerAttributionSummary}</p>
291
+ {partnerAttributionConfirmLabel ? (
292
+ <label className="mt-2 flex cursor-pointer items-start gap-2">
293
+ <input
294
+ type="checkbox"
295
+ checked={partnerAttributionConfirmed}
296
+ onChange={(e) => onPartnerAttributionConfirmedChange(e.target.checked)}
297
+ />
298
+ <span className="text-xs font-medium leading-5 text-blue-800">
299
+ {partnerAttributionConfirmLabel}
300
+ </span>
301
+ </label>
302
+ ) : null}
303
+ </div>
304
+ ) : null}
305
+ {!showCheckoutModal && !showAdminPaymentChoice ? (
306
+ <>
307
+ <button
308
+ type="button"
309
+ onClick={onCheckout}
310
+ disabled={
311
+ loading ||
312
+ !termsAccepted ||
313
+ Boolean(partnerAttributionConfirmLabel && !partnerAttributionConfirmed)
314
+ }
315
+ className={styles.submitBtn}
316
+ >
317
+ {loading
318
+ ? isProviderChangeMode
319
+ ? 'Processing...'
320
+ : (t('booking.creatingReservation') || 'Creating reservation...')
321
+ : isProviderChangeMode
322
+ ? `Change booking (${formattedAmountDueToday})`
323
+ : `${partnerDeferredInvoiceSubmitLabel || t('booking.continueToPayment') || 'Continue to Payment'} (${formattedAmountDueToday})`}
324
+ </button>
325
+ <p className={styles.secureNote}>
326
+ {t('booking.securePayment') || 'Secure payment powered by Stripe'}
327
+ </p>
328
+ </>
329
+ ) : null}
330
+ </div>
331
+ </>
332
+ );
333
+ }
@@ -0,0 +1,95 @@
1
+ import type { Product } from '../../lib/booking-api';
2
+ import { formatCurrencyAmount } from '../../lib/currency';
3
+ import type { Locale } from '../../lib/booking/i18n/config';
4
+ import type { Currency } from './CurrencySwitcher';
5
+ import { ItineraryBuilder } from './ItineraryBuilder';
6
+ import {
7
+ PRIVATE_SHUTTLE_ADDITIONAL_HOUR_PRICE,
8
+ } from './usePrivateShuttlePriceSummary';
9
+ import styles from './PrivateShuttleBookingFlow.module.css';
10
+
11
+ type ItineraryBuilderConfig = NonNullable<Product['itineraryBuilder']>;
12
+
13
+ export interface PrivateShuttleItinerarySectionProps {
14
+ itineraryBuilder?: ItineraryBuilderConfig;
15
+ optionBlacklist: string[];
16
+ selectedDestinationIds: string[];
17
+ planningNotes: string;
18
+ isAdmin: boolean;
19
+ additionalHoursCount: number;
20
+ additionalHoursAmount: number;
21
+ currency: Currency;
22
+ locale: Locale;
23
+ onDestinationsChange: (destinationIds: string[]) => void;
24
+ onPlanningNotesChange: (notes: string) => void;
25
+ onAdditionalHoursChange: (count: number) => void;
26
+ }
27
+
28
+ export function PrivateShuttleItinerarySection({
29
+ itineraryBuilder,
30
+ optionBlacklist,
31
+ selectedDestinationIds,
32
+ planningNotes,
33
+ isAdmin,
34
+ additionalHoursCount,
35
+ additionalHoursAmount,
36
+ currency,
37
+ locale,
38
+ onDestinationsChange,
39
+ onPlanningNotesChange,
40
+ onAdditionalHoursChange,
41
+ }: PrivateShuttleItinerarySectionProps) {
42
+ if (!itineraryBuilder) return null;
43
+
44
+ return (
45
+ <div className={`${styles.section} itinerary-builder-section`}>
46
+ <ItineraryBuilder
47
+ destinations={itineraryBuilder.destinations}
48
+ optionBlacklist={optionBlacklist}
49
+ selectedDestinationIds={selectedDestinationIds}
50
+ planningNotes={planningNotes}
51
+ onDestinationsChange={onDestinationsChange}
52
+ onPlanningNotesChange={onPlanningNotesChange}
53
+ />
54
+ {isAdmin && (
55
+ <div className={styles.adminBox}>
56
+ <label className={`${styles.sectionLabel} private-shuttle-section-label`}>
57
+ Add on hours (admin)
58
+ </label>
59
+ <p className="text-sm text-stone-600 mb-3">
60
+ Each additional hour adds{' '}
61
+ {formatCurrencyAmount(PRIVATE_SHUTTLE_ADDITIONAL_HOUR_PRICE, currency, locale)}{' '}
62
+ and extends the tour duration.
63
+ </p>
64
+ <div className="flex items-center gap-3">
65
+ <button
66
+ type="button"
67
+ onClick={() => onAdditionalHoursChange(Math.max(0, additionalHoursCount - 1))}
68
+ disabled={additionalHoursCount <= 0}
69
+ className={styles.qtyBtn}
70
+ >
71
+ -
72
+ </button>
73
+ <span className="w-8 text-center font-medium tabular-nums">
74
+ {additionalHoursCount}
75
+ </span>
76
+ <button
77
+ type="button"
78
+ onClick={() => onAdditionalHoursChange(additionalHoursCount + 1)}
79
+ className={styles.qtyBtn}
80
+ >
81
+ +
82
+ </button>
83
+ <span className="text-sm text-stone-600">
84
+ {additionalHoursCount === 0
85
+ ? 'No extra hours'
86
+ : additionalHoursCount === 1
87
+ ? `+1 hour - ${formatCurrencyAmount(PRIVATE_SHUTTLE_ADDITIONAL_HOUR_PRICE, currency, locale)}`
88
+ : `+${additionalHoursCount} hours - ${formatCurrencyAmount(additionalHoursAmount, currency, locale)}`}
89
+ </span>
90
+ </div>
91
+ </div>
92
+ )}
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,62 @@
1
+ import { useBookingHost } from '../../runtime';
2
+ import type { Locale } from '../../lib/booking/i18n/config';
3
+ import type { VideoSources } from '../../constants/products';
4
+ import { BookingFlowCollage } from './BookingFlowCollage';
5
+ import { TourDescription } from './TourDescription';
6
+
7
+ export interface PrivateShuttleMediaIntroProps {
8
+ productId?: string;
9
+ productName: string;
10
+ locale: Locale;
11
+ showCollage?: boolean;
12
+ showTourDescription?: boolean;
13
+ }
14
+
15
+ export function PrivateShuttleMediaIntro({
16
+ productId,
17
+ productName,
18
+ locale,
19
+ showCollage = true,
20
+ showTourDescription = true,
21
+ }: PrivateShuttleMediaIntroProps) {
22
+ const { strings: defaultStrings, catalog } = useBookingHost();
23
+ const config = productId
24
+ ? (catalog.getProductByIdOrSlug(productId) as {
25
+ display?: {
26
+ collageImageIds?: string[];
27
+ imageIds?: string[];
28
+ };
29
+ } | null)
30
+ : null;
31
+ const displayProducts = catalog.getProducts(defaultStrings) as Record<
32
+ string,
33
+ { id: string; videoUrl?: VideoSources }
34
+ >;
35
+ const displayProduct = Object.values(displayProducts).find((candidate) => candidate.id === productId);
36
+ const collageImageIds = config?.display?.collageImageIds ?? config?.display?.imageIds ?? [];
37
+ const hasVideo = !!displayProduct?.videoUrl;
38
+ const hasImages = collageImageIds.length > 0;
39
+
40
+ return (
41
+ <>
42
+ {productId && showCollage && (hasVideo || hasImages) && (
43
+ <div className="booking-collage-wrapper">
44
+ <BookingFlowCollage
45
+ video={displayProduct?.videoUrl}
46
+ videoPosterImageId={config?.display?.imageIds?.[0]}
47
+ imageIds={
48
+ hasImages
49
+ ? collageImageIds
50
+ : (config?.display?.imageIds?.[0] ? [config.display.imageIds[0]] : [])
51
+ }
52
+ altPrefix={productName}
53
+ />
54
+ </div>
55
+ )}
56
+
57
+ {showTourDescription && (
58
+ <TourDescription productSlug={productId} locale={locale} />
59
+ )}
60
+ </>
61
+ );
62
+ }
@@ -0,0 +1,81 @@
1
+ import type { Product } from '../../lib/booking-api';
2
+ import { formatCurrencyAmount } from '../../lib/currency';
3
+ import type { Locale } from '../../lib/booking/i18n/config';
4
+ import type { Currency } from './CurrencySwitcher';
5
+ import styles from './PrivateShuttleBookingFlow.module.css';
6
+
7
+ type TranslationFn = (key: string, params?: Record<string, string | number>) => string;
8
+ type ProductOption = NonNullable<Product['options']>[number];
9
+
10
+ export interface PrivateShuttleOptionSectionProps {
11
+ selectedDate: string;
12
+ options: ProductOption[];
13
+ selectedOption: string;
14
+ currency: Currency;
15
+ locale: Locale;
16
+ t: TranslationFn;
17
+ getOptionPrice: (optionId: string) => number;
18
+ onOptionSelect: (optionId: string) => void;
19
+ }
20
+
21
+ export function PrivateShuttleOptionSection({
22
+ selectedDate,
23
+ options,
24
+ selectedOption,
25
+ currency,
26
+ locale,
27
+ t,
28
+ getOptionPrice,
29
+ onOptionSelect,
30
+ }: PrivateShuttleOptionSectionProps) {
31
+ if (!selectedDate || options.length === 0) return null;
32
+
33
+ return (
34
+ <div className={styles.section}>
35
+ <label className={`${styles.sectionLabel} private-shuttle-section-label`}>
36
+ {t('booking.selectTourOption')}
37
+ </label>
38
+ <div className={styles.optionGrid}>
39
+ {[...options]
40
+ .sort((a, b) => (b.mostPopular ? 1 : 0) - (a.mostPopular ? 1 : 0))
41
+ .map((option) => {
42
+ const isSelected = selectedOption === option.optionId;
43
+ const baseDurationMinutes = option.privateShuttleConfig?.baseDurationMinutes ?? 0;
44
+ const hours = baseDurationMinutes / 60;
45
+ const price = getOptionPrice(option.optionId);
46
+ const formattedPrice = formatCurrencyAmount(price, currency, locale);
47
+ const hoursRounded = Math.round(hours);
48
+ const isMostPopular = options.length > 1 && option.mostPopular;
49
+ return (
50
+ <button
51
+ key={option.optionId}
52
+ type="button"
53
+ onClick={() => onOptionSelect(option.optionId)}
54
+ className={`${styles.btnOption} private-shuttle-btn-option ${
55
+ isMostPopular ? styles.btnOptionWithBadge : ''
56
+ } ${isSelected ? styles.btnOptionSelected : styles.btnOptionDefault}`}
57
+ >
58
+ <div className="font-semibold">{option.name}</div>
59
+ {price > 0 && hours >= 1 && (
60
+ <div
61
+ className={`mt-1 text-xs ${
62
+ isSelected ? 'text-emerald-100' : 'text-stone-500'
63
+ }`}
64
+ >
65
+ {(t('booking.startingAtForHours') ?? 'Starting at {price} per shuttle for {hours}.')
66
+ .replace('{price}', formattedPrice)
67
+ .replace('{hours}', `${hoursRounded} ${t('booking.hoursUnit') || 'hours'}`)}
68
+ </div>
69
+ )}
70
+ {isMostPopular && (
71
+ <span className={styles.badge}>
72
+ {t('booking.mostPopular')}
73
+ </span>
74
+ )}
75
+ </button>
76
+ );
77
+ })}
78
+ </div>
79
+ </div>
80
+ );
81
+ }