@ticketboothapp/booking 1.2.123 → 1.2.125

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 +155 -3
  111. package/src/lib/pricing-v2-shadow.ts +272 -0
  112. package/test/change-booking-helpers.test.ts +206 -0
@@ -0,0 +1,105 @@
1
+ import { AdminPaymentChoiceModal } from './AdminPaymentChoiceModal';
2
+ import { CheckoutModal } from './CheckoutModal';
3
+ import type {
4
+ AdminChangeCheckoutModalData,
5
+ AdminChangePaymentChoiceData,
6
+ } from './admin-change-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 AdminChangeCheckoutDialogsProps {
14
+ showAdminPaymentChoice: boolean;
15
+ adminChoiceData: AdminChangePaymentChoiceData | null;
16
+ checkoutModalData: AdminChangeCheckoutModalData | 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 AdminChangeCheckoutDialogs({
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
+ }: AdminChangeCheckoutDialogsProps) {
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
+ checkoutModalData.successUrlOverride ??
76
+ (getSuccessUrl
77
+ ? getSuccessUrl({
78
+ reservationRef: checkoutModalData.reservationReference,
79
+ lastName: checkoutModalData.customerLastName ?? '',
80
+ focusDate: checkoutModalData.bookingDate,
81
+ })
82
+ : undefined)
83
+ }
84
+ ticketLines={checkoutModalData.ticketLines}
85
+ feeLineItems={checkoutModalData.feeLineItems}
86
+ returnPriceAdjustment={checkoutModalData.returnPriceAdjustment}
87
+ cancellationPolicyFee={checkoutModalData.cancellationPolicyFee}
88
+ cancellationPolicyLabel={checkoutModalData.cancellationPolicyLabel}
89
+ subtotal={checkoutModalData.subtotal}
90
+ tax={checkoutModalData.tax}
91
+ total={checkoutModalData.total}
92
+ promoDiscountAmount={checkoutModalData.promoDiscountAmount ?? 0}
93
+ discountLabel={checkoutModalData.discountLabel}
94
+ totalQuantity={checkoutModalData.totalQuantity}
95
+ isTaxIncludedInPrice={checkoutModalData.isTaxIncludedInPrice}
96
+ taxRate={checkoutModalData.taxRate}
97
+ changeTotals={checkoutModalData.changeTotals}
98
+ currency={currency}
99
+ locale={locale}
100
+ t={t}
101
+ />
102
+ )}
103
+ </>
104
+ );
105
+ }
@@ -0,0 +1,307 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { Destination, PickupLocation } from '../../lib/booking-api';
3
+ import { CheckoutForm } from './CheckoutForm';
4
+ import type { ChangeBookingFlowProps } from './booking-flow-types';
5
+ import type { Currency } from './CurrencySwitcher';
6
+ import type { PriceSummaryLine } from './PriceSummary';
7
+ import type {
8
+ ProviderDashboardChangePricingUi,
9
+ ProviderDashboardPricingLine,
10
+ } from './booking-flow-ui';
11
+ import { AdminChangeReceiptComparison } from './AdminChangeReceiptComparison';
12
+ import {
13
+ AdminChangePricingAdjustments,
14
+ AdminChangePricingMessages,
15
+ type AdminCustomReceiptLine,
16
+ } from './AdminChangePricingPanel';
17
+
18
+ type TranslationFn = (key: string, params?: Record<string, string>) => string;
19
+
20
+ type AdminCustomReceiptLinePatch = Partial<
21
+ Pick<AdminCustomReceiptLine, 'label' | 'amountInput' | 'amountSign'>
22
+ >;
23
+
24
+ export interface AdminChangeCheckoutPanelProps {
25
+ priceSummaryLines: PriceSummaryLine[];
26
+ replacePriceSummary?: ReactNode;
27
+ changeFlowAmountDue: number;
28
+ receiptSubtotal: number;
29
+ receiptTax: number;
30
+ receiptTotal: number;
31
+ originalReceipt: ChangeBookingFlowProps['originalReceipt'];
32
+ priceSummaryLinesIncludeTaxRow: boolean;
33
+ isTaxIncludedInPrice: boolean;
34
+ taxRate?: number;
35
+ currency: Currency;
36
+ locale: string;
37
+ t: TranslationFn;
38
+ showProviderPricingInlineEditor: boolean;
39
+ providerPricingUi?: ProviderDashboardChangePricingUi;
40
+ providerQuotedLines: ProviderDashboardPricingLine[];
41
+ debugPanel: ReactNode;
42
+ showChangeFlowManualPriceLines: boolean;
43
+ showAdminCustomLineEditor: boolean;
44
+ adminCustomReceiptLines: AdminCustomReceiptLine[];
45
+ onAddAdminCustomReceiptLine: () => void;
46
+ onUpdateAdminCustomReceiptLine: (id: string, patch: AdminCustomReceiptLinePatch) => void;
47
+ onRemoveAdminCustomReceiptLine: (id: string) => void;
48
+ firstName: string;
49
+ lastName: string;
50
+ email: string;
51
+ onFirstNameChange: (value: string) => void;
52
+ onLastNameChange: (value: string) => void;
53
+ onEmailChange: (value: string) => void;
54
+ onClearError: () => void;
55
+ selectedDate: string;
56
+ pickupLocations?: PickupLocation[];
57
+ destinations?: Destination[];
58
+ pickupLocationId: string | null;
59
+ pickupLocationSkipped: boolean;
60
+ selectedPickupLocation?: PickupLocation | null;
61
+ highlightedPickupLocationIds?: string[];
62
+ onPickupLocationIdChange: (locationId: string | null) => void;
63
+ onPickupLocationSkippedChange: (skipped: boolean) => void;
64
+ termsAccepted: boolean;
65
+ onTermsAcceptedChange: (checked: boolean) => void;
66
+ onTermsAcceptedAtChange: (value: string | null) => void;
67
+ isAdmin: boolean;
68
+ skipConfirmationCommunications: boolean;
69
+ disableAutoCommunications: boolean;
70
+ onSkipConfirmationChange: (checked: boolean) => void;
71
+ onDisableCommunicationsChange: (checked: boolean) => void;
72
+ error: string;
73
+ loading: boolean;
74
+ totalQuantity: number;
75
+ onCheckout: () => void;
76
+ submitLabel?: string;
77
+ showCheckoutModal: boolean;
78
+ showAdminPaymentChoice: boolean;
79
+ hasEffectiveChangeSelection: boolean;
80
+ isChangeQuoteBlocked: boolean;
81
+ submitDisabled: boolean;
82
+ attributionSummary?: string;
83
+ attributionConfirmLabel?: string;
84
+ attributionConfirmed: boolean;
85
+ onAttributionConfirmedChange: (checked: boolean) => void;
86
+ lineAmountInputs?: Record<string, string>;
87
+ onLineAmountInputChange?: (lineKey: string, value: string) => void;
88
+ onLineAmountInputBlur?: (lineKey: string) => void;
89
+ onLineAmountReset?: (lineKey: string) => void;
90
+ lineLabelInputs?: Record<string, string>;
91
+ }
92
+
93
+ export function AdminChangeCheckoutPanel({
94
+ priceSummaryLines,
95
+ replacePriceSummary,
96
+ changeFlowAmountDue,
97
+ receiptSubtotal,
98
+ receiptTax,
99
+ receiptTotal,
100
+ originalReceipt,
101
+ priceSummaryLinesIncludeTaxRow,
102
+ isTaxIncludedInPrice,
103
+ taxRate,
104
+ currency,
105
+ locale,
106
+ t,
107
+ showProviderPricingInlineEditor,
108
+ providerPricingUi,
109
+ providerQuotedLines,
110
+ debugPanel,
111
+ showChangeFlowManualPriceLines,
112
+ showAdminCustomLineEditor,
113
+ adminCustomReceiptLines,
114
+ onAddAdminCustomReceiptLine,
115
+ onUpdateAdminCustomReceiptLine,
116
+ onRemoveAdminCustomReceiptLine,
117
+ firstName,
118
+ lastName,
119
+ email,
120
+ onFirstNameChange,
121
+ onLastNameChange,
122
+ onEmailChange,
123
+ onClearError,
124
+ selectedDate,
125
+ pickupLocations,
126
+ destinations,
127
+ pickupLocationId,
128
+ pickupLocationSkipped,
129
+ selectedPickupLocation,
130
+ highlightedPickupLocationIds,
131
+ onPickupLocationIdChange,
132
+ onPickupLocationSkippedChange,
133
+ termsAccepted,
134
+ onTermsAcceptedChange,
135
+ onTermsAcceptedAtChange,
136
+ isAdmin,
137
+ skipConfirmationCommunications,
138
+ disableAutoCommunications,
139
+ onSkipConfirmationChange,
140
+ onDisableCommunicationsChange,
141
+ error,
142
+ loading,
143
+ totalQuantity,
144
+ onCheckout,
145
+ submitLabel,
146
+ showCheckoutModal,
147
+ showAdminPaymentChoice,
148
+ hasEffectiveChangeSelection,
149
+ isChangeQuoteBlocked,
150
+ submitDisabled,
151
+ attributionSummary,
152
+ attributionConfirmLabel,
153
+ attributionConfirmed,
154
+ onAttributionConfirmedChange,
155
+ lineAmountInputs,
156
+ onLineAmountInputChange,
157
+ onLineAmountInputBlur,
158
+ onLineAmountReset,
159
+ lineLabelInputs,
160
+ }: AdminChangeCheckoutPanelProps) {
161
+ const translatedTotalLabel = t('booking.totalOwedForBookingChange');
162
+ const totalSummaryLabel =
163
+ changeFlowAmountDue < -0.005
164
+ ? 'Refund owed (vs original booking)'
165
+ : translatedTotalLabel && translatedTotalLabel !== 'booking.totalOwedForBookingChange'
166
+ ? translatedTotalLabel
167
+ : 'Total owed for booking difference';
168
+ const pricingAdjustments = (
169
+ <AdminChangePricingAdjustments
170
+ showChangeFlowManualPriceLines={showChangeFlowManualPriceLines}
171
+ showProviderPricingInlineEditor={showProviderPricingInlineEditor}
172
+ providerPricingUi={providerPricingUi}
173
+ showAdminCustomLineEditor={showAdminCustomLineEditor}
174
+ adminCustomReceiptLines={adminCustomReceiptLines}
175
+ onAddAdminCustomReceiptLine={onAddAdminCustomReceiptLine}
176
+ onUpdateAdminCustomReceiptLine={onUpdateAdminCustomReceiptLine}
177
+ onRemoveAdminCustomReceiptLine={onRemoveAdminCustomReceiptLine}
178
+ />
179
+ );
180
+ const adminReceiptComparison =
181
+ isAdmin && originalReceipt ? (
182
+ <AdminChangeReceiptComparison
183
+ originalReceipt={originalReceipt}
184
+ newReceipt={{
185
+ subtotal: receiptSubtotal,
186
+ tax: receiptTax,
187
+ total: receiptTotal,
188
+ }}
189
+ newPriceSummaryLines={priceSummaryLines}
190
+ newPriceSummaryLinesIncludeTaxRow={priceSummaryLinesIncludeTaxRow}
191
+ amountDue={changeFlowAmountDue}
192
+ amountDueLabel={totalSummaryLabel}
193
+ currency={currency}
194
+ locale={locale}
195
+ t={t}
196
+ taxRate={taxRate}
197
+ newReceiptAdjustments={pricingAdjustments}
198
+ lineAmountInputs={lineAmountInputs}
199
+ onLineAmountInputChange={onLineAmountInputChange}
200
+ onLineAmountInputBlur={onLineAmountInputBlur}
201
+ onLineAmountReset={onLineAmountReset}
202
+ lineLabelInputs={lineLabelInputs}
203
+ />
204
+ ) : null;
205
+
206
+ return (
207
+ <CheckoutForm
208
+ priceSummaryLines={priceSummaryLines}
209
+ replacePriceSummary={adminReceiptComparison ?? replacePriceSummary}
210
+ totalPrice={changeFlowAmountDue}
211
+ totalSummaryLabel={totalSummaryLabel}
212
+ subtotal={receiptSubtotal}
213
+ taxAmount={
214
+ !isTaxIncludedInPrice && receiptTax > 0 && !priceSummaryLinesIncludeTaxRow
215
+ ? receiptTax
216
+ : 0
217
+ }
218
+ taxRate={taxRate}
219
+ currency={currency}
220
+ locale={locale}
221
+ t={t}
222
+ extraBetweenTaxAndTotal={
223
+ <AdminChangePricingMessages
224
+ showProviderPricingInlineEditor={showProviderPricingInlineEditor}
225
+ providerPricingUi={providerPricingUi}
226
+ providerQuotedLines={providerQuotedLines}
227
+ debugPanel={debugPanel}
228
+ />
229
+ }
230
+ extraBeforeSubtotal={
231
+ adminReceiptComparison ? undefined : pricingAdjustments
232
+ }
233
+ firstName={firstName}
234
+ lastName={lastName}
235
+ email={email}
236
+ onFirstNameChange={(value) => {
237
+ onFirstNameChange(value);
238
+ onClearError();
239
+ }}
240
+ onLastNameChange={(value) => {
241
+ onLastNameChange(value);
242
+ onClearError();
243
+ }}
244
+ onEmailChange={(value) => {
245
+ onEmailChange(value);
246
+ onClearError();
247
+ }}
248
+ readOnlyContactFields
249
+ pickupLocations={selectedDate && pickupLocations && pickupLocations.length > 0 ? pickupLocations : undefined}
250
+ destinations={destinations}
251
+ pickupLocationId={pickupLocationId}
252
+ pickupLocationSkipped={pickupLocationSkipped}
253
+ selectedPickupLocation={selectedPickupLocation}
254
+ highlightedPickupLocationIds={highlightedPickupLocationIds}
255
+ onLocationSelect={(locationId) => {
256
+ onPickupLocationIdChange(locationId);
257
+ onClearError();
258
+ if (locationId === null && pickupLocationSkipped) {
259
+ onPickupLocationSkippedChange(false);
260
+ } else if (locationId !== null) {
261
+ onPickupLocationSkippedChange(false);
262
+ }
263
+ }}
264
+ onSkip={() => {
265
+ onPickupLocationSkippedChange(true);
266
+ onPickupLocationIdChange(null);
267
+ onClearError();
268
+ }}
269
+ onChangePickup={() => {
270
+ onPickupLocationIdChange(null);
271
+ onPickupLocationSkippedChange(false);
272
+ }}
273
+ termsAccepted={termsAccepted}
274
+ onTermsChange={(checked) => {
275
+ onTermsAcceptedChange(checked);
276
+ onTermsAcceptedAtChange(checked ? new Date().toISOString() : null);
277
+ }}
278
+ isAdmin={isAdmin}
279
+ showCommunicationAdminSection={false}
280
+ skipConfirmationCommunications={skipConfirmationCommunications}
281
+ disableAutoCommunications={disableAutoCommunications}
282
+ onSkipConfirmationChange={onSkipConfirmationChange}
283
+ onDisableCommunicationsChange={onDisableCommunicationsChange}
284
+ error={error}
285
+ loading={loading}
286
+ totalQuantity={totalQuantity}
287
+ onCheckout={onCheckout}
288
+ submitLabel={submitLabel}
289
+ hideSubmitButton={
290
+ showCheckoutModal ||
291
+ showAdminPaymentChoice ||
292
+ !hasEffectiveChangeSelection ||
293
+ isChangeQuoteBlocked
294
+ }
295
+ submitDisabled={submitDisabled}
296
+ attributionSummary={attributionSummary}
297
+ attributionConfirmLabel={attributionConfirmLabel}
298
+ attributionConfirmed={attributionConfirmed}
299
+ onAttributionConfirmedChange={onAttributionConfirmedChange}
300
+ lineAmountInputs={lineAmountInputs}
301
+ onLineAmountInputChange={onLineAmountInputChange}
302
+ onLineAmountInputBlur={onLineAmountInputBlur}
303
+ onLineAmountReset={onLineAmountReset}
304
+ lineLabelInputs={lineLabelInputs}
305
+ />
306
+ );
307
+ }
@@ -0,0 +1,244 @@
1
+ import type { ReactNode } from 'react';
2
+ import type {
3
+ ProviderDashboardChangePricingUi,
4
+ ProviderDashboardPricingLine,
5
+ } from './booking-flow-ui';
6
+
7
+ export type AdminCustomReceiptLine = {
8
+ id: string;
9
+ label: string;
10
+ amountInput: string;
11
+ amountSign: 1 | -1;
12
+ };
13
+
14
+ type AdminCustomReceiptLinePatch = Partial<
15
+ Pick<AdminCustomReceiptLine, 'label' | 'amountInput' | 'amountSign'>
16
+ >;
17
+
18
+ export interface AdminChangePricingMessagesProps {
19
+ showProviderPricingInlineEditor: boolean;
20
+ providerPricingUi?: ProviderDashboardChangePricingUi;
21
+ providerQuotedLines: ProviderDashboardPricingLine[];
22
+ debugPanel: ReactNode;
23
+ }
24
+
25
+ export function AdminChangePricingMessages({
26
+ showProviderPricingInlineEditor,
27
+ providerPricingUi,
28
+ providerQuotedLines,
29
+ debugPanel,
30
+ }: AdminChangePricingMessagesProps) {
31
+ return (
32
+ <>
33
+ {showProviderPricingInlineEditor && providerPricingUi?.error ? (
34
+ <div className="mt-2 text-sm text-red-700">{providerPricingUi.error}</div>
35
+ ) : null}
36
+ {showProviderPricingInlineEditor &&
37
+ providerPricingUi?.loading &&
38
+ providerQuotedLines.length === 0 ? (
39
+ <div className="mt-2 text-sm text-stone-500">Loading price lines...</div>
40
+ ) : null}
41
+ {showProviderPricingInlineEditor &&
42
+ providerPricingUi?.helperText &&
43
+ !providerPricingUi.error ? (
44
+ <div className="mt-2 text-xs text-stone-500">{providerPricingUi.helperText}</div>
45
+ ) : null}
46
+ {debugPanel}
47
+ </>
48
+ );
49
+ }
50
+
51
+ export interface AdminChangePricingAdjustmentsProps {
52
+ showChangeFlowManualPriceLines: boolean;
53
+ showProviderPricingInlineEditor: boolean;
54
+ providerPricingUi?: ProviderDashboardChangePricingUi;
55
+ showAdminCustomLineEditor: boolean;
56
+ adminCustomReceiptLines: AdminCustomReceiptLine[];
57
+ onAddAdminCustomReceiptLine: () => void;
58
+ onUpdateAdminCustomReceiptLine: (id: string, patch: AdminCustomReceiptLinePatch) => void;
59
+ onRemoveAdminCustomReceiptLine: (id: string) => void;
60
+ }
61
+
62
+ function normalizeAmountInput(value: string): string {
63
+ const digitsAndDots = value.replace(/[^0-9.]/g, '');
64
+ const parts = digitsAndDots.split('.');
65
+ if (parts.length <= 1) return digitsAndDots;
66
+ return `${parts[0]}.${parts.slice(1).join('').replace(/\./g, '')}`;
67
+ }
68
+
69
+ export function AdminChangePricingAdjustments({
70
+ showChangeFlowManualPriceLines,
71
+ showProviderPricingInlineEditor,
72
+ providerPricingUi,
73
+ showAdminCustomLineEditor,
74
+ adminCustomReceiptLines,
75
+ onAddAdminCustomReceiptLine,
76
+ onUpdateAdminCustomReceiptLine,
77
+ onRemoveAdminCustomReceiptLine,
78
+ }: AdminChangePricingAdjustmentsProps) {
79
+ if (!showChangeFlowManualPriceLines && !showAdminCustomLineEditor) return null;
80
+
81
+ const providerAdjustments = providerPricingUi?.additionalAdjustments ?? [];
82
+
83
+ return (
84
+ <>
85
+ {showChangeFlowManualPriceLines &&
86
+ showProviderPricingInlineEditor &&
87
+ providerAdjustments.length > 0 ? (
88
+ <div className="space-y-1">
89
+ {providerAdjustments.map((adj) => (
90
+ <div key={adj.id} className="flex items-center justify-between gap-2 text-sm">
91
+ <div className="flex min-w-0 items-center gap-1">
92
+ <button
93
+ type="button"
94
+ className="rounded border border-stone-300 px-1 text-xs text-stone-600 hover:bg-stone-100"
95
+ onClick={() => providerPricingUi?.onRemoveAdditionalAdjustment?.(adj.id)}
96
+ >
97
+ -
98
+ </button>
99
+ <input
100
+ type="text"
101
+ className="admin-custom-receipt-input w-40 rounded border border-stone-300 bg-white text-sm text-stone-800 placeholder:text-stone-400"
102
+ placeholder="Line description"
103
+ value={adj.label}
104
+ onChange={(e) =>
105
+ providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, { label: e.target.value })
106
+ }
107
+ />
108
+ </div>
109
+ <div className="flex items-center gap-1">
110
+ <select
111
+ className="rounded border border-stone-300 px-1 py-0.5 text-xs"
112
+ value={adj.mode}
113
+ onChange={(e) =>
114
+ providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
115
+ mode: e.target.value as 'DISCOUNT' | 'CHARGE',
116
+ })
117
+ }
118
+ >
119
+ <option value="DISCOUNT">-</option>
120
+ <option value="CHARGE">+</option>
121
+ </select>
122
+ <input
123
+ type="text"
124
+ inputMode="decimal"
125
+ className="admin-custom-receipt-input-amount w-[7rem] shrink-0 rounded-md border border-stone-300 bg-white text-right text-sm font-medium tabular-nums text-stone-800 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-400/80"
126
+ placeholder="0.00"
127
+ value={adj.amountInput}
128
+ onChange={(e) =>
129
+ providerPricingUi?.onUpdateAdditionalAdjustment?.(adj.id, {
130
+ amountInput: e.target.value,
131
+ })
132
+ }
133
+ />
134
+ </div>
135
+ </div>
136
+ ))}
137
+ <button
138
+ type="button"
139
+ className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
140
+ onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
141
+ >
142
+ + add line item
143
+ </button>
144
+ </div>
145
+ ) : showChangeFlowManualPriceLines && showProviderPricingInlineEditor ? (
146
+ <button
147
+ type="button"
148
+ className="rounded border border-stone-300 px-2 py-0.5 text-xs text-stone-600 hover:bg-stone-100"
149
+ onClick={() => providerPricingUi?.onAddAdditionalAdjustment?.()}
150
+ >
151
+ + add line item
152
+ </button>
153
+ ) : null}
154
+ {showAdminCustomLineEditor ? (
155
+ <div className="space-y-3">
156
+ <div className="flex justify-center">
157
+ <button
158
+ type="button"
159
+ className="admin-custom-receipt-line-add inline-flex items-center gap-1.5 rounded-full border border-dashed border-stone-300 bg-stone-50/80 text-xs font-medium text-stone-600 hover:border-stone-400 hover:bg-stone-100"
160
+ onClick={onAddAdminCustomReceiptLine}
161
+ >
162
+ <span className="text-base font-semibold leading-none text-stone-700" aria-hidden>
163
+ +
164
+ </span>
165
+ Add custom line
166
+ </button>
167
+ </div>
168
+ {adminCustomReceiptLines.map((adj) => {
169
+ const sign = adj.amountSign ?? 1;
170
+ return (
171
+ <div
172
+ key={adj.id}
173
+ className="admin-custom-receipt-line flex flex-wrap items-center gap-2 text-sm sm:flex-nowrap"
174
+ >
175
+ <input
176
+ type="text"
177
+ className="admin-custom-receipt-input min-w-[8rem] flex-1 rounded-md border border-stone-300 bg-white text-sm text-stone-800 placeholder:text-stone-400"
178
+ placeholder="Description"
179
+ value={adj.label}
180
+ onChange={(e) =>
181
+ onUpdateAdminCustomReceiptLine(adj.id, { label: e.target.value })
182
+ }
183
+ />
184
+ <div
185
+ className="flex shrink-0 rounded-lg border border-stone-300 bg-stone-100/90 p-0.5 shadow-sm"
186
+ role="group"
187
+ aria-label="Line adds to total or reduces it"
188
+ >
189
+ <button
190
+ type="button"
191
+ className={`admin-custom-receipt-segment rounded-md text-xs font-semibold transition-colors sm:text-sm ${
192
+ sign === 1
193
+ ? 'bg-white text-emerald-800 shadow-sm ring-1 ring-stone-200/80'
194
+ : 'text-stone-500 hover:bg-stone-200/60 hover:text-stone-700'
195
+ }`}
196
+ aria-pressed={sign === 1}
197
+ onClick={() => onUpdateAdminCustomReceiptLine(adj.id, { amountSign: 1 })}
198
+ >
199
+ Charge (+)
200
+ </button>
201
+ <button
202
+ type="button"
203
+ className={`admin-custom-receipt-segment rounded-md text-xs font-semibold transition-colors sm:text-sm ${
204
+ sign === -1
205
+ ? 'bg-white text-red-800 shadow-sm ring-1 ring-stone-200/80'
206
+ : 'text-stone-500 hover:bg-stone-200/60 hover:text-stone-700'
207
+ }`}
208
+ aria-pressed={sign === -1}
209
+ onClick={() => onUpdateAdminCustomReceiptLine(adj.id, { amountSign: -1 })}
210
+ >
211
+ Credit (&minus;)
212
+ </button>
213
+ </div>
214
+ <input
215
+ type="text"
216
+ inputMode="decimal"
217
+ className="admin-custom-receipt-input-amount w-[7rem] shrink-0 rounded-md border border-stone-300 bg-white text-right text-sm font-medium tabular-nums text-stone-800 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-400/80"
218
+ placeholder="0.00"
219
+ aria-label="Amount"
220
+ value={adj.amountInput}
221
+ onChange={(e) =>
222
+ onUpdateAdminCustomReceiptLine(adj.id, {
223
+ amountInput: normalizeAmountInput(e.target.value),
224
+ })
225
+ }
226
+ />
227
+ <button
228
+ type="button"
229
+ className="admin-custom-receipt-remove ml-auto shrink-0 rounded-md text-stone-400 hover:bg-stone-100 hover:text-stone-700 sm:ml-0"
230
+ aria-label="Remove this line"
231
+ onClick={() => onRemoveAdminCustomReceiptLine(adj.id)}
232
+ >
233
+ <span className="block text-xl leading-none" aria-hidden>
234
+ &times;
235
+ </span>
236
+ </button>
237
+ </div>
238
+ );
239
+ })}
240
+ </div>
241
+ ) : null}
242
+ </>
243
+ );
244
+ }