@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
@@ -0,0 +1,224 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Separator } from "@voyantjs/ui/components";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card";
5
+ import { Checkbox } from "@voyantjs/ui/components/checkbox";
6
+ import { Input } from "@voyantjs/ui/components/input";
7
+ import { Label } from "@voyantjs/ui/components/label";
8
+ import { RadioGroup, RadioGroupItem } from "@voyantjs/ui/components/radio-group";
9
+ import { Textarea } from "@voyantjs/ui/components/textarea";
10
+ import { useEffect, useRef, useState } from "react";
11
+ import { PaymentScheduleSection, } from "../../../components/payment-schedule-section.js";
12
+ import { emptyVoucherPickerValue, VoucherPickerSection, } from "../../../components/voucher-picker-section.js";
13
+ import { useBookingsUiMessagesOrDefault } from "../../../i18n/index.js";
14
+ import { setPayment } from "../../lib/draft-state.js";
15
+ import { paymentScheduleValueToRows, rowsToPaymentScheduleValue, } from "../../lib/payment-schedule.js";
16
+ // ─────────────────────────────────────────────────────────────────
17
+ // Payment
18
+ // ─────────────────────────────────────────────────────────────────
19
+ export function PaymentStep({ draft, setDraft, shape, capabilities, renderProviderStep, surface, pricing, }) {
20
+ const messages = useBookingsUiMessagesOrDefault();
21
+ // The descriptor lists what the *engine* supports; capabilities
22
+ // narrow further to what the *deployment* turned on. Both must
23
+ // accept an intent for the user to see it.
24
+ const allowed = shape.paymentIntents.filter((i) => isCapabilityEnabled(i, capabilities));
25
+ const intent = draft.payment.intent;
26
+ // Admin simplification: when the only choices are reserve-now (hold) and an
27
+ // online payment link (card), don't make it a radio — the booking is always
28
+ // reserved; a single checkbox decides whether to ALSO send a payment link.
29
+ const simpleHoldCard = surface === "admin" &&
30
+ allowed.length > 0 &&
31
+ allowed.includes("hold") &&
32
+ allowed.includes("card") &&
33
+ allowed.every((i) => i === "hold" || i === "card");
34
+ // Snap the draft's intent to a sensible value when the current pick isn't on
35
+ // the list — covers descriptor changes mid-flow (e.g. owned→sourced narrows
36
+ // the list). In checkbox mode the baseline is always "hold".
37
+ if (allowed.length > 0 && !allowed.includes(intent)) {
38
+ setDraft(setPayment(draft, {
39
+ ...draft.payment,
40
+ intent: (simpleHoldCard ? "hold" : allowed[0]),
41
+ }));
42
+ }
43
+ return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingJourney.payment.title }) }), _jsx(Separator, {}), _jsxs(CardContent, { className: "space-y-4", children: [allowed.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.bookingJourney.payment.empty })) : simpleHoldCard ? (_jsxs("label", { className: "flex cursor-pointer items-start gap-3 rounded-md border border-input p-3 text-sm transition-colors hover:bg-muted/50", children: [_jsx(Checkbox, { id: "bj-generate-link", checked: intent === "card", onCheckedChange: (v) => setDraft(setPayment(draft, {
44
+ ...draft.payment,
45
+ intent: (v === true ? "card" : "hold"),
46
+ })), className: "mt-0.5" }), _jsxs("div", { className: "space-y-0.5", children: [_jsx("div", { className: "font-medium", children: messages.bookingJourney.payment.generateLinkLabel }), _jsx("div", { className: "text-muted-foreground text-xs", children: messages.bookingJourney.payment.generateLinkHint })] })] })) : (_jsx(RadioGroup, { value: intent, onValueChange: (v) => setDraft(setPayment(draft, { ...draft.payment, intent: v })), className: "grid grid-cols-1 gap-2", children: allowed.map((i) => {
47
+ const meta = intentMeta(i, messages, surface);
48
+ const selected = i === intent;
49
+ return (_jsxs("label", { className: "flex cursor-pointer items-start gap-3 rounded-md border p-3 text-sm transition-colors " +
50
+ (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));
51
+ }) })), surface !== "public" ? (_jsx(PaymentScheduleEditor, { draft: draft, setDraft: setDraft, pricing: pricing })) : null, intent === "card" ? (renderProviderStep ? (_jsx("div", { children: renderProviderStep({
52
+ intent,
53
+ schedule: draft.payment.schedule,
54
+ capabilities,
55
+ }) })) : simpleHoldCard ? null : (_jsx("p", { className: "text-muted-foreground text-sm", children: surface === "admin"
56
+ ? messages.bookingJourney.payment.linkSentAfterConfirm
57
+ : messages.bookingJourney.payment.redirectedAfterConfirm }))) : null, intent === "bank_transfer" ? _jsx(BankTransferDetails, { capabilities: capabilities }) : null, intent === "inquiry" ? (_jsx("p", { className: "rounded-md 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] })] }));
58
+ }
59
+ /**
60
+ * Operator-only payment-schedule editor for the journey. Holds the editor
61
+ * value (`{ mode, installments }`) in local state — stable installment ids
62
+ * survive re-quotes — and syncs it to `draft.paymentSchedules` on every change.
63
+ * Re-initialised from the draft when the step remounts (navigation), preserving
64
+ * any paid-installment metadata via the `notes` round-trip.
65
+ */
66
+ function PaymentScheduleEditor({ draft, setDraft, pricing, }) {
67
+ const departureDate = draft.configure.departureDate ?? null;
68
+ const [value, setValue] = useState(() => rowsToPaymentScheduleValue(draft.paymentSchedules, departureDate));
69
+ const currency = pricing?.currency ?? "";
70
+ // A manual price override is the booking's real total — schedules must sum
71
+ // to it (booking-create enforces this), so the editor anchors on it.
72
+ const total = draft.priceOverride?.amountCents ?? pricing?.total ?? null;
73
+ // Persist the default schedule (a single full-amount payment) to the draft
74
+ // once the total is known — otherwise an operator who never touches the
75
+ // editor commits a booking with NO payment schedule. Seeds once; after that
76
+ // the operator owns it via onChange.
77
+ const seeded = useRef(false);
78
+ // biome-ignore lint/correctness/useExhaustiveDependencies: one-shot seed guarded by the ref; reads latest via closure
79
+ useEffect(() => {
80
+ if (seeded.current || total == null)
81
+ return;
82
+ if (draft.paymentSchedules && draft.paymentSchedules.length > 0) {
83
+ seeded.current = true;
84
+ return;
85
+ }
86
+ const rows = paymentScheduleValueToRows(value, currency, total);
87
+ if (rows && rows.length > 0) {
88
+ seeded.current = true;
89
+ setDraft({ ...draft, paymentSchedules: rows });
90
+ }
91
+ }, [total, currency]);
92
+ return (_jsx(PaymentScheduleSection, { value: value, onChange: (next) => {
93
+ setValue(next);
94
+ setDraft({
95
+ ...draft,
96
+ paymentSchedules: paymentScheduleValueToRows(next, currency, total),
97
+ });
98
+ }, totalAmountCents: total ?? undefined, departureDate: departureDate, currency: currency }));
99
+ }
100
+ /**
101
+ * Operator-only manual price override for the journey. Local string state for
102
+ * the amount field keeps decimal entry smooth; syncs cents to `draft.priceOverride`.
103
+ * The owned handler sends it as `confirmedSellAmountCents` (wins over the quote)
104
+ * with a required reason when it differs from the quoted total.
105
+ */
106
+ function PriceOverrideEditor({ draft, setDraft, pricing, }) {
107
+ const messages = useBookingsUiMessagesOrDefault().bookingJourney.review;
108
+ const quoteTotal = pricing?.total ?? null;
109
+ const currency = pricing?.currency ?? "";
110
+ const override = draft.priceOverride;
111
+ const [amount, setAmount] = useState(() => override ? (override.amountCents / 100).toString() : "");
112
+ const setOverride = (next) => setDraft({ ...draft, priceOverride: next });
113
+ const reasonNeeded = override != null &&
114
+ quoteTotal != null &&
115
+ override.amountCents !== quoteTotal &&
116
+ override.reason.trim().length === 0;
117
+ return (_jsxs("div", { className: "flex flex-col gap-3 rounded-md border p-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Checkbox, { id: "bj-price-override", checked: override != null, onCheckedChange: (v) => {
118
+ if (v === true) {
119
+ const cents = quoteTotal ?? 0;
120
+ setAmount((cents / 100).toString());
121
+ setOverride({ amountCents: cents, reason: "" });
122
+ }
123
+ else {
124
+ setOverride(undefined);
125
+ }
126
+ } }), _jsx(Label, { htmlFor: "bj-price-override", className: "cursor-pointer", children: messages.priceOverrideToggle })] }), override ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs(Label, { htmlFor: "bj-price-override-amount", className: "text-xs", children: [messages.priceOverrideAmount, currency ? ` (${currency})` : ""] }), _jsx(Input, { id: "bj-price-override-amount", type: "number", min: 0, step: "0.01", value: amount, onChange: (e) => {
127
+ setAmount(e.target.value);
128
+ const parsed = Number(e.target.value);
129
+ setOverride({
130
+ amountCents: Number.isFinite(parsed) ? Math.round(parsed * 100) : 0,
131
+ reason: override.reason,
132
+ });
133
+ } })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { htmlFor: "bj-price-override-reason", className: "text-xs", children: messages.priceOverrideReason }), _jsx(Textarea, { id: "bj-price-override-reason", placeholder: messages.priceOverrideReasonPlaceholder, value: override.reason, onChange: (e) => setOverride({
134
+ amountCents: override.amountCents,
135
+ reason: e.target.value,
136
+ }) }), reasonNeeded ? (_jsx("p", { className: "text-destructive text-xs", children: messages.priceOverrideReasonRequired })) : null] })] })) : null] }));
137
+ }
138
+ /**
139
+ * Operator-only voucher editor for the review step. Wraps the shared
140
+ * `VoucherPickerSection` (which validates the code against
141
+ * `/v1/public/vouchers/validate`) and mirrors the picked voucher into
142
+ * `draft.voucherRedemption` so the owned handler redeems it atomically at
143
+ * commit — matching the standalone create-sheet's behaviour. Redeems the
144
+ * full remaining balance, same as the create-sheet.
145
+ */
146
+ function VoucherEditor({ draft, setDraft, pricing, renderVoucherPicker, }) {
147
+ const labels = useBookingsUiMessagesOrDefault().bookingCreateDialog.labels;
148
+ const [voucher, setVoucher] = useState(emptyVoucherPickerValue);
149
+ // Operator surface: an async search combobox (no need to know the code).
150
+ if (renderVoucherPicker) {
151
+ return (_jsx(_Fragment, { children: renderVoucherPicker({
152
+ value: {
153
+ voucherId: draft.voucherRedemption?.voucherId,
154
+ amountCents: draft.voucherRedemption?.amountCents,
155
+ },
156
+ onApply: (picked) => setDraft({ ...draft, voucherRedemption: picked ?? undefined }),
157
+ currency: pricing?.currency,
158
+ amountCents: pricing?.total ?? undefined,
159
+ }) }));
160
+ }
161
+ return (_jsx(VoucherPickerSection, { value: voucher, onChange: (next) => {
162
+ setVoucher(next);
163
+ const redemption = next.picked && next.picked.remainingAmountCents != null
164
+ ? {
165
+ voucherId: next.picked.id,
166
+ amountCents: next.picked.remainingAmountCents,
167
+ }
168
+ : undefined;
169
+ setDraft({ ...draft, voucherRedemption: redemption });
170
+ }, currency: pricing?.currency, amountCents: pricing?.total ?? undefined, labels: {
171
+ heading: labels.voucherHeading,
172
+ codePlaceholder: labels.voucherCodePlaceholder,
173
+ apply: labels.voucherApply,
174
+ clear: labels.voucherClear,
175
+ remainingLabel: labels.voucherRemainingLabel,
176
+ invalidLabel: labels.voucherInvalidLabel,
177
+ } }));
178
+ }
179
+ function BankTransferDetails({ capabilities, }) {
180
+ const messages = useBookingsUiMessagesOrDefault();
181
+ const note = capabilities.config?.bankTransferNote;
182
+ return (_jsxs("div", { className: "rounded-md 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
183
+ ? note
184
+ : messages.bookingJourney.payment.bankTransferDefaultNote })] }));
185
+ }
186
+ function isCapabilityEnabled(intent, capabilities) {
187
+ switch (intent) {
188
+ case "card":
189
+ return capabilities.acceptsCard;
190
+ case "hold":
191
+ return capabilities.acceptsHold;
192
+ case "bank_transfer":
193
+ return capabilities.acceptsBankTransfer === true;
194
+ case "ticket_on_credit":
195
+ return capabilities.acceptsTicketOnCredit;
196
+ case "inquiry":
197
+ return capabilities.acceptsInquiry === true;
198
+ }
199
+ }
200
+ function intentMeta(intent, messages, surface) {
201
+ // On the operator surface "card" isn't an instant charge — the operator
202
+ // generates a hosted payment link the customer pays later (Netopia, Stripe
203
+ // Checkout, etc). Use operator-framed copy so it doesn't read as "charged
204
+ // immediately".
205
+ if (intent === "card" && surface === "admin") {
206
+ return {
207
+ label: messages.bookingJourney.payment.cardOperatorLabel,
208
+ description: messages.bookingJourney.payment.cardOperatorDescription,
209
+ };
210
+ }
211
+ return {
212
+ label: messages.bookingJourney.payment.intentLabels[intent],
213
+ description: messages.bookingJourney.payment.intentDescriptions[intent],
214
+ };
215
+ }
216
+ /**
217
+ * Operator-only PAYMENT-RELATED finalize controls — manual price override and
218
+ * voucher redemption (both change the amount due, so they live in the Payment
219
+ * block). Non-payment finalization (internal notes, document generation) lives
220
+ * in the separate Documents step.
221
+ */
222
+ export function FinalizeControls({ draft, setDraft, pricing, renderVoucherPicker, }) {
223
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(PriceOverrideEditor, { draft: draft, setDraft: setDraft, pricing: pricing }), _jsx(VoucherEditor, { draft: draft, setDraft: setDraft, pricing: pricing, renderVoucherPicker: renderVoucherPicker })] }));
224
+ }
@@ -0,0 +1,27 @@
1
+ import type { Draft } from "../../lib/draft-state.js";
2
+ export declare function ReviewStep({ draft, setDraft, isCommitting, onConfirm, canConfirm, renderExtras, surface, warnings, }: {
3
+ draft: Draft;
4
+ setDraft: (next: Draft) => void;
5
+ isCommitting: boolean;
6
+ onConfirm: () => void;
7
+ warnings?: ReadonlyArray<string>;
8
+ /** Gate the confirm button — when `false`, it's disabled with a hint
9
+ * (stacked layout, where there are no per-step advance gates). The
10
+ * wizard reaches Review only after passing every gate, so it omits
11
+ * this (defaults to enabled). */
12
+ canConfirm?: boolean;
13
+ renderExtras?: () => React.ReactNode;
14
+ /**
15
+ * Drives the notes field. Public storefronts collect
16
+ * customer-facing "anything we should know?" notes; operator
17
+ * surfaces collect operator-only internal notes. Defaults to
18
+ * `admin` so existing operator usage stays unchanged.
19
+ */
20
+ surface?: "admin" | "public";
21
+ /** Live quote total + currency — drives the price-override default. */
22
+ pricing?: {
23
+ total: number;
24
+ currency: string;
25
+ } | null;
26
+ }): React.ReactElement;
27
+ //# sourceMappingURL=review-step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-step.d.ts","sourceRoot":"","sources":["../../../../src/journey/components/journey-steps/review-step.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAOrD,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,UAAU,EACV,YAAY,EACZ,OAAO,EACP,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,KAAK,CAAA;IACZ,QAAQ,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAA;IAC/B,YAAY,EAAE,OAAO,CAAA;IACrB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAChC;;;sCAGkC;IAClC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;IACpC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAA;IAC5B,uEAAuE;IACvE,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CACrD,GAAG,KAAK,CAAC,YAAY,CAiErB"}
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Separator } from "@voyantjs/ui/components";
4
+ import { Button } from "@voyantjs/ui/components/button";
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card";
6
+ import { Label } from "@voyantjs/ui/components/label";
7
+ import { Textarea } from "@voyantjs/ui/components/textarea";
8
+ import { Loader2 } from "lucide-react";
9
+ import { useBookingsUiMessagesOrDefault } from "../../../i18n/index.js";
10
+ import { JourneyWarnings } from "./shared.js";
11
+ // ─────────────────────────────────────────────────────────────────
12
+ // Review
13
+ // ─────────────────────────────────────────────────────────────────
14
+ export function ReviewStep({ draft, setDraft, isCommitting, onConfirm, canConfirm, renderExtras, surface, warnings, }) {
15
+ const messages = useBookingsUiMessagesOrDefault();
16
+ const isPublic = surface === "public";
17
+ return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingJourney.review.title }) }), _jsx(Separator, {}), _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 }) })] })) : null, renderExtras ? _jsx("div", { children: renderExtras() }) : null, _jsx(JourneyWarnings, { warnings: warnings }), _jsxs("div", { className: "space-y-2", children: [_jsx(Button, { onClick: onConfirm, disabled: isCommitting || canConfirm === false, children: isCommitting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), messages.bookingJourney.review.confirming] })) : (messages.bookingJourney.review.confirmBooking) }), canConfirm === false ? (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.bookingJourney.review.completeToConfirm })) : null] })] })] }));
18
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Cross-used leaf helpers + shared types for the journey step components.
3
+ * Everything else imports shared helpers from here.
4
+ */
5
+ import type { BookingDraftShape } from "@voyantjs/catalog/booking-engine";
6
+ import { useBookingsUiMessagesOrDefault } from "../../../i18n/index.js";
7
+ import type { Draft } from "../../lib/draft-state.js";
8
+ import type { DeparturePickerProps, UnitsPickerProps } from "../../types.js";
9
+ /** Injectable departure-picker render slot, threaded from BookingJourneyProps. */
10
+ export type RenderDeparturePicker = (props: DeparturePickerProps) => React.ReactNode;
11
+ /** Injectable units (rooms) render slot, threaded from BookingJourneyProps. */
12
+ export type RenderUnitsPicker = (props: UnitsPickerProps) => React.ReactNode;
13
+ export interface StepCommonProps {
14
+ draft: Draft;
15
+ setDraft: (next: Draft) => void;
16
+ shape: BookingDraftShape;
17
+ }
18
+ /**
19
+ * Soft validation warnings for a step, rendered INSIDE its card (below the
20
+ * content) so they're visibly scoped to the block they belong to.
21
+ */
22
+ export declare function JourneyWarnings({ warnings, }: {
23
+ warnings?: ReadonlyArray<string>;
24
+ }): React.ReactElement | null;
25
+ export declare function Field({ id, label, value, onChange, type, placeholder, }: {
26
+ id: string;
27
+ label: string;
28
+ value: string;
29
+ onChange: (v: string) => void;
30
+ type?: string;
31
+ placeholder?: string;
32
+ }): React.ReactElement;
33
+ export declare function PhoneField({ id, label, value, onChange, }: {
34
+ id: string;
35
+ label: string;
36
+ value: string;
37
+ onChange: (v: string) => void;
38
+ }): React.ReactElement;
39
+ /**
40
+ * Date field that uses the shared `<DatePicker />` from
41
+ * `@voyantjs/ui` with a month + year dropdown caption so users can
42
+ * jump across decades without arrow-clicking. The `range` hint picks
43
+ * a reasonable startMonth/endMonth window per use case:
44
+ *
45
+ * - `"past"` — DOB-style picks (today back ~120 years)
46
+ * - `"future"` — departure / check-in / check-out (today forward ~5 years)
47
+ * - `"document"` — passport / ID expiry (today forward ~20 years)
48
+ */
49
+ export declare function DateField({ id, label, value, onChange, range, }: {
50
+ id: string;
51
+ label: string;
52
+ value: string;
53
+ onChange: (v: string) => void;
54
+ range?: "past" | "future" | "document";
55
+ }): React.ReactElement;
56
+ export declare function SelectField({ id, label, value, options, onChange, }: {
57
+ id: string;
58
+ label: string;
59
+ value: string;
60
+ options: ReadonlyArray<{
61
+ value: string;
62
+ label: string;
63
+ }>;
64
+ onChange: (v: string) => void;
65
+ }): React.ReactElement;
66
+ /**
67
+ * Years between an ISO date-of-birth and today. Returns `null` for
68
+ * unparseable input or future dates so the UI can hide the badge
69
+ * gracefully rather than rendering "age -3".
70
+ */
71
+ export declare function computeAge(dob: string): number | null;
72
+ export declare function ageHint(min: number | undefined, max: number | undefined, messages: ReturnType<typeof useBookingsUiMessagesOrDefault>): string;
73
+ export declare function bucketBy<T>(items: ReadonlyArray<T>, keyFn: (item: T) => string): Map<string, T[]>;
74
+ export declare function cryptoRowId(): string;
75
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../../src/journey/components/journey-steps/shared.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ "use client";
2
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
+ import { DatePicker } from "@voyantjs/ui/components/date-picker";
4
+ import { Input } from "@voyantjs/ui/components/input";
5
+ import { Label } from "@voyantjs/ui/components/label";
6
+ import { PhoneInput } from "@voyantjs/ui/components/phone-input";
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components/select";
8
+ import { formatMessage, useBookingsUiMessagesOrDefault } from "../../../i18n/index.js";
9
+ /**
10
+ * Soft validation warnings for a step, rendered INSIDE its card (below the
11
+ * content) so they're visibly scoped to the block they belong to.
12
+ */
13
+ export function JourneyWarnings({ warnings, }) {
14
+ if (!warnings || warnings.length === 0)
15
+ return null;
16
+ return (_jsx("ul", { className: "space-y-1 rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900 text-sm dark:border-amber-700 dark:bg-amber-950 dark:text-amber-100", children: warnings.map((w) => (_jsxs("li", { children: ["\u26A0 ", w] }, w))) }));
17
+ }
18
+ export function Field({ id, label, value, onChange, type, placeholder, }) {
19
+ 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) })] }));
20
+ }
21
+ export function PhoneField({ id, label, value, onChange, }) {
22
+ 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) : "") })] }));
23
+ }
24
+ /**
25
+ * Date field that uses the shared `<DatePicker />` from
26
+ * `@voyantjs/ui` with a month + year dropdown caption so users can
27
+ * jump across decades without arrow-clicking. The `range` hint picks
28
+ * a reasonable startMonth/endMonth window per use case:
29
+ *
30
+ * - `"past"` — DOB-style picks (today back ~120 years)
31
+ * - `"future"` — departure / check-in / check-out (today forward ~5 years)
32
+ * - `"document"` — passport / ID expiry (today forward ~20 years)
33
+ */
34
+ export function DateField({ id, label, value, onChange, range = "future", }) {
35
+ const today = new Date();
36
+ const todayMonth = new Date(today.getFullYear(), today.getMonth(), 1);
37
+ const startMonth = range === "past"
38
+ ? new Date(today.getFullYear() - 120, 0, 1)
39
+ : range === "document"
40
+ ? todayMonth
41
+ : todayMonth;
42
+ const endMonth = range === "past"
43
+ ? new Date(today.getFullYear() + 1, 11, 1)
44
+ : range === "document"
45
+ ? new Date(today.getFullYear() + 20, 11, 1)
46
+ : new Date(today.getFullYear() + 5, 11, 1);
47
+ const defaultMonth = range === "past" && !value ? new Date(today.getFullYear() - 30, 0, 1) : undefined;
48
+ 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" })] }));
49
+ }
50
+ export function SelectField({ id, label, value, options, onChange, }) {
51
+ const messages = useBookingsUiMessagesOrDefault();
52
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: id, children: label }), _jsxs(Select, { value: value || undefined, onValueChange: (v) => onChange(v ?? ""), children: [_jsx(SelectTrigger, { id: id, className: "w-full", children: _jsx(SelectValue, { placeholder: messages.bookingJourney.values.selectPlaceholder }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] })] }));
53
+ }
54
+ /**
55
+ * Years between an ISO date-of-birth and today. Returns `null` for
56
+ * unparseable input or future dates so the UI can hide the badge
57
+ * gracefully rather than rendering "age -3".
58
+ */
59
+ export function computeAge(dob) {
60
+ const d = new Date(dob);
61
+ if (Number.isNaN(d.getTime()))
62
+ return null;
63
+ const now = new Date();
64
+ let age = now.getFullYear() - d.getFullYear();
65
+ const m = now.getMonth() - d.getMonth();
66
+ if (m < 0 || (m === 0 && now.getDate() < d.getDate()))
67
+ age -= 1;
68
+ return age >= 0 ? age : null;
69
+ }
70
+ export function ageHint(min, max, messages) {
71
+ if (min != null && max != null) {
72
+ return formatMessage(messages.bookingJourney.configure.ageHintRange, {
73
+ min,
74
+ max,
75
+ });
76
+ }
77
+ if (min != null) {
78
+ return formatMessage(messages.bookingJourney.configure.ageHintMinimum, {
79
+ min,
80
+ });
81
+ }
82
+ if (max != null) {
83
+ return formatMessage(messages.bookingJourney.configure.ageHintMaximum, {
84
+ max,
85
+ });
86
+ }
87
+ return "";
88
+ }
89
+ // i18n-literal-ok Generic helper type signature, not user-visible copy.
90
+ export function bucketBy(items, keyFn) {
91
+ const map = new Map();
92
+ for (const item of items) {
93
+ const key = keyFn(item);
94
+ let bucket = map.get(key);
95
+ if (!bucket) {
96
+ bucket = [];
97
+ map.set(key, bucket);
98
+ }
99
+ bucket.push(item);
100
+ }
101
+ return map;
102
+ }
103
+ export function cryptoRowId() {
104
+ if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.randomUUID) {
105
+ return globalThis.crypto.randomUUID();
106
+ }
107
+ return `r_${Math.random().toString(36).slice(2, 10)}`;
108
+ }
@@ -0,0 +1,7 @@
1
+ import type { TravelerContactPickerProps } from "../../types.js";
2
+ import { type StepCommonProps } from "./shared.js";
3
+ export declare function TravelersStep({ draft, setDraft, shape, renderTravelerContactPicker, warnings, }: StepCommonProps & {
4
+ renderTravelerContactPicker?: (props: TravelerContactPickerProps) => React.ReactNode;
5
+ warnings?: ReadonlyArray<string>;
6
+ }): React.ReactElement;
7
+ //# sourceMappingURL=travelers-step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"travelers-step.d.ts","sourceRoot":"","sources":["../../../../src/journey/components/journey-steps/travelers-step.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA;AAEhE,OAAO,EAQL,KAAK,eAAe,EACrB,MAAM,aAAa,CAAA;AA+BpB,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,KAAK,EACL,2BAA2B,EAC3B,QAAQ,GACT,EAAE,eAAe,GAAG;IACnB,2BAA2B,CAAC,EAAE,CAAC,KAAK,EAAE,0BAA0B,KAAK,KAAK,CAAC,SAAS,CAAA;IACpF,QAAQ,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACjC,GAAG,KAAK,CAAC,YAAY,CAwGrB"}