@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.
- package/package.json +1 -1
- package/src/components/booking/AdminChangeBookingContent.tsx +15 -1
- package/src/components/booking/AdminChangeBookingFlow.tsx +274 -94
- package/src/components/booking/AdminChangeCheckoutPanel.tsx +49 -13
- package/src/components/booking/AdminChangePricingPanel.tsx +400 -134
- package/src/components/booking/AdminChangePromoEditor.tsx +97 -0
- package/src/components/booking/AdminChangeReceiptComparison.tsx +172 -147
- package/src/components/booking/ChangeBookingFlow.tsx +6 -3
- package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +7 -2
- package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +6 -0
- package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +80 -0
- package/src/components/booking/NewBookingFlow.tsx +14 -2
- package/src/components/booking/PickupLocationSelector.module.css +22 -0
- package/src/components/booking/PickupLocationSelector.tsx +3 -3
- package/src/components/booking/PriceBreakdown.tsx +66 -15
- package/src/components/booking/PriceSummary.tsx +16 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +52 -14
- package/src/components/booking/PrivateShuttleCheckoutSection.tsx +95 -54
- package/src/components/booking/admin-adjustment-components.ts +44 -0
- package/src/components/booking/admin-adjustment-tax-behavior.ts +50 -0
- package/src/components/booking/admin-change-flow-state-helpers.ts +10 -0
- package/src/components/booking/admin-change-provider-payload.ts +12 -1
- package/src/components/booking/admin-change-quote-request-key.ts +41 -1
- package/src/components/booking/admin-change-receipt-lines.ts +49 -0
- package/src/components/booking/admin-change-v2-quote-request.ts +109 -0
- package/src/components/booking/admin-refund-decision-context.ts +122 -0
- package/src/components/booking/admin-refund-disposition.ts +71 -2
- package/src/components/booking/availability-date-selection.ts +16 -0
- package/src/components/booking/booking-flow-types.ts +4 -0
- package/src/components/booking/booking-flow.css +5 -0
- package/src/components/booking/change-booking-error-message.ts +137 -0
- package/src/components/booking/change-booking-flow-helpers.ts +77 -2
- package/src/components/booking/change-booking-quote-guards.ts +4 -1
- package/src/components/booking/change-booking-quote-state.ts +44 -1
- package/src/components/booking/incompatible-add-on-selections.ts +19 -0
- package/src/components/booking/private-shuttle-availability.ts +14 -0
- package/src/components/booking/private-shuttle-cancellation-policy.ts +9 -0
- package/src/components/booking/private-shuttle-checkout-controller.ts +5 -0
- package/src/components/booking/private-shuttle-provider-change-runner.ts +21 -0
- package/src/components/booking/private-shuttle-reservation-runner.ts +5 -3
- package/src/components/booking/provider-dashboard-change-booking.ts +18 -2
- package/src/components/booking/use-standard-booking-availability.ts +5 -2
- package/src/components/booking/useAdminChangeCheckoutController.ts +56 -1
- package/src/components/booking/useAdminChangeProductReset.ts +6 -0
- package/src/components/booking/useAdminChangeProtectedPricing.ts +4 -0
- package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +8 -6
- package/src/components/booking/useAdminChangeQuotePreview.ts +146 -61
- package/src/components/booking/useAdminCustomReceiptLines.ts +171 -10
- package/src/components/booking/useAdminProviderPricingAdjustments.ts +50 -19
- package/src/components/booking/useBookingAvailabilityAddOns.ts +26 -4
- package/src/components/booking/useBookingPromoAndQuantityController.ts +16 -2
- package/src/components/booking/useChangeBookingAddOnFloorController.ts +15 -9
- package/src/components/booking/useChangeBookingAutoSelections.ts +60 -17
- package/src/components/booking/useChangeBookingInitialHydration.ts +10 -7
- package/src/components/booking/useChangeBookingQuotePreview.ts +3 -1
- package/src/components/booking/useChangeBookingSelectionDetails.ts +23 -17
- package/src/components/booking/useStandardBookingAutoSelections.ts +55 -7
- package/src/lib/booking/booking-cutoffs.ts +22 -0
- package/src/lib/booking/change-booking-server-preview.ts +92 -5
- package/src/lib/booking/change-flow-pricing.ts +12 -0
- package/src/lib/booking/i18n/messages/en.json +1 -0
- package/src/lib/booking/i18n/messages/fr.json +1 -0
- package/src/lib/booking-api.ts +76 -1
- package/test/change-booking-helpers.test.ts +1319 -7
- 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
|
-
}, [
|
|
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=
|
|
920
|
-
<div className=
|
|
921
|
-
<svg
|
|
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.
|
|
218
|
-
<div
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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}
|