@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,110 @@
|
|
|
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 { Badge } from "@voyant-travel/ui/components/badge";
|
|
5
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
6
|
+
import { Plane } from "lucide-react";
|
|
7
|
+
import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
|
|
8
|
+
import { AirlineLogo } from "./airline-logo.js";
|
|
9
|
+
/**
|
|
10
|
+
* Carrier-aware itinerary renderer. One itinerary = one direction of travel
|
|
11
|
+
* (outbound, return, or one leg of an open-jaw). Surfaces:
|
|
12
|
+
* - per-segment carrier + flight number
|
|
13
|
+
* - operating-vs-marketing carrier ("Operated by …") for codeshares
|
|
14
|
+
* - layover dwell time chips between segments
|
|
15
|
+
* - aircraft per segment
|
|
16
|
+
* - total journey duration
|
|
17
|
+
*
|
|
18
|
+
* `compact` strips the per-segment cards down to a single timeline row —
|
|
19
|
+
* suitable for the booking ledger.
|
|
20
|
+
*/
|
|
21
|
+
export function FlightItinerary({ itinerary, label, sublabel, carrierName, airportName, aircraftName, compact, className, }) {
|
|
22
|
+
const messages = useFlightsUiMessagesOrDefault();
|
|
23
|
+
const segs = itinerary.segments;
|
|
24
|
+
const first = segs[0];
|
|
25
|
+
const last = segs[segs.length - 1];
|
|
26
|
+
if (!first || !last)
|
|
27
|
+
return null;
|
|
28
|
+
const stops = segs.length - 1;
|
|
29
|
+
const totalDuration = itinerary.duration ?? deriveDuration(first, last);
|
|
30
|
+
const carriers = Array.from(new Set(segs.map((s) => s.carrierCode)));
|
|
31
|
+
if (compact) {
|
|
32
|
+
return (_jsxs("div", { className: cn("flex flex-col gap-1.5", className), children: [(label || sublabel) && (_jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [label && (_jsx("span", { className: "font-medium text-[11px] uppercase tracking-wider text-muted-foreground", children: label })), sublabel && _jsx("span", { className: "text-[11px] text-muted-foreground", children: sublabel })] })), _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: 20 }, code))) }), _jsxs("span", { className: "font-mono text-xs text-foreground", children: [first.departure.iataCode, " \u2192 ", last.arrival.iataCode] }), _jsxs("span", { className: "text-[11px] text-muted-foreground", children: [formatTime(first.departure.at), " \u2013 ", formatTime(last.arrival.at)] }), _jsxs("span", { className: "ml-auto text-[11px] text-muted-foreground", children: [formatStops(stops, messages), totalDuration && ` · ${formatDuration(totalDuration)}`] })] })] }));
|
|
33
|
+
}
|
|
34
|
+
return (_jsxs("div", { className: cn("flex flex-col gap-3", className), children: [(label || sublabel) && (_jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [label && (_jsx("h4", { className: "font-medium text-[11px] uppercase tracking-wider text-muted-foreground", children: label })), (sublabel || totalDuration) && (_jsxs("span", { className: "text-[11px] text-muted-foreground", children: [sublabel, sublabel && totalDuration && " · ", totalDuration &&
|
|
35
|
+
formatMessage(messages.flightItinerary.totalDuration, {
|
|
36
|
+
duration: formatDuration(totalDuration),
|
|
37
|
+
})] }))] })), _jsx("div", { className: "flex flex-col", children: segs.map((seg, idx) => (_jsx(SegmentBlock, { segment: seg, carrierName: carrierName, airportName: airportName, aircraftName: aircraftName, messages: messages, layoverBefore: idx > 0 ? layoverBetween(segs[idx - 1], seg) : null }, seg.segmentId))) })] }));
|
|
38
|
+
}
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
function SegmentBlock({ segment, carrierName, airportName, aircraftName, messages, layoverBefore, }) {
|
|
41
|
+
const isCodeshare = segment.operatingCarrierCode != null && segment.operatingCarrierCode !== segment.carrierCode;
|
|
42
|
+
return (_jsxs(_Fragment, { children: [layoverBefore && (_jsxs("div", { className: "my-1 flex items-center gap-2 px-2 text-xs text-muted-foreground", children: [_jsx("div", { className: "h-px flex-1 bg-border" }), _jsxs("span", { className: "rounded-full bg-muted px-2 py-0.5 text-[11px]", children: [messages.flightItinerary.layover, " \u00B7", " ", formatMessage(messages.flightItinerary.layoverIn, {
|
|
43
|
+
duration: layoverBefore.dwell,
|
|
44
|
+
airport: layoverBefore.airport,
|
|
45
|
+
})] }), _jsx("div", { className: "h-px flex-1 bg-border" })] })), _jsxs("div", { className: "rounded-lg border bg-card p-3", children: [_jsxs("div", { className: "mb-2 flex flex-wrap items-center gap-2", children: [_jsx(AirlineLogo, { iataCode: segment.carrierCode, name: carrierName?.(segment.carrierCode), size: 24 }), _jsx("span", { className: "font-medium text-sm", children: carrierName?.(segment.carrierCode) ?? segment.carrierCode }), _jsxs("span", { className: "font-mono text-muted-foreground text-xs", children: [segment.carrierCode, segment.flightNumber] }), isCodeshare && (_jsx(Badge, { variant: "secondary", className: "text-[10px] uppercase tracking-wide", children: formatMessage(messages.flightItinerary.operatedBy, {
|
|
46
|
+
carrier: carrierName?.(segment.operatingCarrierCode ?? "") ??
|
|
47
|
+
segment.operatingCarrierCode ??
|
|
48
|
+
"",
|
|
49
|
+
}) })), _jsx(Badge, { variant: "outline", className: "ml-auto capitalize", children: messages.common.cabinLabels[segment.cabin] })] }), _jsxs("div", { className: "grid grid-cols-[1fr_auto_1fr] items-center gap-3", children: [_jsx(Endpoint, { at: segment.departure.at, iata: segment.departure.iataCode, terminal: segment.departure.terminal, airportName: airportName?.(segment.departure.iataCode), messages: messages }), _jsxs("div", { className: "flex flex-col items-center gap-1 text-muted-foreground text-xs", children: [_jsx(Plane, { className: "h-3.5 w-3.5" }), segment.duration && _jsx("span", { children: formatDuration(segment.duration) })] }), _jsx(Endpoint, { at: segment.arrival.at, iata: segment.arrival.iataCode, terminal: segment.arrival.terminal, airportName: airportName?.(segment.arrival.iataCode), align: "end", messages: messages })] }), segment.aircraft && (_jsxs("div", { className: "mt-2 text-[11px] text-muted-foreground", children: [messages.flightItinerary.aircraft, " ", _jsx("span", { className: "text-foreground", children: aircraftName?.(segment.aircraft) ?? segment.aircraft })] }))] })] }));
|
|
50
|
+
}
|
|
51
|
+
function Endpoint({ at, iata, terminal, airportName, messages, align = "start", }) {
|
|
52
|
+
return (_jsxs("div", { className: cn("flex flex-col leading-tight", align === "end" ? "items-end" : "items-start"), children: [_jsx("span", { className: "font-semibold text-lg tabular-nums", children: formatTime(at) }), _jsx("span", { className: "font-mono text-muted-foreground text-xs", children: iata }), airportName && _jsx("span", { className: "text-[11px] text-muted-foreground", children: airportName }), terminal && (_jsx("span", { className: "text-[10px] text-muted-foreground", children: formatMessage(messages.flightItinerary.terminal, { terminal }) })), _jsx("span", { className: "mt-0.5 text-[10px] text-muted-foreground", children: formatDate(at) })] }));
|
|
53
|
+
}
|
|
54
|
+
function formatStops(stops, messages) {
|
|
55
|
+
if (stops === 0)
|
|
56
|
+
return messages.common.stops.nonstop;
|
|
57
|
+
return formatMessage(stops === 1 ? messages.common.stops.oneStop : messages.common.stops.manyStops, {
|
|
58
|
+
count: stops,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// ── Formatters ───────────────────────────────────────────────────────────────
|
|
62
|
+
function formatTime(iso) {
|
|
63
|
+
const d = new Date(iso);
|
|
64
|
+
if (Number.isNaN(d.getTime()))
|
|
65
|
+
return iso;
|
|
66
|
+
return new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit" }).format(d);
|
|
67
|
+
}
|
|
68
|
+
function formatDate(iso) {
|
|
69
|
+
const d = new Date(iso);
|
|
70
|
+
if (Number.isNaN(d.getTime()))
|
|
71
|
+
return iso;
|
|
72
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
73
|
+
weekday: "short",
|
|
74
|
+
day: "numeric",
|
|
75
|
+
month: "short",
|
|
76
|
+
}).format(d);
|
|
77
|
+
}
|
|
78
|
+
function formatDuration(iso) {
|
|
79
|
+
if (!iso)
|
|
80
|
+
return "";
|
|
81
|
+
const m = /^PT(?:(\d+)H)?(?:(\d+)M)?$/.exec(iso);
|
|
82
|
+
if (!m)
|
|
83
|
+
return iso;
|
|
84
|
+
const h = m[1] ? `${m[1]}h` : "";
|
|
85
|
+
const min = m[2] ? `${m[2]}m` : "";
|
|
86
|
+
return [h, min].filter(Boolean).join(" ") || iso;
|
|
87
|
+
}
|
|
88
|
+
function layoverBetween(prev, next) {
|
|
89
|
+
if (!prev)
|
|
90
|
+
return null;
|
|
91
|
+
const a = new Date(prev.arrival.at).getTime();
|
|
92
|
+
const b = new Date(next.departure.at).getTime();
|
|
93
|
+
if (Number.isNaN(a) || Number.isNaN(b) || b <= a)
|
|
94
|
+
return null;
|
|
95
|
+
const minutes = Math.round((b - a) / 60000);
|
|
96
|
+
const h = Math.floor(minutes / 60);
|
|
97
|
+
const m = minutes % 60;
|
|
98
|
+
const dwell = [h ? `${h}h` : "", m ? `${m}m` : ""].filter(Boolean).join(" ") || `${minutes}m`;
|
|
99
|
+
return { airport: prev.arrival.iataCode, dwell };
|
|
100
|
+
}
|
|
101
|
+
function deriveDuration(first, last) {
|
|
102
|
+
const a = new Date(first.departure.at).getTime();
|
|
103
|
+
const b = new Date(last.arrival.at).getTime();
|
|
104
|
+
if (Number.isNaN(a) || Number.isNaN(b) || b <= a)
|
|
105
|
+
return undefined;
|
|
106
|
+
const minutes = Math.round((b - a) / 60000);
|
|
107
|
+
const h = Math.floor(minutes / 60);
|
|
108
|
+
const m = minutes % 60;
|
|
109
|
+
return `PT${h}H${m}M`;
|
|
110
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FlightOffer } from "@voyant-travel/flights/contract/types";
|
|
2
|
+
export interface FlightOfferDetailProps {
|
|
3
|
+
offer: FlightOffer;
|
|
4
|
+
/** Resolves IATA carrier code → human-readable name. */
|
|
5
|
+
carrierName?: (iataCode: string) => string | undefined;
|
|
6
|
+
/** Resolves IATA airport code → "City Name (IATA)" or similar. */
|
|
7
|
+
airportName?: (iataCode: string) => string | undefined;
|
|
8
|
+
/** Resolves IATA aircraft code → "Boeing 737-800" or similar. */
|
|
9
|
+
aircraftName?: (iataCode: string) => string | undefined;
|
|
10
|
+
/** Override per-itinerary labels (defaults to "Outbound" / "Return" / "Leg N"). */
|
|
11
|
+
itineraryLabels?: string[];
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Full-fidelity flight offer view for the detail sheet. Composes the shared
|
|
16
|
+
* `FlightItinerary` renderer for each leg, then a fare breakdown + offer
|
|
17
|
+
* metadata. Codeshare segments and layover dwell times are surfaced by the
|
|
18
|
+
* itinerary component itself.
|
|
19
|
+
*/
|
|
20
|
+
export declare function FlightOfferDetail({ offer, carrierName, airportName, aircraftName, itineraryLabels, className, }: FlightOfferDetailProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=flight-offer-detail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-offer-detail.d.ts","sourceRoot":"","sources":["../../src/components/flight-offer-detail.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAiB,WAAW,EAAE,MAAM,uCAAuC,CAAA;AAQvF,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,WAAW,CAAA;IAClB,wDAAwD;IACxD,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACtD,kEAAkE;IAClE,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACtD,iEAAiE;IACjE,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACvD,mFAAmF;IACnF,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,WAAW,EACX,WAAW,EACX,YAAY,EACZ,eAAe,EACf,SAAS,GACV,EAAE,sBAAsB,2CA0ExB"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { formatMessage } from "@voyant-travel/i18n";
|
|
4
|
+
import { Badge } from "@voyant-travel/ui/components/badge";
|
|
5
|
+
import { Separator } from "@voyant-travel/ui/components/separator";
|
|
6
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
7
|
+
import { useFlightsUiI18nOrDefault } from "../i18n/index.js";
|
|
8
|
+
import { FlightItinerary } from "./flight-itinerary.js";
|
|
9
|
+
/**
|
|
10
|
+
* Full-fidelity flight offer view for the detail sheet. Composes the shared
|
|
11
|
+
* `FlightItinerary` renderer for each leg, then a fare breakdown + offer
|
|
12
|
+
* metadata. Codeshare segments and layover dwell times are surfaced by the
|
|
13
|
+
* itinerary component itself.
|
|
14
|
+
*/
|
|
15
|
+
export function FlightOfferDetail({ offer, carrierName, airportName, aircraftName, itineraryLabels, className, }) {
|
|
16
|
+
const i18n = useFlightsUiI18nOrDefault();
|
|
17
|
+
const messages = i18n.messages;
|
|
18
|
+
return (_jsxs("div", { className: cn("flex flex-col gap-6", className), children: [_jsx("section", { className: "flex flex-col gap-5", children: offer.itineraries.map((itin, i) => (_jsx(FlightItinerary
|
|
19
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: itineraries are positional (outbound/return) -- owner: flights-react; existing suppression is intentional pending typed cleanup.
|
|
20
|
+
, { itinerary: itin, label: itineraryLabels?.[i] ?? defaultItineraryLabel(i, offer.itineraries.length, messages), carrierName: carrierName, airportName: airportName, aircraftName: aircraftName }, i))) }), _jsxs("section", { className: "flex flex-col gap-2", children: [_jsx("h4", { className: "font-medium text-[11px] uppercase tracking-wider text-muted-foreground", children: messages.flightOfferDetail.fareBreakdown }), _jsxs("div", { className: "overflow-hidden rounded-lg border", children: [offer.fareBreakdowns.map((b, i) => (_jsx(FareRow, { breakdown: b, i18n: i18n }, i))), _jsx(Separator, {}), _jsxs("div", { className: "flex items-center justify-between bg-muted/30 px-4 py-3", children: [_jsx("span", { className: "font-medium text-sm", children: messages.common.total }), _jsx("span", { className: "font-semibold text-base tabular-nums", children: formatMoney(offer.totalPrice.amount, offer.totalPrice.currency, i18n) })] })] })] }), (offer.validatingCarrier ||
|
|
21
|
+
offer.expiresAt ||
|
|
22
|
+
offer.lastTicketingDate ||
|
|
23
|
+
offer.instantTicketing) && (_jsxs("section", { className: "flex flex-wrap items-center gap-2 text-muted-foreground text-xs", children: [offer.validatingCarrier && (_jsxs("span", { children: [messages.flightOfferDetail.validatingCarrier, " ", _jsx("span", { className: "font-mono text-foreground", children: offer.validatingCarrier })] })), offer.expiresAt && (_jsxs("span", { children: ["\u00B7", " ", formatMessage(messages.flightOfferDetail.expires, {
|
|
24
|
+
date: i18n.formatDateTime(offer.expiresAt),
|
|
25
|
+
})] })), offer.lastTicketingDate && (_jsxs("span", { children: ["\u00B7", " ", formatMessage(messages.flightOfferDetail.lastTicketing, {
|
|
26
|
+
date: i18n.formatDate(offer.lastTicketingDate),
|
|
27
|
+
})] })), offer.instantTicketing && (_jsx(Badge, { variant: "secondary", children: messages.flightOfferDetail.instantTicketing }))] }))] }));
|
|
28
|
+
}
|
|
29
|
+
function defaultItineraryLabel(idx, total, messages) {
|
|
30
|
+
if (total === 1)
|
|
31
|
+
return messages.common.legLabels.itinerary;
|
|
32
|
+
if (total === 2)
|
|
33
|
+
return idx === 0 ? messages.common.legLabels.outbound : messages.common.legLabels.return;
|
|
34
|
+
return formatMessage(messages.common.legLabels.leg, { number: idx + 1 });
|
|
35
|
+
}
|
|
36
|
+
function FareRow({ breakdown, i18n, }) {
|
|
37
|
+
const messages = i18n.messages;
|
|
38
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-4 px-4 py-2.5 text-sm", children: [_jsxs("div", { className: "flex flex-col leading-tight", children: [_jsxs("span", { className: "capitalize", children: [breakdown.passengerCount, "\u00D7 ", messages.common.passengerTypeLabels[breakdown.passengerType]] }), breakdown.fareFamily && (_jsx("span", { className: "text-muted-foreground text-xs", children: breakdown.fareFamily }))] }), _jsxs("div", { className: "flex items-center gap-4 text-muted-foreground text-xs tabular-nums", children: [_jsx("span", { children: formatMessage(messages.flightOfferDetail.base, {
|
|
39
|
+
amount: formatMoney(breakdown.baseFare.amount, breakdown.baseFare.currency, i18n),
|
|
40
|
+
}) }), _jsx("span", { children: formatMessage(messages.flightOfferDetail.tax, {
|
|
41
|
+
amount: formatMoney(breakdown.taxes.amount, breakdown.taxes.currency, i18n),
|
|
42
|
+
}) }), _jsx("span", { className: "font-medium text-foreground text-sm", children: formatMoney(breakdown.total.amount, breakdown.total.currency, i18n) })] })] }));
|
|
43
|
+
}
|
|
44
|
+
function formatMoney(amount, currency, i18n) {
|
|
45
|
+
const n = Number(amount);
|
|
46
|
+
if (!Number.isFinite(n))
|
|
47
|
+
return `${amount} ${currency}`;
|
|
48
|
+
return i18n.formatCurrency(n, currency, { maximumFractionDigits: 0 });
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FlightOffer } from "@voyant-travel/flights/contract/types";
|
|
2
|
+
export interface FlightOfferRowProps {
|
|
3
|
+
offer: FlightOffer;
|
|
4
|
+
/** Click handler — typically opens the detail sheet. */
|
|
5
|
+
onClick?: (offer: FlightOffer) => void;
|
|
6
|
+
/** "Select" CTA — when set, renders a primary button alongside the price. */
|
|
7
|
+
onSelect?: (offer: FlightOffer) => void;
|
|
8
|
+
/** Customize the select CTA label. Defaults to "Select". */
|
|
9
|
+
selectLabel?: string;
|
|
10
|
+
/** Optional carrier name resolver — used for the logo `alt` text. */
|
|
11
|
+
carrierName?: (iataCode: string) => string | undefined;
|
|
12
|
+
/** Highlight ring (e.g. when this offer is currently picked). */
|
|
13
|
+
selected?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* One row in the search-results list. Lays out each itinerary on its own
|
|
18
|
+
* line: carriers · departure time · journey · arrival time · stops ·
|
|
19
|
+
* duration. Total price sits on the right with an optional "Select" CTA.
|
|
20
|
+
*
|
|
21
|
+
* For per-leg searches, each offer carries one itinerary; for combined
|
|
22
|
+
* round-trip searches it carries two. The renderer handles both shapes.
|
|
23
|
+
*/
|
|
24
|
+
export declare function FlightOfferRow({ offer, onClick, onSelect, selectLabel, carrierName, selected, className, }: FlightOfferRowProps): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
//# sourceMappingURL=flight-offer-row.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-offer-row.d.ts","sourceRoot":"","sources":["../../src/components/flight-offer-row.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,uCAAuC,CAAA;AAQnF,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,WAAW,CAAA;IAClB,wDAAwD;IACxD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACtC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACvC,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,qEAAqE;IACrE,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACtD,iEAAiE;IACjE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,EACP,QAAQ,EACR,WAAW,EACX,WAAW,EACX,QAAQ,EACR,SAAS,GACV,EAAE,mBAAmB,2CA0CrB"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { formatMessage } from "@voyant-travel/i18n";
|
|
4
|
+
import { Badge } from "@voyant-travel/ui/components/badge";
|
|
5
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
6
|
+
import { Plane } from "lucide-react";
|
|
7
|
+
import { useFlightsUiI18nOrDefault } from "../i18n/index.js";
|
|
8
|
+
import { AirlineLogo } from "./airline-logo.js";
|
|
9
|
+
/**
|
|
10
|
+
* One row in the search-results list. Lays out each itinerary on its own
|
|
11
|
+
* line: carriers · departure time · journey · arrival time · stops ·
|
|
12
|
+
* duration. Total price sits on the right with an optional "Select" CTA.
|
|
13
|
+
*
|
|
14
|
+
* For per-leg searches, each offer carries one itinerary; for combined
|
|
15
|
+
* round-trip searches it carries two. The renderer handles both shapes.
|
|
16
|
+
*/
|
|
17
|
+
export function FlightOfferRow({ offer, onClick, onSelect, selectLabel, carrierName, selected, className, }) {
|
|
18
|
+
const i18n = useFlightsUiI18nOrDefault();
|
|
19
|
+
const messages = i18n.messages;
|
|
20
|
+
const interactive = !!onClick;
|
|
21
|
+
const Container = interactive ? "button" : "div";
|
|
22
|
+
return (_jsxs(Container, { type: interactive ? "button" : undefined, onClick: onClick ? () => onClick(offer) : undefined, className: cn("flex w-full items-stretch gap-4 rounded-lg border bg-card p-4 text-left shadow-sm transition-colors", interactive && "hover:border-primary/40 hover:bg-accent/30", selected && "border-primary ring-1 ring-primary/40", className), children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-3", children: offer.itineraries.map((itin, i) => (_jsx(ItineraryRow, { itinerary: itin, carrierName: carrierName, messages: messages }, i))) }), _jsxs("div", { className: "flex shrink-0 flex-col items-end justify-center gap-2 border-l pl-4", children: [_jsx("div", { className: "font-semibold text-2xl tabular-nums", children: formatMoney(offer.totalPrice.amount, offer.totalPrice.currency, i18n) }), _jsx(PriceFootnote, { offer: offer, i18n: i18n }), onSelect && (_jsx("button", { type: "button", onClick: (e) => {
|
|
23
|
+
e.stopPropagation();
|
|
24
|
+
onSelect(offer);
|
|
25
|
+
}, className: "mt-1 inline-flex h-8 items-center justify-center rounded-md bg-primary px-3 font-medium text-primary-foreground text-xs hover:bg-primary/90", children: selectLabel ?? messages.flightOfferRow.select }))] })] }));
|
|
26
|
+
}
|
|
27
|
+
function PriceFootnote({ offer, i18n, }) {
|
|
28
|
+
const messages = i18n.messages;
|
|
29
|
+
const totalPax = offer.fareBreakdowns.reduce((n, b) => n + b.passengerCount, 0);
|
|
30
|
+
const adult = offer.fareBreakdowns.find((b) => b.passengerType === "adult");
|
|
31
|
+
if (totalPax <= 1) {
|
|
32
|
+
return _jsx("div", { className: "text-muted-foreground text-xs", children: messages.common.total });
|
|
33
|
+
}
|
|
34
|
+
return (_jsxs("div", { className: "flex flex-col items-end gap-0.5 text-muted-foreground text-xs", children: [_jsxs("span", { children: [messages.common.total, " \u00B7 ", totalPax, " ", messages.common.pax] }), adult && (_jsxs("span", { children: [formatMoney(adult.total.amount, adult.total.currency, i18n), _jsx("span", { className: "ml-0.5 text-muted-foreground/70", children: messages.common.adultPerPassenger })] }))] }));
|
|
35
|
+
}
|
|
36
|
+
function ItineraryRow({ itinerary, carrierName, messages, }) {
|
|
37
|
+
const segs = itinerary.segments;
|
|
38
|
+
const first = segs[0];
|
|
39
|
+
const last = segs[segs.length - 1];
|
|
40
|
+
if (!first || !last)
|
|
41
|
+
return null;
|
|
42
|
+
const carriers = Array.from(new Set(segs.map((s) => s.carrierCode)));
|
|
43
|
+
const stops = segs.length - 1;
|
|
44
|
+
const hasCodeshare = segs.some((s) => s.operatingCarrierCode != null && s.operatingCarrierCode !== s.carrierCode);
|
|
45
|
+
const hasInterline = carriers.length > 1;
|
|
46
|
+
return (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex shrink-0 items-center -space-x-1.5", children: carriers.map((code) => (_jsx(AirlineLogo, { iataCode: code, name: carrierName?.(code), size: 28 }, code))) }), _jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-3", children: [_jsx(Endpoint, { at: first.departure.at, iata: first.departure.iataCode }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col items-center gap-1", children: [_jsx("div", { className: "text-[11px] text-muted-foreground", children: formatDuration(itinerary.duration) }), _jsxs("div", { className: "flex w-full items-center gap-1.5", children: [_jsx("div", { className: "h-px flex-1 bg-border" }), _jsx(Plane, { className: "h-3 w-3 text-muted-foreground" }), _jsx("div", { className: "h-px flex-1 bg-border" })] }), _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-1.5 text-[11px] text-muted-foreground", children: [stops === 0 ? (_jsx("span", { className: "font-medium text-emerald-600", children: messages.common.stops.nonstop })) : (_jsx("span", { children: formatMessage(messages.common.stops.via, {
|
|
47
|
+
stops: formatMessage(stops === 1 ? messages.common.stops.oneStop : messages.common.stops.manyStops, { count: stops }),
|
|
48
|
+
airports: segs
|
|
49
|
+
.slice(0, -1)
|
|
50
|
+
.map((s) => s.arrival.iataCode)
|
|
51
|
+
.join(", "),
|
|
52
|
+
}) })), hasInterline && (_jsx(Badge, { variant: "secondary", className: "px-1.5 py-0 text-[9px]", children: messages.flightOfferRow.interline })), hasCodeshare && (_jsx(Badge, { variant: "secondary", className: "px-1.5 py-0 text-[9px]", children: messages.flightOfferRow.codeshare }))] })] }), _jsx(Endpoint, { at: last.arrival.at, iata: last.arrival.iataCode, align: "end" })] }), _jsx(Badge, { variant: "outline", className: "shrink-0 capitalize", children: messages.common.cabinLabels[first.cabin] })] }));
|
|
53
|
+
}
|
|
54
|
+
function Endpoint({ at, iata, align = "start", }) {
|
|
55
|
+
return (_jsxs("div", { className: cn("flex shrink-0 flex-col leading-tight", align === "end" ? "items-end" : "items-start"), children: [_jsx("span", { className: "font-semibold text-base tabular-nums", children: formatTime(at) }), _jsx("span", { className: "font-mono text-muted-foreground text-xs", children: iata })] }));
|
|
56
|
+
}
|
|
57
|
+
function formatMoney(amount, currency, i18n) {
|
|
58
|
+
const n = Number(amount);
|
|
59
|
+
if (!Number.isFinite(n))
|
|
60
|
+
return `${amount} ${currency}`;
|
|
61
|
+
return i18n.formatCurrency(n, currency, { maximumFractionDigits: 0 });
|
|
62
|
+
}
|
|
63
|
+
function formatTime(iso) {
|
|
64
|
+
const d = new Date(iso);
|
|
65
|
+
if (Number.isNaN(d.getTime()))
|
|
66
|
+
return iso;
|
|
67
|
+
return new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit" }).format(d);
|
|
68
|
+
}
|
|
69
|
+
function formatDuration(iso) {
|
|
70
|
+
if (!iso)
|
|
71
|
+
return "";
|
|
72
|
+
const m = /^PT(?:(\d+)H)?(?:(\d+)M)?$/.exec(iso);
|
|
73
|
+
if (!m)
|
|
74
|
+
return iso;
|
|
75
|
+
const h = m[1] ? `${m[1]}h` : "";
|
|
76
|
+
const min = m[2] ? `${m[2]}m` : "";
|
|
77
|
+
return [h, min].filter(Boolean).join(" ") || iso;
|
|
78
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FlightOrder } from "@voyant-travel/flights/contract/types";
|
|
2
|
+
export interface FlightOrderConfirmationProps {
|
|
3
|
+
order: FlightOrder;
|
|
4
|
+
/** Optional cancel button — pass when the order is still cancellable. */
|
|
5
|
+
onCancel?: (order: FlightOrder) => void;
|
|
6
|
+
cancelLoading?: boolean;
|
|
7
|
+
/** IATA → human-readable resolvers, forwarded to the embedded offer detail. */
|
|
8
|
+
carrierName?: (iataCode: string) => string | undefined;
|
|
9
|
+
airportName?: (iataCode: string) => string | undefined;
|
|
10
|
+
aircraftName?: (iataCode: string) => string | undefined;
|
|
11
|
+
}
|
|
12
|
+
export declare function FlightOrderConfirmation({ order, onCancel, cancelLoading, carrierName, airportName, aircraftName, }: FlightOrderConfirmationProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=flight-order-confirmation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-order-confirmation.d.ts","sourceRoot":"","sources":["../../src/components/flight-order-confirmation.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAA;AAWxE,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,WAAW,CAAA;IAClB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACvC,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,+EAA+E;IAC/E,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,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;CACxD;AAaD,wBAAgB,uBAAuB,CAAC,EACtC,KAAK,EACL,QAAQ,EACR,aAAa,EACb,WAAW,EACX,WAAW,EACX,YAAY,GACb,EAAE,4BAA4B,2CAgI9B"}
|
|
@@ -0,0 +1,46 @@
|
|
|
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 { Badge } from "@voyant-travel/ui/components/badge";
|
|
5
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
6
|
+
import { Separator } from "@voyant-travel/ui/components/separator";
|
|
7
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
8
|
+
import { CheckCircle2, Clock, Mail, Phone, Ticket, XCircle } from "lucide-react";
|
|
9
|
+
import { useFlightsUiI18nOrDefault } from "../i18n/index.js";
|
|
10
|
+
import { FlightOfferDetail } from "./flight-offer-detail.js";
|
|
11
|
+
const STATUS_VARIANTS = {
|
|
12
|
+
pending: { tone: "pending", icon: _jsx(Clock, { className: "h-4 w-4" }) },
|
|
13
|
+
confirmed: { tone: "ok", icon: _jsx(CheckCircle2, { className: "h-4 w-4" }) },
|
|
14
|
+
ticketed: { tone: "ok", icon: _jsx(Ticket, { className: "h-4 w-4" }) },
|
|
15
|
+
cancelled: { tone: "bad", icon: _jsx(XCircle, { className: "h-4 w-4" }) },
|
|
16
|
+
failed: { tone: "bad", icon: _jsx(XCircle, { className: "h-4 w-4" }) },
|
|
17
|
+
};
|
|
18
|
+
export function FlightOrderConfirmation({ order, onCancel, cancelLoading, carrierName, airportName, aircraftName, }) {
|
|
19
|
+
const i18n = useFlightsUiI18nOrDefault();
|
|
20
|
+
const messages = i18n.messages;
|
|
21
|
+
const status = STATUS_VARIANTS[order.status];
|
|
22
|
+
const isCancellable = onCancel != null && (order.status === "confirmed" || order.status === "ticketed");
|
|
23
|
+
return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("div", { className: "rounded-xl border bg-card p-5 shadow-sm", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("span", { className: "text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: messages.flightOrderConfirmation.bookingConfirmed }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("h2", { className: "font-mono text-2xl font-semibold tracking-wider", children: order.pnr ?? order.orderId }), _jsxs(Badge, { variant: status.tone === "ok" ? "default" : "secondary", className: cn("gap-1.5", status.tone === "bad" && "bg-destructive/10 text-destructive"), children: [status.icon, messages.common.orderStatusLabels[order.status]] })] }), _jsx("span", { className: "font-mono text-[11px] text-muted-foreground", children: order.orderId })] }), _jsxs("div", { className: "text-right", children: [_jsx("div", { className: "text-2xl font-semibold tabular-nums", children: formatMoney(order.totalPrice.amount, order.totalPrice.currency, i18n) }), _jsx("div", { className: "text-xs text-muted-foreground", children: messages.common.total })] })] }), order.paymentDeadline && order.status === "confirmed" && (_jsxs("div", { className: "mt-4 flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900", children: [_jsx(Clock, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }), _jsx("div", { children: formatMessage(messages.flightOrderConfirmation.ticketDeadline, {
|
|
24
|
+
date: i18n.formatDateTime(order.paymentDeadline),
|
|
25
|
+
}) })] }))] }), _jsx(Section, { title: messages.flightOrderConfirmation.passengers, children: _jsx("div", { className: "flex flex-col gap-2", children: order.passengers.map((p) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border bg-card px-3 py-2.5 text-sm", children: [_jsxs("div", { className: "flex flex-col leading-tight", children: [_jsx("span", { className: "font-medium", children: [p.firstName, p.middleName, p.lastName].filter(Boolean).join(" ") }), _jsxs("span", { className: "text-xs text-muted-foreground capitalize", children: [p.type, p.dateOfBirth &&
|
|
26
|
+
` · ${formatMessage(messages.flightOrderConfirmation.dob, {
|
|
27
|
+
date: p.dateOfBirth,
|
|
28
|
+
})}`] })] }), order.tickets && (_jsx(TicketChip, { number: order.tickets.find((t) => t.passengerId === p.passengerId)?.ticketNumber }))] }, p.passengerId))) }) }), (order.contact?.email || order.contact?.phone) && (_jsx(Section, { title: messages.flightOrderConfirmation.contact, children: _jsxs("div", { className: "flex flex-wrap gap-4 text-sm", children: [order.contact.email && (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), order.contact.email] })), order.contact.phone && (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), order.contact.phone] }))] }) })), _jsx(Section, { title: messages.flightOrderConfirmation.itinerary, children: _jsx(FlightOfferDetail, { offer: order.offer, carrierName: carrierName, airportName: airportName, aircraftName: aircraftName }) }), isCancellable && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { variant: "destructive", onClick: () => onCancel(order), disabled: cancelLoading, children: cancelLoading
|
|
29
|
+
? messages.flightOrderConfirmation.cancelling
|
|
30
|
+
: messages.flightOrderConfirmation.cancelBooking }) })] }))] }));
|
|
31
|
+
}
|
|
32
|
+
function Section({ title, children }) {
|
|
33
|
+
return (_jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h3", { className: "text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: title }), children] }));
|
|
34
|
+
}
|
|
35
|
+
function TicketChip({ number }) {
|
|
36
|
+
if (!number) {
|
|
37
|
+
return _jsx("span", { className: "text-xs text-muted-foreground", children: "-" });
|
|
38
|
+
}
|
|
39
|
+
return _jsx("code", { className: "rounded bg-muted px-2 py-1 font-mono text-[11px]", children: number });
|
|
40
|
+
}
|
|
41
|
+
function formatMoney(amount, currency, i18n) {
|
|
42
|
+
const n = Number(amount);
|
|
43
|
+
if (!Number.isFinite(n))
|
|
44
|
+
return `${amount} ${currency}`;
|
|
45
|
+
return i18n.formatCurrency(n, currency, { maximumFractionDigits: 0 });
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { FlightPassenger, PassengerCounts, PassengerType } from "@voyant-travel/flights/contract/types";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
/** Subset of FlightPassenger that a picker can pre-fill into a card. */
|
|
4
|
+
export type PassengerPrefill = Partial<Pick<FlightPassenger, "firstName" | "middleName" | "lastName" | "dateOfBirth" | "gender" | "email" | "phone">>;
|
|
5
|
+
export interface FlightPassengerFormProps {
|
|
6
|
+
/** How many passengers of each type the booking is for. */
|
|
7
|
+
counts: PassengerCounts;
|
|
8
|
+
/** Current passenger details (one entry per pax slot). */
|
|
9
|
+
value: FlightPassenger[];
|
|
10
|
+
onChange: (next: FlightPassenger[]) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Surface a banner above the cards prompting the operator to add travel
|
|
13
|
+
* documents — typically true when the route looks international. Documents
|
|
14
|
+
* remain optional (skip-to-check-in is one click), but the prompt nudges
|
|
15
|
+
* the operator to fill them up-front.
|
|
16
|
+
*/
|
|
17
|
+
documentsRequired?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Optional render slot for a person picker. Rendered next to each
|
|
20
|
+
* passenger card title — typically a "Pick from contacts" button. Calling
|
|
21
|
+
* `onPicked` with prefill data merges it into the card without
|
|
22
|
+
* overwriting fields the picker doesn't carry (e.g. CRM has no DOB).
|
|
23
|
+
*/
|
|
24
|
+
renderPicker?: (slot: {
|
|
25
|
+
passengerId: string;
|
|
26
|
+
type: PassengerType;
|
|
27
|
+
}, onPicked: (prefill: PassengerPrefill) => void) => ReactNode;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Editable list of passenger forms — one card per pax slot derived from
|
|
31
|
+
* `counts`. Synthesizes stable `passengerId`s ("pax_adult_1", "pax_child_1",
|
|
32
|
+
* etc.) so the booking adapter can link tickets to passengers.
|
|
33
|
+
*
|
|
34
|
+
* Required fields per pax: type (set), firstName, lastName, dateOfBirth.
|
|
35
|
+
* Gender + email/phone are optional. A travel-document subsection lives on
|
|
36
|
+
* each card — collapsed by default ("Add at check-in instead"), expanded
|
|
37
|
+
* when the operator opts to capture passport / national-id up front. When
|
|
38
|
+
* filled, the document is written to `value.documents[0]` so it ships
|
|
39
|
+
* straight through `bookFlight`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function FlightPassengerForm({ counts, value, onChange, documentsRequired, renderPicker, }: FlightPassengerFormProps): import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* Validate a list of passengers — returns the list of errors keyed by
|
|
44
|
+
* passenger id, or an empty object when everything's filled. Used by the
|
|
45
|
+
* journey to gate the Continue button. Documents are optional, but when
|
|
46
|
+
* the operator opted to add one its required fields must be filled.
|
|
47
|
+
*/
|
|
48
|
+
export declare function validatePassengers(value: FlightPassenger[]): Record<string, string>;
|
|
49
|
+
//# sourceMappingURL=flight-passenger-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-passenger-form.d.ts","sourceRoot":"","sources":["../../src/components/flight-passenger-form.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,aAAa,EAEd,MAAM,uCAAuC,CAAA;AAc9C,OAAO,EAAE,KAAK,SAAS,EAAsB,MAAM,OAAO,CAAA;AAI1D,wEAAwE;AACxE,MAAM,MAAM,gBAAgB,GAAG,OAAO,CACpC,IAAI,CACF,eAAe,EACf,WAAW,GAAG,YAAY,GAAG,UAAU,GAAG,aAAa,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CACvF,CACF,CAAA;AAcD,MAAM,WAAW,wBAAwB;IACvC,2DAA2D;IAC3D,MAAM,EAAE,eAAe,CAAA;IACvB,0DAA0D;IAC1D,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,IAAI,CAAA;IAC3C;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CACb,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,aAAa,CAAA;KAAE,EAClD,QAAQ,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,KAC1C,SAAS,CAAA;CACf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,KAAK,EACL,QAAQ,EACR,iBAAiB,EACjB,YAAY,GACb,EAAE,wBAAwB,2CAkD1B;AA2PD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkBnF"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Checkbox } from "@voyant-travel/ui/components/checkbox";
|
|
4
|
+
import { CountryCombobox } from "@voyant-travel/ui/components/country-combobox";
|
|
5
|
+
import { DatePicker } from "@voyant-travel/ui/components/date-picker";
|
|
6
|
+
import { Input } from "@voyant-travel/ui/components/input";
|
|
7
|
+
import { Label } from "@voyant-travel/ui/components/label";
|
|
8
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyant-travel/ui/components/select";
|
|
9
|
+
import { CircleAlert, IdCard } from "lucide-react";
|
|
10
|
+
import { useEffect, useMemo } from "react";
|
|
11
|
+
import { flightsUiEn } from "../i18n/en.js";
|
|
12
|
+
import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
|
|
13
|
+
// Date-of-birth range — anyone born within the last 110 years is plausible;
|
|
14
|
+
// future dates make no sense. Computed once at module load.
|
|
15
|
+
const DOB_END_MONTH = new Date();
|
|
16
|
+
const DOB_START_MONTH = new Date(DOB_END_MONTH.getFullYear() - 110, 0, 1);
|
|
17
|
+
// Travel-document expiry range — passports/national IDs are typically valid
|
|
18
|
+
// 10 years; visas can extend further. Cap at +30 years to keep the year
|
|
19
|
+
// dropdown manageable. Past dates are disabled (an expired doc isn't usable
|
|
20
|
+
// for booking).
|
|
21
|
+
const EXPIRY_START_MONTH = new Date();
|
|
22
|
+
const EXPIRY_END_MONTH = new Date(EXPIRY_START_MONTH.getFullYear() + 30, 11, 31);
|
|
23
|
+
/**
|
|
24
|
+
* Editable list of passenger forms — one card per pax slot derived from
|
|
25
|
+
* `counts`. Synthesizes stable `passengerId`s ("pax_adult_1", "pax_child_1",
|
|
26
|
+
* etc.) so the booking adapter can link tickets to passengers.
|
|
27
|
+
*
|
|
28
|
+
* Required fields per pax: type (set), firstName, lastName, dateOfBirth.
|
|
29
|
+
* Gender + email/phone are optional. A travel-document subsection lives on
|
|
30
|
+
* each card — collapsed by default ("Add at check-in instead"), expanded
|
|
31
|
+
* when the operator opts to capture passport / national-id up front. When
|
|
32
|
+
* filled, the document is written to `value.documents[0]` so it ships
|
|
33
|
+
* straight through `bookFlight`.
|
|
34
|
+
*/
|
|
35
|
+
export function FlightPassengerForm({ counts, value, onChange, documentsRequired, renderPicker, }) {
|
|
36
|
+
const messages = useFlightsUiMessagesOrDefault();
|
|
37
|
+
// Derive the canonical pax slots from `counts`. Passenger ids are stable
|
|
38
|
+
// by position so re-ordering is safe.
|
|
39
|
+
const slots = useMemo(() => buildSlots(counts), [counts]);
|
|
40
|
+
// Materialize missing rows so the UI always has one value per slot.
|
|
41
|
+
// (Keeps existing values when the user navigates back to this step.)
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (slots.length === value.length)
|
|
44
|
+
return;
|
|
45
|
+
const next = slots.map((slot) => {
|
|
46
|
+
const existing = value.find((p) => p.passengerId === slot.passengerId);
|
|
47
|
+
return existing ?? blankPassenger(slot.passengerId, slot.type);
|
|
48
|
+
});
|
|
49
|
+
onChange(next);
|
|
50
|
+
// We intentionally only re-sync when slot identities change (counts edit).
|
|
51
|
+
}, [slots, value.length, value.find, onChange]);
|
|
52
|
+
const set = (passengerId, patch) => {
|
|
53
|
+
onChange(value.map((p) => (p.passengerId === passengerId ? { ...p, ...patch } : p)));
|
|
54
|
+
};
|
|
55
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [documentsRequired && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-amber-500/40 bg-amber-500/5 p-3 text-amber-700 text-sm", children: [_jsx(CircleAlert, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsx("span", { children: messages.flightPassengerForm.documentsRequiredNotice })] })), slots.map((slot, i) => {
|
|
56
|
+
const pax = value.find((p) => p.passengerId === slot.passengerId) ??
|
|
57
|
+
blankPassenger(slot.passengerId, slot.type);
|
|
58
|
+
const idx = sameTypeIndex(slots, slot, i) + 1;
|
|
59
|
+
return (_jsx(PassengerCard, { label: `${labelFor(slot.type, messages)} ${idx}`, messages: messages, value: pax, onChange: (patch) => set(slot.passengerId, patch), picker: renderPicker?.(slot, (prefill) => set(slot.passengerId, stripUndefined(prefill))) }, slot.passengerId));
|
|
60
|
+
})] }));
|
|
61
|
+
}
|
|
62
|
+
function stripUndefined(obj) {
|
|
63
|
+
const out = {};
|
|
64
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
65
|
+
if (v !== undefined && v !== null)
|
|
66
|
+
out[k] = v;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function PassengerCard({ label, value, onChange, picker, messages, }) {
|
|
71
|
+
const doc = value.documents?.[0] ?? null;
|
|
72
|
+
const setDoc = (next) => {
|
|
73
|
+
onChange({ documents: next ? [next] : undefined });
|
|
74
|
+
};
|
|
75
|
+
const updateDoc = (patch) => {
|
|
76
|
+
setDoc({ ...(doc ?? emptyDoc()), ...patch });
|
|
77
|
+
};
|
|
78
|
+
return (_jsxs("div", { className: "rounded-lg border bg-card p-4 shadow-sm", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between gap-2", children: [_jsx("h3", { className: "font-medium text-sm", children: label }), picker] }), _jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-3", children: [_jsx(Field, { label: messages.flightPassengerForm.fields.firstName, required: true, children: _jsx(Input, { value: value.firstName, onChange: (e) => onChange({ firstName: e.target.value }), placeholder: messages.flightPassengerForm.placeholders.asOnPassport }) }), _jsx(Field, { label: messages.flightPassengerForm.fields.middleName, children: _jsx(Input, { value: value.middleName ?? "", onChange: (e) => onChange({ middleName: e.target.value }), placeholder: messages.flightPassengerForm.placeholders.optional }) }), _jsx(Field, { label: messages.flightPassengerForm.fields.lastName, required: true, children: _jsx(Input, { value: value.lastName, onChange: (e) => onChange({ lastName: e.target.value }), placeholder: messages.flightPassengerForm.placeholders.asOnPassport }) }), _jsx(Field, { label: messages.flightPassengerForm.fields.dateOfBirth, required: true, children: _jsx(DatePicker, { value: value.dateOfBirth || null, onChange: (v) => onChange({ dateOfBirth: v ?? "" }), placeholder: messages.flightPassengerForm.placeholders.selectDate, className: "w-full", captionLayout: "dropdown", startMonth: DOB_START_MONTH, endMonth: DOB_END_MONTH, disabled: { after: DOB_END_MONTH } }) }), _jsx(Field, { label: messages.flightPassengerForm.fields.gender, children: _jsxs(Select, { value: value.gender ?? "", onValueChange: (v) => {
|
|
79
|
+
if (v)
|
|
80
|
+
onChange({ gender: v });
|
|
81
|
+
}, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: messages.flightPassengerForm.placeholders.select }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "M", children: messages.common.genderLabels.M }), _jsx(SelectItem, { value: "F", children: messages.common.genderLabels.F }), _jsx(SelectItem, { value: "X", children: messages.common.genderLabels.X })] })] }) })] }), _jsxs("div", { className: "mt-4 border-t pt-4", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between gap-2", children: [_jsxs("span", { className: "flex items-center gap-1.5 font-medium text-sm", children: [_jsx(IdCard, { className: "h-3.5 w-3.5 text-muted-foreground" }), messages.flightPassengerForm.fields.travelDocument] }), _jsxs("div", { className: "flex items-center gap-2 text-muted-foreground text-xs", children: [_jsx(Checkbox, { id: `pax-${value.passengerId}-doc-toggle`, checked: doc != null, onCheckedChange: (v) => setDoc(v ? emptyDoc() : null) }), _jsx("label", { htmlFor: `pax-${value.passengerId}-doc-toggle`, className: "cursor-pointer", children: messages.flightPassengerForm.addNow })] })] }), !doc && (_jsx("p", { className: "text-muted-foreground text-xs", children: messages.flightPassengerForm.skipDocuments })), doc && (_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-3", children: [_jsx(Field, { label: messages.flightPassengerForm.fields.documentType, required: true, children: _jsxs(Select, { value: doc.type, onValueChange: (v) => {
|
|
82
|
+
if (!v)
|
|
83
|
+
return;
|
|
84
|
+
updateDoc({ type: v });
|
|
85
|
+
}, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "passport", children: messages.common.documentTypeLabels.passport }), _jsx(SelectItem, { value: "national_id", children: messages.common.documentTypeLabels.national_id }), _jsx(SelectItem, { value: "visa", children: messages.common.documentTypeLabels.visa })] })] }) }), _jsx(Field, { label: messages.flightPassengerForm.fields.documentNumber, required: true, children: _jsx(Input, { value: doc.number, onChange: (e) => updateDoc({ number: e.target.value }), placeholder: messages.flightPassengerForm.placeholders.asPrintedOnDocument, autoCapitalize: "characters" }) }), _jsx(Field, { label: messages.flightPassengerForm.fields.countryOfIssue, required: true, children: _jsx(CountryCombobox, { value: doc.countryOfIssue || null, onChange: (code) => updateDoc({ countryOfIssue: code ?? "" }) }) }), _jsx(Field, { label: messages.flightPassengerForm.fields.countryOfNationality, children: _jsx(CountryCombobox, { value: doc.countryOfNationality ?? null, onChange: (code) => updateDoc({ countryOfNationality: code ?? undefined }) }) }), _jsx(Field, { label: messages.flightPassengerForm.fields.expiryDate, required: true, children: _jsx(DatePicker, { value: doc.expiryDate ?? null, onChange: (v) => updateDoc({ expiryDate: v ?? undefined }), placeholder: messages.flightPassengerForm.placeholders.selectDate, className: "w-full", captionLayout: "dropdown", startMonth: EXPIRY_START_MONTH, endMonth: EXPIRY_END_MONTH, disabled: { before: EXPIRY_START_MONTH } }) })] }))] })] }));
|
|
86
|
+
}
|
|
87
|
+
function emptyDoc() {
|
|
88
|
+
return {
|
|
89
|
+
type: "passport",
|
|
90
|
+
number: "",
|
|
91
|
+
countryOfIssue: "",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function Field({ label, required, children, }) {
|
|
95
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: "text-[11px] uppercase tracking-wider text-muted-foreground", children: [label, required && _jsx("span", { className: "ml-0.5 text-destructive", children: "*" })] }), children] }));
|
|
96
|
+
}
|
|
97
|
+
function buildSlots(counts) {
|
|
98
|
+
const slots = [];
|
|
99
|
+
for (let i = 1; i <= counts.adults; i++) {
|
|
100
|
+
slots.push({ passengerId: `pax_adult_${i}`, type: "adult" });
|
|
101
|
+
}
|
|
102
|
+
for (let i = 1; i <= (counts.children ?? 0); i++) {
|
|
103
|
+
slots.push({ passengerId: `pax_child_${i}`, type: "child" });
|
|
104
|
+
}
|
|
105
|
+
for (let i = 1; i <= (counts.infants ?? 0); i++) {
|
|
106
|
+
slots.push({ passengerId: `pax_infant_${i}`, type: "infant" });
|
|
107
|
+
}
|
|
108
|
+
return slots;
|
|
109
|
+
}
|
|
110
|
+
function blankPassenger(passengerId, type) {
|
|
111
|
+
return {
|
|
112
|
+
passengerId,
|
|
113
|
+
type,
|
|
114
|
+
firstName: "",
|
|
115
|
+
lastName: "",
|
|
116
|
+
dateOfBirth: "",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function labelFor(type, messages) {
|
|
120
|
+
return messages.common.passengerTypeLabels[type];
|
|
121
|
+
}
|
|
122
|
+
function sameTypeIndex(slots, slot, idx) {
|
|
123
|
+
let count = 0;
|
|
124
|
+
for (let i = 0; i < idx; i++) {
|
|
125
|
+
if (slots[i]?.type === slot.type)
|
|
126
|
+
count++;
|
|
127
|
+
}
|
|
128
|
+
return count;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Validate a list of passengers — returns the list of errors keyed by
|
|
132
|
+
* passenger id, or an empty object when everything's filled. Used by the
|
|
133
|
+
* journey to gate the Continue button. Documents are optional, but when
|
|
134
|
+
* the operator opted to add one its required fields must be filled.
|
|
135
|
+
*/
|
|
136
|
+
export function validatePassengers(value) {
|
|
137
|
+
const messages = flightsUiEn.flightPassengerForm.validation;
|
|
138
|
+
const errors = {};
|
|
139
|
+
for (const p of value) {
|
|
140
|
+
if (!p.firstName.trim())
|
|
141
|
+
errors[p.passengerId] = messages.firstNameRequired;
|
|
142
|
+
else if (!p.lastName.trim())
|
|
143
|
+
errors[p.passengerId] = messages.lastNameRequired;
|
|
144
|
+
else if (!p.dateOfBirth)
|
|
145
|
+
errors[p.passengerId] = messages.dateOfBirthRequired;
|
|
146
|
+
else {
|
|
147
|
+
const doc = p.documents?.[0];
|
|
148
|
+
if (doc) {
|
|
149
|
+
if (!doc.number.trim())
|
|
150
|
+
errors[p.passengerId] = messages.documentNumberRequired;
|
|
151
|
+
else if (!doc.countryOfIssue.trim())
|
|
152
|
+
errors[p.passengerId] = messages.documentCountryRequired;
|
|
153
|
+
else if (!doc.expiryDate)
|
|
154
|
+
errors[p.passengerId] = messages.documentExpiryRequired;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return errors;
|
|
159
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PaymentIntent } from "@voyant-travel/flights/contract/types";
|
|
2
|
+
export interface FlightPaymentSelectorProps {
|
|
3
|
+
value: PaymentIntent;
|
|
4
|
+
onChange: (next: PaymentIntent) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Which intents to show. Defaults to all three. Hide options the
|
|
7
|
+
* configured connector doesn't declare (e.g. drop `hold` when
|
|
8
|
+
* `flight/holds` capability isn't declared).
|
|
9
|
+
*/
|
|
10
|
+
available?: Array<PaymentIntent["type"]>;
|
|
11
|
+
}
|
|
12
|
+
export declare function FlightPaymentSelector({ value, onChange, available }: FlightPaymentSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=flight-payment-selector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-payment-selector.d.ts","sourceRoot":"","sources":["../../src/components/flight-payment-selector.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAA;AAM1E,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,aAAa,CAAA;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAA;IACvC;;;;OAIG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;CACzC;AA0BD,wBAAgB,qBAAqB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,0BAA0B,2CAqD/F"}
|