@voyantjs/bookings-ui 0.107.0 → 0.108.0
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/dist/components/option-units-stepper-section.d.ts +9 -1
- package/dist/components/option-units-stepper-section.d.ts.map +1 -1
- package/dist/components/option-units-stepper-section.js +10 -2
- package/dist/components/person-picker-section.d.ts +7 -1
- package/dist/components/person-picker-section.d.ts.map +1 -1
- package/dist/components/person-picker-section.js +2 -2
- package/dist/i18n/en.d.ts +37 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +40 -4
- package/dist/i18n/messages.d.ts +37 -1
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +74 -2
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +37 -1
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +39 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/journey/components/booking-journey.d.ts.map +1 -1
- package/dist/journey/components/booking-journey.js +270 -27
- package/dist/journey/components/journey-steps/accommodation-step.d.ts +3 -0
- package/dist/journey/components/journey-steps/accommodation-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/accommodation-step.js +71 -0
- package/dist/journey/components/journey-steps/addons-step.d.ts +3 -0
- package/dist/journey/components/journey-steps/addons-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/addons-step.js +40 -0
- package/dist/journey/components/journey-steps/billing-step.d.ts +8 -0
- package/dist/journey/components/journey-steps/billing-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/billing-step.js +78 -0
- package/dist/journey/components/journey-steps/configure-steps.d.ts +28 -0
- package/dist/journey/components/journey-steps/configure-steps.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/configure-steps.js +231 -0
- package/dist/journey/components/journey-steps/documents-step.d.ts +11 -0
- package/dist/journey/components/journey-steps/documents-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/documents-step.js +36 -0
- package/dist/journey/components/journey-steps/payment-step.d.ts +29 -0
- package/dist/journey/components/journey-steps/payment-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/payment-step.js +224 -0
- package/dist/journey/components/journey-steps/review-step.d.ts +27 -0
- package/dist/journey/components/journey-steps/review-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/review-step.js +18 -0
- package/dist/journey/components/journey-steps/shared.d.ts +75 -0
- package/dist/journey/components/journey-steps/shared.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/shared.js +108 -0
- package/dist/journey/components/journey-steps/travelers-step.d.ts +7 -0
- package/dist/journey/components/journey-steps/travelers-step.d.ts.map +1 -0
- package/dist/journey/components/journey-steps/travelers-step.js +201 -0
- package/dist/journey/components/journey-steps.d.ts +13 -39
- package/dist/journey/components/journey-steps.d.ts.map +1 -1
- package/dist/journey/components/journey-steps.js +16 -613
- package/dist/journey/components/side-panel.d.ts +7 -2
- package/dist/journey/components/side-panel.d.ts.map +1 -1
- package/dist/journey/components/side-panel.js +73 -24
- package/dist/journey/index.d.ts +2 -2
- package/dist/journey/index.d.ts.map +1 -1
- package/dist/journey/index.js +1 -1
- package/dist/journey/lib/pax-band-dependencies.d.ts +27 -0
- package/dist/journey/lib/pax-band-dependencies.d.ts.map +1 -0
- package/dist/journey/lib/pax-band-dependencies.js +50 -0
- package/dist/journey/lib/payment-schedule.d.ts +19 -0
- package/dist/journey/lib/payment-schedule.d.ts.map +1 -0
- package/dist/journey/lib/payment-schedule.js +90 -0
- package/dist/journey/types.d.ts +141 -8
- package/dist/journey/types.d.ts.map +1 -1
- package/dist/journey/types.js +3 -1
- package/package.json +32 -32
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useBookingsUiMessagesOrDefault } from "../../i18n/index.js";
|
|
2
|
+
import type { JourneyStep, SidePanelState } from "../types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Right-rail summary panel. Shows what's being booked, an
|
|
4
5
|
* accordion-per-step recap of the user's input, and the live
|
|
@@ -6,7 +7,11 @@ import type { SidePanelState } from "../types.js";
|
|
|
6
7
|
* expanded by default; users can click any step to peek at what
|
|
7
8
|
* they've filled in elsewhere.
|
|
8
9
|
*/
|
|
9
|
-
export declare function PriceSidePanel({ pricing, isQuoting, invalidReason, entitySummary, currentStep, steps, draft, className, }: SidePanelState & {
|
|
10
|
+
export declare function PriceSidePanel({ pricing, isQuoting, invalidReason, entitySummary, currentStep, steps, shape, draft, className, pricingExtras, }: SidePanelState & {
|
|
10
11
|
className?: string;
|
|
12
|
+
/** Operator-only price controls (override + voucher) rendered with the
|
|
13
|
+
* pricing, where they're most in context. */
|
|
14
|
+
pricingExtras?: React.ReactNode;
|
|
11
15
|
}): React.ReactElement;
|
|
16
|
+
export declare function stepHeadline(step: JourneyStep, draft: NonNullable<SidePanelState["draft"]>, messages: ReturnType<typeof useBookingsUiMessagesOrDefault>): string;
|
|
12
17
|
//# sourceMappingURL=side-panel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"side-panel.d.ts","sourceRoot":"","sources":["../../../src/journey/components/side-panel.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"side-panel.d.ts","sourceRoot":"","sources":["../../../src/journey/components/side-panel.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAiB,8BAA8B,EAAE,MAAM,qBAAqB,CAAA;AACnF,OAAO,KAAK,EAAwB,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEpF;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,SAAS,EACT,aAAa,EACb,aAAa,EACb,WAAW,EACX,KAAK,EACL,KAAK,EACL,KAAK,EACL,SAAS,EACT,aAAa,GACd,EAAE,cAAc,GAAG;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;kDAC8C;IAC9C,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAChC,GAAG,KAAK,CAAC,YAAY,CA2FrB;AAoFD,wBAAgB,YAAY,CAC1B,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,WAAW,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,EAC3C,QAAQ,EAAE,UAAU,CAAC,OAAO,8BAA8B,CAAC,GAC1D,MAAM,CAgER"}
|
|
@@ -11,22 +11,44 @@ import { formatMessage, useBookingsUiMessagesOrDefault } from "../../i18n/index.
|
|
|
11
11
|
* expanded by default; users can click any step to peek at what
|
|
12
12
|
* they've filled in elsewhere.
|
|
13
13
|
*/
|
|
14
|
-
export function PriceSidePanel({ pricing, isQuoting, invalidReason, entitySummary, currentStep, steps, draft, className, }) {
|
|
14
|
+
export function PriceSidePanel({ pricing, isQuoting, invalidReason, entitySummary, currentStep, steps, shape, draft, className, pricingExtras, }) {
|
|
15
15
|
const messages = useBookingsUiMessagesOrDefault();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
// Only surface pricing once the user has configured what actually drives
|
|
17
|
+
// the price — otherwise the quote's baseline shows a misleading total
|
|
18
|
+
// (e.g. a per-pax "from" price before any room is picked). Room products
|
|
19
|
+
// (an `option-units` sub-step) require a room selection; everything else
|
|
20
|
+
// requires at least one traveler.
|
|
21
|
+
const configuredPax = draft?.configure?.pax
|
|
22
|
+
? Object.values(draft.configure.pax).reduce((sum, n) => sum + (n ?? 0), 0)
|
|
23
|
+
: 0;
|
|
24
|
+
const isRoomProduct = (shape?.configureSubSteps ?? []).some((s) => s.kind === "option-units");
|
|
25
|
+
const roomsPicked = (draft?.configure?.optionSelections?.length ?? 0) > 0;
|
|
26
|
+
const showPricing = isRoomProduct ? roomsPicked : configuredPax > 0;
|
|
27
|
+
const pricingHint = isRoomProduct
|
|
28
|
+
? messages.bookingJourney.sidePanel.pricingHintRooms
|
|
29
|
+
: messages.bookingJourney.sidePanel.pricingHint;
|
|
30
|
+
// Departure shows directly under the product title — it's the most-glanced
|
|
31
|
+
// fact, so it doesn't belong buried in the recap accordion.
|
|
32
|
+
const departureText = draft ? stepHeadline("departure", draft, messages) : "";
|
|
33
|
+
return (_jsxs(Card, { className: className, children: [entitySummary?.heroImageUrl ? (_jsx("img", { src: entitySummary.heroImageUrl, alt: entitySummary.name, className: "aspect-video w-full object-cover" })) : null, entitySummary ? (_jsx(EntityHeader, { summary: entitySummary, departureText: departureText })) : null, _jsxs(CardContent, { className: "space-y-4", children: [steps && steps.length > 0 && currentStep && draft ? (_jsx(StepRecap, { steps: steps, currentStep: currentStep, draft: draft })) : null, invalidReason ? _jsx("p", { className: "text-destructive text-sm", children: invalidReason }) : null, !showPricing ? (_jsx("p", { className: "border-t pt-4 text-muted-foreground text-sm", children: pricingHint })) : isQuoting && !pricing ? (_jsxs("div", { className: "space-y-2 border-t pt-4", children: [_jsx(Skeleton, { className: "h-4 w-24" }), _jsx(Skeleton, { className: "h-4 w-32" }), _jsx(Skeleton, { className: "h-4 w-20" })] })) : null, showPricing && pricing ? (_jsxs("div", { className: "space-y-2 border-t pt-4", children: [_jsx("ul", { className: "space-y-1 text-sm", children: pricing.lines.map((line) => (_jsxs("li", { className: "flex justify-between", children: [_jsxs("span", { children: [line.label, line.quantity ? (_jsxs("span", { className: "text-muted-foreground", children: [" \u00D7 ", line.quantity] })) : null] }), _jsx("span", { children: formatMoney(line.totalAmount, pricing.currency) })] }, `${line.kind}-${line.label}-${line.totalAmount}`))) }), pricing.taxes.length > 0 ? (_jsx("ul", { className: "space-y-1 border-t pt-2 text-sm text-muted-foreground", children: pricing.taxes.map((tax) => (_jsxs("li", { className: "flex justify-between", children: [_jsxs("span", { children: [tax.label, tax.rate > 0 ? ` (${formatTaxRate(tax.rate)})` : ""] }), _jsx("span", { children: formatMoney(tax.amount, pricing.currency) })] }, tax.code))) })) : null, _jsxs("div", { className: "flex justify-between border-t pt-2 font-medium", children: [_jsx("span", { children: messages.bookingJourney.sidePanel.total }), _jsx("span", { children: formatMoney(pricing.total, pricing.currency) })] })] })) : null, pricingExtras ? _jsx("div", { className: "border-t pt-4", children: pricingExtras }) : null] })] }));
|
|
34
|
+
}
|
|
35
|
+
function EntityHeader({ summary, departureText, }) {
|
|
36
|
+
// Text block only — the hero image is rendered as a direct Card child above.
|
|
37
|
+
return (_jsxs("div", { className: "space-y-1 px-6 pt-4 pb-2", children: [_jsx("div", { className: "font-semibold leading-tight", children: summary.name }), departureText ? _jsx("div", { className: "text-muted-foreground text-sm", children: departureText }) : null, summary.subtitle ? (_jsx("div", { className: "text-muted-foreground text-sm", children: summary.subtitle })) : null, summary.whenLabel || summary.locationLabel ? (_jsx("div", { className: "text-muted-foreground text-xs", children: [summary.whenLabel, summary.locationLabel].filter(Boolean).join(" · ") })) : null] }));
|
|
21
38
|
}
|
|
22
39
|
function StepRecap({ steps, currentStep, draft, }) {
|
|
23
40
|
const messages = useBookingsUiMessagesOrDefault();
|
|
24
41
|
if (!draft)
|
|
25
42
|
return null;
|
|
43
|
+
// Departure shows in the header; payment + documents aren't worth a recap
|
|
44
|
+
// row — so the accordion covers the substantive in-between steps only.
|
|
45
|
+
const recapSteps = steps.filter((step) => step !== "departure" && step !== "payment" && step !== "documents");
|
|
46
|
+
if (recapSteps.length === 0)
|
|
47
|
+
return null;
|
|
26
48
|
// Default-open the current step. Uncontrolled — users can toggle
|
|
27
49
|
// freely. Re-mounts when currentStep changes (key) so the
|
|
28
50
|
// newly-active step opens automatically as the user advances.
|
|
29
|
-
return (_jsx(Accordion, { defaultValue: [currentStep], multiple: true, children:
|
|
51
|
+
return (_jsx(Accordion, { defaultValue: [currentStep], multiple: true, children: recapSteps.map((step) => (_jsxs(AccordionItem, { value: step, children: [_jsx(AccordionTrigger, { className: "py-3", children: _jsxs("div", { className: "flex flex-1 flex-col text-left", children: [_jsx("span", { className: step === currentStep ? "font-semibold" : "text-muted-foreground font-medium", children: stepLabel(step, messages) }), _jsx(StepSummaryLine, { step: step, draft: draft })] }) }), _jsx(AccordionContent, { children: _jsx(StepDetails, { step: step, draft: draft }) })] }, step))) }, currentStep));
|
|
30
52
|
}
|
|
31
53
|
function StepSummaryLine({ step, draft, }) {
|
|
32
54
|
const messages = useBookingsUiMessagesOrDefault();
|
|
@@ -35,17 +57,24 @@ function StepSummaryLine({ step, draft, }) {
|
|
|
35
57
|
return null;
|
|
36
58
|
return _jsx("span", { className: "text-muted-foreground text-xs", children: text });
|
|
37
59
|
}
|
|
38
|
-
function stepHeadline(step, draft, messages) {
|
|
60
|
+
export function stepHeadline(step, draft, messages) {
|
|
39
61
|
switch (step) {
|
|
40
|
-
case "
|
|
41
|
-
const total = paxTotal(draft);
|
|
42
|
-
const slot = draft.configure?.departureSlotId ?? draft.configure?.departureDate ?? undefined;
|
|
62
|
+
case "departure": {
|
|
43
63
|
const range = draft.configure?.dateRange;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
if (range?.checkIn && range?.checkOut)
|
|
65
|
+
return `${range.checkIn} → ${range.checkOut}`;
|
|
66
|
+
// Never surface the raw slot id — show the departure date.
|
|
67
|
+
return draft.configure?.departureDate
|
|
68
|
+
? formatConfigureDate(draft.configure.departureDate)
|
|
69
|
+
: "";
|
|
70
|
+
}
|
|
71
|
+
case "options": {
|
|
72
|
+
const rooms = (draft.configure?.optionSelections ?? []).reduce((sum, s) => sum + (s.quantity ?? 0), 0);
|
|
73
|
+
if (rooms <= 0)
|
|
74
|
+
return "";
|
|
75
|
+
return `${rooms} ${rooms === 1
|
|
76
|
+
? messages.bookingJourney.sidePanel.roomSingular
|
|
77
|
+
: messages.bookingJourney.sidePanel.roomPlural}`;
|
|
49
78
|
}
|
|
50
79
|
case "billing": {
|
|
51
80
|
const c = draft.billing.contact;
|
|
@@ -94,8 +123,10 @@ function stepHeadline(step, draft, messages) {
|
|
|
94
123
|
function StepDetails({ step, draft, }) {
|
|
95
124
|
const messages = useBookingsUiMessagesOrDefault();
|
|
96
125
|
switch (step) {
|
|
97
|
-
case "
|
|
98
|
-
return _jsx(
|
|
126
|
+
case "departure":
|
|
127
|
+
return _jsx(DepartureDetails, { draft: draft });
|
|
128
|
+
case "options":
|
|
129
|
+
return _jsx(OptionsDetails, { draft: draft });
|
|
99
130
|
case "billing":
|
|
100
131
|
return _jsx(BillingDetails, { draft: draft });
|
|
101
132
|
case "travelers":
|
|
@@ -112,11 +143,17 @@ function StepDetails({ step, draft, }) {
|
|
|
112
143
|
return null;
|
|
113
144
|
}
|
|
114
145
|
}
|
|
115
|
-
function
|
|
146
|
+
function DepartureDetails({ draft, }) {
|
|
116
147
|
const messages = useBookingsUiMessagesOrDefault();
|
|
117
148
|
const cfg = draft.configure ?? {};
|
|
118
149
|
const range = cfg.dateRange;
|
|
119
|
-
return (_jsxs("dl", { className: "space-y-1 text-xs", children: [
|
|
150
|
+
return (_jsxs("dl", { className: "space-y-1 text-xs", children: [cfg.departureDate ? (_jsx(Row, { label: messages.bookingJourney.sidePanel.departure, value: formatConfigureDate(cfg.departureDate) })) : null, range?.checkIn ? (_jsx(Row, { label: messages.bookingJourney.sidePanel.checkIn, value: range.checkIn })) : null, range?.checkOut ? (_jsx(Row, { label: messages.bookingJourney.sidePanel.checkOut, value: range.checkOut })) : null] }));
|
|
151
|
+
}
|
|
152
|
+
function OptionsDetails({ draft, }) {
|
|
153
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
154
|
+
const cfg = draft.configure ?? {};
|
|
155
|
+
const selections = (cfg.optionSelections ?? []).filter((s) => (s.quantity ?? 0) > 0);
|
|
156
|
+
return (_jsxs("dl", { className: "space-y-1 text-xs", children: [selections.map((selection) => (_jsx(Row, { label: selection.optionUnitName ?? selection.optionName ?? selection.optionId, value: `× ${selection.quantity}` }, selection.optionUnitId ?? selection.optionId))), cfg.cabinCategoryId ? (_jsx(Row, { label: messages.bookingJourney.sidePanel.cabin, value: cfg.cabinCategoryId })) : null] }));
|
|
120
157
|
}
|
|
121
158
|
function BillingDetails({ draft, }) {
|
|
122
159
|
const messages = useBookingsUiMessagesOrDefault();
|
|
@@ -181,10 +218,6 @@ function stepLabel(step, messages) {
|
|
|
181
218
|
return messages.bookingJourney.steps.billingAndContact;
|
|
182
219
|
return messages.bookingJourney.steps[step];
|
|
183
220
|
}
|
|
184
|
-
function paxTotal(draft) {
|
|
185
|
-
const pax = draft.configure?.pax ?? {};
|
|
186
|
-
return (pax.adult ?? 0) + (pax.child ?? 0) + (pax.infant ?? 0);
|
|
187
|
-
}
|
|
188
221
|
function formatMoney(cents, currency) {
|
|
189
222
|
try {
|
|
190
223
|
return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(cents / 100);
|
|
@@ -193,3 +226,19 @@ function formatMoney(cents, currency) {
|
|
|
193
226
|
return `${(cents / 100).toFixed(2)} ${currency}`;
|
|
194
227
|
}
|
|
195
228
|
}
|
|
229
|
+
/** Tax rate is stored as a fraction (0.19 → "19%"). Trim trailing zeros. */
|
|
230
|
+
function formatTaxRate(rate) {
|
|
231
|
+
return `${Number((rate * 100).toFixed(2))}%`;
|
|
232
|
+
}
|
|
233
|
+
/** Format an ISO date (YYYY-MM-DD or full ISO) for the recap, falling back
|
|
234
|
+
* to the raw value when unparseable. Never surfaces a raw slot id. */
|
|
235
|
+
function formatConfigureDate(iso) {
|
|
236
|
+
const date = new Date(iso);
|
|
237
|
+
if (Number.isNaN(date.getTime()))
|
|
238
|
+
return iso;
|
|
239
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
240
|
+
year: "numeric",
|
|
241
|
+
month: "short",
|
|
242
|
+
day: "numeric",
|
|
243
|
+
}).format(date);
|
|
244
|
+
}
|
package/dist/journey/index.d.ts
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export { BookingJourney } from "./components/booking-journey.js";
|
|
12
12
|
export { type ContractAcceptance, ContractPreviewDialog, type ContractPreviewDialogProps, } from "./components/contract-preview-dialog.js";
|
|
13
|
-
export { AccommodationStep, AddonsStep, BillingStep,
|
|
13
|
+
export { AccommodationStep, AddonsStep, BillingStep, DepartureStep, OptionsStep, PaymentStep, ReviewStep, TravelersStep, } from "./components/journey-steps.js";
|
|
14
14
|
export { PriceSidePanel } from "./components/side-panel.js";
|
|
15
15
|
export { StepHeader } from "./components/step-header.js";
|
|
16
16
|
export { type Draft, emptyDraft, patchBilling, patchConfigure, patchPaxCount, setAccommodation, setAddons, setPayment, setTravelers, totalPax, } from "./lib/draft-state.js";
|
|
17
|
-
export { type BookingEntitySummary, type BookingJourneyCheckoutContext, type BookingJourneyProps, type BookingJourneyTransitionGuard, type BookingJourneyTransitionGuardContext, type BookingJourneyTransitionGuardResult, type ContractAcceptanceEvent, JOURNEY_STEP_ORDER, type JourneyHeaderState, type JourneyStep, type JourneySurface, type LeadContactPickerProps, type PaymentProviderCapabilities, type PaymentProviderStepRenderProps, type SidePanelState, type TravelerContactPickerProps, } from "./types.js";
|
|
17
|
+
export { type BillingExtrasContext, type BookingEntitySummary, type BookingJourneyCheckoutContext, type BookingJourneyProps, type BookingJourneyTransitionGuard, type BookingJourneyTransitionGuardContext, type BookingJourneyTransitionGuardResult, type ContractAcceptanceEvent, type DeparturePickerProps, JOURNEY_STEP_ORDER, type JourneyHeaderState, type JourneyOptionSelection, type JourneyStep, type JourneySurface, type LeadContactPickerProps, type PaymentProviderCapabilities, type PaymentProviderStepRenderProps, type SidePanelState, type TravelerContactPickerProps, type UnitsPickerProps, type VoucherPickerProps, } from "./types.js";
|
|
18
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/journey/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAA;AAChE,OAAO,EACL,KAAK,kBAAkB,EACvB,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAA;AAChD,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,WAAW,EACX,aAAa,EACb,WAAW,EACX,UAAU,EACV,aAAa,GACd,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AACxD,OAAO,EACL,KAAK,KAAK,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,UAAU,EACV,YAAY,EACZ,QAAQ,GACT,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EACxB,KAAK,6BAA6B,EAClC,KAAK,oCAAoC,EACzC,KAAK,mCAAmC,EACxC,KAAK,uBAAuB,EAC5B,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,KAAK,8BAA8B,EACnC,KAAK,cAAc,EACnB,KAAK,0BAA0B,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/journey/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAA;AAChE,OAAO,EACL,KAAK,kBAAkB,EACvB,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAA;AAChD,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,WAAW,EACX,aAAa,EACb,WAAW,EACX,WAAW,EACX,UAAU,EACV,aAAa,GACd,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AACxD,OAAO,EACL,KAAK,KAAK,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,UAAU,EACV,YAAY,EACZ,QAAQ,GACT,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EACxB,KAAK,6BAA6B,EAClC,KAAK,oCAAoC,EACzC,KAAK,mCAAmC,EACxC,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,KAAK,8BAA8B,EACnC,KAAK,cAAc,EACnB,KAAK,0BAA0B,EAC/B,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,YAAY,CAAA"}
|
package/dist/journey/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export { BookingJourney } from "./components/booking-journey.js";
|
|
12
12
|
export { ContractPreviewDialog, } from "./components/contract-preview-dialog.js";
|
|
13
|
-
export { AccommodationStep, AddonsStep, BillingStep,
|
|
13
|
+
export { AccommodationStep, AddonsStep, BillingStep, DepartureStep, OptionsStep, PaymentStep, ReviewStep, TravelersStep, } from "./components/journey-steps.js";
|
|
14
14
|
export { PriceSidePanel } from "./components/side-panel.js";
|
|
15
15
|
export { StepHeader } from "./components/step-header.js";
|
|
16
16
|
export { emptyDraft, patchBilling, patchConfigure, patchPaxCount, setAccommodation, setAddons, setPayment, setTravelers, totalPax, } from "./lib/draft-state.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PaxBandDependency } from "@voyantjs/catalog/booking-engine";
|
|
2
|
+
/**
|
|
3
|
+
* A pax-band occupancy rule that the picked traveler counts violate.
|
|
4
|
+
* Carries resolved band labels + the rule kind so the UI can render a
|
|
5
|
+
* localized message without re-resolving codes.
|
|
6
|
+
*/
|
|
7
|
+
export interface PaxBandDependencyViolation {
|
|
8
|
+
type: PaxBandDependency["type"];
|
|
9
|
+
dependentCode: string;
|
|
10
|
+
masterCode: string;
|
|
11
|
+
dependentLabel: string;
|
|
12
|
+
masterLabel: string;
|
|
13
|
+
/** The numeric limit for `limits_per_master` / `limits_sum`. */
|
|
14
|
+
limit?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Evaluate the product's cross-band occupancy rules against the picked
|
|
18
|
+
* pax counts. Returns one violation per broken rule (empty when valid).
|
|
19
|
+
*
|
|
20
|
+
* Rules only fire when at least one dependent is picked — an empty
|
|
21
|
+
* booking never violates "Child requires Adult".
|
|
22
|
+
*/
|
|
23
|
+
export declare function evaluatePaxBandDependencies(pax: Record<string, number> | undefined, dependencies: ReadonlyArray<PaxBandDependency> | undefined, bands: ReadonlyArray<{
|
|
24
|
+
code: string;
|
|
25
|
+
label: string;
|
|
26
|
+
}>): PaxBandDependencyViolation[];
|
|
27
|
+
//# sourceMappingURL=pax-band-dependencies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pax-band-dependencies.d.ts","sourceRoot":"","sources":["../../../src/journey/lib/pax-band-dependencies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAA;AAEzE;;;;GAIG;AACH,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAA;IAC/B,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EACvC,YAAY,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAAG,SAAS,EAC1D,KAAK,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GACpD,0BAA0B,EAAE,CAwC9B"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluate the product's cross-band occupancy rules against the picked
|
|
3
|
+
* pax counts. Returns one violation per broken rule (empty when valid).
|
|
4
|
+
*
|
|
5
|
+
* Rules only fire when at least one dependent is picked — an empty
|
|
6
|
+
* booking never violates "Child requires Adult".
|
|
7
|
+
*/
|
|
8
|
+
export function evaluatePaxBandDependencies(pax, dependencies, bands) {
|
|
9
|
+
if (!dependencies || dependencies.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
const counts = pax ?? {};
|
|
12
|
+
const labelByCode = new Map(bands.map((b) => [b.code, b.label]));
|
|
13
|
+
const label = (code) => labelByCode.get(code) ?? code;
|
|
14
|
+
const violations = [];
|
|
15
|
+
for (const dep of dependencies) {
|
|
16
|
+
const dependent = counts[dep.dependentCode] ?? 0;
|
|
17
|
+
const master = counts[dep.masterCode] ?? 0;
|
|
18
|
+
// No dependents picked → nothing to enforce.
|
|
19
|
+
if (dependent <= 0)
|
|
20
|
+
continue;
|
|
21
|
+
const base = {
|
|
22
|
+
type: dep.type,
|
|
23
|
+
dependentCode: dep.dependentCode,
|
|
24
|
+
masterCode: dep.masterCode,
|
|
25
|
+
dependentLabel: label(dep.dependentCode),
|
|
26
|
+
masterLabel: label(dep.masterCode),
|
|
27
|
+
};
|
|
28
|
+
switch (dep.type) {
|
|
29
|
+
case "requires":
|
|
30
|
+
if (master <= 0)
|
|
31
|
+
violations.push(base);
|
|
32
|
+
break;
|
|
33
|
+
case "excludes":
|
|
34
|
+
if (master > 0)
|
|
35
|
+
violations.push(base);
|
|
36
|
+
break;
|
|
37
|
+
case "limits_per_master":
|
|
38
|
+
if (dep.maxPerMaster != null && dependent > master * dep.maxPerMaster) {
|
|
39
|
+
violations.push({ ...base, limit: dep.maxPerMaster });
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
case "limits_sum":
|
|
43
|
+
if (dep.maxDependentSum != null && dependent > dep.maxDependentSum) {
|
|
44
|
+
violations.push({ ...base, limit: dep.maxDependentSum });
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return violations;
|
|
50
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert between the `PaymentScheduleSection` editor value
|
|
3
|
+
* (`{ mode, installments }`) and the booking draft's `paymentSchedules` rows.
|
|
4
|
+
*
|
|
5
|
+
* The row shape is derived from `Draft` so it always matches the engine
|
|
6
|
+
* contract. Paid-installment metadata (date / method / reference) is encoded
|
|
7
|
+
* as JSON in the row `notes` — the SAME format the owned create-sheet uses —
|
|
8
|
+
* so a schedule round-trips losslessly through the draft (and both flows write
|
|
9
|
+
* an identical wire shape).
|
|
10
|
+
*/
|
|
11
|
+
import { type PaymentScheduleValue } from "../../components/payment-schedule-section.js";
|
|
12
|
+
import type { Draft } from "./draft-state.js";
|
|
13
|
+
type PaymentScheduleRow = NonNullable<Draft["paymentSchedules"]>[number];
|
|
14
|
+
/** Editor value → draft rows. Returns `[]` when nothing is schedulable yet. */
|
|
15
|
+
export declare function paymentScheduleValueToRows(value: PaymentScheduleValue, currency: string, totalAmountCents: number | null): PaymentScheduleRow[];
|
|
16
|
+
/** Draft rows → editor value (re-init on step remount; preserves paid metadata). */
|
|
17
|
+
export declare function rowsToPaymentScheduleValue(rows: PaymentScheduleRow[] | undefined, departureDate: string | null): PaymentScheduleValue;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=payment-schedule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-schedule.d.ts","sourceRoot":"","sources":["../../../src/journey/lib/payment-schedule.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,8CAA8C,CAAA;AACrD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAE7C,KAAK,kBAAkB,GAAG,WAAW,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AAmBxE,+EAA+E;AAC/E,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,oBAAoB,EAC3B,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAC9B,kBAAkB,EAAE,CA8BtB;AAED,oFAAoF;AACpF,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,kBAAkB,EAAE,GAAG,SAAS,EACtC,aAAa,EAAE,MAAM,GAAG,IAAI,GAC3B,oBAAoB,CA8BtB"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert between the `PaymentScheduleSection` editor value
|
|
3
|
+
* (`{ mode, installments }`) and the booking draft's `paymentSchedules` rows.
|
|
4
|
+
*
|
|
5
|
+
* The row shape is derived from `Draft` so it always matches the engine
|
|
6
|
+
* contract. Paid-installment metadata (date / method / reference) is encoded
|
|
7
|
+
* as JSON in the row `notes` — the SAME format the owned create-sheet uses —
|
|
8
|
+
* so a schedule round-trips losslessly through the draft (and both flows write
|
|
9
|
+
* an identical wire shape).
|
|
10
|
+
*/
|
|
11
|
+
import { createInstallment, createPaymentScheduleValue, } from "../../components/payment-schedule-section.js";
|
|
12
|
+
function paidNotes(installment) {
|
|
13
|
+
if (!installment.alreadyPaid)
|
|
14
|
+
return null;
|
|
15
|
+
return JSON.stringify({
|
|
16
|
+
alreadyPaid: true,
|
|
17
|
+
paymentDate: installment.paymentDate,
|
|
18
|
+
paymentMethod: installment.paymentMethod,
|
|
19
|
+
paymentReference: installment.paymentReference.trim() || null,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Editor value → draft rows. Returns `[]` when nothing is schedulable yet. */
|
|
23
|
+
export function paymentScheduleValueToRows(value, currency, totalAmountCents) {
|
|
24
|
+
if (!currency)
|
|
25
|
+
return [];
|
|
26
|
+
if (value.mode === "full") {
|
|
27
|
+
const inst = value.installments[0];
|
|
28
|
+
if (!inst?.dueDate || totalAmountCents === null)
|
|
29
|
+
return [];
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
scheduleType: "balance",
|
|
33
|
+
status: inst.alreadyPaid ? "paid" : "due",
|
|
34
|
+
dueDate: inst.dueDate,
|
|
35
|
+
currency,
|
|
36
|
+
amountCents: totalAmountCents,
|
|
37
|
+
notes: paidNotes(inst),
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
// split — N installments; first defaults to `due`, the rest to `pending`.
|
|
42
|
+
const rows = [];
|
|
43
|
+
for (const [idx, inst] of value.installments.entries()) {
|
|
44
|
+
if (!inst.dueDate || inst.amountCents == null)
|
|
45
|
+
continue;
|
|
46
|
+
rows.push({
|
|
47
|
+
scheduleType: "installment",
|
|
48
|
+
status: inst.alreadyPaid ? "paid" : idx === 0 ? "due" : "pending",
|
|
49
|
+
dueDate: inst.dueDate,
|
|
50
|
+
currency,
|
|
51
|
+
amountCents: inst.amountCents,
|
|
52
|
+
notes: paidNotes(inst),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return rows;
|
|
56
|
+
}
|
|
57
|
+
/** Draft rows → editor value (re-init on step remount; preserves paid metadata). */
|
|
58
|
+
export function rowsToPaymentScheduleValue(rows, departureDate) {
|
|
59
|
+
if (!rows || rows.length === 0)
|
|
60
|
+
return createPaymentScheduleValue(departureDate);
|
|
61
|
+
const installments = rows.map((row) => {
|
|
62
|
+
let alreadyPaid = row.status === "paid";
|
|
63
|
+
let paymentDate = null;
|
|
64
|
+
let paymentMethod = "bank_transfer";
|
|
65
|
+
let paymentReference = "";
|
|
66
|
+
if (row.notes) {
|
|
67
|
+
try {
|
|
68
|
+
const meta = JSON.parse(row.notes);
|
|
69
|
+
if (meta.alreadyPaid) {
|
|
70
|
+
alreadyPaid = true;
|
|
71
|
+
paymentDate = meta.paymentDate ?? null;
|
|
72
|
+
paymentMethod = meta.paymentMethod ?? "bank_transfer";
|
|
73
|
+
paymentReference = meta.paymentReference ?? "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// `notes` wasn't our JSON payload (e.g. a free-text note) — ignore.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return createInstallment({
|
|
81
|
+
amountCents: row.amountCents,
|
|
82
|
+
dueDate: row.dueDate,
|
|
83
|
+
alreadyPaid,
|
|
84
|
+
paymentDate,
|
|
85
|
+
paymentMethod,
|
|
86
|
+
paymentReference,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
return { mode: rows.length > 1 ? "split" : "full", installments };
|
|
90
|
+
}
|
package/dist/journey/types.d.ts
CHANGED
|
@@ -8,27 +8,45 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { BookingDraftShape, BookingDraftV1, BookResponseV1, PricingBreakdownV1 } from "@voyantjs/catalog/booking-engine";
|
|
10
10
|
import type { ReactNode } from "react";
|
|
11
|
-
export type JourneyStep = "
|
|
11
|
+
export type JourneyStep = "departure" | "billing" | "travelers" | "options" | "accommodation" | "addons" | "payment" | "documents" | "review";
|
|
12
12
|
export declare const JOURNEY_STEP_ORDER: ReadonlyArray<JourneyStep>;
|
|
13
13
|
export interface JourneySurface {
|
|
14
14
|
/** Operator-side or storefront — drives default slot behavior. */
|
|
15
15
|
kind: "admin" | "public";
|
|
16
16
|
}
|
|
17
17
|
export interface LeadContactPickerProps {
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
/** Current buyer type — the picker should search PEOPLE for B2C and
|
|
19
|
+
* ORGANIZATIONS for B2B. */
|
|
20
|
+
buyerType: "B2C" | "B2B";
|
|
21
|
+
/** Apply a picked lead to the draft's billing fields. A PARTIAL — only
|
|
22
|
+
* the provided fields are merged, so separate CRM lookups (person/org
|
|
23
|
+
* record, then its address) can each fill their slice without clobbering
|
|
24
|
+
* the others. B2C fills the person; B2B fills companyName/taxId; both
|
|
25
|
+
* fill the billing address from the CRM record. */
|
|
22
26
|
apply: (contact: {
|
|
23
|
-
firstName
|
|
24
|
-
lastName
|
|
27
|
+
firstName?: string;
|
|
28
|
+
lastName?: string;
|
|
25
29
|
email?: string;
|
|
26
30
|
phone?: string;
|
|
27
31
|
personId?: string;
|
|
32
|
+
organizationId?: string;
|
|
33
|
+
companyName?: string;
|
|
34
|
+
taxId?: string;
|
|
35
|
+
address?: {
|
|
36
|
+
line1?: string;
|
|
37
|
+
line2?: string;
|
|
38
|
+
city?: string;
|
|
39
|
+
postal?: string;
|
|
40
|
+
country?: string;
|
|
41
|
+
};
|
|
28
42
|
}) => void;
|
|
29
43
|
}
|
|
30
44
|
export interface TravelerContactPickerProps {
|
|
31
45
|
rowIndex: number;
|
|
46
|
+
/** The CRM person currently linked to this traveler row, if any. The
|
|
47
|
+
* picker should reflect it in its combobox — so e.g. "Copy from billing"
|
|
48
|
+
* (which links the billing person) shows that person as selected. */
|
|
49
|
+
selectedPersonId?: string;
|
|
32
50
|
/** Apply a picked contact to the traveler at `rowIndex`. */
|
|
33
51
|
apply: (contact: {
|
|
34
52
|
firstName: string;
|
|
@@ -38,6 +56,94 @@ export interface TravelerContactPickerProps {
|
|
|
38
56
|
personId?: string;
|
|
39
57
|
}) => void;
|
|
40
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Context handed to `renderBillingExtras` — the picked lead + the departure —
|
|
61
|
+
* so a template can run lead-aware checks (e.g. "this customer already booked
|
|
62
|
+
* this departure") next to the billing block.
|
|
63
|
+
*/
|
|
64
|
+
export interface BillingExtrasContext {
|
|
65
|
+
buyerType: "B2C" | "B2B";
|
|
66
|
+
personId?: string;
|
|
67
|
+
organizationId?: string;
|
|
68
|
+
productId: string;
|
|
69
|
+
departureSlotId?: string;
|
|
70
|
+
departureDate?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Props for the injectable voucher picker. The operator surface wires an async
|
|
74
|
+
* combobox (search the admin vouchers list) so staff pick a voucher without
|
|
75
|
+
* knowing the exact code; the storefront keeps the customer code-entry form.
|
|
76
|
+
*/
|
|
77
|
+
export interface VoucherPickerProps {
|
|
78
|
+
/** Currently-linked voucher redemption on the draft, if any. */
|
|
79
|
+
value: {
|
|
80
|
+
voucherId?: string;
|
|
81
|
+
amountCents?: number;
|
|
82
|
+
};
|
|
83
|
+
/** Apply a picked voucher's full remaining balance — or clear with `null`. */
|
|
84
|
+
onApply: (picked: {
|
|
85
|
+
voucherId: string;
|
|
86
|
+
amountCents: number;
|
|
87
|
+
} | null) => void;
|
|
88
|
+
/** Booking currency + payable total, to display/cap the redemption. */
|
|
89
|
+
currency?: string;
|
|
90
|
+
amountCents?: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Props for the injectable departure picker rendered in the Configure
|
|
94
|
+
* step for a `"departure"` sub-step. The template wires this with a
|
|
95
|
+
* scheduled-departures source (e.g. operator availability) so the
|
|
96
|
+
* operator picks a real departure rather than typing a free date.
|
|
97
|
+
*
|
|
98
|
+
* The picker owns its own data-loading and should fall back to a free
|
|
99
|
+
* date when the product has no scheduled departures — the journey just
|
|
100
|
+
* stores whatever it reports via `onChange`.
|
|
101
|
+
*/
|
|
102
|
+
export interface DeparturePickerProps {
|
|
103
|
+
/** The owned product whose departures to load. */
|
|
104
|
+
productId: string;
|
|
105
|
+
/** Selected product option, used to filter departures (null = any). */
|
|
106
|
+
optionId: string | null;
|
|
107
|
+
/** Currently-picked scheduled departure id, or null. */
|
|
108
|
+
slotId: string | null;
|
|
109
|
+
/** Currently-entered departure date (free-date fallback), or null. */
|
|
110
|
+
departureDate: string | null;
|
|
111
|
+
/** Currently-entered departure time (free-date fallback), or null. */
|
|
112
|
+
departureTime: string | null;
|
|
113
|
+
/** Report a change — any omitted field is left unchanged on the draft. */
|
|
114
|
+
onChange: (next: {
|
|
115
|
+
slotId?: string | null;
|
|
116
|
+
departureDate?: string | null;
|
|
117
|
+
departureTime?: string | null;
|
|
118
|
+
}) => void;
|
|
119
|
+
}
|
|
120
|
+
/** A picked inventory unit (room) selection on the draft's configure. */
|
|
121
|
+
export interface JourneyOptionSelection {
|
|
122
|
+
optionId: string;
|
|
123
|
+
optionName?: string;
|
|
124
|
+
optionUnitId?: string;
|
|
125
|
+
optionUnitName?: string;
|
|
126
|
+
quantity: number;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Props for the injectable units (rooms) picker rendered in the Configure
|
|
130
|
+
* step for an `"option-units"` sub-step. The template wires it to an
|
|
131
|
+
* inventory source (operator availability) so the operator picks room
|
|
132
|
+
* quantities for the chosen option + departure; the journey stores the
|
|
133
|
+
* result on `configure.optionSelections`.
|
|
134
|
+
*/
|
|
135
|
+
export interface UnitsPickerProps {
|
|
136
|
+
/** The owned product whose units to load. */
|
|
137
|
+
productId: string;
|
|
138
|
+
/** Currently-selected product option (drives which units show), or null. */
|
|
139
|
+
optionId: string | null;
|
|
140
|
+
/** Currently-picked departure slot (drives per-slot availability), or null. */
|
|
141
|
+
slotId: string | null;
|
|
142
|
+
/** Current unit selections on the draft. */
|
|
143
|
+
selections: ReadonlyArray<JourneyOptionSelection>;
|
|
144
|
+
/** Report the new full set of unit selections. */
|
|
145
|
+
onChange: (selections: JourneyOptionSelection[]) => void;
|
|
146
|
+
}
|
|
41
147
|
/**
|
|
42
148
|
* Capabilities supplied by the template — checkout-ui's PaymentStep
|
|
43
149
|
* consumes these to render the right provider widget. Each flag is
|
|
@@ -102,6 +208,16 @@ export interface BookingJourneyProps {
|
|
|
102
208
|
sourceRef?: string;
|
|
103
209
|
/** Surface — drives audience defaults and slot wiring. */
|
|
104
210
|
surface?: "admin" | "public";
|
|
211
|
+
/**
|
|
212
|
+
* Layout of the booking flow.
|
|
213
|
+
* - `"wizard"` — one step at a time with Back/Next (the guided
|
|
214
|
+
* storefront flow).
|
|
215
|
+
* - `"stacked"` — every section rendered as a block on a single
|
|
216
|
+
* scrollable page, nothing hidden (the operator flow — an admin
|
|
217
|
+
* can see travelers while editing options, and jump around freely).
|
|
218
|
+
* Defaults to `"stacked"` on the admin surface and `"wizard"` on
|
|
219
|
+
* public, keeping the two processes deliberately separate. */
|
|
220
|
+
layout?: "wizard" | "stacked";
|
|
105
221
|
/** Stable draft id — caller persists in URL or session storage so
|
|
106
222
|
* the journey survives page refresh. */
|
|
107
223
|
draftId: string;
|
|
@@ -134,6 +250,21 @@ export interface BookingJourneyProps {
|
|
|
134
250
|
/** Operator: pulls from CRM. Storefront: bare inline form. */
|
|
135
251
|
renderLeadContactPicker?: (props: LeadContactPickerProps) => ReactNode;
|
|
136
252
|
renderTravelerContactPicker?: (props: TravelerContactPickerProps) => ReactNode;
|
|
253
|
+
/** Operator-only voucher picker (async search). When omitted, the voucher
|
|
254
|
+
* control falls back to the customer code-entry form. */
|
|
255
|
+
renderVoucherPicker?: (props: VoucherPickerProps) => ReactNode;
|
|
256
|
+
/**
|
|
257
|
+
* Renders the Configure step's `"departure"` sub-step. Operator
|
|
258
|
+
* surfaces wire this to a scheduled-departure picker (availability);
|
|
259
|
+
* when omitted, the journey renders a free date/time fallback.
|
|
260
|
+
*/
|
|
261
|
+
renderDeparturePicker?: (props: DeparturePickerProps) => ReactNode;
|
|
262
|
+
/**
|
|
263
|
+
* Renders the Configure step's `"option-units"` sub-step (room/unit
|
|
264
|
+
* quantity selection). Operator surfaces wire this to an inventory
|
|
265
|
+
* units picker; when omitted, the sub-step renders nothing.
|
|
266
|
+
*/
|
|
267
|
+
renderUnitsPicker?: (props: UnitsPickerProps) => ReactNode;
|
|
137
268
|
/** Hook for the actual payment-provider widget — checkout-ui's
|
|
138
269
|
* PaymentStep is the canonical implementation. When omitted, the
|
|
139
270
|
* shell renders a "Hold only — no card collected" stub. */
|
|
@@ -149,7 +280,9 @@ export interface BookingJourneyProps {
|
|
|
149
280
|
* template wants to inject a custom block (e.g. coupon code
|
|
150
281
|
* banner, marketing opt-in). */
|
|
151
282
|
renderConfigureExtras?: () => ReactNode;
|
|
152
|
-
|
|
283
|
+
/** Billing extras — receives the picked lead + departure so a template can,
|
|
284
|
+
* e.g., warn that this customer already has a booking on this departure. */
|
|
285
|
+
renderBillingExtras?: (ctx: BillingExtrasContext) => ReactNode;
|
|
153
286
|
renderReviewExtras?: () => ReactNode;
|
|
154
287
|
/** Fired on successful commit — typically a navigation. */
|
|
155
288
|
onCommitted?: (result: BookResponseV1) => void;
|