@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,617 +1,20 @@
1
- "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Button } from "@voyantjs/ui/components/button";
4
- import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card";
5
- import { CountryCombobox } from "@voyantjs/ui/components/country-combobox";
6
- import { DatePicker } from "@voyantjs/ui/components/date-picker";
7
- import { Input } from "@voyantjs/ui/components/input";
8
- import { Label } from "@voyantjs/ui/components/label";
9
- import { PhoneInput } from "@voyantjs/ui/components/phone-input";
10
- import { RadioGroup, RadioGroupItem } from "@voyantjs/ui/components/radio-group";
11
- import { Textarea } from "@voyantjs/ui/components/textarea";
12
- import { Loader2 } from "lucide-react";
13
- import { formatMessage, useBookingsUiMessagesOrDefault } from "../../i18n/index.js";
14
- import { canCopyBillingContactToTraveler, patchBilling, patchConfigure, patchPaxCount, setAccommodation, setAddons, setPayment, setTravelers, totalPax, } from "../lib/draft-state.js";
15
- // ─────────────────────────────────────────────────────────────────
16
- // Configure
17
- // ─────────────────────────────────────────────────────────────────
18
- export function ConfigureStep({ draft, setDraft, shape, }) {
19
- const messages = useBookingsUiMessagesOrDefault();
20
- return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingJourney.steps.configure }) }), _jsxs(CardContent, { className: "space-y-6", children: [_jsx(PaxBands, { draft: draft, setDraft: setDraft, shape: shape }), _jsx(DepartureFields, { draft: draft, setDraft: setDraft, shape: shape })] })] }));
21
- }
22
- function PaxBands({ draft, setDraft, shape }) {
23
- const messages = useBookingsUiMessagesOrDefault();
24
- return (_jsxs("div", { className: "space-y-3", children: [_jsx(Label, { children: messages.bookingJourney.configure.travelers }), _jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: shape.paxBands.map((band) => {
25
- const value = draft.configure.pax?.[band.code] ?? 0;
26
- return (_jsxs("div", { className: "flex items-center justify-between rounded border p-3", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium", children: band.label }), band.minAge != null || band.maxAge != null ? (_jsx("div", { className: "text-muted-foreground text-xs", children: ageHint(band.minAge, band.maxAge, messages) })) : null] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", type: "button", disabled: value <= band.minCount, onClick: () => setDraft(patchPaxCount(draft, band.code, value - 1)), children: "\u2212" }), _jsx("span", { className: "min-w-6 text-center", children: value }), _jsx(Button, { variant: "outline", size: "sm", type: "button", disabled: value >= band.maxCount, onClick: () => setDraft(patchPaxCount(draft, band.code, value + 1)), children: "+" })] })] }, band.code));
27
- }) }), _jsx(PaxValidation, { draft: draft, shape: shape })] }));
28
- }
29
- function PaxValidation({ draft, shape, }) {
30
- const messages = useBookingsUiMessagesOrDefault();
31
- const total = totalPax(draft);
32
- const { min, max } = shape.paxBandsAllowedTotal;
33
- if (total < min) {
34
- return (_jsx("p", { className: "text-sm text-amber-600", children: formatMessage(messages.bookingJourney.validation.addAtLeastTravelers, {
35
- count: min,
36
- plural: min === 1 ? "" : "s",
37
- }) }));
38
- }
39
- if (total > max) {
40
- return (_jsx("p", { className: "text-sm text-destructive", children: formatMessage(messages.bookingJourney.validation.maxTravelersPerBooking, {
41
- count: max,
42
- }) }));
43
- }
44
- return null;
45
- }
46
- function DepartureFields({ draft, setDraft, shape }) {
47
- const subSteps = shape.configureSubSteps ?? [];
48
- // Render every sub-step kind the descriptor declares. Cruise
49
- // (cabin-category, cabin-number) lands here in Phase F.
50
- if (subSteps.length === 0) {
51
- return _jsx(DepartureBasic, { draft: draft, setDraft: setDraft });
52
- }
53
- return (_jsx("div", { className: "space-y-4", children: subSteps.map((sub) => {
54
- // Sub-step kinds are unique per descriptor — kind serves as
55
- // a stable key.
56
- if (sub.kind === "departure") {
57
- return _jsx(DepartureBasic, { draft: draft, setDraft: setDraft }, "departure");
58
- }
59
- if (sub.kind === "product-option") {
60
- return (_jsx(ProductOptionFields, { draft: draft, setDraft: setDraft, options: sub.options }, "product-option"));
61
- }
62
- if (sub.kind === "date-range") {
63
- return (_jsx(DateRangeFields, { draft: draft, setDraft: setDraft, minNights: sub.minNights, maxNights: sub.maxNights }, "date-range"));
64
- }
65
- if (sub.kind === "cabin-category") {
66
- return (_jsx(CabinCategoryFields, { draft: draft, setDraft: setDraft, categories: sub.categories }, "cabin-category"));
67
- }
68
- if (sub.kind === "cabin-number") {
69
- return (_jsx(CabinNumberFields, { draft: draft, setDraft: setDraft, perCategory: sub.perCategory }, "cabin-number"));
70
- }
71
- if (sub.kind === "air-arrangement") {
72
- return _jsx(AirArrangementFields, { draft: draft, setDraft: setDraft }, "air-arrangement");
73
- }
74
- // "occupancy" — already rendered as PaxBands above; no sub-row.
75
- return null;
76
- }) }));
77
- }
78
- function ProductOptionFields({ draft, setDraft, options, }) {
79
- const messages = useBookingsUiMessagesOrDefault();
80
- const selectedId = draft.configure.variantId;
81
- if (options.length === 0)
82
- return null;
83
- return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: messages.bookingJourney.configure.option }), _jsx("div", { className: "grid grid-cols-1 gap-2", children: options.map((option) => {
84
- const selected = option.id === selectedId;
85
- return (_jsxs("button", { type: "button", onClick: () => setDraft(patchConfigure(draft, { variantId: option.id })), className: `w-full rounded border p-3 text-left text-sm ${selected ? "border-primary ring-2 ring-primary" : ""}`, children: [_jsx("span", { className: "font-medium", children: option.name }), option.code ? (_jsx("span", { className: "ml-2 text-muted-foreground text-xs uppercase", children: option.code })) : null, option.description ? (_jsx("span", { className: "mt-1 block text-muted-foreground text-xs", children: option.description })) : null] }, option.id));
86
- }) })] }));
87
- }
88
- function DepartureBasic({ draft, setDraft, }) {
89
- const messages = useBookingsUiMessagesOrDefault();
90
- return (_jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [_jsx(DateField, { id: "bj-departure-date", label: messages.bookingJourney.configure.departureDate, value: draft.configure.departureDate ?? "", onChange: (v) => setDraft(patchConfigure(draft, { departureDate: v })), range: "future" }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "bj-departure-time", children: messages.bookingJourney.configure.timeOptional }), _jsx(Input, { id: "bj-departure-time", type: "time", value: draft.configure.departureTime ?? "", onChange: (e) => setDraft(patchConfigure(draft, { departureTime: e.target.value })) })] })] }));
91
- }
92
- function DateRangeFields({ draft, setDraft, minNights, maxNights, }) {
93
- const messages = useBookingsUiMessagesOrDefault();
94
- const range = draft.configure.dateRange;
95
- return (_jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [_jsx(DateField, { id: "bj-checkin", label: messages.bookingJourney.configure.checkIn, value: range?.checkIn ?? "", onChange: (v) => setDraft(patchConfigure(draft, {
96
- dateRange: { checkIn: v, checkOut: range?.checkOut ?? "" },
97
- })), range: "future" }), _jsx(DateField, { id: "bj-checkout", label: formatMessage(messages.bookingJourney.configure.checkOutWithNights, {
98
- minNights,
99
- maxNights,
100
- }), value: range?.checkOut ?? "", onChange: (v) => setDraft(patchConfigure(draft, {
101
- dateRange: { checkIn: range?.checkIn ?? "", checkOut: v },
102
- })), range: "future" })] }));
103
- }
104
- function CabinCategoryFields({ draft, setDraft, categories, }) {
105
- const messages = useBookingsUiMessagesOrDefault();
106
- return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: messages.bookingJourney.configure.cabinCategory }), _jsx("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-2", children: categories.map((cat) => {
107
- const selected = draft.configure.cabinCategoryId === cat.id;
108
- return (_jsxs("button", { type: "button", className: `rounded border p-3 text-left ${selected ? "border-primary ring-2 ring-primary" : ""}`, onClick: () => setDraft(patchConfigure(draft, { cabinCategoryId: cat.id, cabinNumberId: undefined })), children: [_jsx("div", { className: "font-medium", children: cat.name }), cat.description ? (_jsx("div", { className: "text-muted-foreground text-xs", children: cat.description })) : null] }, cat.id));
109
- }) })] }));
110
- }
111
- function AirArrangementFields({ draft, setDraft, }) {
112
- const messages = useBookingsUiMessagesOrDefault();
113
- const current = draft.configure.airArrangement;
114
- const options = [
115
- {
116
- value: "cruise_line",
117
- label: messages.bookingJourney.configure.airOptions.cruise_line.label,
118
- description: messages.bookingJourney.configure.airOptions.cruise_line.description,
119
- },
120
- {
121
- value: "independent",
122
- label: messages.bookingJourney.configure.airOptions.independent.label,
123
- description: messages.bookingJourney.configure.airOptions.independent.description,
124
- },
125
- {
126
- value: "none",
127
- label: messages.bookingJourney.configure.airOptions.none.label,
128
- description: messages.bookingJourney.configure.airOptions.none.description,
129
- },
130
- ];
131
- return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: messages.bookingJourney.configure.airArrangements }), _jsx("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-3", children: options.map((opt) => {
132
- const selected = current === opt.value;
133
- return (_jsxs("button", { type: "button", className: `rounded border p-3 text-left text-sm ${selected ? "border-primary ring-2 ring-primary" : ""}`, onClick: () => setDraft(patchConfigure(draft, { airArrangement: opt.value })), children: [_jsx("div", { className: "font-medium", children: opt.label }), _jsx("div", { className: "text-muted-foreground text-xs", children: opt.description })] }, opt.value));
134
- }) })] }));
135
- }
136
- function CabinNumberFields({ draft, setDraft, perCategory, }) {
137
- const messages = useBookingsUiMessagesOrDefault();
138
- const catId = draft.configure.cabinCategoryId;
139
- if (!catId)
140
- return null;
141
- const cabins = perCategory[catId] ?? [];
142
- return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: messages.bookingJourney.configure.cabinNumber }), _jsx("div", { className: "grid grid-cols-2 gap-2 sm:grid-cols-4", children: cabins.map((cabin) => {
143
- const selected = draft.configure.cabinNumberId === cabin.id;
144
- return (_jsx("button", { type: "button", className: `rounded border p-2 text-sm ${selected ? "border-primary ring-2 ring-primary" : ""}`, onClick: () => setDraft(patchConfigure(draft, { cabinNumberId: cabin.id })), children: cabin.label }, cabin.id));
145
- }) })] }));
146
- }
147
- // ─────────────────────────────────────────────────────────────────
148
- // Billing
149
- // ─────────────────────────────────────────────────────────────────
150
- export function BillingStep({ draft, setDraft, renderLeadContactPicker, renderExtras, }) {
151
- const messages = useBookingsUiMessagesOrDefault();
152
- const billing = draft.billing;
153
- const apply = (contact) => {
154
- setDraft(patchBilling(draft, {
155
- contact: {
156
- firstName: contact.firstName,
157
- lastName: contact.lastName,
158
- email: contact.email ?? "",
159
- phone: contact.phone,
160
- },
161
- }));
162
- };
163
- return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingJourney.billing.title }) }), _jsxs(CardContent, { className: "space-y-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: messages.bookingJourney.billing.buyerType }), _jsxs(RadioGroup, { value: billing.buyerType, onValueChange: (v) => setDraft(patchBilling(draft, { buyerType: v })), className: "flex gap-4", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx(RadioGroupItem, { value: "B2C" }), " ", messages.bookingJourney.billing.individual] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx(RadioGroupItem, { value: "B2B" }), " ", messages.bookingJourney.billing.company] })] })] }), renderLeadContactPicker ? _jsx("div", { children: renderLeadContactPicker({ apply }) }) : null, _jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [_jsx(Field, { id: "bj-billing-firstName", label: messages.bookingJourney.billing.firstName, value: billing.contact.firstName, onChange: (v) => setDraft(patchBilling(draft, { contact: { ...billing.contact, firstName: v } })) }), _jsx(Field, { id: "bj-billing-lastName", label: messages.bookingJourney.billing.lastName, value: billing.contact.lastName, onChange: (v) => setDraft(patchBilling(draft, { contact: { ...billing.contact, lastName: v } })) }), _jsx(Field, { id: "bj-billing-email", label: messages.bookingJourney.billing.email, type: "email", value: billing.contact.email, onChange: (v) => setDraft(patchBilling(draft, { contact: { ...billing.contact, email: v } })) }), _jsx(PhoneField, { id: "bj-billing-phone", label: messages.bookingJourney.billing.phone, value: billing.contact.phone ?? "", onChange: (v) => setDraft(patchBilling(draft, { contact: { ...billing.contact, phone: v } })) })] }), _jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [_jsx(Field, { id: "bj-billing-line1", label: messages.bookingJourney.billing.addressLine1, value: billing.address.line1 ?? "", onChange: (v) => setDraft(patchBilling(draft, { address: { ...billing.address, line1: v } })) }), _jsx(Field, { id: "bj-billing-line2", label: messages.bookingJourney.billing.addressLine2Optional, value: billing.address.line2 ?? "", onChange: (v) => setDraft(patchBilling(draft, { address: { ...billing.address, line2: v } })) }), _jsx(Field, { id: "bj-billing-city", label: messages.bookingJourney.billing.city, value: billing.address.city ?? "", onChange: (v) => setDraft(patchBilling(draft, { address: { ...billing.address, city: v } })) }), _jsx(Field, { id: "bj-billing-postal", label: messages.bookingJourney.billing.postalCode, value: billing.address.postal ?? "", onChange: (v) => setDraft(patchBilling(draft, { address: { ...billing.address, postal: v } })) }), _jsxs("div", { className: "space-y-1 sm:col-span-2", children: [_jsx(Label, { htmlFor: "bj-billing-country", children: messages.bookingJourney.billing.country }), _jsx(CountryCombobox, { value: billing.address.country ?? null, onChange: (code) => setDraft(patchBilling(draft, {
164
- address: { ...billing.address, country: code ?? "" },
165
- })) })] })] }), billing.buyerType === "B2B" ? (_jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [_jsx(Field, { id: "bj-billing-companyName", label: messages.bookingJourney.billing.companyName, value: billing.company?.name ?? "", onChange: (v) => setDraft(patchBilling(draft, {
166
- company: { ...(billing.company ?? { name: "" }), name: v },
167
- })) }), _jsx(Field, { id: "bj-billing-vatId", label: messages.bookingJourney.billing.vatId, value: billing.company?.vatId ?? "", onChange: (v) => setDraft(patchBilling(draft, {
168
- company: { ...(billing.company ?? { name: "" }), vatId: v },
169
- })) })] })) : null, renderExtras ? _jsx("div", { children: renderExtras() }) : null] })] }));
170
- }
171
- // ─────────────────────────────────────────────────────────────────
172
- // Travelers
173
- // ─────────────────────────────────────────────────────────────────
174
- export function TravelersStep({ draft, setDraft, shape, renderTravelerContactPicker, }) {
175
- const messages = useBookingsUiMessagesOrDefault();
176
- const total = totalPax(draft);
177
- // Auto-resize the travelers list to match pax counts. Newly-added
178
- // rows pick a band based on the lowest-count band that's not yet
179
- // saturated — naive but predictable.
180
- const ensured = ensureTravelerRows(draft, total, shape);
181
- if (ensured !== draft.travelers) {
182
- setDraft(setTravelers(draft, ensured));
183
- }
184
- // Bands are bookkeeping for the pricing engine — the user only
185
- // sees a flat list of travelers. Adding a traveler bumps the
186
- // adult-band counter by default; the row's band is reassigned
187
- // automatically once a DOB is entered (see TravelerCard's onDob).
188
- const defaultAddBand = shape.paxBands.find((b) => b.code === "adult") ?? shape.paxBands[0];
189
- const totalCap = shape.paxBandsAllowedTotal.max;
190
- const canAddMore = ensured.length < totalCap && Boolean(defaultAddBand);
191
- const removeTraveler = (rowIdx) => {
192
- const band = ensured[rowIdx]?.band;
193
- if (!band)
194
- return;
195
- const current = draft.configure.pax?.[band] ?? 0;
196
- const spec = shape.paxBands.find((b) => b.code === band);
197
- if (spec && current <= spec.minCount)
198
- return;
199
- setDraft(patchPaxCount(draft, band, current - 1));
200
- };
201
- const addTraveler = () => {
202
- if (!defaultAddBand)
203
- return;
204
- const current = draft.configure.pax?.[defaultAddBand.code] ?? 0;
205
- if (current >= defaultAddBand.maxCount)
206
- return;
207
- setDraft(patchPaxCount(draft, defaultAddBand.code, current + 1));
208
- };
209
- return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingJourney.travelers.title }) }), _jsxs(CardContent, { className: "space-y-4", children: [ensured.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.bookingJourney.travelers.empty })) : null, ensured.map((traveler, idx) => {
210
- const apply = (contact) => {
211
- const next = [...ensured];
212
- next[idx] = {
213
- ...next[idx],
214
- firstName: contact.firstName,
215
- lastName: contact.lastName,
216
- email: contact.email,
217
- phone: contact.phone,
218
- };
219
- setDraft(setTravelers(draft, next));
220
- };
221
- const bandSpec = shape.paxBands.find((b) => b.code === traveler.band);
222
- const currentBandCount = draft.configure.pax?.[traveler.band] ?? 0;
223
- const canRemove = !bandSpec || currentBandCount > bandSpec.minCount;
224
- return (_jsx(TravelerCard, { idx: idx, traveler: traveler, shape: shape, draft: draft, setDraft: setDraft, renderTravelerContactPicker: renderTravelerContactPicker, apply: apply, onRemove: canRemove ? () => removeTraveler(idx) : undefined }, traveler.rowId ?? idx));
225
- }), canAddMore ? (_jsx("div", { className: "border-t pt-3", children: _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: addTraveler, children: messages.bookingJourney.travelers.addTraveler }) })) : null] })] }));
226
- }
227
1
  /**
228
- * One traveler block name, optional contact, age, and any
229
- * descriptor-driven document fields. Honors `appliesToBands` so DOB
230
- * is required for child / infant bands and adult-only fields like
231
- * passport drop off the form for non-adult travelers.
2
+ * Step components rendered inside `<BookingJourney />`. Each takes a
3
+ * draft + setDraft pair plus the active descriptor; updates flow up
4
+ * via setDraft and the shell re-quotes on the next debounce tick.
232
5
  *
233
- * The first three canonical keys (firstName/lastName/email) are
234
- * rendered inline as fixed widgets; the rest comes off
235
- * `shape.travelerFields` so verticals can extend the set without
236
- * touching the wizard.
237
- */
238
- function TravelerCard({ idx, traveler, shape, draft, setDraft, renderTravelerContactPicker, apply, onRemove, }) {
239
- const messages = useBookingsUiMessagesOrDefault();
240
- // The band is bookkeeping — only show fields that apply to this
241
- // band per the descriptor, but never expose the band tag in the UI.
242
- // The user just sees a flat traveler list.
243
- const applicableFields = shape.travelerFields.filter((f) => {
244
- if (!f.appliesToBands || f.appliesToBands.length === 0)
245
- return true;
246
- return f.appliesToBands.includes(traveler.band);
247
- });
248
- const dobField = applicableFields.find((f) => f.key === "dateOfBirth");
249
- const phoneField = applicableFields.find((f) => f.key === "phone");
250
- const dynamicFields = applicableFields.filter((f) => !["firstName", "lastName", "email", "phone", "dateOfBirth"].includes(f.key));
251
- // Live age from DOB — surfaces in the header so the user gets
252
- // feedback as they pick a date.
253
- const computedAge = traveler.dateOfBirth ? computeAge(traveler.dateOfBirth) : null;
254
- // The DOB drives band assignment. When the user picks a date that
255
- // would land in a different band, we silently reband the row and
256
- // shift the pax counters so the engine quotes the right price.
257
- // No "Move to X" prompts — the system just does the right thing.
258
- const onDobChange = (v) => {
259
- const age = v ? computeAge(v) : null;
260
- let next = updateTravelerImmutable(draft, idx, { dateOfBirth: v });
261
- if (age != null) {
262
- const targetBand = shape.paxBands.find((b) => {
263
- if (b.minAge != null && age < b.minAge)
264
- return false;
265
- if (b.maxAge != null && age > b.maxAge)
266
- return false;
267
- return true;
268
- });
269
- if (targetBand && targetBand.code !== traveler.band) {
270
- next = rebandTraveler(next, idx, traveler.band, targetBand.code);
271
- }
272
- }
273
- setDraft(next);
274
- };
275
- // Out-of-range warning — only fires when the entered DOB doesn't
276
- // fit ANY of the descriptor's bands (e.g. older than the supplier
277
- // accepts). We can't auto-fix that one — the booking would be
278
- // rejected and the user needs to know.
279
- const ageOutOfBounds = computedAge != null &&
280
- !shape.paxBands.some((b) => {
281
- if (b.minAge != null && computedAge < b.minAge)
282
- return false;
283
- if (b.maxAge != null && computedAge > b.maxAge)
284
- return false;
285
- return true;
286
- });
287
- // Quick-fill from billing — useful when the lead booker is also a
288
- // traveler (the common B2C case). Doesn't touch travel-document
289
- // fields since those are personal to each traveler.
290
- const billingContact = draft.billing.contact;
291
- const canCopyFromBilling = canCopyBillingContactToTraveler(billingContact);
292
- const copyFromBilling = () => {
293
- updateTraveler(draft, setDraft, idx, {
294
- firstName: billingContact.firstName,
295
- lastName: billingContact.lastName,
296
- email: billingContact.email || undefined,
297
- phone: billingContact.phone || undefined,
298
- });
299
- };
300
- return (_jsxs("div", { className: "rounded border p-4 space-y-3", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsx("div", { className: "space-y-0.5", children: _jsxs("div", { className: "font-medium", children: [formatMessage(messages.bookingJourney.travelers.travelerNumber, {
301
- number: idx + 1,
302
- }), computedAge != null ? (_jsxs("span", { className: "text-muted-foreground font-normal", children: [" ", "\u00B7", " ", formatMessage(messages.bookingJourney.travelers.ageLabel, {
303
- age: computedAge,
304
- })] })) : null] }) }), _jsxs("div", { className: "flex items-center gap-2", children: [canCopyFromBilling ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: copyFromBilling, children: messages.bookingJourney.travelers.copyFromBilling })) : null, renderTravelerContactPicker
305
- ? renderTravelerContactPicker({ rowIndex: idx, apply })
306
- : null, onRemove ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", onClick: onRemove, children: messages.bookingJourney.travelers.remove })) : null] })] }), _jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [_jsx(Field, { id: `bj-trav-${idx}-first`, label: messages.bookingJourney.billing.firstName, value: traveler.firstName, onChange: (v) => updateTraveler(draft, setDraft, idx, { firstName: v }) }), _jsx(Field, { id: `bj-trav-${idx}-last`, label: messages.bookingJourney.billing.lastName, value: traveler.lastName, onChange: (v) => updateTraveler(draft, setDraft, idx, { lastName: v }) }), applicableFields.some((f) => f.key === "email") ? (_jsx(Field, { id: `bj-trav-${idx}-email`, label: messages.bookingJourney.billing.email, type: "email", value: traveler.email ?? "", onChange: (v) => updateTraveler(draft, setDraft, idx, { email: v }) })) : null, phoneField ? (_jsx(PhoneField, { id: `bj-trav-${idx}-phone`,
307
- // i18n-literal-ok Required marker appended to a descriptor-supplied field label.
308
- label: phoneField.label + (phoneField.required ? " *" : ""), value: traveler.phone ?? "", onChange: (v) => updateTraveler(draft, setDraft, idx, { phone: v }) })) : null, dobField ? (_jsxs("div", { className: "space-y-1", children: [_jsx(DateField, { id: `bj-trav-${idx}-dob`,
309
- // i18n-literal-ok Required marker appended to a descriptor-supplied field label.
310
- label: dobField.label + (dobField.required ? " *" : ""), value: traveler.dateOfBirth ?? "", onChange: onDobChange, range: "past" }), ageOutOfBounds ? (_jsxs("p", { className: "text-amber-600 text-xs dark:text-amber-400", children: ["\u26A0", " ", formatMessage(messages.bookingJourney.validation.ageOutOfRange, {
311
- age: computedAge,
312
- })] })) : null] })) : null, dynamicFields.map((field) => {
313
- const value = traveler.documents?.[field.key] ?? "";
314
- const onFieldChange = (v) => updateTraveler(draft, setDraft, idx, {
315
- documents: { ...traveler.documents, [field.key]: v },
316
- });
317
- // i18n-literal-ok Required marker appended to a descriptor-supplied field label.
318
- const labelText = field.label + (field.required ? " *" : "");
319
- if (field.type === "select" && field.options) {
320
- return (_jsx(SelectField, { id: `bj-trav-${idx}-${field.key}`, label: labelText, value: value, options: field.options, onChange: onFieldChange }, field.key));
321
- }
322
- if (field.type === "date") {
323
- return (_jsx(DateField, { id: `bj-trav-${idx}-${field.key}`, label: labelText, value: value, onChange: onFieldChange, range: field.key === "documentExpiry" ? "document" : "future" }, field.key));
324
- }
325
- return (_jsx(Field, { id: `bj-trav-${idx}-${field.key}`, label: labelText, type: "text", value: value, onChange: onFieldChange }, field.key));
326
- })] })] }));
327
- }
328
- function ensureTravelerRows(draft, total, shape) {
329
- // Return the same reference when no resize is needed — the caller
330
- // uses identity equality to decide whether to call setDraft, and a
331
- // fresh array on every render would loop infinitely (set during
332
- // render → re-render → set again).
333
- if (draft.travelers.length === total)
334
- return draft.travelers;
335
- const list = [...draft.travelers];
336
- while (list.length > total)
337
- list.pop();
338
- while (list.length < total) {
339
- const idx = list.length;
340
- const band = pickBandForIndex(draft, idx, shape);
341
- list.push({
342
- rowId: cryptoRowId(),
343
- firstName: "",
344
- lastName: "",
345
- band,
346
- });
347
- }
348
- return list;
349
- }
350
- function pickBandForIndex(draft, idx, shape) {
351
- // Pick by remaining quota: distribute travelers in band order based
352
- // on counts in `configure.pax`.
353
- let cursor = 0;
354
- for (const band of shape.paxBands) {
355
- const count = draft.configure.pax?.[band.code] ?? 0;
356
- if (idx < cursor + count) {
357
- return band.code ?? "adult";
358
- }
359
- cursor += count;
360
- }
361
- return "adult";
362
- }
363
- function updateTraveler(draft, setDraft, idx, patch) {
364
- setDraft(updateTravelerImmutable(draft, idx, patch));
365
- }
366
- /**
367
- * Patch one traveler row and return a new draft. Useful when the
368
- * caller wants to chain multiple draft updates (e.g. setting DOB +
369
- * rebanding) into a single setDraft call.
370
- */
371
- function updateTravelerImmutable(draft, idx, patch) {
372
- const next = [...draft.travelers];
373
- if (!next[idx])
374
- return draft;
375
- next[idx] = { ...next[idx], ...patch };
376
- return setTravelers(draft, next);
377
- }
378
- /**
379
- * Move one traveler row from one band to another, keeping the pax
380
- * counters consistent. Used by the auto-reband path on DOB change.
381
- */
382
- function rebandTraveler(draft, idx, fromBand, toBand) {
383
- const fromCount = draft.configure.pax?.[fromBand] ?? 0;
384
- const toCount = draft.configure.pax?.[toBand] ?? 0;
385
- let next = updateTravelerImmutable(draft, idx, { band: toBand });
386
- next = patchPaxCount(next, fromBand, Math.max(0, fromCount - 1));
387
- next = patchPaxCount(next, toBand, toCount + 1);
388
- return next;
389
- }
390
- // ─────────────────────────────────────────────────────────────────
391
- // Accommodation
392
- // ─────────────────────────────────────────────────────────────────
393
- export function AccommodationStep({ draft, setDraft, shape }) {
394
- const messages = useBookingsUiMessagesOrDefault();
395
- const subSteps = shape.accommodation?.subSteps ?? [];
396
- const rooms = shape.accommodation?.roomOptions ?? [];
397
- const accommodation = draft.accommodation ?? { rooms: [], travelerAssignments: {} };
398
- return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingJourney.accommodation.title }) }), _jsx(CardContent, { className: "space-y-4", children: rooms.length === 0 && subSteps.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.bookingJourney.accommodation.empty })) : (_jsxs("div", { className: "space-y-3", children: [rooms.map((room) => {
399
- const current = accommodation.rooms.find((r) => r.optionUnitId === room.id);
400
- const ratePlans = room.ratePlans ?? [];
401
- return (_jsxs("div", { className: "space-y-3 rounded border p-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium", children: room.name }), room.description ? (_jsx("div", { className: "text-muted-foreground text-xs", children: room.description })) : null] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", type: "button", onClick: () => {
402
- const list = accommodation.rooms.filter((r) => r.optionUnitId !== room.id);
403
- const qty = (current?.quantity ?? 0) - 1;
404
- if (qty > 0) {
405
- list.push({
406
- optionUnitId: room.id,
407
- quantity: qty,
408
- ratePlanId: current?.ratePlanId,
409
- });
410
- }
411
- setDraft(setAccommodation(draft, { ...accommodation, rooms: list }));
412
- }, children: "\u2212" }), _jsx("span", { className: "min-w-6 text-center", children: current?.quantity ?? 0 }), _jsx(Button, { variant: "outline", size: "sm", type: "button", onClick: () => {
413
- const list = accommodation.rooms.filter((r) => r.optionUnitId !== room.id);
414
- const qty = (current?.quantity ?? 0) + 1;
415
- // Auto-select the only rate plan when there's
416
- // exactly one — saves a click on the common case.
417
- const ratePlanId = current?.ratePlanId ??
418
- (ratePlans.length === 1 ? ratePlans[0]?.id : undefined);
419
- list.push({ optionUnitId: room.id, quantity: qty, ratePlanId });
420
- setDraft(setAccommodation(draft, { ...accommodation, rooms: list }));
421
- }, children: "+" })] })] }), current && current.quantity > 0 && ratePlans.length > 0 ? (_jsx(RatePlanPicker, { roomId: room.id, ratePlans: ratePlans, selected: current.ratePlanId, onSelect: (planId) => {
422
- const list = accommodation.rooms.map((r) => r.optionUnitId === room.id ? { ...r, ratePlanId: planId } : r);
423
- setDraft(setAccommodation(draft, { ...accommodation, rooms: list }));
424
- } })) : null] }, room.id));
425
- }), subSteps.map((sub) => sub.kind === "extensions" ? (_jsx("div", { className: "rounded border p-3 text-muted-foreground text-sm", children: formatMessage(messages.bookingJourney.accommodation.extensionsAvailable, {
426
- count: sub.options.length,
427
- plural: sub.options.length === 1 ? "" : "s",
428
- }) }, "extensions")) : null)] })) })] }));
429
- }
430
- function RatePlanPicker({ roomId, ratePlans, selected, onSelect, }) {
431
- const messages = useBookingsUiMessagesOrDefault();
432
- return (_jsxs("div", { className: "space-y-2 border-t pt-3", children: [_jsx(Label, { htmlFor: `bj-rate-plan-${roomId}`, children: messages.bookingJourney.accommodation.ratePlan }), _jsx("div", { className: "space-y-2", children: ratePlans.map((plan) => {
433
- const isSelected = plan.id === selected;
434
- return (_jsxs("button", { type: "button", onClick: () => onSelect(plan.id), className: `w-full rounded border p-2 text-left text-sm ${isSelected ? "border-primary ring-2 ring-primary" : ""}`, children: [_jsx("div", { className: "font-medium", children: plan.name }), plan.description ? (_jsx("div", { className: "text-muted-foreground text-xs", children: plan.description })) : null, plan.cancellationPolicy ? (_jsxs("div", { className: "text-muted-foreground text-xs", children: [messages.bookingJourney.accommodation.cancellationPrefix, " ", plan.cancellationPolicy] })) : null, plan.inclusions && plan.inclusions.length > 0 ? (_jsxs("div", { className: "text-muted-foreground text-xs", children: [messages.bookingJourney.accommodation.includesPrefix, " ", plan.inclusions.join(", ")] })) : null] }, plan.id));
435
- }) })] }));
436
- }
437
- // ─────────────────────────────────────────────────────────────────
438
- // Add-ons
439
- // ─────────────────────────────────────────────────────────────────
440
- export function AddonsStep({ draft, setDraft, shape }) {
441
- const messages = useBookingsUiMessagesOrDefault();
442
- const flat = shape.addons?.catalog ?? [];
443
- const groups = shape.addons?.groups ?? [];
444
- const all = [...flat, ...groups.flatMap((g) => g.items)];
445
- return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingJourney.addons.title }) }), _jsxs(CardContent, { className: "space-y-4", children: [all.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.bookingJourney.addons.empty })) : null, groups.map((group) => {
446
- // Group by port/day when the descriptor asks — cruise
447
- // excursions arrive grouped by port name.
448
- const buckets = group.groupBy === "port" || group.groupBy === "day"
449
- ? bucketBy(group.items, (i) => i.groupKey ?? messages.bookingJourney.addons.otherBucket)
450
- : new Map([["", group.items]]);
451
- return (_jsxs("div", { className: "space-y-3", children: [_jsx("div", { className: "font-medium text-sm", children: group.label }), [...buckets.entries()].map(([bucket, items]) => (_jsxs("div", { className: "space-y-2", children: [bucket ? (_jsx("div", { className: "text-muted-foreground text-xs uppercase", children: bucket })) : null, items.map((item) => (_jsx(AddonRow, { draft: draft, setDraft: setDraft, item: item }, item.id)))] }, bucket || "all")))] }, group.label));
452
- }), flat.length > 0 && groups.length === 0 ? (_jsx("div", { className: "space-y-2", children: flat.map((item) => (_jsx(AddonRow, { draft: draft, setDraft: setDraft, item: item }, item.id))) })) : null] })] }));
453
- }
454
- function AddonRow({ draft, setDraft, item, }) {
455
- const current = draft.addons.find((a) => a.extraId === item.id);
456
- return (_jsxs("div", { className: "flex items-center justify-between rounded border p-3", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium", children: item.name }), item.description ? (_jsx("div", { className: "text-muted-foreground text-xs", children: item.description })) : null] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", type: "button", onClick: () => {
457
- const list = draft.addons.filter((a) => a.extraId !== item.id);
458
- const qty = (current?.quantity ?? 0) - 1;
459
- if (qty > 0)
460
- list.push({ extraId: item.id, quantity: qty });
461
- setDraft(setAddons(draft, list));
462
- }, children: "\u2212" }), _jsx("span", { className: "min-w-6 text-center", children: current?.quantity ?? 0 }), _jsx(Button, { variant: "outline", size: "sm", type: "button", onClick: () => {
463
- const list = draft.addons.filter((a) => a.extraId !== item.id);
464
- const qty = (current?.quantity ?? 0) + 1;
465
- list.push({ extraId: item.id, quantity: qty });
466
- setDraft(setAddons(draft, list));
467
- }, children: "+" })] })] }));
468
- }
469
- // ─────────────────────────────────────────────────────────────────
470
- // Payment
471
- // ─────────────────────────────────────────────────────────────────
472
- export function PaymentStep({ draft, setDraft, shape, capabilities, renderProviderStep, }) {
473
- const messages = useBookingsUiMessagesOrDefault();
474
- // The descriptor lists what the *engine* supports; capabilities
475
- // narrow further to what the *deployment* turned on. Both must
476
- // accept an intent for the user to see it.
477
- const allowed = shape.paymentIntents.filter((i) => isCapabilityEnabled(i, capabilities));
478
- const intent = draft.payment.intent;
479
- // Snap the draft's intent to the first allowed value when the
480
- // current pick isn't on the list — covers descriptor changes
481
- // mid-flow (e.g. owned→sourced switch narrows the list).
482
- if (allowed.length > 0 && !allowed.includes(intent)) {
483
- setDraft(setPayment(draft, { ...draft.payment, intent: allowed[0] }));
484
- }
485
- return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingJourney.payment.title }) }), _jsxs(CardContent, { className: "space-y-4", children: [allowed.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.bookingJourney.payment.empty })) : (_jsx(RadioGroup, { value: intent, onValueChange: (v) => setDraft(setPayment(draft, { ...draft.payment, intent: v })), className: "grid grid-cols-1 gap-2", children: allowed.map((i) => {
486
- const meta = intentMeta(i, messages);
487
- const selected = i === intent;
488
- return (_jsxs("label", { className: "flex cursor-pointer items-start gap-3 rounded border p-3 text-sm transition-colors " +
489
- (selected ? "border-primary bg-primary/5" : "border-input hover:bg-muted/50"), children: [_jsx(RadioGroupItem, { value: i, className: "mt-0.5" }), _jsxs("div", { className: "space-y-0.5", children: [_jsx("div", { className: "font-medium", children: meta.label }), _jsx("div", { className: "text-muted-foreground text-xs", children: meta.description })] })] }, i));
490
- }) })), intent === "card" ? (renderProviderStep ? (_jsx("div", { children: renderProviderStep({
491
- intent,
492
- schedule: draft.payment.schedule,
493
- capabilities,
494
- }) })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.bookingJourney.payment.redirectedAfterConfirm }))) : null, intent === "bank_transfer" ? _jsx(BankTransferDetails, { capabilities: capabilities }) : null, intent === "inquiry" ? (_jsx("p", { className: "rounded border border-amber-300 bg-amber-50 p-3 text-amber-900 text-xs dark:border-amber-700 dark:bg-amber-950 dark:text-amber-100", children: messages.bookingJourney.payment.inquiryNotice })) : null] })] }));
495
- }
496
- function BankTransferDetails({ capabilities, }) {
497
- const messages = useBookingsUiMessagesOrDefault();
498
- const note = capabilities.config?.bankTransferNote;
499
- return (_jsxs("div", { className: "rounded border bg-muted/30 p-3 text-sm", children: [_jsx("p", { className: "font-medium", children: messages.bookingJourney.payment.bankTransferInstructions }), _jsx("p", { className: "text-muted-foreground text-xs", children: typeof note === "string" && note.length > 0
500
- ? note
501
- : messages.bookingJourney.payment.bankTransferDefaultNote })] }));
502
- }
503
- function isCapabilityEnabled(intent, capabilities) {
504
- switch (intent) {
505
- case "card":
506
- return capabilities.acceptsCard;
507
- case "hold":
508
- return capabilities.acceptsHold;
509
- case "bank_transfer":
510
- return capabilities.acceptsBankTransfer === true;
511
- case "ticket_on_credit":
512
- return capabilities.acceptsTicketOnCredit;
513
- case "inquiry":
514
- return capabilities.acceptsInquiry === true;
515
- }
516
- }
517
- function intentMeta(intent, messages) {
518
- return {
519
- label: messages.bookingJourney.payment.intentLabels[intent],
520
- description: messages.bookingJourney.payment.intentDescriptions[intent],
521
- };
522
- }
523
- // ─────────────────────────────────────────────────────────────────
524
- // Review
525
- // ─────────────────────────────────────────────────────────────────
526
- export function ReviewStep({ draft, setDraft, isCommitting, onConfirm, renderExtras, surface, }) {
527
- const messages = useBookingsUiMessagesOrDefault();
528
- const isPublic = surface === "public";
529
- return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingJourney.review.title }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium", children: messages.bookingJourney.review.leadContact }), _jsxs("div", { className: "text-muted-foreground text-sm", children: [draft.billing.contact.firstName, " ", draft.billing.contact.lastName, " \u00B7", " ", draft.billing.contact.email] })] }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: messages.bookingJourney.review.travelers }), _jsx("ul", { className: "text-muted-foreground text-sm", children: draft.travelers.map((t, i) => (_jsxs("li", { children: [t.firstName, " ", t.lastName, " (", t.band, ")"] }, t.rowId ?? i))) })] }), isPublic ? (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "bj-customer-notes", children: messages.bookingJourney.review.customerNotes }), _jsx(Textarea, { id: "bj-customer-notes", placeholder: messages.bookingJourney.review.customerNotesPlaceholder, value: draft.customerNotes ?? "", onChange: (e) => setDraft({ ...draft, customerNotes: e.target.value }) })] })) : (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "bj-internal-notes", children: messages.bookingJourney.review.internalNotes }), _jsx(Textarea, { id: "bj-internal-notes", value: draft.internalNotes ?? "", onChange: (e) => setDraft({ ...draft, internalNotes: e.target.value }) })] })), renderExtras ? _jsx("div", { children: renderExtras() }) : null, _jsx(Button, { onClick: onConfirm, disabled: isCommitting, children: isCommitting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), messages.bookingJourney.review.confirming] })) : (messages.bookingJourney.review.confirmBooking) })] })] }));
530
- }
531
- // ─────────────────────────────────────────────────────────────────
532
- // Helpers
533
- // ─────────────────────────────────────────────────────────────────
534
- function Field({ id, label, value, onChange, type, placeholder, }) {
535
- return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: id, children: label }), _jsx(Input, { id: id, type: type ?? "text", value: value, placeholder: placeholder, onChange: (e) => onChange(e.target.value) })] }));
536
- }
537
- function PhoneField({ id, label, value, onChange, }) {
538
- return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: id, children: label }), _jsx(PhoneInput, { id: id, defaultCountry: "GB", international: true, value: value || undefined, onChange: (v) => onChange(v ? String(v) : "") })] }));
539
- }
540
- /**
541
- * Date field that uses the shared `<DatePicker />` from
542
- * `@voyantjs/ui` with a month + year dropdown caption so users can
543
- * jump across decades without arrow-clicking. The `range` hint picks
544
- * a reasonable startMonth/endMonth window per use case:
6
+ * Per booking-journey-architecture §3.
545
7
  *
546
- * - `"past"` DOB-style picks (today back ~120 years)
547
- * - `"future"` — departure / check-in / check-out (today forward ~5 years)
548
- * - `"document"` passport / ID expiry (today forward ~20 years)
549
- */
550
- function DateField({ id, label, value, onChange, range = "future", }) {
551
- const today = new Date();
552
- const todayMonth = new Date(today.getFullYear(), today.getMonth(), 1);
553
- const startMonth = range === "past"
554
- ? new Date(today.getFullYear() - 120, 0, 1)
555
- : range === "document"
556
- ? todayMonth
557
- : todayMonth;
558
- const endMonth = range === "past"
559
- ? new Date(today.getFullYear() + 1, 11, 1)
560
- : range === "document"
561
- ? new Date(today.getFullYear() + 20, 11, 1)
562
- : new Date(today.getFullYear() + 5, 11, 1);
563
- const defaultMonth = range === "past" && !value ? new Date(today.getFullYear() - 30, 0, 1) : undefined;
564
- return (_jsxs("div", { className: "space-y-1", id: id, children: [_jsx(Label, { children: label }), _jsx(DatePicker, { value: value || null, onChange: (v) => onChange(v ?? ""), captionLayout: "dropdown", startMonth: startMonth, endMonth: endMonth, defaultMonth: defaultMonth, displayFormat: "PPP" })] }));
565
- }
566
- function SelectField({ id, label, value, options, onChange, }) {
567
- const messages = useBookingsUiMessagesOrDefault();
568
- return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: id, children: label }), _jsxs("select", { id: id, className: "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", value: value, onChange: (e) => onChange(e.target.value), children: [_jsx("option", { value: "", children: messages.bookingJourney.values.selectPlaceholder }), options.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value)))] })] }));
569
- }
570
- /**
571
- * Years between an ISO date-of-birth and today. Returns `null` for
572
- * unparseable input or future dates so the UI can hide the badge
573
- * gracefully rather than rendering "age -3".
8
+ * This module is a thin barrel the implementations live in the
9
+ * `./journey-steps/` directory, split by step. Consumers import the
10
+ * same public symbols from here as before.
574
11
  */
575
- function computeAge(dob) {
576
- const d = new Date(dob);
577
- if (Number.isNaN(d.getTime()))
578
- return null;
579
- const now = new Date();
580
- let age = now.getFullYear() - d.getFullYear();
581
- const m = now.getMonth() - d.getMonth();
582
- if (m < 0 || (m === 0 && now.getDate() < d.getDate()))
583
- age -= 1;
584
- return age >= 0 ? age : null;
585
- }
586
- function ageHint(min, max, messages) {
587
- if (min != null && max != null) {
588
- return formatMessage(messages.bookingJourney.configure.ageHintRange, { min, max });
589
- }
590
- if (min != null) {
591
- return formatMessage(messages.bookingJourney.configure.ageHintMinimum, { min });
592
- }
593
- if (max != null) {
594
- return formatMessage(messages.bookingJourney.configure.ageHintMaximum, { max });
595
- }
596
- return "";
597
- }
598
- // i18n-literal-ok Generic helper type signature, not user-visible copy.
599
- function bucketBy(items, keyFn) {
600
- const map = new Map();
601
- for (const item of items) {
602
- const key = keyFn(item);
603
- let bucket = map.get(key);
604
- if (!bucket) {
605
- bucket = [];
606
- map.set(key, bucket);
607
- }
608
- bucket.push(item);
609
- }
610
- return map;
611
- }
612
- function cryptoRowId() {
613
- if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.randomUUID) {
614
- return globalThis.crypto.randomUUID();
615
- }
616
- return `r_${Math.random().toString(36).slice(2, 10)}`;
617
- }
12
+ export { AccommodationStep } from "./journey-steps/accommodation-step.js";
13
+ export { AddonsStep } from "./journey-steps/addons-step.js";
14
+ export { BillingStep } from "./journey-steps/billing-step.js";
15
+ export { DepartureStep, OptionsStep } from "./journey-steps/configure-steps.js";
16
+ export { DocumentsStep } from "./journey-steps/documents-step.js";
17
+ export { FinalizeControls, PaymentStep } from "./journey-steps/payment-step.js";
18
+ export { ReviewStep } from "./journey-steps/review-step.js";
19
+ export { JourneyWarnings } from "./journey-steps/shared.js";
20
+ export { TravelersStep } from "./journey-steps/travelers-step.js";