@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.
Files changed (67) hide show
  1. package/dist/components/option-units-stepper-section.d.ts +9 -1
  2. package/dist/components/option-units-stepper-section.d.ts.map +1 -1
  3. package/dist/components/option-units-stepper-section.js +10 -2
  4. package/dist/components/person-picker-section.d.ts +7 -1
  5. package/dist/components/person-picker-section.d.ts.map +1 -1
  6. package/dist/components/person-picker-section.js +2 -2
  7. package/dist/i18n/en.d.ts +37 -1
  8. package/dist/i18n/en.d.ts.map +1 -1
  9. package/dist/i18n/en.js +40 -4
  10. package/dist/i18n/messages.d.ts +37 -1
  11. package/dist/i18n/messages.d.ts.map +1 -1
  12. package/dist/i18n/provider.d.ts +74 -2
  13. package/dist/i18n/provider.d.ts.map +1 -1
  14. package/dist/i18n/ro.d.ts +37 -1
  15. package/dist/i18n/ro.d.ts.map +1 -1
  16. package/dist/i18n/ro.js +39 -3
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -1
  20. package/dist/journey/components/booking-journey.d.ts.map +1 -1
  21. package/dist/journey/components/booking-journey.js +270 -27
  22. package/dist/journey/components/journey-steps/accommodation-step.d.ts +3 -0
  23. package/dist/journey/components/journey-steps/accommodation-step.d.ts.map +1 -0
  24. package/dist/journey/components/journey-steps/accommodation-step.js +71 -0
  25. package/dist/journey/components/journey-steps/addons-step.d.ts +3 -0
  26. package/dist/journey/components/journey-steps/addons-step.d.ts.map +1 -0
  27. package/dist/journey/components/journey-steps/addons-step.js +40 -0
  28. package/dist/journey/components/journey-steps/billing-step.d.ts +8 -0
  29. package/dist/journey/components/journey-steps/billing-step.d.ts.map +1 -0
  30. package/dist/journey/components/journey-steps/billing-step.js +78 -0
  31. package/dist/journey/components/journey-steps/configure-steps.d.ts +28 -0
  32. package/dist/journey/components/journey-steps/configure-steps.d.ts.map +1 -0
  33. package/dist/journey/components/journey-steps/configure-steps.js +231 -0
  34. package/dist/journey/components/journey-steps/documents-step.d.ts +11 -0
  35. package/dist/journey/components/journey-steps/documents-step.d.ts.map +1 -0
  36. package/dist/journey/components/journey-steps/documents-step.js +36 -0
  37. package/dist/journey/components/journey-steps/payment-step.d.ts +29 -0
  38. package/dist/journey/components/journey-steps/payment-step.d.ts.map +1 -0
  39. package/dist/journey/components/journey-steps/payment-step.js +224 -0
  40. package/dist/journey/components/journey-steps/review-step.d.ts +27 -0
  41. package/dist/journey/components/journey-steps/review-step.d.ts.map +1 -0
  42. package/dist/journey/components/journey-steps/review-step.js +18 -0
  43. package/dist/journey/components/journey-steps/shared.d.ts +75 -0
  44. package/dist/journey/components/journey-steps/shared.d.ts.map +1 -0
  45. package/dist/journey/components/journey-steps/shared.js +108 -0
  46. package/dist/journey/components/journey-steps/travelers-step.d.ts +7 -0
  47. package/dist/journey/components/journey-steps/travelers-step.d.ts.map +1 -0
  48. package/dist/journey/components/journey-steps/travelers-step.js +201 -0
  49. package/dist/journey/components/journey-steps.d.ts +13 -39
  50. package/dist/journey/components/journey-steps.d.ts.map +1 -1
  51. package/dist/journey/components/journey-steps.js +16 -613
  52. package/dist/journey/components/side-panel.d.ts +7 -2
  53. package/dist/journey/components/side-panel.d.ts.map +1 -1
  54. package/dist/journey/components/side-panel.js +73 -24
  55. package/dist/journey/index.d.ts +2 -2
  56. package/dist/journey/index.d.ts.map +1 -1
  57. package/dist/journey/index.js +1 -1
  58. package/dist/journey/lib/pax-band-dependencies.d.ts +27 -0
  59. package/dist/journey/lib/pax-band-dependencies.d.ts.map +1 -0
  60. package/dist/journey/lib/pax-band-dependencies.js +50 -0
  61. package/dist/journey/lib/payment-schedule.d.ts +19 -0
  62. package/dist/journey/lib/payment-schedule.d.ts.map +1 -0
  63. package/dist/journey/lib/payment-schedule.js +90 -0
  64. package/dist/journey/types.d.ts +141 -8
  65. package/dist/journey/types.d.ts.map +1 -1
  66. package/dist/journey/types.js +3 -1
  67. package/package.json +32 -32
@@ -1,4 +1,5 @@
1
- import type { SidePanelState } from "../types.js";
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":"AAWA,OAAO,KAAK,EAAqC,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,SAAS,GACV,EAAE,cAAc,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,CAuD9D"}
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
- return (_jsxs(Card, { className: className, children: [entitySummary ? _jsx(EntityHeader, { summary: entitySummary }) : null, _jsxs(CardContent, { className: "space-y-4", children: [steps && steps.length > 0 && currentStep && draft ? (_jsx(StepRecap, { steps: steps, currentStep: currentStep, draft: draft })) : null, isQuoting && !pricing ? (_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-24" }), _jsx(Skeleton, { className: "h-4 w-32" }), _jsx(Skeleton, { className: "h-4 w-20" })] })) : null, invalidReason ? _jsx("p", { className: "text-destructive text-sm", children: invalidReason }) : null, 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: [_jsx("span", { children: tax.label }), _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] })] }));
17
- }
18
- function EntityHeader({ summary }) {
19
- const messages = useBookingsUiMessagesOrDefault();
20
- return (_jsxs("div", { className: "overflow-hidden rounded-t-xl", children: [summary.heroImageUrl ? (_jsx("img", { src: summary.heroImageUrl, alt: summary.name, className: "aspect-video w-full object-cover" })) : null, _jsxs("div", { className: "space-y-1 px-6 pt-4 pb-2", children: [_jsx("div", { className: "text-muted-foreground text-xs uppercase tracking-wide", children: messages.bookingJourney.sidePanel.youAreBooking }), _jsx("div", { className: "font-semibold leading-tight", children: summary.name }), 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] })] }));
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: steps.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));
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 "configure": {
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
- const when = range?.checkIn && range?.checkOut ? `${range.checkIn} → ${range.checkOut}` : slot;
45
- const guestLabel = total === 1
46
- ? messages.bookingJourney.sidePanel.guestSingular
47
- : messages.bookingJourney.sidePanel.guestPlural;
48
- return when ? `${total} ${guestLabel} · ${when}` : `${total} ${guestLabel}`;
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 "configure":
98
- return _jsx(ConfigureDetails, { draft: draft });
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 ConfigureDetails({ draft, }) {
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: [_jsx(Row, { label: messages.bookingJourney.sidePanel.adults, value: String(cfg.pax?.adult ?? 0) }), (cfg.pax?.child ?? 0) > 0 ? (_jsx(Row, { label: messages.bookingJourney.sidePanel.children, value: String(cfg.pax.child) })) : null, (cfg.pax?.infant ?? 0) > 0 ? (_jsx(Row, { label: messages.bookingJourney.sidePanel.infants, value: String(cfg.pax.infant) })) : null, cfg.departureSlotId ? (_jsx(Row, { label: messages.bookingJourney.sidePanel.departure, value: cfg.departureSlotId })) : null, cfg.departureDate ? (_jsx(Row, { label: messages.bookingJourney.sidePanel.date, value: 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, cfg.cabinCategoryId ? (_jsx(Row, { label: messages.bookingJourney.sidePanel.cabin, value: cfg.cabinCategoryId })) : null] }));
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
+ }
@@ -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, ConfigureStep, PaymentStep, ReviewStep, TravelersStep, } from "./components/journey-steps.js";
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,GAChC,MAAM,YAAY,CAAA"}
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"}
@@ -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, ConfigureStep, PaymentStep, ReviewStep, TravelersStep, } from "./components/journey-steps.js";
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
+ }
@@ -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 = "configure" | "billing" | "travelers" | "accommodation" | "addons" | "payment" | "review";
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
- /** Apply a picked contact to the draft's billing fields. Email is
19
- * optional because CRM-backed people may not have one stored —
20
- * the billing form will surface it as empty for the operator to
21
- * fill in. */
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: string;
24
- lastName: string;
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
- renderBillingExtras?: () => ReactNode;
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;