@ticketboothapp/booking 1.2.183 → 1.2.185
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 +2 -1
- package/src/components/BackgroundVideo.tsx +126 -0
- package/src/components/BookingDetails.ts +1 -0
- package/src/components/booking/AdminChangeBookingContent.tsx +1 -0
- package/src/components/booking/AdminChangeBookingFlow.tsx +2 -0
- package/src/components/booking/AdminChangeCheckoutPanel.tsx +13 -2
- package/src/components/booking/AdminChangeReceiptComparison.tsx +67 -2
- package/src/components/booking/BookingFlowCollage.module.css +1 -1
- package/src/components/booking/BookingFlowCollage.tsx +4 -2
- package/src/components/booking/BookingProductGrid.tsx +5 -3
- package/src/components/booking/ChangeBookingDialog.tsx +1 -0
- package/src/components/booking/PriceSummary.tsx +2 -0
- package/src/components/booking/useAdminCustomReceiptLines.ts +38 -0
- package/src/index.ts +2 -0
- package/src/lib/booking/change-booking-server-preview.ts +6 -0
- package/src/strings/en.json +1 -1
- package/src/strings/es.json +1 -1
- package/src/strings/fr.json +1 -1
- package/test/change-booking-helpers.test.ts +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ticketboothapp/booking",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.185",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
".": "./src/index.ts",
|
|
14
|
+
"./background-video": "./src/components/BackgroundVideo.tsx",
|
|
14
15
|
"./booking-flow.css": "./src/components/booking/booking-flow.css",
|
|
15
16
|
"./runtime": "./src/runtime/index.ts",
|
|
16
17
|
"./contexts/booking-app-context": "./src/contexts/BookingAppContext.tsx",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
forwardRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type CSSProperties,
|
|
10
|
+
type ForwardedRef,
|
|
11
|
+
type VideoHTMLAttributes,
|
|
12
|
+
} from 'react';
|
|
13
|
+
|
|
14
|
+
export type BackgroundVideoLoading = 'eager' | 'viewport';
|
|
15
|
+
|
|
16
|
+
export interface BackgroundVideoProps
|
|
17
|
+
extends Omit<VideoHTMLAttributes<HTMLVideoElement>, 'children' | 'poster' | 'preload' | 'src'> {
|
|
18
|
+
src: string;
|
|
19
|
+
webm?: string;
|
|
20
|
+
poster?: string;
|
|
21
|
+
preload?: 'auto' | 'metadata' | 'none';
|
|
22
|
+
loading?: BackgroundVideoLoading;
|
|
23
|
+
className?: string;
|
|
24
|
+
style?: CSSProperties;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assignRef(ref: ForwardedRef<HTMLVideoElement>, value: HTMLVideoElement | null) {
|
|
28
|
+
if (typeof ref === 'function') ref(value);
|
|
29
|
+
else if (ref) ref.current = value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Lightweight background video with real multi-format sources. Viewport videos
|
|
34
|
+
* do not request media until they are near the screen and pause again offscreen.
|
|
35
|
+
*/
|
|
36
|
+
export const BackgroundVideo = forwardRef<HTMLVideoElement, BackgroundVideoProps>(
|
|
37
|
+
function BackgroundVideo(
|
|
38
|
+
{
|
|
39
|
+
src,
|
|
40
|
+
webm,
|
|
41
|
+
poster,
|
|
42
|
+
preload,
|
|
43
|
+
loading = 'viewport',
|
|
44
|
+
className,
|
|
45
|
+
style,
|
|
46
|
+
autoPlay = true,
|
|
47
|
+
muted = true,
|
|
48
|
+
loop = true,
|
|
49
|
+
playsInline = true,
|
|
50
|
+
...videoProps
|
|
51
|
+
},
|
|
52
|
+
forwardedRef,
|
|
53
|
+
) {
|
|
54
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
55
|
+
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
56
|
+
const [shouldLoad, setShouldLoad] = useState(loading === 'eager');
|
|
57
|
+
const [shouldPlay, setShouldPlay] = useState(loading === 'eager');
|
|
58
|
+
|
|
59
|
+
const setVideoRef = useCallback((element: HTMLVideoElement | null) => {
|
|
60
|
+
videoRef.current = element;
|
|
61
|
+
assignRef(forwardedRef, element);
|
|
62
|
+
}, [forwardedRef]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (loading === 'eager') {
|
|
66
|
+
setShouldLoad(true);
|
|
67
|
+
setShouldPlay(true);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const container = containerRef.current;
|
|
72
|
+
if (!container || typeof IntersectionObserver === 'undefined') {
|
|
73
|
+
setShouldLoad(true);
|
|
74
|
+
setShouldPlay(true);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const observer = new IntersectionObserver(
|
|
79
|
+
([entry]) => {
|
|
80
|
+
if (entry.isIntersecting) setShouldLoad(true);
|
|
81
|
+
setShouldPlay(entry.isIntersecting);
|
|
82
|
+
},
|
|
83
|
+
{ rootMargin: '300px 0px' },
|
|
84
|
+
);
|
|
85
|
+
observer.observe(container);
|
|
86
|
+
return () => observer.disconnect();
|
|
87
|
+
}, [loading]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const video = videoRef.current;
|
|
91
|
+
if (!video || !shouldLoad || !autoPlay) return;
|
|
92
|
+
if (shouldPlay) void video.play().catch(() => undefined);
|
|
93
|
+
else video.pause();
|
|
94
|
+
}, [autoPlay, shouldLoad, shouldPlay, src, webm]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
ref={containerRef}
|
|
99
|
+
className={[className, 'next-video-bg'].filter(Boolean).join(' ')}
|
|
100
|
+
style={{ width: '100%', height: '100%', display: 'grid', ...style }}
|
|
101
|
+
>
|
|
102
|
+
<video
|
|
103
|
+
{...videoProps}
|
|
104
|
+
ref={setVideoRef}
|
|
105
|
+
className="next-video-bg-video"
|
|
106
|
+
autoPlay={autoPlay && shouldPlay}
|
|
107
|
+
muted={muted}
|
|
108
|
+
loop={loop}
|
|
109
|
+
playsInline={playsInline}
|
|
110
|
+
poster={poster}
|
|
111
|
+
preload={shouldLoad ? (preload ?? (loading === 'eager' ? 'auto' : 'metadata')) : 'none'}
|
|
112
|
+
style={{
|
|
113
|
+
gridArea: '1 / 1',
|
|
114
|
+
width: '100%',
|
|
115
|
+
height: '100%',
|
|
116
|
+
minHeight: 0,
|
|
117
|
+
objectFit: 'cover',
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{shouldLoad && webm ? <source src={webm} type="video/webm" /> : null}
|
|
121
|
+
{shouldLoad ? <source src={src} type="video/mp4" /> : null}
|
|
122
|
+
</video>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
@@ -337,6 +337,7 @@ export function AdminChangeBookingFlow({
|
|
|
337
337
|
handleUpdateAdminCustomReceiptLine,
|
|
338
338
|
handleRemoveAdminCustomReceiptLine,
|
|
339
339
|
handleApplyAdminCustomReceiptLines: handleApplyAdminCustomReceiptLineDrafts,
|
|
340
|
+
handleSetAdminReceiptLineOverride,
|
|
340
341
|
} = useAdminCustomReceiptLines(
|
|
341
342
|
selectedChangeProductId,
|
|
342
343
|
latestChangeQuote?.reversibleAdjustments,
|
|
@@ -1299,6 +1300,7 @@ export function AdminChangeBookingFlow({
|
|
|
1299
1300
|
onUpdateAdminCustomReceiptLine: handleUpdateAdminCustomReceiptLine,
|
|
1300
1301
|
onRemoveAdminCustomReceiptLine: handleRemoveAdminCustomReceiptLine,
|
|
1301
1302
|
onApplyAdminCustomReceiptLines: handleApplyAdminCustomReceiptLines,
|
|
1303
|
+
onSetAdminReceiptLineOverride: handleSetAdminReceiptLineOverride,
|
|
1302
1304
|
onFirstNameChange: setFirstName, onLastNameChange: setLastName, onEmailChange: setEmail,
|
|
1303
1305
|
onClearError: () => setError(''),
|
|
1304
1306
|
pickupLocations: product.pickupLocations, destinations: product.destinations, highlightedPickupLocationIds,
|
|
@@ -80,6 +80,13 @@ export interface AdminChangeCheckoutPanelProps {
|
|
|
80
80
|
onUpdateAdminCustomReceiptLine: (id: string, patch: AdminCustomReceiptLinePatch) => void;
|
|
81
81
|
onRemoveAdminCustomReceiptLine: (id: string) => void;
|
|
82
82
|
onApplyAdminCustomReceiptLines: () => void;
|
|
83
|
+
onSetAdminReceiptLineOverride: (input: {
|
|
84
|
+
componentId: string;
|
|
85
|
+
lineKey: string;
|
|
86
|
+
label: string;
|
|
87
|
+
baseAmount: number;
|
|
88
|
+
targetAmount: number;
|
|
89
|
+
}) => void;
|
|
83
90
|
firstName: string;
|
|
84
91
|
lastName: string;
|
|
85
92
|
email: string;
|
|
@@ -131,7 +138,6 @@ export function AdminChangeCheckoutPanel({
|
|
|
131
138
|
changeFlowAmountDue,
|
|
132
139
|
receiptSubtotal,
|
|
133
140
|
receiptTax,
|
|
134
|
-
receiptTotal,
|
|
135
141
|
originalReceipt,
|
|
136
142
|
priceSummaryLinesIncludeTaxRow,
|
|
137
143
|
serverAmendmentLines,
|
|
@@ -172,6 +178,7 @@ export function AdminChangeCheckoutPanel({
|
|
|
172
178
|
onUpdateAdminCustomReceiptLine,
|
|
173
179
|
onRemoveAdminCustomReceiptLine,
|
|
174
180
|
onApplyAdminCustomReceiptLines,
|
|
181
|
+
onSetAdminReceiptLineOverride,
|
|
175
182
|
firstName,
|
|
176
183
|
lastName,
|
|
177
184
|
email,
|
|
@@ -232,7 +239,9 @@ export function AdminChangeCheckoutPanel({
|
|
|
232
239
|
showProviderPricingInlineEditor={showProviderPricingInlineEditor}
|
|
233
240
|
providerPricingUi={providerPricingUi}
|
|
234
241
|
showAdminCustomLineEditor={showAdminCustomLineEditor}
|
|
235
|
-
adminCustomReceiptLines={adminCustomReceiptLines
|
|
242
|
+
adminCustomReceiptLines={adminCustomReceiptLines.filter(
|
|
243
|
+
(line) => !line.overrideComponentId,
|
|
244
|
+
)}
|
|
236
245
|
hasPendingAdminCustomReceiptLineChanges={hasPendingAdminCustomReceiptLineChanges}
|
|
237
246
|
pendingAdminCustomReceiptLineIds={pendingAdminCustomReceiptLineIds}
|
|
238
247
|
adjustmentComponentOptions={adjustmentComponentOptions}
|
|
@@ -273,6 +282,8 @@ export function AdminChangeCheckoutPanel({
|
|
|
273
282
|
promoEditor={promoEditor}
|
|
274
283
|
promoCode={promoCode}
|
|
275
284
|
adjustments={pricingAdjustments}
|
|
285
|
+
adminCustomReceiptLines={adminCustomReceiptLines}
|
|
286
|
+
onSetReceiptLineOverride={onSetAdminReceiptLineOverride}
|
|
276
287
|
/>
|
|
277
288
|
) : null;
|
|
278
289
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useState, type ReactNode } from 'react';
|
|
2
2
|
import type {
|
|
3
3
|
AdminAmendmentOperationSnapshot,
|
|
4
4
|
AdminAmendmentRefundDisposition,
|
|
@@ -10,6 +10,7 @@ import type { ChangeBookingFlowProps } from './booking-flow-types';
|
|
|
10
10
|
import type { Currency } from './CurrencySwitcher';
|
|
11
11
|
import { PriceSummary, type PriceSummaryLine } from './PriceSummary';
|
|
12
12
|
import { collapseAdminPromoLines } from './admin-change-receipt-lines';
|
|
13
|
+
import type { AdminCustomReceiptLine } from './useAdminCustomReceiptLines';
|
|
13
14
|
import {
|
|
14
15
|
noRefundRemovedValue,
|
|
15
16
|
refundDecisionsComplete,
|
|
@@ -63,6 +64,14 @@ export interface AdminChangeReceiptComparisonProps {
|
|
|
63
64
|
refundQuoteLoading?: boolean;
|
|
64
65
|
sourceProductName?: string | null;
|
|
65
66
|
targetProductName?: string | null;
|
|
67
|
+
adminCustomReceiptLines?: AdminCustomReceiptLine[];
|
|
68
|
+
onSetReceiptLineOverride?: (input: {
|
|
69
|
+
componentId: string;
|
|
70
|
+
lineKey: string;
|
|
71
|
+
label: string;
|
|
72
|
+
baseAmount: number;
|
|
73
|
+
targetAmount: number;
|
|
74
|
+
}) => void;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
function normalizeLocale(locale: string): Locale {
|
|
@@ -117,7 +126,10 @@ export function AdminChangeReceiptComparison({
|
|
|
117
126
|
removedValueByOperationId = {},
|
|
118
127
|
refundDispositionsByOperationId = {},
|
|
119
128
|
refundQuoteLoading = false,
|
|
129
|
+
adminCustomReceiptLines = [],
|
|
130
|
+
onSetReceiptLineOverride,
|
|
120
131
|
}: AdminChangeReceiptComparisonProps) {
|
|
132
|
+
const [overrideDrafts, setOverrideDrafts] = useState<Record<string, string>>({});
|
|
121
133
|
const readableQuoteError = friendlyChangeBookingError(quoteError);
|
|
122
134
|
const displayLocale = normalizeLocale(locale);
|
|
123
135
|
const existingLines = originalLines(originalReceipt);
|
|
@@ -202,9 +214,59 @@ export function AdminChangeReceiptComparison({
|
|
|
202
214
|
existingLines,
|
|
203
215
|
familyConversionReceiptLines,
|
|
204
216
|
);
|
|
205
|
-
const
|
|
217
|
+
const rawSummaryDisplayLines = isProductFamilyConversion
|
|
206
218
|
? familyConversionReceiptLines
|
|
207
219
|
: amendmentDisplayLines;
|
|
220
|
+
const overrideByComponentId = new Map(
|
|
221
|
+
adminCustomReceiptLines.flatMap((line) =>
|
|
222
|
+
line.overrideComponentId ? [[line.overrideComponentId, line] as const] : [],
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
const summaryDisplayLines = rawSummaryDisplayLines
|
|
226
|
+
.filter((line) => !(line.kind === 'line' && line.label.startsWith('Override · ')))
|
|
227
|
+
.map((line) => {
|
|
228
|
+
const type = line.kind === 'ticket' ? 'TICKET' : String(line.type ?? '').toUpperCase();
|
|
229
|
+
const protectedType = ['TAX', 'PROMO_CODE', 'DISCOUNT', 'GIFT_CARD', 'PAYMENT_CREDIT', 'BOOKING_CHANGE']
|
|
230
|
+
.includes(type);
|
|
231
|
+
return {
|
|
232
|
+
...line,
|
|
233
|
+
editable: Boolean(
|
|
234
|
+
!protectedType && line.componentId && line.lineKey && onSetReceiptLineOverride,
|
|
235
|
+
),
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
const quotedOverrideAmountInputs = Object.fromEntries(
|
|
239
|
+
summaryDisplayLines.flatMap((line) => {
|
|
240
|
+
if (!line.componentId || !line.lineKey) return [];
|
|
241
|
+
const baseAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
|
|
242
|
+
const override = overrideByComponentId.get(line.componentId);
|
|
243
|
+
const target = override
|
|
244
|
+
? baseAmount + (override.amountSign ?? 1) * Number(override.amountInput)
|
|
245
|
+
: baseAmount;
|
|
246
|
+
return [[line.lineKey, Number.isFinite(target) ? target.toFixed(2) : baseAmount.toFixed(2)]];
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
const handleOverrideChange = (lineKey: string, value: string) => {
|
|
250
|
+
setOverrideDrafts((current) => ({ ...current, [lineKey]: value }));
|
|
251
|
+
};
|
|
252
|
+
const handleOverrideBlur = (lineKey: string) => {
|
|
253
|
+
const line = summaryDisplayLines.find((candidate) => candidate.lineKey === lineKey);
|
|
254
|
+
const targetAmount = Number(overrideDrafts[lineKey] ?? quotedOverrideAmountInputs[lineKey]);
|
|
255
|
+
if (!line?.componentId || !Number.isFinite(targetAmount)) return;
|
|
256
|
+
const baseAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
|
|
257
|
+
onSetReceiptLineOverride?.({
|
|
258
|
+
componentId: line.componentId,
|
|
259
|
+
lineKey,
|
|
260
|
+
label: line.kind === 'ticket' ? line.category : line.label,
|
|
261
|
+
baseAmount,
|
|
262
|
+
targetAmount,
|
|
263
|
+
});
|
|
264
|
+
setOverrideDrafts((current) => {
|
|
265
|
+
const next = { ...current };
|
|
266
|
+
delete next[lineKey];
|
|
267
|
+
return next;
|
|
268
|
+
});
|
|
269
|
+
};
|
|
208
270
|
const summarySubtotal = isProductFamilyConversion
|
|
209
271
|
? familyConversionReceiptLines.reduce((sum, line) => {
|
|
210
272
|
if (line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX') return sum;
|
|
@@ -313,6 +375,9 @@ export function AdminChangeReceiptComparison({
|
|
|
313
375
|
subtotal={summarySubtotal}
|
|
314
376
|
size="sm"
|
|
315
377
|
t={t}
|
|
378
|
+
lineAmountInputs={{ ...quotedOverrideAmountInputs, ...overrideDrafts }}
|
|
379
|
+
onLineAmountInputChange={handleOverrideChange}
|
|
380
|
+
onLineAmountInputBlur={handleOverrideBlur}
|
|
316
381
|
/>
|
|
317
382
|
)}
|
|
318
383
|
</section>
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
min-height: 100% !important;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/* Constrain
|
|
71
|
+
/* Constrain the shared background video output. */
|
|
72
72
|
.videoSlot :global(video),
|
|
73
73
|
.videoSlot :global(.next-video-bg-video),
|
|
74
74
|
.videoSlot :global(.next-video-bg-poster),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
'use client';
|
|
3
3
|
|
|
4
4
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
5
|
-
import
|
|
5
|
+
import { BackgroundVideo } from '../BackgroundVideo';
|
|
6
6
|
import type { VideoSources } from '../../constants/products';
|
|
7
7
|
import { resolvePublicAssetUrl, useBookingHost } from '../../runtime';
|
|
8
8
|
import { useTranslations } from '../../lib/booking/i18n';
|
|
@@ -100,9 +100,11 @@ export function BookingFlowCollage({
|
|
|
100
100
|
<div className={`${styles.collage} ${!hasGridImages ? styles.videoOnly : ''}`}>
|
|
101
101
|
<div className={styles.videoSlot}>
|
|
102
102
|
<div className={styles.videoWrapper}>
|
|
103
|
-
<
|
|
103
|
+
<BackgroundVideo
|
|
104
104
|
ref={videoRef}
|
|
105
105
|
{...videoForCollage}
|
|
106
|
+
loading="eager"
|
|
107
|
+
preload="auto"
|
|
106
108
|
autoPlay
|
|
107
109
|
muted
|
|
108
110
|
loop
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { resolvePublicAssetUrl, useBookingHost } from '../../runtime';
|
|
11
11
|
import { useLocale, useTranslations } from '../../lib/booking/i18n';
|
|
12
12
|
import type { Product } from '../../constants/products';
|
|
13
|
-
import
|
|
13
|
+
import { BackgroundVideo } from '../BackgroundVideo';
|
|
14
14
|
import styles from './BookingProductGrid.module.css';
|
|
15
15
|
|
|
16
16
|
/** Locale strings needed by the product grid tiles (Via Via host). */
|
|
@@ -48,7 +48,7 @@ const FILTER_PRODUCT_IDS: Record<string, string[]> = {
|
|
|
48
48
|
export const FILTER_IDS = ['all', 'sunrise', 'moraine-lake', 'lake-louise', 'emerald-lake', 'private'] as const;
|
|
49
49
|
export type FilterId = (typeof FILTER_IDS)[number];
|
|
50
50
|
|
|
51
|
-
/** Only `src` / `webm` — never spread catalog
|
|
51
|
+
/** Only `src` / `webm` — never spread catalog long-source fields onto the DOM. */
|
|
52
52
|
function backgroundPlayerSources(
|
|
53
53
|
product: Product,
|
|
54
54
|
staticAssetOrigin: string,
|
|
@@ -220,8 +220,10 @@ function BookingProductTileExpanded({
|
|
|
220
220
|
<path d="M10 6l8 6-8 6" />
|
|
221
221
|
</motion.svg>
|
|
222
222
|
</button>
|
|
223
|
-
<
|
|
223
|
+
<BackgroundVideo
|
|
224
224
|
{...backgroundPlayerSources(product, env.STATIC_ASSET_ORIGIN)}
|
|
225
|
+
loading="eager"
|
|
226
|
+
preload="auto"
|
|
225
227
|
autoPlay
|
|
226
228
|
muted
|
|
227
229
|
loop
|
|
@@ -428,6 +428,7 @@ export default function ChangeBookingDialog({
|
|
|
428
428
|
currency={receiptCurrency}
|
|
429
429
|
contentRef={contentRef}
|
|
430
430
|
bookingSourceAttribution={bookingSourceAttribution}
|
|
431
|
+
availabilityPricingProfileId={booking.pricingProfileId ?? null}
|
|
431
432
|
onSuccess={() => {
|
|
432
433
|
onChangeCompleted?.(newBookingPreview);
|
|
433
434
|
onClose();
|
|
@@ -16,6 +16,7 @@ export type PriceSummaryLine =
|
|
|
16
16
|
lineKey?: string;
|
|
17
17
|
/** Pricing V2 amendment operation that produced this row, when applicable. */
|
|
18
18
|
operationId?: string;
|
|
19
|
+
componentId?: string;
|
|
19
20
|
editable?: boolean;
|
|
20
21
|
category: string;
|
|
21
22
|
qty: number;
|
|
@@ -34,6 +35,7 @@ export type PriceSummaryLine =
|
|
|
34
35
|
| {
|
|
35
36
|
kind: 'line';
|
|
36
37
|
lineKey?: string;
|
|
38
|
+
componentId?: string;
|
|
37
39
|
label: string;
|
|
38
40
|
amount: number;
|
|
39
41
|
/** Drives styling: discount (red, -), return add-on (green, +), default (stone). Receipt types: TICKET, FEE, RETURN_OPTION, PROMO_CODE, CANCELLATION_UPGRADE, TAX, etc. */
|
|
@@ -19,6 +19,9 @@ export type AdminCustomReceiptLine = {
|
|
|
19
19
|
sourceAdjustmentId?: string;
|
|
20
20
|
replacementAdjustmentId?: string;
|
|
21
21
|
isDirty?: boolean;
|
|
22
|
+
overrideComponentId?: string;
|
|
23
|
+
overrideBaseAmount?: number;
|
|
24
|
+
overrideLineKey?: string;
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
type AdminCustomReceiptLinePatch = Partial<
|
|
@@ -146,6 +149,40 @@ export function useAdminCustomReceiptLines(
|
|
|
146
149
|
setAppliedRemovedExistingAdjustmentIds(removedExistingAdjustmentIds);
|
|
147
150
|
}, [adminCustomReceiptLines, removedExistingAdjustmentIds]);
|
|
148
151
|
|
|
152
|
+
const handleSetAdminReceiptLineOverride = useCallback((input: {
|
|
153
|
+
componentId: string;
|
|
154
|
+
lineKey: string;
|
|
155
|
+
label: string;
|
|
156
|
+
baseAmount: number;
|
|
157
|
+
targetAmount: number;
|
|
158
|
+
}) => {
|
|
159
|
+
const delta = Math.round((input.targetAmount - input.baseAmount) * 100) / 100;
|
|
160
|
+
const existing = adminCustomReceiptLines.find(
|
|
161
|
+
(line) => line.overrideComponentId === input.componentId,
|
|
162
|
+
);
|
|
163
|
+
const next = Math.abs(delta) < 0.005
|
|
164
|
+
? adminCustomReceiptLines.filter((line) => line.id !== existing?.id)
|
|
165
|
+
: [
|
|
166
|
+
...adminCustomReceiptLines.filter((line) => line.id !== existing?.id),
|
|
167
|
+
{
|
|
168
|
+
id: existing?.id ?? nextDraftId('admin-rcpt'),
|
|
169
|
+
label: `Override · ${input.label}`,
|
|
170
|
+
amountInput: Math.abs(delta).toFixed(2),
|
|
171
|
+
amountSign: (delta < 0 ? -1 : 1) as 1 | -1,
|
|
172
|
+
calculation: 'FIXED_AMOUNT' as const,
|
|
173
|
+
scope: 'SELECTED_COMPONENTS' as const,
|
|
174
|
+
selectedComponentIds: [input.componentId],
|
|
175
|
+
taxBehavior: delta < 0 ? 'PRE_TAX_DISCOUNT' as const : 'TAXABLE' as const,
|
|
176
|
+
reasonCode: 'LINE_ITEM_OVERRIDE',
|
|
177
|
+
overrideComponentId: input.componentId,
|
|
178
|
+
overrideBaseAmount: input.baseAmount,
|
|
179
|
+
overrideLineKey: input.lineKey,
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
setAdminCustomReceiptLines(next);
|
|
183
|
+
setAppliedAdminCustomReceiptLines(next);
|
|
184
|
+
}, [adminCustomReceiptLines, nextDraftId]);
|
|
185
|
+
|
|
149
186
|
useEffect(() => {
|
|
150
187
|
if (previousResetKeyRef.current === resetKey) return;
|
|
151
188
|
previousResetKeyRef.current = resetKey;
|
|
@@ -225,5 +262,6 @@ export function useAdminCustomReceiptLines(
|
|
|
225
262
|
handleUpdateAdminCustomReceiptLine,
|
|
226
263
|
handleRemoveAdminCustomReceiptLine,
|
|
227
264
|
handleApplyAdminCustomReceiptLines,
|
|
265
|
+
handleSetAdminReceiptLineOverride,
|
|
228
266
|
};
|
|
229
267
|
}
|
package/src/index.ts
CHANGED
|
@@ -62,6 +62,8 @@ export type {
|
|
|
62
62
|
export { formatBookingRefForDisplay } from './lib/format-booking-ref';
|
|
63
63
|
export { default as BookingProductGrid } from './components/booking/BookingProductGrid';
|
|
64
64
|
export { DefaultTermsContent as TermsContent } from './components/booking/DefaultTermsContent';
|
|
65
|
+
export { BackgroundVideo } from './components/BackgroundVideo';
|
|
66
|
+
export type { BackgroundVideoLoading, BackgroundVideoProps } from './components/BackgroundVideo';
|
|
65
67
|
|
|
66
68
|
export type {
|
|
67
69
|
BookingAppMode,
|
|
@@ -212,7 +212,9 @@ export function mapQuoteLineItemsToPriceSummaryLines(
|
|
|
212
212
|
: type;
|
|
213
213
|
out.push({
|
|
214
214
|
kind: 'ticket',
|
|
215
|
+
lineKey: isPricingV2Line(item) ? item.lineId ?? undefined : undefined,
|
|
215
216
|
operationId: isPricingV2Line(item) ? item.metadata?.operationId : undefined,
|
|
217
|
+
componentId: isPricingV2Line(item) ? item.metadata?.componentId : undefined,
|
|
216
218
|
category,
|
|
217
219
|
qty: qty > 0 ? qty : 1,
|
|
218
220
|
itemTotal: amount,
|
|
@@ -234,7 +236,9 @@ export function mapQuoteLineItemsToPriceSummaryLines(
|
|
|
234
236
|
) {
|
|
235
237
|
out.push({
|
|
236
238
|
kind: 'ticket',
|
|
239
|
+
lineKey: isPricingV2Line(item) ? item.lineId ?? undefined : undefined,
|
|
237
240
|
operationId: isPricingV2Line(item) ? item.metadata?.operationId : undefined,
|
|
241
|
+
componentId: isPricingV2Line(item) ? item.metadata?.componentId : undefined,
|
|
238
242
|
category: lettersOnly,
|
|
239
243
|
qty,
|
|
240
244
|
itemTotal: amount,
|
|
@@ -247,6 +251,8 @@ export function mapQuoteLineItemsToPriceSummaryLines(
|
|
|
247
251
|
/** Align with checkout drift keys (`ChangeBookingFlow` uses `type: 'return'`). */
|
|
248
252
|
out.push({
|
|
249
253
|
kind: 'line',
|
|
254
|
+
lineKey: isPricingV2Line(item) ? item.lineId ?? undefined : undefined,
|
|
255
|
+
componentId: isPricingV2Line(item) ? item.metadata?.componentId : undefined,
|
|
250
256
|
label: label || type || 'Line',
|
|
251
257
|
amount,
|
|
252
258
|
type,
|
package/src/strings/en.json
CHANGED
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
"home": {
|
|
241
241
|
"hero": {
|
|
242
242
|
"tagline": "Tours to Moraine Lake, Lake Louise, Emerald Lake & more for families, friends, and unforgettable days",
|
|
243
|
-
"googleReviewsCount": "
|
|
243
|
+
"googleReviewsCount": "2500+ reviews",
|
|
244
244
|
"centerCallout": "now open ✨"
|
|
245
245
|
},
|
|
246
246
|
"seasonClosure": {
|
package/src/strings/es.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"home": {
|
|
40
40
|
"hero": {
|
|
41
41
|
"tagline": "Descubre Moraine Lake, Lake Louise, y mucho más. Excursiones inolvidables para todos los públicos.",
|
|
42
|
-
"googleReviewsCount": "Mas de
|
|
42
|
+
"googleReviewsCount": "Mas de 2500 opioniones",
|
|
43
43
|
"centerCallout": "ahora abierto ✨"
|
|
44
44
|
},
|
|
45
45
|
"seasonClosure": {
|
package/src/strings/fr.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"home": {
|
|
40
40
|
"hero": {
|
|
41
41
|
"tagline": "Partez à la découverte du lac Moraine, du lac Louise, du lac Émeraude et plus encore ! Excursions en famille ou entre amis, pour vivre une expérience inoubliable",
|
|
42
|
-
"googleReviewsCount": "
|
|
42
|
+
"googleReviewsCount": "2500+ avis",
|
|
43
43
|
"centerCallout": "maintenant ouvert ✨"
|
|
44
44
|
},
|
|
45
45
|
"seasonClosure": {
|
|
@@ -314,6 +314,28 @@ test('legacy booking-change comparison rows are omitted from the purchase breakd
|
|
|
314
314
|
);
|
|
315
315
|
});
|
|
316
316
|
|
|
317
|
+
test('Pricing V2 amendment rows retain line and component identity for admin overrides', () => {
|
|
318
|
+
const [line] = mapQuoteLineItemsToPriceSummaryLines([
|
|
319
|
+
{
|
|
320
|
+
lineId: 'component_adult:amendment:gross',
|
|
321
|
+
type: 'TICKET',
|
|
322
|
+
label: 'Refund · ADULT',
|
|
323
|
+
amount: -197.99,
|
|
324
|
+
quantity: 9,
|
|
325
|
+
billable: true,
|
|
326
|
+
metadata: {
|
|
327
|
+
category: 'ADULT',
|
|
328
|
+
operationId: 'op_move_adult',
|
|
329
|
+
componentId: 'component_adult',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
assert.equal(line?.lineKey, 'component_adult:amendment:gross');
|
|
335
|
+
assert.equal(line?.componentId, 'component_adult');
|
|
336
|
+
assert.equal(line?.kind === 'ticket' ? line.operationId : null, 'op_move_adult');
|
|
337
|
+
});
|
|
338
|
+
|
|
317
339
|
test('server preview exposes the resulting receipt separately from ledger allocation lines', () => {
|
|
318
340
|
const preview = buildChangeBookingServerPreview(
|
|
319
341
|
quote({
|