@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 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAE5B,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAElF,eAAO,MAAM,cAAc,EAAE,aACoB,CAAA;AAEjD,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;gBAEV,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO;CAM3D;AAaD,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,aAAa,CAAA;CACvB;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAC5C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EACvB,OAAO,EAAE,0BAA0B,EACnC,IAAI,CAAC,EAAE,WAAW,GACjB,OAAO,CAAC,IAAI,CAAC,CAgCf;AAkBD,MAAM,MAAM,eAAe,GACvB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,GACT,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;AAEpC,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,MAAM,CAY7F"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const defaultFetcher = (url, init) => fetch(url, { credentials: "include", ...init });
|
|
2
|
+
export class VoyantApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
body;
|
|
5
|
+
constructor(message, status, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "VoyantApiError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.body = body;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function extractErrorMessage(status, statusText, body) {
|
|
13
|
+
if (typeof body === "object" && body !== null && "error" in body) {
|
|
14
|
+
const err = body.error;
|
|
15
|
+
if (typeof err === "string")
|
|
16
|
+
return err;
|
|
17
|
+
if (typeof err === "object" && err !== null && "message" in err) {
|
|
18
|
+
return String(err.message);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return `Voyant API error: ${status} ${statusText}`;
|
|
22
|
+
}
|
|
23
|
+
export async function fetchWithValidation(path, schema, options, init) {
|
|
24
|
+
const url = joinUrl(options.baseUrl, path);
|
|
25
|
+
const headers = new Headers(init?.headers);
|
|
26
|
+
if (init?.body !== undefined && !headers.has("Content-Type")) {
|
|
27
|
+
headers.set("Content-Type", "application/json");
|
|
28
|
+
}
|
|
29
|
+
const response = await options.fetcher(url, { ...init, headers });
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const body = await safeJson(response);
|
|
32
|
+
throw new VoyantApiError(extractErrorMessage(response.status, response.statusText, body), response.status, body);
|
|
33
|
+
}
|
|
34
|
+
if (response.status === 204) {
|
|
35
|
+
return schema.parse(undefined);
|
|
36
|
+
}
|
|
37
|
+
const body = await safeJson(response);
|
|
38
|
+
const parsed = schema.safeParse(body);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
throw new VoyantApiError(`Voyant API response failed validation: ${parsed.error.message}`, response.status, body);
|
|
41
|
+
}
|
|
42
|
+
return parsed.data;
|
|
43
|
+
}
|
|
44
|
+
async function safeJson(response) {
|
|
45
|
+
const text = await response.text();
|
|
46
|
+
if (!text)
|
|
47
|
+
return undefined;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(text);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function joinUrl(baseUrl, path) {
|
|
56
|
+
const trimmedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
57
|
+
const trimmedPath = path.startsWith("/") ? path : `/${path}`;
|
|
58
|
+
return `${trimmedBase}${trimmedPath}`;
|
|
59
|
+
}
|
|
60
|
+
export function withQueryParams(path, params) {
|
|
61
|
+
const search = new URLSearchParams();
|
|
62
|
+
for (const [key, value] of Object.entries(params)) {
|
|
63
|
+
if (value === undefined || value === null)
|
|
64
|
+
continue;
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
for (const v of value)
|
|
67
|
+
search.append(key, String(v));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
search.set(key, String(value));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const qs = search.toString();
|
|
74
|
+
return qs ? `${path}?${qs}` : path;
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface AirlineLogoProps {
|
|
2
|
+
/** 2- or 3-char IATA carrier code. */
|
|
3
|
+
iataCode: string;
|
|
4
|
+
/** Optional override; defaults to Kayak's public logo CDN. */
|
|
5
|
+
logoUrl?: string | null;
|
|
6
|
+
/** Display name for `alt` text + initials fallback. */
|
|
7
|
+
name?: string;
|
|
8
|
+
/** Pixel size of the logo box. Default 28. */
|
|
9
|
+
size?: number;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Inline carrier logo. Falls back to a colored initials chip when the
|
|
14
|
+
* image fails to load (e.g. unknown carrier, blocked CDN). Initials are
|
|
15
|
+
* the IATA code so they're never wrong even when the airline name isn't
|
|
16
|
+
* known to the operator's reference data.
|
|
17
|
+
*/
|
|
18
|
+
export declare function AirlineLogo({ iataCode, logoUrl, name, size, className }: AirlineLogoProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=airline-logo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"airline-logo.d.ts","sourceRoot":"","sources":["../../src/components/airline-logo.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC/B,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAS,EAAE,SAAS,EAAE,EAAE,gBAAgB,2CAgC9F"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
/**
|
|
6
|
+
* Inline carrier logo. Falls back to a colored initials chip when the
|
|
7
|
+
* image fails to load (e.g. unknown carrier, blocked CDN). Initials are
|
|
8
|
+
* the IATA code so they're never wrong even when the airline name isn't
|
|
9
|
+
* known to the operator's reference data.
|
|
10
|
+
*/
|
|
11
|
+
export function AirlineLogo({ iataCode, logoUrl, name, size = 28, className }) {
|
|
12
|
+
const [errored, setErrored] = useState(false);
|
|
13
|
+
const url = logoUrl ?? `https://www.kayak.com/h/run/airline-logos/${iataCode}.png`;
|
|
14
|
+
if (errored || !iataCode) {
|
|
15
|
+
return (_jsx("div", { className: cn("flex shrink-0 items-center justify-center rounded bg-muted font-mono text-[10px] font-medium text-muted-foreground", className), style: { width: size, height: size }, role: "img", "aria-label": name ?? iataCode, children: iataCode }));
|
|
16
|
+
}
|
|
17
|
+
return (_jsx("img", { src: url, alt: name ?? iataCode, width: size, height: size, loading: "lazy", onError: () => setErrored(true), className: cn("shrink-0 rounded object-contain", className), style: { width: size, height: size } }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AirportDto } from "../index.js";
|
|
2
|
+
export interface AirportComboboxProps {
|
|
3
|
+
/** Selected IATA code, or null when nothing is selected. */
|
|
4
|
+
value: string | null;
|
|
5
|
+
onChange: (next: string | null, airport: AirportDto | null) => void;
|
|
6
|
+
/** Trigger placeholder when nothing is selected (e.g. "From", "To"). */
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Single-line typeahead airport picker. Trigger reads as one of:
|
|
13
|
+
* - placeholder (no selection)
|
|
14
|
+
* - "LHR · London" (selection in current result set)
|
|
15
|
+
* - "LHR" (selection but airport not in current result page)
|
|
16
|
+
*
|
|
17
|
+
* Backed by `useAirportSearch` (debounced server query).
|
|
18
|
+
*/
|
|
19
|
+
export declare function AirportCombobox({ value, onChange, placeholder, className, disabled, }: AirportComboboxProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=airport-combobox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"airport-combobox.d.ts","sourceRoot":"","sources":["../../src/components/airport-combobox.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,UAAU,EAAoB,MAAM,aAAa,CAAA;AAE/D,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAA;IACnE,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,WAAW,EACX,SAAS,EACT,QAAQ,GACT,EAAE,oBAAoB,2CA2EtB"}
|
|
@@ -0,0 +1,31 @@
|
|
|
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 { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@voyant-travel/ui/components/command";
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@voyant-travel/ui/components/popover";
|
|
6
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
7
|
+
import { ChevronDown, MapPin } from "lucide-react";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
|
|
10
|
+
import { useAirportSearch } from "../index.js";
|
|
11
|
+
/**
|
|
12
|
+
* Single-line typeahead airport picker. Trigger reads as one of:
|
|
13
|
+
* - placeholder (no selection)
|
|
14
|
+
* - "LHR · London" (selection in current result set)
|
|
15
|
+
* - "LHR" (selection but airport not in current result page)
|
|
16
|
+
*
|
|
17
|
+
* Backed by `useAirportSearch` (debounced server query).
|
|
18
|
+
*/
|
|
19
|
+
export function AirportCombobox({ value, onChange, placeholder, className, disabled, }) {
|
|
20
|
+
const messages = useFlightsUiMessagesOrDefault().airportCombobox;
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
const [input, setInput] = useState("");
|
|
23
|
+
const search = useAirportSearch(input, { enabled: open, limit: 30 });
|
|
24
|
+
const airports = search.data?.data ?? [];
|
|
25
|
+
const selected = value ? airports.find((a) => a.iataCode === value) : null;
|
|
26
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { type: "button", variant: "outline", disabled: disabled, className: cn("h-10 justify-between gap-2 px-3", className) }), children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [_jsx(MapPin, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), value ? (_jsxs("span", { className: "truncate text-sm", children: [_jsx("span", { className: "font-mono font-medium", children: value }), selected && (_jsx("span", { className: "ml-1.5 font-normal text-muted-foreground", children: selected.city }))] })) : (_jsx("span", { className: "truncate text-sm text-muted-foreground", children: placeholder ?? messages.placeholder }))] }), _jsx(ChevronDown, { className: "h-4 w-4 shrink-0 text-muted-foreground" })] }), _jsx(PopoverContent, { className: "w-[320px] p-0", align: "start", children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { value: input, onValueChange: setInput, placeholder: messages.searchPlaceholder }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: search.isLoading ? messages.searching : messages.empty }), _jsx(CommandGroup, { children: airports.map((a) => (_jsxs(CommandItem, { value: `${a.iataCode} ${a.city} ${a.name}`, onSelect: () => {
|
|
27
|
+
onChange(a.iataCode, a);
|
|
28
|
+
setOpen(false);
|
|
29
|
+
setInput("");
|
|
30
|
+
}, children: [_jsx("span", { className: "mr-2 inline-flex w-10 justify-center rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] font-medium", children: a.iataCode }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate text-sm", children: a.city }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: a.name })] }), _jsx("span", { className: "ml-2 text-[10px] uppercase text-muted-foreground", children: a.country })] }, a.iataCode))) })] })] }) })] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BillingValue } from "./flight-billing-step.js";
|
|
2
|
+
export interface BillingPersonPickerProps {
|
|
3
|
+
apply: (prefill: Partial<BillingValue>) => void;
|
|
4
|
+
onPersonSelected?: (personId: string | null) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Billing-step CRM person picker. It searches `/v1/relationships/people`, prefers a
|
|
8
|
+
* billing/primary address, and maps the selected person into `BillingValue`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function BillingPersonPicker({ apply, onPersonSelected }: BillingPersonPickerProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export interface BillingOrgPickerProps {
|
|
12
|
+
apply: (prefill: Partial<BillingValue>) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Billing-step CRM organization picker. It searches organizations and maps
|
|
16
|
+
* the selected organization, address, and contact points into `BillingValue`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function BillingOrgPicker({ apply }: BillingOrgPickerProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=billing-pickers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"billing-pickers.d.ts","sourceRoot":"","sources":["../../src/components/billing-pickers.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE5D,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAA;IAC/C,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CACrD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,wBAAwB,2CA6ExF;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAA;CAChD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE,qBAAqB,2CA0EhE"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useOrganizations, usePeople } from "@voyant-travel/relationships-react";
|
|
4
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
5
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@voyant-travel/ui/components/command";
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@voyant-travel/ui/components/popover";
|
|
7
|
+
import { Building2, ChevronDown, Users } from "lucide-react";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Billing-step CRM person picker. It searches `/v1/relationships/people`, prefers a
|
|
12
|
+
* billing/primary address, and maps the selected person into `BillingValue`.
|
|
13
|
+
*/
|
|
14
|
+
export function BillingPersonPicker({ apply, onPersonSelected }) {
|
|
15
|
+
const messages = useFlightsUiMessagesOrDefault().billingPickers;
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
const [search, setSearch] = useState("");
|
|
18
|
+
const peopleQuery = usePeople({
|
|
19
|
+
search: search.trim() || undefined,
|
|
20
|
+
limit: 30,
|
|
21
|
+
enabled: open,
|
|
22
|
+
});
|
|
23
|
+
const people = (peopleQuery.data?.data ?? []).filter((person) => isAdult(person.dateOfBirth));
|
|
24
|
+
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.personTrigger, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsx(PopoverContent, { className: "w-[340px] p-0", align: "end", children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { value: search, onValueChange: setSearch, placeholder: messages.personSearchPlaceholder }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: peopleQuery.isLoading ? messages.peopleSearching : messages.peopleEmpty }), _jsx(CommandGroup, { children: people.map((person) => {
|
|
25
|
+
const fullName = `${person.firstName} ${person.lastName}`.trim();
|
|
26
|
+
return (_jsx(CommandItem, { value: `${fullName} ${person.email ?? ""}`, onSelect: async () => {
|
|
27
|
+
const address = await fetchPreferredAddress("person", person.id);
|
|
28
|
+
apply({
|
|
29
|
+
mode: "personal",
|
|
30
|
+
firstName: person.firstName,
|
|
31
|
+
lastName: person.lastName,
|
|
32
|
+
email: person.email ?? "",
|
|
33
|
+
phone: person.phone ?? undefined,
|
|
34
|
+
line1: address?.line1 ?? "",
|
|
35
|
+
line2: address?.line2 ?? undefined,
|
|
36
|
+
city: address?.city ?? "",
|
|
37
|
+
region: address?.region ?? undefined,
|
|
38
|
+
postalCode: address?.postalCode ?? undefined,
|
|
39
|
+
countryCode: address?.country ?? "",
|
|
40
|
+
});
|
|
41
|
+
onPersonSelected?.(person.id);
|
|
42
|
+
setOpen(false);
|
|
43
|
+
setSearch("");
|
|
44
|
+
}, children: _jsxs("div", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [_jsx("span", { className: "truncate font-medium text-sm", children: fullName || messages.emptyName }), person.email && (_jsx("span", { className: "truncate text-muted-foreground text-xs", children: person.email }))] }) }, person.id));
|
|
45
|
+
}) })] })] }) })] }));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Billing-step CRM organization picker. It searches organizations and maps
|
|
49
|
+
* the selected organization, address, and contact points into `BillingValue`.
|
|
50
|
+
*/
|
|
51
|
+
export function BillingOrgPicker({ apply }) {
|
|
52
|
+
const messages = useFlightsUiMessagesOrDefault().billingPickers;
|
|
53
|
+
const [open, setOpen] = useState(false);
|
|
54
|
+
const [search, setSearch] = useState("");
|
|
55
|
+
const orgsQuery = useOrganizations({
|
|
56
|
+
search: search.trim() || undefined,
|
|
57
|
+
limit: 30,
|
|
58
|
+
enabled: open,
|
|
59
|
+
});
|
|
60
|
+
const orgs = orgsQuery.data?.data ?? [];
|
|
61
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "gap-2" }), children: [_jsx(Building2, { className: "h-3.5 w-3.5" }), messages.orgTrigger, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsx(PopoverContent, { className: "w-[340px] p-0", align: "end", children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { value: search, onValueChange: setSearch, placeholder: messages.orgSearchPlaceholder }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: orgsQuery.isLoading ? messages.orgsSearching : messages.orgsEmpty }), _jsx(CommandGroup, { children: orgs.map((org) => (_jsx(CommandItem, { value: org.name, onSelect: async () => {
|
|
62
|
+
const [address, contactPoints] = await Promise.all([
|
|
63
|
+
fetchPreferredAddress("organization", org.id),
|
|
64
|
+
fetchContactPoints("organization", org.id),
|
|
65
|
+
]);
|
|
66
|
+
apply({
|
|
67
|
+
mode: "company",
|
|
68
|
+
companyName: org.name,
|
|
69
|
+
...(org.taxId ? { vatNumber: org.taxId } : {}),
|
|
70
|
+
email: contactPoints.email ?? "",
|
|
71
|
+
...(contactPoints.phone ? { phone: contactPoints.phone } : {}),
|
|
72
|
+
line1: address?.line1 ?? "",
|
|
73
|
+
line2: address?.line2 ?? undefined,
|
|
74
|
+
city: address?.city ?? "",
|
|
75
|
+
region: address?.region ?? undefined,
|
|
76
|
+
postalCode: address?.postalCode ?? undefined,
|
|
77
|
+
countryCode: address?.country ?? "",
|
|
78
|
+
});
|
|
79
|
+
setOpen(false);
|
|
80
|
+
setSearch("");
|
|
81
|
+
}, children: _jsxs("div", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [_jsx("span", { className: "truncate font-medium text-sm", children: org.name }), org.legalName && (_jsx("span", { className: "truncate text-muted-foreground text-xs", children: org.legalName }))] }) }, org.id))) })] })] }) })] }));
|
|
82
|
+
}
|
|
83
|
+
function isAdult(dateOfBirth) {
|
|
84
|
+
if (!dateOfBirth)
|
|
85
|
+
return true;
|
|
86
|
+
const dob = new Date(dateOfBirth);
|
|
87
|
+
if (Number.isNaN(dob.getTime()))
|
|
88
|
+
return true;
|
|
89
|
+
const now = new Date();
|
|
90
|
+
let years = now.getFullYear() - dob.getFullYear();
|
|
91
|
+
const beforeBirthdayThisYear = now.getMonth() < dob.getMonth() ||
|
|
92
|
+
(now.getMonth() === dob.getMonth() && now.getDate() < dob.getDate());
|
|
93
|
+
if (beforeBirthdayThisYear)
|
|
94
|
+
years -= 1;
|
|
95
|
+
return years >= 18;
|
|
96
|
+
}
|
|
97
|
+
async function fetchContactPoints(entity, id) {
|
|
98
|
+
const entityType = entity === "person" ? "person" : "organization";
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(`/v1/identity/entities/${entityType}/${encodeURIComponent(id)}/contact-points`, { headers: { accept: "application/json" } });
|
|
101
|
+
if (!res.ok)
|
|
102
|
+
return { email: null, phone: null };
|
|
103
|
+
const json = (await res.json());
|
|
104
|
+
const list = json.data ?? [];
|
|
105
|
+
return {
|
|
106
|
+
email: pickContactPoint(list, "email"),
|
|
107
|
+
phone: pickContactPoint(list, "phone"),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return { email: null, phone: null };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function pickContactPoint(list, kind) {
|
|
115
|
+
const matches = list.filter((contactPoint) => contactPoint.kind === kind);
|
|
116
|
+
if (matches.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
const billing = matches.find((contactPoint) => contactPoint.label === "billing");
|
|
119
|
+
if (billing)
|
|
120
|
+
return billing.value;
|
|
121
|
+
const primary = matches.find((contactPoint) => contactPoint.isPrimary);
|
|
122
|
+
if (primary)
|
|
123
|
+
return primary.value;
|
|
124
|
+
return matches[0]?.value ?? null;
|
|
125
|
+
}
|
|
126
|
+
async function fetchPreferredAddress(entity, id) {
|
|
127
|
+
const path = entity === "person" ? "people" : "organizations";
|
|
128
|
+
try {
|
|
129
|
+
const res = await fetch(`/v1/relationships/${path}/${encodeURIComponent(id)}/addresses`, {
|
|
130
|
+
headers: { accept: "application/json" },
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok)
|
|
133
|
+
return null;
|
|
134
|
+
const json = (await res.json());
|
|
135
|
+
const list = json.data ?? [];
|
|
136
|
+
if (list.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
const billing = list.find((address) => address.label === "billing");
|
|
139
|
+
if (billing)
|
|
140
|
+
return billing;
|
|
141
|
+
const primary = list.find((address) => address.label === "primary") ??
|
|
142
|
+
list.find((address) => address.isPrimary);
|
|
143
|
+
return primary ?? list[0] ?? null;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AncillaryCatalog, AncillarySelection, FlightOffer, FlightPassenger, PassengerCounts } from "@voyant-travel/flights/contract/types";
|
|
2
|
+
type BaggagePicks = NonNullable<AncillarySelection["baggage"]>;
|
|
3
|
+
export interface FlightBaggageStepProps {
|
|
4
|
+
/** Per-leg catalogs. Outbound is required; return only when round-trip. */
|
|
5
|
+
outboundCatalog: AncillaryCatalog | null;
|
|
6
|
+
returnCatalog?: AncillaryCatalog | null;
|
|
7
|
+
/** Carrier-friendly leg labels for cards. */
|
|
8
|
+
outboundOffer: FlightOffer;
|
|
9
|
+
returnOffer?: FlightOffer;
|
|
10
|
+
/**
|
|
11
|
+
* Passengers, in slot order. Pulled from the passengers step for labels;
|
|
12
|
+
* if some are still blank-named, fall back to "Adult 1", "Child 1", etc.
|
|
13
|
+
*/
|
|
14
|
+
passengers: FlightPassenger[];
|
|
15
|
+
/** Fallback when passengers haven't been entered yet. */
|
|
16
|
+
passengerCounts: PassengerCounts;
|
|
17
|
+
value: BaggagePicks;
|
|
18
|
+
onChange: (next: BaggagePicks) => void;
|
|
19
|
+
/** Mirror outbound picks to return — UI toggle, defaults true on round-trip. */
|
|
20
|
+
sameForBothDirections: boolean;
|
|
21
|
+
onSameForBothDirectionsChange: (next: boolean) => void;
|
|
22
|
+
loading?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Wizz-style baggage step. Tiered grid (10/20/26/32 kg with "Recommended"
|
|
26
|
+
* highlight) per passenger per leg, plus a "skip checked bag" path. The
|
|
27
|
+
* "same for both directions" toggle mirrors outbound picks to the return
|
|
28
|
+
* leg — kept on by default per LCC convention.
|
|
29
|
+
*/
|
|
30
|
+
export declare function FlightBaggageStep({ outboundCatalog, returnCatalog, outboundOffer, returnOffer, passengers, passengerCounts, value, onChange, sameForBothDirections, onSameForBothDirectionsChange, loading, }: FlightBaggageStepProps): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=flight-baggage-step.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-baggage-step.d.ts","sourceRoot":"","sources":["../../src/components/flight-baggage-step.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,gBAAgB,EAChB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,eAAe,EAChB,MAAM,uCAAuC,CAAA;AAS9C,KAAK,YAAY,GAAG,WAAW,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAA;AAG9D,MAAM,WAAW,sBAAsB;IACrC,2EAA2E;IAC3E,eAAe,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACxC,aAAa,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACvC,6CAA6C;IAC7C,aAAa,EAAE,WAAW,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;OAGG;IACH,UAAU,EAAE,eAAe,EAAE,CAAA;IAC7B,yDAAyD;IACzD,eAAe,EAAE,eAAe,CAAA;IAChC,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;IACtC,gFAAgF;IAChF,qBAAqB,EAAE,OAAO,CAAA;IAC9B,6BAA6B,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtD,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,eAAe,EACf,aAAa,EACb,aAAa,EACb,WAAW,EACX,UAAU,EACV,eAAe,EACf,KAAK,EACL,QAAQ,EACR,qBAAqB,EACrB,6BAA6B,EAC7B,OAAO,GACR,EAAE,sBAAsB,2CA0FxB"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { formatMessage } from "@voyant-travel/i18n";
|
|
4
|
+
import { Checkbox } from "@voyant-travel/ui/components/checkbox";
|
|
5
|
+
import { Label } from "@voyant-travel/ui/components/label";
|
|
6
|
+
import { cn } from "@voyant-travel/ui/lib/utils";
|
|
7
|
+
import { Briefcase, CheckCircle2, Luggage } from "lucide-react";
|
|
8
|
+
import { useMemo } from "react";
|
|
9
|
+
import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Wizz-style baggage step. Tiered grid (10/20/26/32 kg with "Recommended"
|
|
12
|
+
* highlight) per passenger per leg, plus a "skip checked bag" path. The
|
|
13
|
+
* "same for both directions" toggle mirrors outbound picks to the return
|
|
14
|
+
* leg — kept on by default per LCC convention.
|
|
15
|
+
*/
|
|
16
|
+
export function FlightBaggageStep({ outboundCatalog, returnCatalog, outboundOffer, returnOffer, passengers, passengerCounts, value, onChange, sameForBothDirections, onSameForBothDirectionsChange, loading, }) {
|
|
17
|
+
const messages = useFlightsUiMessagesOrDefault();
|
|
18
|
+
const isRoundTrip = !!returnOffer;
|
|
19
|
+
const paxRows = useMemo(() => buildPassengerRows(passengers, passengerCounts, messages), [passengers, passengerCounts, messages]);
|
|
20
|
+
if (loading) {
|
|
21
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { className: "h-8 w-64 animate-pulse rounded bg-muted/40" }), _jsx("div", { className: "h-40 animate-pulse rounded-xl bg-muted/40" }), _jsx("div", { className: "h-40 animate-pulse rounded-xl bg-muted/40" })] }));
|
|
22
|
+
}
|
|
23
|
+
if (!outboundCatalog) {
|
|
24
|
+
return (_jsx("div", { className: "rounded-xl border border-dashed p-6 text-center text-muted-foreground text-sm", children: messages.flightBaggageStep.unavailable }));
|
|
25
|
+
}
|
|
26
|
+
const setPick = (next, removeMatch) => {
|
|
27
|
+
const filtered = value.filter((p) => !(p.passengerId === removeMatch.passengerId && p.sliceIndex === removeMatch.sliceIndex));
|
|
28
|
+
const updated = next ? [...filtered, next] : filtered;
|
|
29
|
+
if (sameForBothDirections && isRoundTrip && removeMatch.sliceIndex === 0) {
|
|
30
|
+
// Mirror to return leg (slice 1) — strip then re-add the same option.
|
|
31
|
+
const noReturn = updated.filter((p) => !(p.passengerId === removeMatch.passengerId && p.sliceIndex === 1));
|
|
32
|
+
const mirrored = next ? [...noReturn, { ...next, sliceIndex: 1 }] : noReturn;
|
|
33
|
+
onChange(mirrored);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
onChange(updated);
|
|
37
|
+
};
|
|
38
|
+
return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h2", { className: "font-semibold text-base", children: messages.flightBaggageStep.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: messages.flightBaggageStep.description })] }), isRoundTrip && (_jsxs("div", { className: "flex shrink-0 items-center gap-2 text-sm", children: [_jsx(Checkbox, { id: "baggage-same-for-both", checked: sameForBothDirections, onCheckedChange: (v) => onSameForBothDirectionsChange(!!v) }), _jsx("label", { htmlFor: "baggage-same-for-both", className: "cursor-pointer", children: messages.flightBaggageStep.sameForBothDirections })] }))] }), _jsx(BaggageLegSection, { legLabel: messages.common.legLabels.outbound, catalog: outboundCatalog, offer: outboundOffer, passengers: paxRows, sliceIndex: 0, value: value, messages: messages, onPick: setPick }), isRoundTrip && returnCatalog && !sameForBothDirections && (_jsx(BaggageLegSection, { legLabel: messages.common.legLabels.return, catalog: returnCatalog, offer: returnOffer ?? outboundOffer, passengers: paxRows, sliceIndex: 1, value: value, messages: messages, onPick: setPick }))] }));
|
|
39
|
+
}
|
|
40
|
+
function BaggageLegSection({ legLabel, catalog, offer, passengers, sliceIndex, value, onPick, messages, }) {
|
|
41
|
+
const itin = offer.itineraries[0];
|
|
42
|
+
const first = itin?.segments[0];
|
|
43
|
+
const last = itin?.segments[itin.segments.length - 1];
|
|
44
|
+
return (_jsxs("section", { className: "rounded-xl border bg-card p-5 shadow-sm", children: [_jsxs("header", { className: "mb-4 flex items-baseline justify-between gap-2", children: [_jsxs("h3", { className: "font-medium text-sm", children: [_jsx(Luggage, { className: "mr-1.5 inline h-3.5 w-3.5 -translate-y-px text-muted-foreground" }), formatMessage(messages.flightBaggageStep.bags, { leg: legLabel })] }), first && last && (_jsxs("span", { className: "text-muted-foreground text-xs", children: [first.departure.iataCode, " \u2192 ", last.arrival.iataCode, " \u00B7 ", formatDate(first.departure.at)] }))] }), _jsx("div", { className: "flex flex-col gap-5", children: passengers.map((pax) => {
|
|
45
|
+
const pick = value.find((p) => p.passengerId === pax.passengerId && p.sliceIndex === sliceIndex);
|
|
46
|
+
return (_jsx(PaxBaggageRow, { pax: pax, options: catalog.baggage, selectedOptionId: pick?.optionId ?? null, messages: messages, onSelect: (optionId) => onPick(optionId
|
|
47
|
+
? { passengerId: pax.passengerId, sliceIndex, optionId, quantity: 1 }
|
|
48
|
+
: null, { passengerId: pax.passengerId, sliceIndex, optionId: "" }) }, pax.passengerId));
|
|
49
|
+
}) })] }));
|
|
50
|
+
}
|
|
51
|
+
function PaxBaggageRow({ pax, options, selectedOptionId, onSelect, messages, }) {
|
|
52
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(Label, { className: "font-medium text-sm", children: pax.label }), selectedOptionId == null && (_jsx("span", { className: "text-[11px] text-muted-foreground", children: messages.flightBaggageStep.noCheckedBag }))] }), _jsxs("div", { className: "grid grid-cols-2 gap-2 md:grid-cols-5", children: [_jsxs("button", { type: "button", onClick: () => onSelect(null), className: cn("relative flex flex-col items-center justify-center gap-1.5 rounded-lg border bg-card p-3 text-center transition-colors", selectedOptionId == null
|
|
53
|
+
? "border-primary ring-2 ring-primary/20"
|
|
54
|
+
: "hover:border-primary/40 hover:bg-accent/30"), children: [selectedOptionId == null && (_jsx(CheckCircle2, { className: "absolute top-2 right-2 h-3.5 w-3.5 text-primary" })), _jsx(Briefcase, { className: "h-7 w-7 text-muted-foreground/70" }), _jsx("span", { className: "font-semibold text-base", children: messages.flightBaggageStep.noCheckedBag }), _jsx("span", { className: "font-mono text-[11px] text-muted-foreground", children: messages.common.included })] }), options.map((opt) => {
|
|
55
|
+
const isSelected = selectedOptionId === opt.id;
|
|
56
|
+
return (_jsxs("button", { type: "button", onClick: () => onSelect(isSelected ? null : opt.id), className: cn("relative flex flex-col items-center justify-center gap-1.5 rounded-lg border bg-card p-3 text-center transition-colors", isSelected
|
|
57
|
+
? "border-primary ring-2 ring-primary/20"
|
|
58
|
+
: "hover:border-primary/40 hover:bg-accent/30", opt.recommended && !isSelected && "border-primary/40"), children: [opt.recommended && (_jsx("span", { className: "-translate-y-1/2 absolute top-0 left-1/2 -translate-x-1/2 rounded-full bg-primary px-2 py-0.5 font-medium text-[9px] text-primary-foreground uppercase tracking-wider", children: messages.common.recommended })), isSelected && (_jsx(CheckCircle2, { className: "absolute top-2 right-2 h-3.5 w-3.5 text-primary" })), _jsx(Briefcase, { className: "h-7 w-7 text-muted-foreground" }), _jsx("span", { className: "font-semibold text-base", children: opt.weightKg ? `${opt.weightKg} kg` : opt.label }), _jsx("span", { className: "font-mono text-[11px] text-muted-foreground", children: opt.price.amount === "0.00"
|
|
59
|
+
? messages.common.included
|
|
60
|
+
: `+${formatMoney(opt.price.amount, opt.price.currency)}` })] }, opt.id));
|
|
61
|
+
})] })] }));
|
|
62
|
+
}
|
|
63
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
64
|
+
function buildPassengerRows(passengers, counts, messages) {
|
|
65
|
+
if (passengers.length > 0) {
|
|
66
|
+
return passengers.map((p) => ({
|
|
67
|
+
passengerId: p.passengerId,
|
|
68
|
+
label: nameOrFallback(p, messages),
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
// Synthesize from counts when passengers haven't been filled yet.
|
|
72
|
+
const out = [];
|
|
73
|
+
for (let i = 1; i <= counts.adults; i++) {
|
|
74
|
+
out.push({
|
|
75
|
+
passengerId: `pax_adult_${i}`,
|
|
76
|
+
label: `${messages.common.passengerTypeLabels.adult} ${i}`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
for (let i = 1; i <= (counts.children ?? 0); i++) {
|
|
80
|
+
out.push({
|
|
81
|
+
passengerId: `pax_child_${i}`,
|
|
82
|
+
label: `${messages.common.passengerTypeLabels.child} ${i}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
for (let i = 1; i <= (counts.infants ?? 0); i++) {
|
|
86
|
+
out.push({
|
|
87
|
+
passengerId: `pax_infant_${i}`,
|
|
88
|
+
label: `${messages.common.passengerTypeLabels.infant} ${i}`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function nameOrFallback(p, messages) {
|
|
94
|
+
const full = `${p.firstName} ${p.lastName}`.trim();
|
|
95
|
+
if (full)
|
|
96
|
+
return full;
|
|
97
|
+
const idx = p.passengerId.match(/_(\d+)$/)?.[1] ?? "1";
|
|
98
|
+
return `${messages.common.passengerTypeLabels[p.type]} ${idx}`;
|
|
99
|
+
}
|
|
100
|
+
function formatMoney(amount, currency) {
|
|
101
|
+
const n = Number(amount);
|
|
102
|
+
if (!Number.isFinite(n))
|
|
103
|
+
return `${amount} ${currency}`;
|
|
104
|
+
return new Intl.NumberFormat(undefined, {
|
|
105
|
+
style: "currency",
|
|
106
|
+
currency,
|
|
107
|
+
maximumFractionDigits: 0,
|
|
108
|
+
}).format(n);
|
|
109
|
+
}
|
|
110
|
+
function formatDate(iso) {
|
|
111
|
+
const d = new Date(iso);
|
|
112
|
+
if (Number.isNaN(d.getTime()))
|
|
113
|
+
return iso;
|
|
114
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
115
|
+
weekday: "short",
|
|
116
|
+
day: "numeric",
|
|
117
|
+
month: "short",
|
|
118
|
+
}).format(d);
|
|
119
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Tab id — Privat (personal) vs Companie (business invoicing). */
|
|
3
|
+
export type BillingMode = "personal" | "company";
|
|
4
|
+
export interface BillingValue {
|
|
5
|
+
mode: BillingMode;
|
|
6
|
+
/** Personal: pax / billing-recipient first name. */
|
|
7
|
+
firstName: string;
|
|
8
|
+
/** Personal: pax / billing-recipient last name. */
|
|
9
|
+
lastName: string;
|
|
10
|
+
email: string;
|
|
11
|
+
phone?: string;
|
|
12
|
+
/** Postal address — same shape as `BillingAddress` from the contract. */
|
|
13
|
+
line1: string;
|
|
14
|
+
line2?: string;
|
|
15
|
+
city: string;
|
|
16
|
+
region?: string;
|
|
17
|
+
postalCode?: string;
|
|
18
|
+
/** ISO 3166-1 alpha-2. */
|
|
19
|
+
countryCode: string;
|
|
20
|
+
/** Company tab — required when mode === "company". */
|
|
21
|
+
companyName?: string;
|
|
22
|
+
vatNumber?: string;
|
|
23
|
+
/** "Save as default" toggle — parent decides what to do with it. */
|
|
24
|
+
saveAsDefault?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** A booking passenger eligible to be the billing recipient. */
|
|
27
|
+
export interface BillingEligiblePassenger {
|
|
28
|
+
id: string;
|
|
29
|
+
firstName: string;
|
|
30
|
+
middleName?: string;
|
|
31
|
+
lastName: string;
|
|
32
|
+
}
|
|
33
|
+
export interface FlightBillingStepProps {
|
|
34
|
+
value: BillingValue;
|
|
35
|
+
onChange: (next: BillingValue) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Adult passengers from the booking who can stand in as the billing
|
|
38
|
+
* recipient. Children + infants are filtered out by the parent — a kid
|
|
39
|
+
* can never be the billing person. When non-empty, a "Pick from
|
|
40
|
+
* passengers" trigger appears alongside the contact picker.
|
|
41
|
+
*/
|
|
42
|
+
eligiblePassengers?: BillingEligiblePassenger[];
|
|
43
|
+
/**
|
|
44
|
+
* Render slot for a person picker (e.g. CRM "Use details from contact").
|
|
45
|
+
* The parent supplies a CRM-aware picker that, on selection, calls
|
|
46
|
+
* `applyPrefill` with the relevant fields. Set null/undefined to omit.
|
|
47
|
+
*/
|
|
48
|
+
renderPersonPicker?: (apply: (prefill: Partial<BillingValue>) => void) => ReactNode;
|
|
49
|
+
/**
|
|
50
|
+
* Render slot for an organization picker (Companie tab). On selection,
|
|
51
|
+
* `applyPrefill` is called with company name + VAT + address.
|
|
52
|
+
*/
|
|
53
|
+
renderOrgPicker?: (apply: (prefill: Partial<BillingValue>) => void) => ReactNode;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Two-tab billing step with Privat (personal) + Companie (business / VAT)
|
|
57
|
+
* shapes. Address fields are structured (line1/city/postal/country) so the
|
|
58
|
+
* payload maps cleanly to `BillingAddress` on the payment intent. Pickers
|
|
59
|
+
* for prefill from CRM are supplied as render-prop slots so this component
|
|
60
|
+
* stays decoupled from the CRM data layer.
|
|
61
|
+
*/
|
|
62
|
+
export declare function FlightBillingStep({ value, onChange, eligiblePassengers, renderPersonPicker, renderOrgPicker, }: FlightBillingStepProps): import("react/jsx-runtime").JSX.Element;
|
|
63
|
+
export declare function emptyBillingValue(): BillingValue;
|
|
64
|
+
/**
|
|
65
|
+
* Validate the billing value. Returns the first error message, or null
|
|
66
|
+
* when valid. Drives the journey's Continue gate.
|
|
67
|
+
*/
|
|
68
|
+
export declare function validateBilling(v: BillingValue): string | null;
|
|
69
|
+
//# sourceMappingURL=flight-billing-step.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-billing-step.d.ts","sourceRoot":"","sources":["../../src/components/flight-billing-step.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAIhD,mEAAmE;AACnE,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,SAAS,CAAA;AAEhD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,CAAA;IACjB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,gEAAgE;AAChE,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;IACtC;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,wBAAwB,EAAE,CAAA;IAC/C;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,KAAK,SAAS,CAAA;IACnF;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,KAAK,SAAS,CAAA;CACjF;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,EACR,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,GAChB,EAAE,sBAAsB,2CAiExB;AAkPD,wBAAgB,iBAAiB,IAAI,YAAY,CAUhD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,CAe9D"}
|