@voyant-travel/flights-react 0.119.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +74 -0
- package/dist/admin/index.d.ts +134 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +122 -0
- package/dist/admin/pages/flight-book-page.d.ts +12 -0
- package/dist/admin/pages/flight-book-page.d.ts.map +1 -0
- package/dist/admin/pages/flight-book-page.js +40 -0
- package/dist/admin/pages/flights-index-page.d.ts +14 -0
- package/dist/admin/pages/flights-index-page.d.ts.map +1 -0
- package/dist/admin/pages/flights-index-page.js +28 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +75 -0
- package/dist/components/airline-logo.d.ts +19 -0
- package/dist/components/airline-logo.d.ts.map +1 -0
- package/dist/components/airline-logo.js +18 -0
- package/dist/components/airport-combobox.d.ts +20 -0
- package/dist/components/airport-combobox.d.ts.map +1 -0
- package/dist/components/airport-combobox.js +31 -0
- package/dist/components/billing-pickers.d.ts +19 -0
- package/dist/components/billing-pickers.d.ts.map +1 -0
- package/dist/components/billing-pickers.js +148 -0
- package/dist/components/flight-baggage-step.d.ts +32 -0
- package/dist/components/flight-baggage-step.d.ts.map +1 -0
- package/dist/components/flight-baggage-step.js +119 -0
- package/dist/components/flight-billing-step.d.ts +69 -0
- package/dist/components/flight-billing-step.d.ts.map +1 -0
- package/dist/components/flight-billing-step.js +117 -0
- package/dist/components/flight-booking-journey.d.ts +31 -0
- package/dist/components/flight-booking-journey.d.ts.map +1 -0
- package/dist/components/flight-booking-journey.js +103 -0
- package/dist/components/flight-booking-ledger.d.ts +53 -0
- package/dist/components/flight-booking-ledger.d.ts.map +1 -0
- package/dist/components/flight-booking-ledger.js +104 -0
- package/dist/components/flight-booking-page.d.ts +25 -0
- package/dist/components/flight-booking-page.d.ts.map +1 -0
- package/dist/components/flight-booking-page.js +175 -0
- package/dist/components/flight-booking-shell-helpers.d.ts +29 -0
- package/dist/components/flight-booking-shell-helpers.d.ts.map +1 -0
- package/dist/components/flight-booking-shell-helpers.js +204 -0
- package/dist/components/flight-booking-shell-panels.d.ts +24 -0
- package/dist/components/flight-booking-shell-panels.d.ts.map +1 -0
- package/dist/components/flight-booking-shell-panels.js +39 -0
- package/dist/components/flight-booking-shell-types.d.ts +49 -0
- package/dist/components/flight-booking-shell-types.d.ts.map +1 -0
- package/dist/components/flight-booking-shell-types.js +18 -0
- package/dist/components/flight-booking-shell.d.ts +12 -0
- package/dist/components/flight-booking-shell.d.ts.map +1 -0
- package/dist/components/flight-booking-shell.js +210 -0
- package/dist/components/flight-contact-form.d.ts +16 -0
- package/dist/components/flight-contact-form.d.ts.map +1 -0
- package/dist/components/flight-contact-form.js +25 -0
- package/dist/components/flight-fare-upsell-step.d.ts +26 -0
- package/dist/components/flight-fare-upsell-step.d.ts.map +1 -0
- package/dist/components/flight-fare-upsell-step.js +169 -0
- package/dist/components/flight-filters-bar.d.ts +19 -0
- package/dist/components/flight-filters-bar.d.ts.map +1 -0
- package/dist/components/flight-filters-bar.js +98 -0
- package/dist/components/flight-itinerary.d.ts +28 -0
- package/dist/components/flight-itinerary.d.ts.map +1 -0
- package/dist/components/flight-itinerary.js +110 -0
- package/dist/components/flight-offer-detail.d.ts +21 -0
- package/dist/components/flight-offer-detail.d.ts.map +1 -0
- package/dist/components/flight-offer-detail.js +49 -0
- package/dist/components/flight-offer-row.d.ts +25 -0
- package/dist/components/flight-offer-row.d.ts.map +1 -0
- package/dist/components/flight-offer-row.js +78 -0
- package/dist/components/flight-order-confirmation.d.ts +13 -0
- package/dist/components/flight-order-confirmation.d.ts.map +1 -0
- package/dist/components/flight-order-confirmation.js +46 -0
- package/dist/components/flight-passenger-form.d.ts +49 -0
- package/dist/components/flight-passenger-form.d.ts.map +1 -0
- package/dist/components/flight-passenger-form.js +159 -0
- package/dist/components/flight-payment-selector.d.ts +13 -0
- package/dist/components/flight-payment-selector.d.ts.map +1 -0
- package/dist/components/flight-payment-selector.js +32 -0
- package/dist/components/flight-payment-step.d.ts +32 -0
- package/dist/components/flight-payment-step.d.ts.map +1 -0
- package/dist/components/flight-payment-step.js +81 -0
- package/dist/components/flight-search-form.d.ts +14 -0
- package/dist/components/flight-search-form.d.ts.map +1 -0
- package/dist/components/flight-search-form.js +58 -0
- package/dist/components/flight-seat-map.d.ts +32 -0
- package/dist/components/flight-seat-map.d.ts.map +1 -0
- package/dist/components/flight-seat-map.js +101 -0
- package/dist/components/flight-seats-step.d.ts +40 -0
- package/dist/components/flight-seats-step.d.ts.map +1 -0
- package/dist/components/flight-seats-step.js +214 -0
- package/dist/components/flight-services-step.d.ts +27 -0
- package/dist/components/flight-services-step.d.ts.map +1 -0
- package/dist/components/flight-services-step.js +123 -0
- package/dist/components/flights-page-panels.d.ts +27 -0
- package/dist/components/flights-page-panels.d.ts.map +1 -0
- package/dist/components/flights-page-panels.js +40 -0
- package/dist/components/flights-page-types.d.ts +39 -0
- package/dist/components/flights-page-types.d.ts.map +1 -0
- package/dist/components/flights-page-types.js +1 -0
- package/dist/components/flights-page-utils.d.ts +14 -0
- package/dist/components/flights-page-utils.d.ts.map +1 -0
- package/dist/components/flights-page-utils.js +79 -0
- package/dist/components/flights-page.d.ts +4 -0
- package/dist/components/flights-page.d.ts.map +1 -0
- package/dist/components/flights-page.js +209 -0
- package/dist/components/passenger-contact-picker.d.ts +16 -0
- package/dist/components/passenger-contact-picker.d.ts.map +1 -0
- package/dist/components/passenger-contact-picker.js +45 -0
- package/dist/components/pax-cabin-popover.d.ts +18 -0
- package/dist/components/pax-cabin-popover.d.ts.map +1 -0
- package/dist/components/pax-cabin-popover.js +35 -0
- package/dist/components/popular-routes.d.ts +42 -0
- package/dist/components/popular-routes.d.ts.map +1 -0
- package/dist/components/popular-routes.js +108 -0
- package/dist/hooks/index.d.ts +13 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +12 -0
- package/dist/hooks/use-aircraft.d.ts +17 -0
- package/dist/hooks/use-aircraft.d.ts.map +1 -0
- package/dist/hooks/use-aircraft.js +18 -0
- package/dist/hooks/use-airlines.d.ts +18 -0
- package/dist/hooks/use-airlines.d.ts.map +1 -0
- package/dist/hooks/use-airlines.js +18 -0
- package/dist/hooks/use-airport-search.d.ts +28 -0
- package/dist/hooks/use-airport-search.d.ts.map +1 -0
- package/dist/hooks/use-airport-search.js +23 -0
- package/dist/hooks/use-airports.d.ts +21 -0
- package/dist/hooks/use-airports.d.ts.map +1 -0
- package/dist/hooks/use-airports.js +17 -0
- package/dist/hooks/use-flight-ancillaries.d.ts +63 -0
- package/dist/hooks/use-flight-ancillaries.d.ts.map +1 -0
- package/dist/hooks/use-flight-ancillaries.js +24 -0
- package/dist/hooks/use-flight-book.d.ts +139 -0
- package/dist/hooks/use-flight-book.d.ts.map +1 -0
- package/dist/hooks/use-flight-book.js +24 -0
- package/dist/hooks/use-flight-offer.d.ts +106 -0
- package/dist/hooks/use-flight-offer.d.ts.map +1 -0
- package/dist/hooks/use-flight-offer.js +20 -0
- package/dist/hooks/use-flight-order.d.ts +286 -0
- package/dist/hooks/use-flight-order.d.ts.map +1 -0
- package/dist/hooks/use-flight-order.js +38 -0
- package/dist/hooks/use-flight-orders.d.ts +147 -0
- package/dist/hooks/use-flight-orders.d.ts.map +1 -0
- package/dist/hooks/use-flight-orders.js +31 -0
- package/dist/hooks/use-flight-search.d.ts +110 -0
- package/dist/hooks/use-flight-search.d.ts.map +1 -0
- package/dist/hooks/use-flight-search.js +18 -0
- package/dist/hooks/use-flight-seat-map.d.ts +49 -0
- package/dist/hooks/use-flight-seat-map.d.ts.map +1 -0
- package/dist/hooks/use-flight-seat-map.js +23 -0
- package/dist/hooks/use-saved-payment-methods.d.ts +23 -0
- package/dist/hooks/use-saved-payment-methods.d.ts.map +1 -0
- package/dist/hooks/use-saved-payment-methods.js +20 -0
- package/dist/i18n/en.d.ts +465 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +520 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/messages.d.ts +392 -0
- package/dist/i18n/messages.d.ts.map +1 -0
- package/dist/i18n/messages.js +1 -0
- package/dist/i18n/provider.d.ts +952 -0
- package/dist/i18n/provider.d.ts.map +1 -0
- package/dist/i18n/provider.js +44 -0
- package/dist/i18n/ro.d.ts +465 -0
- package/dist/i18n/ro.d.ts.map +1 -0
- package/dist/i18n/ro.js +520 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +1 -0
- package/dist/query-keys.d.ts +42 -0
- package/dist/query-keys.d.ts.map +1 -0
- package/dist/query-keys.js +22 -0
- package/dist/query-options.d.ts +827 -0
- package/dist/query-options.d.ts.map +1 -0
- package/dist/query-options.js +58 -0
- package/dist/schemas.d.ts +1658 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +295 -0
- package/dist/ui.d.ts +31 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +28 -0
- package/package.json +148 -0
- package/src/styles.css +11 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
4
|
+
import { Checkbox } from "@voyant-travel/ui/components/checkbox";
|
|
5
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@voyant-travel/ui/components/command";
|
|
6
|
+
import { CountryCombobox } from "@voyant-travel/ui/components/country-combobox";
|
|
7
|
+
import { Input } from "@voyant-travel/ui/components/input";
|
|
8
|
+
import { Label } from "@voyant-travel/ui/components/label";
|
|
9
|
+
import { PhoneInput } from "@voyant-travel/ui/components/phone-input";
|
|
10
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@voyant-travel/ui/components/popover";
|
|
11
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@voyant-travel/ui/components/tabs";
|
|
12
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
13
|
+
import { Building2, ChevronDown, User, Users } from "lucide-react";
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { flightsUiEn } from "../i18n/en.js";
|
|
16
|
+
import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
|
|
17
|
+
/**
|
|
18
|
+
* Two-tab billing step with Privat (personal) + Companie (business / VAT)
|
|
19
|
+
* shapes. Address fields are structured (line1/city/postal/country) so the
|
|
20
|
+
* payload maps cleanly to `BillingAddress` on the payment intent. Pickers
|
|
21
|
+
* for prefill from CRM are supplied as render-prop slots so this component
|
|
22
|
+
* stays decoupled from the CRM data layer.
|
|
23
|
+
*/
|
|
24
|
+
export function FlightBillingStep({ value, onChange, eligiblePassengers, renderPersonPicker, renderOrgPicker, }) {
|
|
25
|
+
const messages = useFlightsUiMessagesOrDefault();
|
|
26
|
+
const apply = (prefill) => onChange({ ...value, ...prefill });
|
|
27
|
+
const set = (patch) => onChange({ ...value, ...patch });
|
|
28
|
+
const hasPassengerOptions = (eligiblePassengers?.length ?? 0) > 0;
|
|
29
|
+
return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("h2", { className: "font-semibold text-base", children: messages.flightBillingStep.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: messages.flightBillingStep.description })] }), _jsxs(Tabs, { value: value.mode, onValueChange: (v) => set({ mode: v ?? "personal" }), children: [_jsxs(TabsList, { children: [_jsxs(TabsTrigger, { value: "personal", children: [_jsx(User, { className: "mr-1.5 h-3.5 w-3.5" }), messages.flightBillingStep.tabs.personal] }), _jsxs(TabsTrigger, { value: "company", children: [_jsx(Building2, { className: "mr-1.5 h-3.5 w-3.5" }), messages.flightBillingStep.tabs.company] })] }), _jsxs(TabsContent, { value: "personal", className: "mt-5 flex flex-col gap-4", children: [(hasPassengerOptions || renderPersonPicker) && (_jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [hasPassengerOptions && (_jsx(PassengerPickerTrigger, { passengers: eligiblePassengers ?? [], messages: messages, onPick: (p) => apply({
|
|
30
|
+
mode: "personal",
|
|
31
|
+
firstName: p.firstName,
|
|
32
|
+
...(p.middleName ? { middleName: p.middleName } : {}),
|
|
33
|
+
lastName: p.lastName,
|
|
34
|
+
}) })), renderPersonPicker?.(apply)] })), _jsx(NameRow, { value: value, onChange: set, messages: messages }), _jsx(ContactRow, { value: value, onChange: set, messages: messages }), _jsx(AddressBlock, { value: value, onChange: set, messages: messages }), _jsx(SaveDefaultRow, { value: value, onChange: set, messages: messages })] }), _jsxs(TabsContent, { value: "company", className: "mt-5 flex flex-col gap-4", children: [renderOrgPicker && _jsx("div", { className: "flex justify-end", children: renderOrgPicker(apply) }), _jsx(CompanyRow, { value: value, onChange: set, messages: messages }), _jsx(ContactRow, { value: value, onChange: set, workPhone: true, messages: messages }), _jsx(AddressBlock, { value: value, onChange: set, messages: messages }), _jsx(SaveDefaultRow, { value: value, onChange: set, messages: messages })] })] })] }));
|
|
35
|
+
}
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
function NameRow({ value, onChange, messages, }) {
|
|
38
|
+
return (_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(Field, { label: messages.flightBillingStep.fields.firstName, required: true, children: _jsx(Input, { value: value.firstName, onChange: (e) => onChange({ firstName: e.target.value }) }) }), _jsx(Field, { label: messages.flightBillingStep.fields.lastName, required: true, children: _jsx(Input, { value: value.lastName, onChange: (e) => onChange({ lastName: e.target.value }) }) })] }));
|
|
39
|
+
}
|
|
40
|
+
function CompanyRow({ value, onChange, messages, }) {
|
|
41
|
+
return (_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(Field, { label: messages.flightBillingStep.fields.companyName, required: true, children: _jsx(Input, { value: value.companyName ?? "", onChange: (e) => onChange({ companyName: e.target.value }) }) }), _jsx(Field, { label: messages.flightBillingStep.fields.vatNumber, required: true, children: _jsx(Input, { value: value.vatNumber ?? "", onChange: (e) => onChange({ vatNumber: e.target.value }), placeholder: messages.flightBillingStep.placeholders.vatNumber }) })] }));
|
|
42
|
+
}
|
|
43
|
+
function ContactRow({ value, onChange, workPhone, messages, }) {
|
|
44
|
+
return (_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(Field, { label: messages.flightBillingStep.fields.email, required: true, children: _jsx(Input, { type: "email", value: value.email, onChange: (e) => onChange({ email: e.target.value }) }) }), _jsx(Field, { label: workPhone
|
|
45
|
+
? messages.flightBillingStep.fields.workPhone
|
|
46
|
+
: messages.flightBillingStep.fields.phone, children: _jsx(PhoneInput, { value: (value.phone ?? ""), onChange: (v) => onChange({ phone: v ? String(v) : undefined }), defaultCountry: "RO", international: true }) })] }));
|
|
47
|
+
}
|
|
48
|
+
function AddressBlock({ value, onChange, messages, }) {
|
|
49
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(Field, { label: messages.flightBillingStep.fields.streetAddress, required: true, children: _jsx(Input, { value: value.line1, onChange: (e) => onChange({ line1: e.target.value }), placeholder: messages.flightBillingStep.placeholders.streetAddress }) }), _jsx(Field, { label: messages.flightBillingStep.fields.addressLine2, children: _jsx(Input, { value: value.line2 ?? "", onChange: (e) => onChange({ line2: e.target.value }), placeholder: messages.flightBillingStep.placeholders.addressLine2 }) }), _jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-3", children: [_jsx(Field, { label: messages.flightBillingStep.fields.city, required: true, children: _jsx(Input, { value: value.city, onChange: (e) => onChange({ city: e.target.value }) }) }), _jsx(Field, { label: messages.flightBillingStep.fields.postalCode, children: _jsx(Input, { value: value.postalCode ?? "", onChange: (e) => onChange({ postalCode: e.target.value }) }) }), _jsx(Field, { label: messages.flightBillingStep.fields.country, required: true, children: _jsx(CountryCombobox, { value: value.countryCode || null, onChange: (code) => onChange({ countryCode: code ?? "" }) }) })] })] }));
|
|
50
|
+
}
|
|
51
|
+
function SaveDefaultRow({ value, onChange, messages, }) {
|
|
52
|
+
const id = "billing-save-default";
|
|
53
|
+
return (_jsxs("div", { className: "flex items-center gap-2 text-muted-foreground text-sm", children: [_jsx(Checkbox, { id: id, checked: !!value.saveAsDefault, onCheckedChange: (v) => onChange({ saveAsDefault: !!v }) }), _jsx("label", { htmlFor: id, className: "cursor-pointer", children: messages.flightBillingStep.saveDefault })] }));
|
|
54
|
+
}
|
|
55
|
+
function Field({ label, required, children, }) {
|
|
56
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: cn("text-[11px] uppercase tracking-wider text-muted-foreground"), children: [label, required && _jsx("span", { className: "ml-0.5 text-destructive", children: "*" })] }), children] }));
|
|
57
|
+
}
|
|
58
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
/**
|
|
61
|
+
* Compact "Pick from passengers" popover. Lists adult passengers entered
|
|
62
|
+
* upstream — the operator can click one to copy their first/middle/last name
|
|
63
|
+
* into the billing recipient. Self-contained: doesn't depend on CRM data.
|
|
64
|
+
*/
|
|
65
|
+
function PassengerPickerTrigger({ passengers, messages, onPick, }) {
|
|
66
|
+
const [open, setOpen] = useState(false);
|
|
67
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "gap-2" }), children: [_jsx(Users, { className: "h-3.5 w-3.5" }), messages.flightBillingStep.pickFromPassengers, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsx(PopoverContent, { className: "w-[320px] p-0", align: "end", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: messages.flightBillingStep.placeholders.searchPassengers }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: messages.flightBillingStep.noMatchingPassengers }), _jsx(CommandGroup, { children: passengers.map((p) => {
|
|
68
|
+
const fullName = [p.firstName, p.middleName, p.lastName]
|
|
69
|
+
.filter((s) => s?.trim())
|
|
70
|
+
.join(" ");
|
|
71
|
+
return (_jsx(CommandItem, { value: fullName, onSelect: () => {
|
|
72
|
+
onPick(p);
|
|
73
|
+
setOpen(false);
|
|
74
|
+
}, children: _jsx("span", { className: "truncate font-medium text-sm", children: fullName || messages.common.noValue }) }, p.id));
|
|
75
|
+
}) })] })] }) })] }));
|
|
76
|
+
}
|
|
77
|
+
export function emptyBillingValue() {
|
|
78
|
+
return {
|
|
79
|
+
mode: "personal",
|
|
80
|
+
firstName: "",
|
|
81
|
+
lastName: "",
|
|
82
|
+
email: "",
|
|
83
|
+
line1: "",
|
|
84
|
+
city: "",
|
|
85
|
+
countryCode: "",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Validate the billing value. Returns the first error message, or null
|
|
90
|
+
* when valid. Drives the journey's Continue gate.
|
|
91
|
+
*/
|
|
92
|
+
export function validateBilling(v) {
|
|
93
|
+
const messages = flightsUiEn.flightBillingStep.validation;
|
|
94
|
+
if (!v.email.trim())
|
|
95
|
+
return messages.emailRequired;
|
|
96
|
+
if (!/^\S+@\S+\.\S+$/.test(v.email.trim()))
|
|
97
|
+
return messages.emailInvalid;
|
|
98
|
+
if (!v.line1.trim())
|
|
99
|
+
return messages.streetAddressRequired;
|
|
100
|
+
if (!v.city.trim())
|
|
101
|
+
return messages.cityRequired;
|
|
102
|
+
if (!v.countryCode.trim())
|
|
103
|
+
return messages.countryRequired;
|
|
104
|
+
if (v.mode === "personal") {
|
|
105
|
+
if (!v.firstName.trim())
|
|
106
|
+
return messages.firstNameRequired;
|
|
107
|
+
if (!v.lastName.trim())
|
|
108
|
+
return messages.lastNameRequired;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
if (!v.companyName?.trim())
|
|
112
|
+
return messages.companyNameRequired;
|
|
113
|
+
if (!v.vatNumber?.trim())
|
|
114
|
+
return messages.vatNumberRequired;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FlightBookRequest, FlightOffer, FlightOrder, PassengerCounts } from "@voyant-travel/flights/contract/types";
|
|
2
|
+
import { type FlightPassengerFormProps } from "./flight-passenger-form.js";
|
|
3
|
+
export interface FlightBookingJourneyProps {
|
|
4
|
+
/** The offer to book — usually returned by the priceOffer call. */
|
|
5
|
+
offer: FlightOffer;
|
|
6
|
+
/** Passenger counts captured at search time. */
|
|
7
|
+
passengers: PassengerCounts;
|
|
8
|
+
/** Submit handler — wire to the useFlightBook mutation. */
|
|
9
|
+
onBook: (request: FlightBookRequest) => Promise<FlightOrder> | FlightOrder;
|
|
10
|
+
/** Surfaced via book result; parent typically navigates to /orders/:id. */
|
|
11
|
+
onBooked?: (order: FlightOrder) => void;
|
|
12
|
+
/** "Back to results" affordance. */
|
|
13
|
+
onCancel?: () => void;
|
|
14
|
+
/** Optional formatter so passenger card shows airline/airport names. */
|
|
15
|
+
carrierName?: (iataCode: string) => string | undefined;
|
|
16
|
+
airportName?: (iataCode: string) => string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Optional render slot for a person picker on each passenger card.
|
|
19
|
+
* Forwarded to `FlightPassengerForm.renderPicker` — operators wire a
|
|
20
|
+
* CRM-aware picker here so users can pick existing contacts as travelers.
|
|
21
|
+
*/
|
|
22
|
+
renderPassengerPicker?: FlightPassengerFormProps["renderPicker"];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Multi-step booking journey: Review → Passengers → Contact + Payment →
|
|
26
|
+
* Confirm. Owns the in-progress form state; submits via `onBook` on the
|
|
27
|
+
* final step. Each step gates Continue with its own validator so the user
|
|
28
|
+
* can't advance with incomplete data.
|
|
29
|
+
*/
|
|
30
|
+
export declare function FlightBookingJourney({ offer, passengers, onBook, onBooked, onCancel, carrierName, airportName, renderPassengerPicker, }: FlightBookingJourneyProps): import("react/jsx-runtime").JSX.Element | null;
|
|
31
|
+
//# sourceMappingURL=flight-booking-journey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-booking-journey.d.ts","sourceRoot":"","sources":["../../src/components/flight-booking-journey.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,WAAW,EACX,WAAW,EAEX,eAAe,EAEhB,MAAM,uCAAuC,CAAA;AAY9C,OAAO,EAEL,KAAK,wBAAwB,EAE9B,MAAM,4BAA4B,CAAA;AAYnC,MAAM,WAAW,yBAAyB;IACxC,mEAAmE;IACnE,KAAK,EAAE,WAAW,CAAA;IAClB,gDAAgD;IAChD,UAAU,EAAE,eAAe,CAAA;IAC3B,2DAA2D;IAC3D,MAAM,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAAA;IAC1E,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACvC,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,wEAAwE;IACxE,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACtD,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACtD;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,wBAAwB,CAAC,cAAc,CAAC,CAAA;CACjE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,UAAU,EACV,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,WAAW,EACX,qBAAqB,GACtB,EAAE,yBAAyB,kDA2H3B"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
4
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
5
|
+
import { Check, ChevronLeft, ChevronRight } from "lucide-react";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { useFlightsUiI18nOrDefault } from "../i18n/index.js";
|
|
8
|
+
import { FlightContactForm, validateContact, } from "./flight-contact-form.js";
|
|
9
|
+
import { FlightOfferDetail } from "./flight-offer-detail.js";
|
|
10
|
+
import { FlightPassengerForm, validatePassengers, } from "./flight-passenger-form.js";
|
|
11
|
+
import { FlightPaymentSelector } from "./flight-payment-selector.js";
|
|
12
|
+
const STEPS = [
|
|
13
|
+
{ id: "review" },
|
|
14
|
+
{ id: "passengers" },
|
|
15
|
+
{ id: "contact" },
|
|
16
|
+
{ id: "confirm" },
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Multi-step booking journey: Review → Passengers → Contact + Payment →
|
|
20
|
+
* Confirm. Owns the in-progress form state; submits via `onBook` on the
|
|
21
|
+
* final step. Each step gates Continue with its own validator so the user
|
|
22
|
+
* can't advance with incomplete data.
|
|
23
|
+
*/
|
|
24
|
+
export function FlightBookingJourney({ offer, passengers, onBook, onBooked, onCancel, carrierName, airportName, renderPassengerPicker, }) {
|
|
25
|
+
const i18n = useFlightsUiI18nOrDefault();
|
|
26
|
+
const messages = i18n.messages.flightBookingJourney;
|
|
27
|
+
const [stepIdx, setStepIdx] = useState(0);
|
|
28
|
+
const [paxList, setPaxList] = useState([]);
|
|
29
|
+
const [contact, setContact] = useState({});
|
|
30
|
+
const [payment, setPayment] = useState({ type: "hold" });
|
|
31
|
+
const [submitting, setSubmitting] = useState(false);
|
|
32
|
+
const [error, setError] = useState(null);
|
|
33
|
+
const step = STEPS[stepIdx];
|
|
34
|
+
if (!step)
|
|
35
|
+
return null;
|
|
36
|
+
const paxErrors = validatePassengers(paxList);
|
|
37
|
+
const contactError = validateContact(contact);
|
|
38
|
+
const canContinue = (() => {
|
|
39
|
+
switch (step.id) {
|
|
40
|
+
case "review":
|
|
41
|
+
return true;
|
|
42
|
+
case "passengers":
|
|
43
|
+
return Object.keys(paxErrors).length === 0 && paxList.length > 0;
|
|
44
|
+
case "contact":
|
|
45
|
+
return contactError == null;
|
|
46
|
+
case "confirm":
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
const goNext = () => {
|
|
51
|
+
setStepIdx((i) => Math.min(STEPS.length - 1, i + 1));
|
|
52
|
+
};
|
|
53
|
+
const goBack = () => {
|
|
54
|
+
setStepIdx((i) => Math.max(0, i - 1));
|
|
55
|
+
};
|
|
56
|
+
const submit = async () => {
|
|
57
|
+
setError(null);
|
|
58
|
+
setSubmitting(true);
|
|
59
|
+
try {
|
|
60
|
+
const order = await onBook({
|
|
61
|
+
offerId: offer.offerId,
|
|
62
|
+
offer,
|
|
63
|
+
passengers: paxList,
|
|
64
|
+
contact,
|
|
65
|
+
paymentIntent: payment,
|
|
66
|
+
});
|
|
67
|
+
onBooked?.(order);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
setSubmitting(false);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(Stepper, { currentIdx: stepIdx, messages: messages.steps }), error && (_jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive", children: error })), _jsxs("div", { className: "flex flex-col gap-4", children: [step.id === "review" && (_jsxs("div", { className: "rounded-xl border bg-card p-5 shadow-sm", children: [_jsx("h2", { className: "mb-4 text-base font-semibold", children: messages.reviewTitle }), _jsx(FlightOfferDetail, { offer: offer, carrierName: carrierName, airportName: airportName })] })), step.id === "passengers" && (_jsx(FlightPassengerForm, { counts: passengers, value: paxList, onChange: setPaxList, renderPicker: renderPassengerPicker })), step.id === "contact" && (_jsxs(_Fragment, { children: [_jsx(FlightContactForm, { value: contact, onChange: setContact }), _jsx(FlightPaymentSelector, { value: payment, onChange: setPayment })] })), step.id === "confirm" && (_jsx(ConfirmSummary, { offer: offer, passengers: paxList, contact: contact, payment: payment, i18n: i18n }))] }), _jsxs("div", { className: "flex items-center justify-between border-t pt-4", children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => (stepIdx === 0 ? onCancel?.() : goBack()), disabled: submitting, children: [_jsx(ChevronLeft, { className: "mr-1 h-4 w-4" }), stepIdx === 0 ? messages.backToResults : messages.back] }), step.id === "confirm" ? (_jsx(Button, { onClick: submit, disabled: submitting, children: submitting ? messages.booking : messages.confirmBooking })) : (_jsxs(Button, { onClick: goNext, disabled: !canContinue, children: [messages.continue, _jsx(ChevronRight, { className: "ml-1 h-4 w-4" })] }))] })] }));
|
|
77
|
+
}
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
// Stepper indicator
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
function Stepper({ currentIdx, messages, }) {
|
|
82
|
+
return (_jsx("ol", { className: "flex items-center gap-2", children: STEPS.map((s, i) => {
|
|
83
|
+
const isActive = i === currentIdx;
|
|
84
|
+
const isComplete = i < currentIdx;
|
|
85
|
+
return (_jsxs("li", { className: "flex flex-1 items-center gap-2", children: [_jsx("div", { className: cn("flex h-7 w-7 shrink-0 items-center justify-center rounded-full border text-xs font-medium tabular-nums", isComplete && "border-primary bg-primary text-primary-foreground", isActive && !isComplete && "border-primary text-primary", !isActive && !isComplete && "border-border text-muted-foreground"), children: isComplete ? _jsx(Check, { className: "h-3.5 w-3.5" }) : i + 1 }), _jsx("span", { className: cn("truncate text-sm", isActive ? "font-medium text-foreground" : "text-muted-foreground"), children: messages[s.id] }), i < STEPS.length - 1 && _jsx("div", { className: "h-px flex-1 bg-border" })] }, s.id));
|
|
86
|
+
}) }));
|
|
87
|
+
}
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
// Final confirmation summary
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
function ConfirmSummary({ offer, passengers, contact, payment, i18n, }) {
|
|
92
|
+
const messages = i18n.messages.flightBookingJourney;
|
|
93
|
+
return (_jsxs("div", { className: "rounded-xl border bg-card p-5 shadow-sm", children: [_jsx("h2", { className: "mb-4 text-base font-semibold", children: messages.confirmBooking }), _jsx(Row, { label: messages.rows.total, children: _jsx("span", { className: "font-semibold tabular-nums", children: formatMoney(offer.totalPrice.amount, offer.totalPrice.currency, i18n) }) }), _jsx(Row, { label: messages.rows.passengers, children: passengers.length }), _jsx(Row, { label: messages.rows.contact, children: contact.email ?? i18n.messages.common.noValue }), _jsx(Row, { label: messages.rows.payment, children: _jsx("span", { className: "capitalize", children: payment.type.replace("_", " ") }) }), offer.expiresAt && (_jsx(Row, { label: messages.rows.offerExpires, children: _jsx("time", { dateTime: offer.expiresAt, children: i18n.formatDateTime(offer.expiresAt) }) })), _jsx("p", { className: "mt-4 text-xs text-muted-foreground", children: messages.confirmDescription })] }));
|
|
94
|
+
}
|
|
95
|
+
function Row({ label, children }) {
|
|
96
|
+
return (_jsxs("div", { className: "flex items-baseline justify-between border-b py-2 text-sm last:border-b-0", children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { children: children })] }));
|
|
97
|
+
}
|
|
98
|
+
function formatMoney(amount, currency, i18n) {
|
|
99
|
+
const n = Number(amount);
|
|
100
|
+
if (!Number.isFinite(n))
|
|
101
|
+
return `${amount} ${currency}`;
|
|
102
|
+
return i18n.formatCurrency(n, currency, { maximumFractionDigits: 0 });
|
|
103
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { FlightOffer, Money, PassengerCounts } from "@voyant-travel/flights/contract/types";
|
|
2
|
+
/**
|
|
3
|
+
* Per-leg selection passed through the booking journey. Two single-itinerary
|
|
4
|
+
* offers when round-trip; one when one-way. The shell synthesizes a combined
|
|
5
|
+
* offer from these at submit time.
|
|
6
|
+
*/
|
|
7
|
+
export interface FlightItinerarySelection {
|
|
8
|
+
outbound: FlightOffer;
|
|
9
|
+
return?: FlightOffer;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Optional ancillary line items rendered nested under the relevant leg.
|
|
13
|
+
* Phases 2/3 wire bag/seat picks here; for Phase 1 it's just a placeholder
|
|
14
|
+
* shape so the ledger doesn't need to change later.
|
|
15
|
+
*/
|
|
16
|
+
export interface LedgerLineItem {
|
|
17
|
+
label: string;
|
|
18
|
+
amount?: Money;
|
|
19
|
+
/** Free-text right-side value (e.g. "Included") when no money applies. */
|
|
20
|
+
meta?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface FlightBookingLedgerProps {
|
|
23
|
+
selection: FlightItinerarySelection;
|
|
24
|
+
passengers: PassengerCounts;
|
|
25
|
+
carrierName?: (iataCode: string) => string | undefined;
|
|
26
|
+
airportName?: (iataCode: string) => string | undefined;
|
|
27
|
+
/** Per-leg ancillary line items (bags / seats / extras). */
|
|
28
|
+
outboundExtras?: LedgerLineItem[];
|
|
29
|
+
returnExtras?: LedgerLineItem[];
|
|
30
|
+
/** Sticky CTA at the bottom — typically "Continue" or "Confirm". */
|
|
31
|
+
cta?: {
|
|
32
|
+
label: string;
|
|
33
|
+
onClick: () => void;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
};
|
|
37
|
+
/** Per-leg edit handlers — open the search results back up. */
|
|
38
|
+
onEditOutbound?: () => void;
|
|
39
|
+
onEditReturn?: () => void;
|
|
40
|
+
/** Step status hints — checks shown next to each section name. */
|
|
41
|
+
completedSections?: ReadonlySet<LedgerSection>;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
export type LedgerSection = "flights" | "passengers" | "bags" | "seats" | "services" | "documents" | "billing" | "payment";
|
|
45
|
+
/**
|
|
46
|
+
* Sticky right-rail price ledger. Mirrors the running total + per-leg
|
|
47
|
+
* breakdown that real airline checkouts use (Wizz/Ryanair/Lufthansa). One
|
|
48
|
+
* source of truth for the total — the shell passes ancillary picks down via
|
|
49
|
+
* `outboundExtras`/`returnExtras`, so the ledger doesn't need to know how
|
|
50
|
+
* they were collected.
|
|
51
|
+
*/
|
|
52
|
+
export declare function FlightBookingLedger({ selection, passengers, carrierName, airportName, outboundExtras, returnExtras, cta, onEditOutbound, onEditReturn, completedSections, className, }: FlightBookingLedgerProps): import("react/jsx-runtime").JSX.Element;
|
|
53
|
+
//# sourceMappingURL=flight-booking-ledger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-booking-ledger.d.ts","sourceRoot":"","sources":["../../src/components/flight-booking-ledger.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAA;AAQhG;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,WAAW,CAAA;IACrB,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,KAAK,CAAA;IACd,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,wBAAwB,CAAA;IACnC,UAAU,EAAE,eAAe,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACtD,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACtD,4DAA4D;IAC5D,cAAc,CAAC,EAAE,cAAc,EAAE,CAAA;IACjC,YAAY,CAAC,EAAE,cAAc,EAAE,CAAA;IAC/B,oEAAoE;IACpE,GAAG,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IACnF,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,YAAY,GACZ,MAAM,GACN,OAAO,GACP,UAAU,GACV,WAAW,GACX,SAAS,GACT,SAAS,CAAA;AAEb;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,EACd,YAAY,EACZ,GAAG,EACH,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,SAAS,GACV,EAAE,wBAAwB,2CA+D1B"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { formatMessage } from "@voyant-travel/i18n";
|
|
4
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
5
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
6
|
+
import { Check, Pencil, Plane, Users } from "lucide-react";
|
|
7
|
+
import { useFlightsUiI18nOrDefault } from "../i18n/index.js";
|
|
8
|
+
import { AirlineLogo } from "./airline-logo.js";
|
|
9
|
+
/**
|
|
10
|
+
* Sticky right-rail price ledger. Mirrors the running total + per-leg
|
|
11
|
+
* breakdown that real airline checkouts use (Wizz/Ryanair/Lufthansa). One
|
|
12
|
+
* source of truth for the total — the shell passes ancillary picks down via
|
|
13
|
+
* `outboundExtras`/`returnExtras`, so the ledger doesn't need to know how
|
|
14
|
+
* they were collected.
|
|
15
|
+
*/
|
|
16
|
+
export function FlightBookingLedger({ selection, passengers, carrierName, airportName, outboundExtras, returnExtras, cta, onEditOutbound, onEditReturn, completedSections, className, }) {
|
|
17
|
+
const i18n = useFlightsUiI18nOrDefault();
|
|
18
|
+
const messages = i18n.messages;
|
|
19
|
+
const total = computeTotal(selection, outboundExtras, returnExtras);
|
|
20
|
+
const paxTotal = (passengers.adults ?? 0) + (passengers.children ?? 0) + (passengers.infants ?? 0);
|
|
21
|
+
return (_jsxs("aside", { className: cn("flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-4 shadow-sm", className), children: [_jsx(LegBlock, { label: selection.return
|
|
22
|
+
? messages.flightBookingLedger.outbound
|
|
23
|
+
: messages.flightBookingLedger.flight, offer: selection.outbound, carrierName: carrierName, airportName: airportName, extras: outboundExtras, onEdit: onEditOutbound, complete: completedSections?.has("flights"), i18n: i18n }), selection.return && (_jsx(LegBlock, { label: messages.flightBookingLedger.return, offer: selection.return, carrierName: carrierName, airportName: airportName, extras: returnExtras, onEdit: onEditReturn, complete: completedSections?.has("flights"), i18n: i18n })), _jsx(SectionRow, { icon: _jsx(Users, { className: "h-3.5 w-3.5" }), label: messages.flightBookingLedger.passengers, right: `${paxTotal} ${messages.common.pax}`, complete: completedSections?.has("passengers") }), _jsx(PlaceholderSections, { completed: completedSections, messages: messages }), _jsxs("div", { className: "mt-2 flex items-center justify-between border-t pt-3", children: [_jsx("span", { className: "font-medium text-sm", children: messages.common.total }), _jsx("span", { className: "font-semibold text-lg tabular-nums", children: formatMoney(total.amount, total.currency, i18n) })] }), cta && (_jsx(Button, { className: "w-full", onClick: cta.onClick, disabled: cta.disabled || cta.loading, children: cta.loading ? messages.flightBookingLedger.working : cta.label }))] }));
|
|
24
|
+
}
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
function LegBlock({ label, offer, carrierName, airportName, extras, onEdit, complete, i18n, }) {
|
|
27
|
+
const messages = i18n.messages;
|
|
28
|
+
const itin = offer.itineraries[0];
|
|
29
|
+
if (!itin)
|
|
30
|
+
return null;
|
|
31
|
+
const segs = itin.segments;
|
|
32
|
+
const first = segs[0];
|
|
33
|
+
const last = segs[segs.length - 1];
|
|
34
|
+
if (!first || !last)
|
|
35
|
+
return null;
|
|
36
|
+
const carriers = Array.from(new Set(segs.map((s) => s.carrierCode)));
|
|
37
|
+
const stops = segs.length - 1;
|
|
38
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [complete ? (_jsx(Check, { className: "h-3.5 w-3.5 text-emerald-600" })) : (_jsx(Plane, { className: "h-3.5 w-3.5 text-muted-foreground" })), _jsx("span", { className: "font-medium text-[11px] uppercase tracking-wider text-muted-foreground", children: label })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-semibold text-sm tabular-nums", children: formatMoney(offer.totalPrice.amount, offer.totalPrice.currency, i18n) }), onEdit && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-6 px-1.5 text-muted-foreground", onClick: onEdit, children: _jsx(Pencil, { className: "h-3 w-3" }) }))] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex shrink-0 items-center -space-x-1", children: carriers.map((code) => (_jsx(AirlineLogo, { iataCode: code, name: carrierName?.(code), size: 18 }, code))) }), _jsxs("div", { className: "flex min-w-0 flex-col leading-tight", children: [_jsxs("span", { className: "truncate font-medium text-sm", children: [airportName?.(first.departure.iataCode) ?? first.departure.iataCode, " \u2192", " ", airportName?.(last.arrival.iataCode) ?? last.arrival.iataCode] }), _jsxs("span", { className: "text-[11px] text-muted-foreground", children: [formatDate(first.departure.at), " \u00B7 ", formatTime(first.departure.at), " \u2013", " ", formatTime(last.arrival.at), " \u00B7 ", formatStops(stops, messages)] })] })] }), extras && extras.length > 0 && (_jsx("ul", { className: "flex flex-col gap-1 border-t pt-2", children: extras.map((x, i) => (_jsxs("li", { className: "flex items-center justify-between text-muted-foreground text-xs", children: [_jsx("span", { children: x.label }), _jsx("span", { className: "tabular-nums", children: x.amount ? formatMoney(x.amount.amount, x.amount.currency, i18n) : (x.meta ?? "") })] }, i))) }))] }));
|
|
39
|
+
}
|
|
40
|
+
function SectionRow({ icon, label, right, complete, }) {
|
|
41
|
+
return (_jsxs("div", { className: "flex items-center justify-between border-t pt-3", children: [_jsxs("span", { className: "flex items-center gap-1.5 font-medium text-[11px] uppercase tracking-wider text-muted-foreground", children: [complete ? (_jsx(Check, { className: "h-3.5 w-3.5 text-emerald-600" })) : (_jsx("span", { className: "text-muted-foreground", children: icon })), label] }), right && _jsx("span", { className: "text-muted-foreground text-xs", children: right })] }));
|
|
42
|
+
}
|
|
43
|
+
function PlaceholderSections({ completed, messages, }) {
|
|
44
|
+
// Phase 1 only renders Passengers above; later phases will replace this with
|
|
45
|
+
// bags/seats/services/documents/billing rows. The shape stays consistent so
|
|
46
|
+
// the ledger doesn't need to change later.
|
|
47
|
+
if (!completed)
|
|
48
|
+
return null;
|
|
49
|
+
const items = [];
|
|
50
|
+
if (completed.has("billing"))
|
|
51
|
+
items.push({ id: "billing", label: messages.flightBookingLedger.billing });
|
|
52
|
+
if (completed.has("payment"))
|
|
53
|
+
items.push({ id: "payment", label: messages.flightBookingLedger.payment });
|
|
54
|
+
if (items.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
return (_jsx(_Fragment, { children: items.map((it) => (_jsx(SectionRow, { icon: _jsx("span", {}), label: it.label, complete: completed.has(it.id) }, it.id))) }));
|
|
57
|
+
}
|
|
58
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
59
|
+
function computeTotal(selection, outboundExtras, returnExtras) {
|
|
60
|
+
const currency = selection.outbound.totalPrice.currency;
|
|
61
|
+
let amount = num(selection.outbound.totalPrice.amount);
|
|
62
|
+
if (selection.return)
|
|
63
|
+
amount += num(selection.return.totalPrice.amount);
|
|
64
|
+
for (const x of outboundExtras ?? [])
|
|
65
|
+
amount += num(x.amount?.amount);
|
|
66
|
+
for (const x of returnExtras ?? [])
|
|
67
|
+
amount += num(x.amount?.amount);
|
|
68
|
+
return { amount: amount.toFixed(2), currency };
|
|
69
|
+
}
|
|
70
|
+
function num(v) {
|
|
71
|
+
if (!v)
|
|
72
|
+
return 0;
|
|
73
|
+
const n = Number(v);
|
|
74
|
+
return Number.isFinite(n) ? n : 0;
|
|
75
|
+
}
|
|
76
|
+
function formatMoney(amount, currency, i18n) {
|
|
77
|
+
const n = Number(amount);
|
|
78
|
+
if (!Number.isFinite(n))
|
|
79
|
+
return `${amount} ${currency}`;
|
|
80
|
+
return i18n.formatCurrency(n, currency, { maximumFractionDigits: 0 });
|
|
81
|
+
}
|
|
82
|
+
function formatTime(iso) {
|
|
83
|
+
const d = new Date(iso);
|
|
84
|
+
if (Number.isNaN(d.getTime()))
|
|
85
|
+
return iso;
|
|
86
|
+
return new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit" }).format(d);
|
|
87
|
+
}
|
|
88
|
+
function formatStops(stops, messages) {
|
|
89
|
+
if (stops === 0)
|
|
90
|
+
return messages.common.stops.nonstop;
|
|
91
|
+
return formatMessage(stops === 1 ? messages.common.stops.oneStop : messages.common.stops.manyStops, {
|
|
92
|
+
count: stops,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function formatDate(iso) {
|
|
96
|
+
const d = new Date(iso);
|
|
97
|
+
if (Number.isNaN(d.getTime()))
|
|
98
|
+
return iso;
|
|
99
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
100
|
+
weekday: "short",
|
|
101
|
+
day: "numeric",
|
|
102
|
+
month: "short",
|
|
103
|
+
}).format(d);
|
|
104
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FlightBookRequest, FlightOrder, PassengerCounts } from "@voyant-travel/flights/contract/types";
|
|
2
|
+
import type { BillingValue } from "./flight-billing-step.js";
|
|
3
|
+
import { type FlightBookingShellProps } from "./flight-booking-shell.js";
|
|
4
|
+
import type { PaymentStepCapabilities } from "./flight-payment-step.js";
|
|
5
|
+
export interface FlightBookingPageProps {
|
|
6
|
+
outboundOfferId: string;
|
|
7
|
+
returnOfferId?: string;
|
|
8
|
+
passengers: PassengerCounts;
|
|
9
|
+
onBackToSearch: () => void;
|
|
10
|
+
onBook: (request: FlightBookRequest) => Promise<FlightOrder> | FlightOrder;
|
|
11
|
+
onBooked?: (order: FlightOrder) => void;
|
|
12
|
+
onEditOutbound?: () => void;
|
|
13
|
+
onEditReturn?: () => void;
|
|
14
|
+
onSaveBillingDefaults?: (value: BillingValue) => void;
|
|
15
|
+
paymentCapabilities?: PaymentStepCapabilities;
|
|
16
|
+
renderPassengerPicker?: FlightBookingShellProps["renderPassengerPicker"];
|
|
17
|
+
renderBillingPersonPicker?: (apply: (prefill: Partial<BillingValue>) => void, helpers: {
|
|
18
|
+
onPersonSelected: (personId: string | null) => void;
|
|
19
|
+
}) => React.ReactNode;
|
|
20
|
+
renderBillingOrgPicker?: (apply: (prefill: Partial<BillingValue>) => void) => React.ReactNode;
|
|
21
|
+
onAddPassengerContact?: () => void;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function FlightBookingPage({ outboundOfferId, returnOfferId, passengers, onBackToSearch, onBook, onBooked, onEditOutbound, onEditReturn, onSaveBillingDefaults, paymentCapabilities, renderPassengerPicker, renderBillingPersonPicker, renderBillingOrgPicker, onAddPassengerContact, className, }: FlightBookingPageProps): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
//# sourceMappingURL=flight-booking-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-booking-page.d.ts","sourceRoot":"","sources":["../../src/components/flight-booking-page.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,iBAAiB,EAEjB,WAAW,EACX,eAAe,EAChB,MAAM,uCAAuC,CAAA;AAiB9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE5D,OAAO,EAKL,KAAK,uBAAuB,EAC7B,MAAM,2BAA2B,CAAA;AAClC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAIvE,MAAM,WAAW,sBAAsB;IACrC,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,eAAe,CAAA;IAC3B,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,MAAM,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAAA;IAC1E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACvC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IACrD,mBAAmB,CAAC,EAAE,uBAAuB,CAAA;IAC7C,qBAAqB,CAAC,EAAE,uBAAuB,CAAC,uBAAuB,CAAC,CAAA;IACxE,yBAAyB,CAAC,EAAE,CAC1B,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,EAC/C,OAAO,EAAE;QAAE,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;KAAE,KAC7D,KAAK,CAAC,SAAS,CAAA;IACpB,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,KAAK,KAAK,CAAC,SAAS,CAAA;IAC7F,qBAAqB,CAAC,EAAE,MAAM,IAAI,CAAA;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,eAAe,EACf,aAAa,EACb,UAAU,EACV,cAAc,EACd,MAAM,EACN,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,sBAAsB,EACtB,qBAAqB,EACrB,SAAS,GACV,EAAE,sBAAsB,2CA2MxB"}
|