@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.
Files changed (187) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +74 -0
  3. package/dist/admin/index.d.ts +134 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +122 -0
  6. package/dist/admin/pages/flight-book-page.d.ts +12 -0
  7. package/dist/admin/pages/flight-book-page.d.ts.map +1 -0
  8. package/dist/admin/pages/flight-book-page.js +40 -0
  9. package/dist/admin/pages/flights-index-page.d.ts +14 -0
  10. package/dist/admin/pages/flights-index-page.d.ts.map +1 -0
  11. package/dist/admin/pages/flights-index-page.js +28 -0
  12. package/dist/client.d.ts +16 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +75 -0
  15. package/dist/components/airline-logo.d.ts +19 -0
  16. package/dist/components/airline-logo.d.ts.map +1 -0
  17. package/dist/components/airline-logo.js +18 -0
  18. package/dist/components/airport-combobox.d.ts +20 -0
  19. package/dist/components/airport-combobox.d.ts.map +1 -0
  20. package/dist/components/airport-combobox.js +31 -0
  21. package/dist/components/billing-pickers.d.ts +19 -0
  22. package/dist/components/billing-pickers.d.ts.map +1 -0
  23. package/dist/components/billing-pickers.js +148 -0
  24. package/dist/components/flight-baggage-step.d.ts +32 -0
  25. package/dist/components/flight-baggage-step.d.ts.map +1 -0
  26. package/dist/components/flight-baggage-step.js +119 -0
  27. package/dist/components/flight-billing-step.d.ts +69 -0
  28. package/dist/components/flight-billing-step.d.ts.map +1 -0
  29. package/dist/components/flight-billing-step.js +117 -0
  30. package/dist/components/flight-booking-journey.d.ts +31 -0
  31. package/dist/components/flight-booking-journey.d.ts.map +1 -0
  32. package/dist/components/flight-booking-journey.js +103 -0
  33. package/dist/components/flight-booking-ledger.d.ts +53 -0
  34. package/dist/components/flight-booking-ledger.d.ts.map +1 -0
  35. package/dist/components/flight-booking-ledger.js +104 -0
  36. package/dist/components/flight-booking-page.d.ts +25 -0
  37. package/dist/components/flight-booking-page.d.ts.map +1 -0
  38. package/dist/components/flight-booking-page.js +175 -0
  39. package/dist/components/flight-booking-shell-helpers.d.ts +29 -0
  40. package/dist/components/flight-booking-shell-helpers.d.ts.map +1 -0
  41. package/dist/components/flight-booking-shell-helpers.js +204 -0
  42. package/dist/components/flight-booking-shell-panels.d.ts +24 -0
  43. package/dist/components/flight-booking-shell-panels.d.ts.map +1 -0
  44. package/dist/components/flight-booking-shell-panels.js +39 -0
  45. package/dist/components/flight-booking-shell-types.d.ts +49 -0
  46. package/dist/components/flight-booking-shell-types.d.ts.map +1 -0
  47. package/dist/components/flight-booking-shell-types.js +18 -0
  48. package/dist/components/flight-booking-shell.d.ts +12 -0
  49. package/dist/components/flight-booking-shell.d.ts.map +1 -0
  50. package/dist/components/flight-booking-shell.js +210 -0
  51. package/dist/components/flight-contact-form.d.ts +16 -0
  52. package/dist/components/flight-contact-form.d.ts.map +1 -0
  53. package/dist/components/flight-contact-form.js +25 -0
  54. package/dist/components/flight-fare-upsell-step.d.ts +26 -0
  55. package/dist/components/flight-fare-upsell-step.d.ts.map +1 -0
  56. package/dist/components/flight-fare-upsell-step.js +169 -0
  57. package/dist/components/flight-filters-bar.d.ts +19 -0
  58. package/dist/components/flight-filters-bar.d.ts.map +1 -0
  59. package/dist/components/flight-filters-bar.js +98 -0
  60. package/dist/components/flight-itinerary.d.ts +28 -0
  61. package/dist/components/flight-itinerary.d.ts.map +1 -0
  62. package/dist/components/flight-itinerary.js +110 -0
  63. package/dist/components/flight-offer-detail.d.ts +21 -0
  64. package/dist/components/flight-offer-detail.d.ts.map +1 -0
  65. package/dist/components/flight-offer-detail.js +49 -0
  66. package/dist/components/flight-offer-row.d.ts +25 -0
  67. package/dist/components/flight-offer-row.d.ts.map +1 -0
  68. package/dist/components/flight-offer-row.js +78 -0
  69. package/dist/components/flight-order-confirmation.d.ts +13 -0
  70. package/dist/components/flight-order-confirmation.d.ts.map +1 -0
  71. package/dist/components/flight-order-confirmation.js +46 -0
  72. package/dist/components/flight-passenger-form.d.ts +49 -0
  73. package/dist/components/flight-passenger-form.d.ts.map +1 -0
  74. package/dist/components/flight-passenger-form.js +159 -0
  75. package/dist/components/flight-payment-selector.d.ts +13 -0
  76. package/dist/components/flight-payment-selector.d.ts.map +1 -0
  77. package/dist/components/flight-payment-selector.js +32 -0
  78. package/dist/components/flight-payment-step.d.ts +32 -0
  79. package/dist/components/flight-payment-step.d.ts.map +1 -0
  80. package/dist/components/flight-payment-step.js +81 -0
  81. package/dist/components/flight-search-form.d.ts +14 -0
  82. package/dist/components/flight-search-form.d.ts.map +1 -0
  83. package/dist/components/flight-search-form.js +58 -0
  84. package/dist/components/flight-seat-map.d.ts +32 -0
  85. package/dist/components/flight-seat-map.d.ts.map +1 -0
  86. package/dist/components/flight-seat-map.js +101 -0
  87. package/dist/components/flight-seats-step.d.ts +40 -0
  88. package/dist/components/flight-seats-step.d.ts.map +1 -0
  89. package/dist/components/flight-seats-step.js +214 -0
  90. package/dist/components/flight-services-step.d.ts +27 -0
  91. package/dist/components/flight-services-step.d.ts.map +1 -0
  92. package/dist/components/flight-services-step.js +123 -0
  93. package/dist/components/flights-page-panels.d.ts +27 -0
  94. package/dist/components/flights-page-panels.d.ts.map +1 -0
  95. package/dist/components/flights-page-panels.js +40 -0
  96. package/dist/components/flights-page-types.d.ts +39 -0
  97. package/dist/components/flights-page-types.d.ts.map +1 -0
  98. package/dist/components/flights-page-types.js +1 -0
  99. package/dist/components/flights-page-utils.d.ts +14 -0
  100. package/dist/components/flights-page-utils.d.ts.map +1 -0
  101. package/dist/components/flights-page-utils.js +79 -0
  102. package/dist/components/flights-page.d.ts +4 -0
  103. package/dist/components/flights-page.d.ts.map +1 -0
  104. package/dist/components/flights-page.js +209 -0
  105. package/dist/components/passenger-contact-picker.d.ts +16 -0
  106. package/dist/components/passenger-contact-picker.d.ts.map +1 -0
  107. package/dist/components/passenger-contact-picker.js +45 -0
  108. package/dist/components/pax-cabin-popover.d.ts +18 -0
  109. package/dist/components/pax-cabin-popover.d.ts.map +1 -0
  110. package/dist/components/pax-cabin-popover.js +35 -0
  111. package/dist/components/popular-routes.d.ts +42 -0
  112. package/dist/components/popular-routes.d.ts.map +1 -0
  113. package/dist/components/popular-routes.js +108 -0
  114. package/dist/hooks/index.d.ts +13 -0
  115. package/dist/hooks/index.d.ts.map +1 -0
  116. package/dist/hooks/index.js +12 -0
  117. package/dist/hooks/use-aircraft.d.ts +17 -0
  118. package/dist/hooks/use-aircraft.d.ts.map +1 -0
  119. package/dist/hooks/use-aircraft.js +18 -0
  120. package/dist/hooks/use-airlines.d.ts +18 -0
  121. package/dist/hooks/use-airlines.d.ts.map +1 -0
  122. package/dist/hooks/use-airlines.js +18 -0
  123. package/dist/hooks/use-airport-search.d.ts +28 -0
  124. package/dist/hooks/use-airport-search.d.ts.map +1 -0
  125. package/dist/hooks/use-airport-search.js +23 -0
  126. package/dist/hooks/use-airports.d.ts +21 -0
  127. package/dist/hooks/use-airports.d.ts.map +1 -0
  128. package/dist/hooks/use-airports.js +17 -0
  129. package/dist/hooks/use-flight-ancillaries.d.ts +63 -0
  130. package/dist/hooks/use-flight-ancillaries.d.ts.map +1 -0
  131. package/dist/hooks/use-flight-ancillaries.js +24 -0
  132. package/dist/hooks/use-flight-book.d.ts +139 -0
  133. package/dist/hooks/use-flight-book.d.ts.map +1 -0
  134. package/dist/hooks/use-flight-book.js +24 -0
  135. package/dist/hooks/use-flight-offer.d.ts +106 -0
  136. package/dist/hooks/use-flight-offer.d.ts.map +1 -0
  137. package/dist/hooks/use-flight-offer.js +20 -0
  138. package/dist/hooks/use-flight-order.d.ts +286 -0
  139. package/dist/hooks/use-flight-order.d.ts.map +1 -0
  140. package/dist/hooks/use-flight-order.js +38 -0
  141. package/dist/hooks/use-flight-orders.d.ts +147 -0
  142. package/dist/hooks/use-flight-orders.d.ts.map +1 -0
  143. package/dist/hooks/use-flight-orders.js +31 -0
  144. package/dist/hooks/use-flight-search.d.ts +110 -0
  145. package/dist/hooks/use-flight-search.d.ts.map +1 -0
  146. package/dist/hooks/use-flight-search.js +18 -0
  147. package/dist/hooks/use-flight-seat-map.d.ts +49 -0
  148. package/dist/hooks/use-flight-seat-map.d.ts.map +1 -0
  149. package/dist/hooks/use-flight-seat-map.js +23 -0
  150. package/dist/hooks/use-saved-payment-methods.d.ts +23 -0
  151. package/dist/hooks/use-saved-payment-methods.d.ts.map +1 -0
  152. package/dist/hooks/use-saved-payment-methods.js +20 -0
  153. package/dist/i18n/en.d.ts +465 -0
  154. package/dist/i18n/en.d.ts.map +1 -0
  155. package/dist/i18n/en.js +520 -0
  156. package/dist/i18n/index.d.ts +5 -0
  157. package/dist/i18n/index.d.ts.map +1 -0
  158. package/dist/i18n/index.js +3 -0
  159. package/dist/i18n/messages.d.ts +392 -0
  160. package/dist/i18n/messages.d.ts.map +1 -0
  161. package/dist/i18n/messages.js +1 -0
  162. package/dist/i18n/provider.d.ts +952 -0
  163. package/dist/i18n/provider.d.ts.map +1 -0
  164. package/dist/i18n/provider.js +44 -0
  165. package/dist/i18n/ro.d.ts +465 -0
  166. package/dist/i18n/ro.d.ts.map +1 -0
  167. package/dist/i18n/ro.js +520 -0
  168. package/dist/index.d.ts +7 -0
  169. package/dist/index.d.ts.map +1 -0
  170. package/dist/index.js +6 -0
  171. package/dist/provider.d.ts +2 -0
  172. package/dist/provider.d.ts.map +1 -0
  173. package/dist/provider.js +1 -0
  174. package/dist/query-keys.d.ts +42 -0
  175. package/dist/query-keys.d.ts.map +1 -0
  176. package/dist/query-keys.js +22 -0
  177. package/dist/query-options.d.ts +827 -0
  178. package/dist/query-options.d.ts.map +1 -0
  179. package/dist/query-options.js +58 -0
  180. package/dist/schemas.d.ts +1658 -0
  181. package/dist/schemas.d.ts.map +1 -0
  182. package/dist/schemas.js +295 -0
  183. package/dist/ui.d.ts +31 -0
  184. package/dist/ui.d.ts.map +1 -0
  185. package/dist/ui.js +28 -0
  186. package/package.json +148 -0
  187. package/src/styles.css +11 -0
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from "@voyant-travel/ui/lib/utils";
4
+ import { Banknote, Clock, CreditCard } from "lucide-react";
5
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
6
+ const INTENTS = [
7
+ {
8
+ id: "hold",
9
+ icon: _jsx(Clock, { className: "h-5 w-5" }),
10
+ build: () => ({ type: "hold" }),
11
+ },
12
+ {
13
+ id: "card",
14
+ icon: _jsx(CreditCard, { className: "h-5 w-5" }),
15
+ build: () => ({ type: "card", token: "demo_card_token" }),
16
+ },
17
+ {
18
+ id: "ticket_on_credit",
19
+ icon: _jsx(Banknote, { className: "h-5 w-5" }),
20
+ build: () => ({ type: "ticket_on_credit" }),
21
+ },
22
+ ];
23
+ export function FlightPaymentSelector({ value, onChange, available }) {
24
+ const messages = useFlightsUiMessagesOrDefault().flightPaymentSelector;
25
+ const visibleIntents = available ? INTENTS.filter((i) => available.includes(i.id)) : INTENTS;
26
+ return (_jsxs("div", { className: "rounded-lg border bg-card p-4 shadow-sm", children: [_jsx("h3", { className: "mb-3 font-medium text-sm", children: messages.title }), _jsx("p", { className: "mb-4 text-xs text-muted-foreground", children: messages.description }), _jsx("div", { role: "radiogroup", className: "flex flex-col gap-2", children: visibleIntents.map((intent) => {
27
+ const isSelected = value.type === intent.id;
28
+ return (_jsxs("button", { type: "button", role: "radio", "aria-checked": isSelected, onClick: () => onChange(intent.build()), className: cn("flex w-full items-start gap-3 rounded-lg border p-3 text-left transition-colors", isSelected ? "border-primary/40 bg-primary/5" : "border-border hover:bg-accent/30"), children: [_jsx("div", { className: cn("shrink-0 rounded-md border p-2", isSelected
29
+ ? "border-primary/30 bg-primary text-primary-foreground"
30
+ : "border-border bg-muted text-muted-foreground"), children: intent.icon }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx("span", { className: "text-sm font-medium", children: messages.intents[intent.id].title }), _jsx("span", { className: "text-xs text-muted-foreground", children: messages.intents[intent.id].description })] }), _jsx("div", { className: cn("mt-1 h-4 w-4 shrink-0 rounded-full border-2", isSelected ? "border-primary bg-primary" : "border-border"), "aria-hidden": true })] }, intent.id));
31
+ }) })] }));
32
+ }
@@ -0,0 +1,32 @@
1
+ import { type PaymentStepCapabilities, type SavedPaymentAccount } from "@voyant-travel/finance-react/checkout-ui";
2
+ import type { PaymentIntent } from "@voyant-travel/flights/contract/types";
3
+ export type { PaymentStepCapabilities, SavedPaymentAccount };
4
+ /** Back-compat alias — older callers used this name; same shape. */
5
+ export type SavedPaymentMethod = SavedPaymentAccount;
6
+ export interface FlightPaymentStepProps {
7
+ /** Flight-contract intent — kept for back-compat with the booking shell. */
8
+ value: PaymentIntent;
9
+ onChange: (next: PaymentIntent) => void;
10
+ /** Saved methods for the picked person — empty array when none on file. */
11
+ savedMethods: SavedPaymentAccount[];
12
+ loadingSavedMethods?: boolean;
13
+ /** Currently selected saved method id (mirror of state held in the parent). */
14
+ selectedSavedId: string | null;
15
+ onSelectSaved: (id: string | null) => void;
16
+ /**
17
+ * What the active processor / template actually offers for immediate
18
+ * charge flows (`chargeSavedCard`, `newCard`). Hold and the
19
+ * "Issue on agency credit" extra are always rendered.
20
+ *
21
+ * See `docs/architecture/payments-architecture.md` §Core Rule 7.
22
+ */
23
+ capabilities?: PaymentStepCapabilities;
24
+ }
25
+ /**
26
+ * Flight-vertical wrapper around `<PaymentStep>` from `@voyant-travel/finance-react/checkout-ui`.
27
+ * Maps the universal `PaymentChoice` event into the flight contract's
28
+ * `PaymentIntent` shape, and contributes the "Issue ticket on agency
29
+ * credit" extra option (flight-specific).
30
+ */
31
+ export declare function FlightPaymentStep({ value, onChange, savedMethods, loadingSavedMethods, selectedSavedId, onSelectSaved, capabilities, }: FlightPaymentStepProps): import("react/jsx-runtime").JSX.Element;
32
+ //# sourceMappingURL=flight-payment-step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-payment-step.d.ts","sourceRoot":"","sources":["../../src/components/flight-payment-step.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,uBAAuB,EAE5B,KAAK,mBAAmB,EACzB,MAAM,0CAA0C,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAA;AAO1E,YAAY,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,CAAA;AAC5D,oEAAoE;AACpE,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,CAAA;AAEpD,MAAM,WAAW,sBAAsB;IACrC,4EAA4E;IAC5E,KAAK,EAAE,aAAa,CAAA;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAA;IACvC,2EAA2E;IAC3E,YAAY,EAAE,mBAAmB,EAAE,CAAA;IACnC,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,+EAA+E;IAC/E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,uBAAuB,CAAA;CACvC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,mBAAmB,EACnB,eAAe,EACf,aAAa,EACb,YAAY,GACb,EAAE,sBAAsB,2CAkExB"}
@@ -0,0 +1,81 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { PaymentStep, } from "@voyant-travel/finance-react/checkout-ui";
4
+ import { Landmark } from "lucide-react";
5
+ import { useMemo } from "react";
6
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
7
+ /**
8
+ * Flight-vertical wrapper around `<PaymentStep>` from `@voyant-travel/finance-react/checkout-ui`.
9
+ * Maps the universal `PaymentChoice` event into the flight contract's
10
+ * `PaymentIntent` shape, and contributes the "Issue ticket on agency
11
+ * credit" extra option (flight-specific).
12
+ */
13
+ export function FlightPaymentStep({ value, onChange, savedMethods, loadingSavedMethods, selectedSavedId, onSelectSaved, capabilities, }) {
14
+ const messages = useFlightsUiMessagesOrDefault().flightPaymentStep;
15
+ const extraAgencyCredit = useMemo(() => ({
16
+ id: "ticket_on_credit",
17
+ label: messages.agencyCreditLabel,
18
+ description: messages.agencyCreditDescription,
19
+ icon: _jsx(Landmark, { className: "h-4 w-4 text-muted-foreground" }),
20
+ }), [messages]);
21
+ const extraOptions = useMemo(() => [extraAgencyCredit], [extraAgencyCredit]);
22
+ const choice = useMemo(() => intentToChoice(value, savedMethods, selectedSavedId, extraAgencyCredit.id), [value, savedMethods, selectedSavedId, extraAgencyCredit.id]);
23
+ return (_jsx(PaymentStep, { value: choice, onChange: (next) => {
24
+ if (!next) {
25
+ onChange({ type: "hold" });
26
+ onSelectSaved(null);
27
+ return;
28
+ }
29
+ if (next.type === "saved_method") {
30
+ onSelectSaved(next.method.id);
31
+ // The CRM `processor_token` for the method isn't on the
32
+ // PublicPaymentAccount projection. The vertical wrapper here
33
+ // emits a placeholder token derived from the account id; the
34
+ // parent is expected to call `useInitiateCheckoutCollection`
35
+ // with `paymentInstrumentId: next.method.id` rather than
36
+ // relying on the flight `PaymentIntent.token` field.
37
+ onChange({
38
+ type: "card",
39
+ token: `acct:${next.method.id}`,
40
+ });
41
+ return;
42
+ }
43
+ onSelectSaved(null);
44
+ if (next.type === "new_card") {
45
+ onChange({
46
+ type: "card",
47
+ token: next.cardToken,
48
+ ...(next.cardholderName ? { cardholderName: next.cardholderName } : {}),
49
+ });
50
+ return;
51
+ }
52
+ if (next.type === "extra" && next.optionId === extraAgencyCredit.id) {
53
+ onChange({ type: "ticket_on_credit" });
54
+ return;
55
+ }
56
+ // `hold` at the contract level — the parent's order-creation flow
57
+ // produces a payment session + landing URL the operator shares.
58
+ onChange({ type: "hold" });
59
+ }, capabilities: capabilities ?? {}, savedMethods: savedMethods, loadingSavedMethods: loadingSavedMethods, extraOptions: extraOptions }));
60
+ }
61
+ // ─────────────────────────────────────────────────────────────────────────────
62
+ // PaymentIntent ⇄ PaymentChoice translation
63
+ // ─────────────────────────────────────────────────────────────────────────────
64
+ function intentToChoice(intent, savedMethods, selectedSavedId, agencyCreditOptionId) {
65
+ if (intent.type === "ticket_on_credit") {
66
+ return { type: "extra", optionId: agencyCreditOptionId };
67
+ }
68
+ if (intent.type === "card") {
69
+ if (selectedSavedId) {
70
+ const method = savedMethods.find((m) => m.id === selectedSavedId);
71
+ if (method)
72
+ return { type: "saved_method", method };
73
+ }
74
+ return {
75
+ type: "new_card",
76
+ cardToken: intent.token,
77
+ ...(intent.cardholderName ? { cardholderName: intent.cardholderName } : {}),
78
+ };
79
+ }
80
+ return { type: "hold" };
81
+ }
@@ -0,0 +1,14 @@
1
+ import type { FlightSearchRequest } from "@voyant-travel/flights/contract/types";
2
+ export type TripType = "one_way" | "round_trip";
3
+ export interface FlightSearchFormProps {
4
+ /** Called when the user submits a complete search. */
5
+ onSearch: (request: FlightSearchRequest) => void;
6
+ /** Disable the submit button (e.g. while a search is in flight). */
7
+ loading?: boolean;
8
+ /** Optional initial values. */
9
+ initial?: Partial<FlightSearchRequest> & {
10
+ tripType?: TripType;
11
+ };
12
+ }
13
+ export declare function FlightSearchForm({ onSearch, loading, initial }: FlightSearchFormProps): import("react/jsx-runtime").JSX.Element;
14
+ //# sourceMappingURL=flight-search-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-search-form.d.ts","sourceRoot":"","sources":["../../src/components/flight-search-form.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,mBAAmB,EAGpB,MAAM,uCAAuC,CAAA;AAU9C,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,YAAY,CAAA;AAE/C,MAAM,WAAW,qBAAqB;IACpC,sDAAsD;IACtD,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAA;IAChD,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,GAAG;QAAE,QAAQ,CAAC,EAAE,QAAQ,CAAA;KAAE,CAAA;CACjE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,qBAAqB,2CAoIrF"}
@@ -0,0 +1,58 @@
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 { DatePicker } from "@voyant-travel/ui/components/date-picker";
5
+ import { ToggleGroup, ToggleGroupItem } from "@voyant-travel/ui/components/toggle-group";
6
+ import { ArrowLeftRight, Search } from "lucide-react";
7
+ import { useState } from "react";
8
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
9
+ import { AirportCombobox } from "./airport-combobox.js";
10
+ import { PaxCabinPopover } from "./pax-cabin-popover.js";
11
+ export function FlightSearchForm({ onSearch, loading, initial }) {
12
+ const messages = useFlightsUiMessagesOrDefault().flightSearchForm;
13
+ const initialSlices = initial?.slices ?? [];
14
+ const [tripType, setTripType] = useState(initial?.tripType ?? (initialSlices.length === 2 ? "round_trip" : "one_way"));
15
+ const [origin, setOrigin] = useState(initialSlices[0]?.origin ?? null);
16
+ const [destination, setDestination] = useState(initialSlices[0]?.destination ?? null);
17
+ const [departureDate, setDepartureDate] = useState(initialSlices[0]?.departureDate ?? null);
18
+ const [returnDate, setReturnDate] = useState(initialSlices[1]?.departureDate ?? null);
19
+ const initialPax = initial?.passengers ?? {
20
+ adults: 1,
21
+ children: 0,
22
+ infants: 0,
23
+ };
24
+ const [passengers, setPassengers] = useState(initialPax);
25
+ const [cabin, setCabin] = useState(initial?.cabin ?? "economy");
26
+ const swap = () => {
27
+ const o = origin;
28
+ setOrigin(destination);
29
+ setDestination(o);
30
+ };
31
+ const ready = origin != null &&
32
+ destination != null &&
33
+ departureDate != null &&
34
+ (tripType === "one_way" || returnDate != null) &&
35
+ passengers.adults > 0;
36
+ const submit = (e) => {
37
+ e.preventDefault();
38
+ if (!ready || origin == null || destination == null || departureDate == null)
39
+ return;
40
+ const slices = [{ origin, destination, departureDate }];
41
+ if (tripType === "round_trip" && returnDate) {
42
+ slices.push({
43
+ origin: destination,
44
+ destination: origin,
45
+ departureDate: returnDate,
46
+ });
47
+ }
48
+ onSearch({ slices, passengers, cabin });
49
+ };
50
+ return (_jsx("form", { onSubmit: submit, className: "rounded-xl border bg-card px-5 py-5 shadow-sm", children: _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs(ToggleGroup, { size: "lg", value: [tripType], onValueChange: (v) => {
51
+ const next = v[0];
52
+ if (next)
53
+ setTripType(next);
54
+ }, children: [_jsx(ToggleGroupItem, { size: "lg", value: "round_trip", children: messages.roundTrip }), _jsx(ToggleGroupItem, { size: "lg", value: "one_way", children: messages.oneWay })] }), _jsxs("div", { className: "flex flex-1 items-center gap-1", children: [_jsx(AirportCombobox, { value: origin, onChange: setOrigin, placeholder: messages.fromPlaceholder, className: "flex-1" }), _jsx(Button, { type: "button", variant: "outline", size: "icon", onClick: swap, "aria-label": messages.swapAriaLabel, className: "size-10 shrink-0", children: _jsx(ArrowLeftRight, { className: "h-4 w-4" }) }), _jsx(AirportCombobox, { value: destination, onChange: setDestination, placeholder: messages.toPlaceholder, className: "flex-1" })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(DatePicker, { value: departureDate, onChange: setDepartureDate, placeholder: messages.departPlaceholder, className: "h-10 flex-1 min-w-32" }), _jsx(DatePicker, { value: returnDate, onChange: setReturnDate, placeholder: messages.returnPlaceholder, disabled: tripType === "one_way", className: "h-10 flex-1 min-w-32" })] }), _jsx(PaxCabinPopover, { passengers: passengers, cabin: cabin, onChange: (next) => {
55
+ setPassengers(next.passengers);
56
+ setCabin(next.cabin);
57
+ } }), _jsxs(Button, { type: "submit", size: "lg", disabled: !ready || loading, className: "shrink-0 px-6", children: [_jsx(Search, { className: "mr-2 h-4 w-4" }), loading ? messages.searching : messages.search] })] }) }));
58
+ }
@@ -0,0 +1,32 @@
1
+ import type { Seat, SeatMap } from "@voyant-travel/flights/contract/types";
2
+ /**
3
+ * Marker rendered on top of a seat to indicate which passenger picked it.
4
+ * Letter is typically the pax index (1, 2, 3) or initial.
5
+ */
6
+ export interface SeatPickMarker {
7
+ seatNumber: string;
8
+ /** Single character / short label shown on the seat tile. */
9
+ label: string;
10
+ /** Tailwind colour utility group, e.g. "bg-primary text-primary-foreground". */
11
+ swatch?: string;
12
+ }
13
+ export interface FlightSeatMapProps {
14
+ seatMap: SeatMap;
15
+ /** Active pick markers — typically one per passenger that picked a seat. */
16
+ picks: SeatPickMarker[];
17
+ /** Click handler — invoked with the seat that was clicked (or null on blocked). */
18
+ onSeatClick?: (seat: Seat) => void;
19
+ /** Marker shown on the row currently being assigned (e.g. "Adult 1"). */
20
+ highlightedPaxLabel?: string;
21
+ className?: string;
22
+ }
23
+ /**
24
+ * Visual seat map. Renders each row of the aircraft using the supplied
25
+ * `columnLayout` (with `null` slots becoming aisles), each seat as a
26
+ * clickable tile colour-coded by status + category. Pick markers overlay
27
+ * the seat to indicate which passenger has that seat.
28
+ *
29
+ * Pure presentational — state lives in the parent step.
30
+ */
31
+ export declare function FlightSeatMap({ seatMap, picks, onSeatClick, highlightedPaxLabel, className, }: FlightSeatMapProps): import("react/jsx-runtime").JSX.Element;
32
+ //# sourceMappingURL=flight-seat-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-seat-map.d.ts","sourceRoot":"","sources":["../../src/components/flight-seat-map.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,uCAAuC,CAAA;AAY1E;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAA;IAChB,4EAA4E;IAC5E,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,mFAAmF;IACnF,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IAClC,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,KAAK,EACL,WAAW,EACX,mBAAmB,EACnB,SAAS,GACV,EAAE,kBAAkB,2CA6BpB"}
@@ -0,0 +1,101 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyant-travel/i18n";
4
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@voyant-travel/ui/components/tooltip";
5
+ import { cn } from "@voyant-travel/ui/lib/utils";
6
+ import { Plane } from "lucide-react";
7
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
8
+ /**
9
+ * Visual seat map. Renders each row of the aircraft using the supplied
10
+ * `columnLayout` (with `null` slots becoming aisles), each seat as a
11
+ * clickable tile colour-coded by status + category. Pick markers overlay
12
+ * the seat to indicate which passenger has that seat.
13
+ *
14
+ * Pure presentational — state lives in the parent step.
15
+ */
16
+ export function FlightSeatMap({ seatMap, picks, onSeatClick, highlightedPaxLabel, className, }) {
17
+ const messages = useFlightsUiMessagesOrDefault();
18
+ const aircraftName = seatMap.providerData?.aircraftName ?? seatMap.aircraft;
19
+ return (_jsx(TooltipProvider, { delay: 200, children: _jsxs("div", { className: cn("flex flex-col items-center gap-3", className), children: [_jsxs("div", { className: "flex items-center gap-2 text-muted-foreground text-xs", children: [_jsx(Plane, { className: "h-3.5 w-3.5" }), aircraftName ?? messages.flightSeatMap.cabin, highlightedPaxLabel && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-foreground/60", children: "\u00B7" }), _jsx("span", { className: "text-foreground", children: formatMessage(messages.flightSeatMap.pickingSeatFor, {
20
+ passenger: highlightedPaxLabel,
21
+ }) })] }))] }), _jsx("div", { className: "rounded-2xl border bg-card p-4", children: _jsx(Cabin, { seatMap: seatMap, picks: picks, onSeatClick: onSeatClick, messages: messages }) }), _jsx(Legend, { messages: messages })] }) }));
22
+ }
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+ function Cabin({ seatMap, picks, onSeatClick, messages, }) {
25
+ const layout = seatMap.columnLayout;
26
+ const pickIndex = new Map(picks.map((p) => [p.seatNumber, p]));
27
+ return (_jsxs("div", { className: "flex flex-col items-center gap-1", children: [_jsx(ColumnHeader, { layout: layout }), seatMap.rows.map((row) => (_jsx(SeatRowView, { rowNumber: row.row, layout: layout, seats: row.seats, pickIndex: pickIndex, onSeatClick: onSeatClick, messages: messages }, row.row)))] }));
28
+ }
29
+ function ColumnHeader({ layout }) {
30
+ return (_jsxs("div", { className: "mb-1 flex items-center gap-1", children: [_jsx("span", { className: "w-6 shrink-0 text-center font-mono text-[10px] text-muted-foreground" }), layout.map((col, i) => col == null ? (_jsx("div", { className: "w-3 shrink-0" }, `gap-${i}`)) : (_jsx("span", { className: "w-7 shrink-0 text-center font-mono text-[10px] text-muted-foreground", children: col }, `col-${i}`)))] }));
31
+ }
32
+ function SeatRowView({ rowNumber, layout, seats, pickIndex, onSeatClick, messages, }) {
33
+ const seatByCol = new Map(seats.map((s) => [s.column, s]));
34
+ return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("span", { className: "w-6 shrink-0 text-center font-mono text-[10px] text-muted-foreground tabular-nums", children: rowNumber }), layout.map((col, i) => {
35
+ if (col == null) {
36
+ return (_jsx("div", { className: "w-3 shrink-0" }, `aisle-${rowNumber}-${i}`));
37
+ }
38
+ const seat = seatByCol.get(col);
39
+ if (!seat) {
40
+ return (_jsx("div", { className: "h-7 w-7 shrink-0 rounded-md border border-dashed border-muted/30" }, `gap-${rowNumber}-${i}`));
41
+ }
42
+ return (_jsx(SeatTile, { seat: seat, pick: pickIndex.get(seat.seatNumber) ?? null, onClick: onSeatClick, messages: messages }, seat.seatNumber));
43
+ })] }));
44
+ }
45
+ function SeatTile({ seat, pick, onClick, messages, }) {
46
+ const isClickable = !!onClick && (seat.status === "available" || seat.status === "selected" || pick != null);
47
+ const tile = (_jsxs("button", { type: "button", disabled: !isClickable, onClick: onClick ? () => onClick(seat) : undefined, className: cn("relative flex h-7 w-7 shrink-0 items-center justify-center rounded-md border font-mono text-[10px] transition-colors", seat.status === "available" && categoryClasses(seat.category), seat.status === "blocked" && "cursor-not-allowed border-transparent bg-muted/60", seat.status === "unavailable" && "cursor-not-allowed border-transparent bg-muted/30", pick && (pick.swatch ?? "bg-primary text-primary-foreground border-primary"), isClickable && !pick && "hover:border-primary hover:bg-primary/5"), children: [pick ? _jsx("span", { className: "font-semibold", children: pick.label }) : null, seat.category === "exit_row" && !pick && (_jsx("span", { className: "absolute top-0 right-0 text-[7px] font-bold leading-none text-amber-600", children: "E" }))] }));
48
+ if (!isClickable && seat.status !== "available")
49
+ return tile;
50
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: tile }), _jsx(TooltipContent, { className: "max-w-[220px]", children: _jsx(SeatTooltip, { seat: seat, pickedBy: pick?.label, messages: messages }) })] }));
51
+ }
52
+ function SeatTooltip({ seat, pickedBy, messages, }) {
53
+ return (_jsxs("div", { className: "flex flex-col gap-1 text-xs", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "font-mono font-semibold", children: seat.seatNumber }), _jsxs("span", { className: "text-muted-foreground", children: [humanCategory(seat.category, messages), seat.window && ` · ${messages.flightSeatMap.window}`, seat.aisle && ` · ${messages.flightSeatMap.aisle}`] })] }), seat.price ? (_jsxs("span", { className: "font-medium", children: ["+", formatMoney(seat.price.amount, seat.price.currency)] })) : (_jsx("span", { className: "text-muted-foreground", children: messages.flightSeatMap.noCharge })), seat.notes && _jsx("span", { className: "text-muted-foreground", children: seat.notes }), pickedBy && (_jsx("span", { className: "text-primary", children: formatMessage(messages.flightSeatMap.pickedBy, { passenger: pickedBy }) }))] }));
54
+ }
55
+ function Legend({ messages }) {
56
+ return (_jsxs("div", { className: "flex flex-wrap items-center justify-center gap-3 text-[11px] text-muted-foreground", children: [_jsx(LegendChip, { className: "border-emerald-500/60 bg-card", label: messages.flightSeatMap.legend.available }), _jsx(LegendChip, { className: "border-cyan-500/60 bg-cyan-500/5", label: messages.flightSeatMap.legend.preferred }), _jsx(LegendChip, { className: "border-amber-500/60 bg-amber-500/5", label: messages.flightSeatMap.legend.exitRow }), _jsx(LegendChip, { className: "bg-primary text-primary-foreground", label: messages.flightSeatMap.legend.picked }), _jsx(LegendChip, { className: "bg-muted/60", label: messages.flightSeatMap.legend.taken })] }));
57
+ }
58
+ function LegendChip({ className, label }) {
59
+ return (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: cn("h-3.5 w-3.5 rounded-sm border", className) }), label] }));
60
+ }
61
+ // ── Helpers ──────────────────────────────────────────────────────────────────
62
+ function categoryClasses(category) {
63
+ switch (category) {
64
+ case "exit_row":
65
+ return "border-amber-500/60 bg-amber-500/5 text-amber-700";
66
+ case "preferred":
67
+ case "extra_legroom":
68
+ return "border-cyan-500/60 bg-cyan-500/5 text-cyan-700";
69
+ case "premium":
70
+ case "bulkhead":
71
+ return "border-violet-500/60 bg-violet-500/5 text-violet-700";
72
+ default:
73
+ return "border-emerald-500/60 bg-card text-emerald-700";
74
+ }
75
+ }
76
+ function humanCategory(c, messages) {
77
+ switch (c) {
78
+ case "exit_row":
79
+ return messages.flightSeatMap.categories.exit_row;
80
+ case "extra_legroom":
81
+ return messages.flightSeatMap.categories.extra_legroom;
82
+ case "preferred":
83
+ return messages.flightSeatMap.categories.preferred;
84
+ case "premium":
85
+ return messages.flightSeatMap.categories.premium;
86
+ case "bulkhead":
87
+ return messages.flightSeatMap.categories.bulkhead;
88
+ default:
89
+ return messages.flightSeatMap.categories.standard;
90
+ }
91
+ }
92
+ function formatMoney(amount, currency) {
93
+ const n = Number(amount);
94
+ if (!Number.isFinite(n))
95
+ return `${amount} ${currency}`;
96
+ return new Intl.NumberFormat(undefined, {
97
+ style: "currency",
98
+ currency,
99
+ maximumFractionDigits: 0,
100
+ }).format(n);
101
+ }
@@ -0,0 +1,40 @@
1
+ import type { AncillarySelection, FlightOffer, FlightPassenger, PassengerCounts, SeatMap } from "@voyant-travel/flights/contract/types";
2
+ type SeatPicks = NonNullable<AncillarySelection["seats"]>;
3
+ type SeatMode = "skip" | "auto" | "now";
4
+ /**
5
+ * Per-segment seat map fetcher contract. The step calls this with the
6
+ * segment id once the user enters "pick now" mode for a given segment;
7
+ * the parent owns the actual TanStack Query call.
8
+ */
9
+ export interface FlightSeatMapSlot {
10
+ /** Map for this segment, or null while loading / errored. */
11
+ seatMap: SeatMap | null;
12
+ loading?: boolean;
13
+ error?: string | null;
14
+ }
15
+ export interface FlightSeatsStepProps {
16
+ outboundOffer: FlightOffer;
17
+ returnOffer?: FlightOffer;
18
+ passengers: FlightPassenger[];
19
+ passengerCounts: PassengerCounts;
20
+ /** Map fetcher invoked with each segment id the user navigates to. */
21
+ getSeatMap: (segment: {
22
+ offerId: string;
23
+ segmentId: string;
24
+ }) => FlightSeatMapSlot;
25
+ value: SeatPicks;
26
+ onChange: (next: SeatPicks) => void;
27
+ /** Tri-option mode the user has chosen — "skip" / "auto" / "now". */
28
+ mode: SeatMode;
29
+ onModeChange: (next: SeatMode) => void;
30
+ }
31
+ /**
32
+ * Wizz-style seat selection step. Tri-option gate up top (Skip / Auto-assign
33
+ * / Pick now) — the first two short-circuit straight to the next step. When
34
+ * the user opens "pick now", they get per-segment tabs and a per-passenger
35
+ * row showing each pax's currently picked seat (or "select"). Clicking a
36
+ * seat assigns it to the active passenger and moves the cursor on.
37
+ */
38
+ export declare function FlightSeatsStep({ outboundOffer, returnOffer, passengers, passengerCounts, getSeatMap, value, onChange, mode, onModeChange, }: FlightSeatsStepProps): import("react/jsx-runtime").JSX.Element;
39
+ export {};
40
+ //# sourceMappingURL=flight-seats-step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-seats-step.d.ts","sourceRoot":"","sources":["../../src/components/flight-seats-step.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,kBAAkB,EAClB,WAAW,EACX,eAAe,EAEf,eAAe,EAEf,OAAO,EACR,MAAM,uCAAuC,CAAA;AAO9C,KAAK,SAAS,GAAG,WAAW,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAA;AAEzD,KAAK,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;AAEvC;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,WAAW,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,UAAU,EAAE,eAAe,EAAE,CAAA;IAC7B,eAAe,EAAE,eAAe,CAAA;IAChC,sEAAsE;IACtE,UAAU,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,iBAAiB,CAAA;IAClF,KAAK,EAAE,SAAS,CAAA;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,qEAAqE;IACrE,IAAI,EAAE,QAAQ,CAAA;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAA;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,EAC9B,aAAa,EACb,WAAW,EACX,UAAU,EACV,eAAe,EACf,UAAU,EACV,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,YAAY,GACb,EAAE,oBAAoB,2CAwGtB"}
@@ -0,0 +1,214 @@
1
+ // agent-quality: file-size exception -- owner: flights-react; existing UI surface stays co-located until a dedicated split preserves behavior and tests.
2
+ "use client";
3
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
4
+ import { cn } from "@voyant-travel/ui/lib/utils";
5
+ import { CheckCircle2, X } from "lucide-react";
6
+ import { useEffect, useMemo, useState } from "react";
7
+ import { useFlightsUiMessagesOrDefault } from "../i18n/index.js";
8
+ import { FlightSeatMap } from "./flight-seat-map.js";
9
+ /**
10
+ * Wizz-style seat selection step. Tri-option gate up top (Skip / Auto-assign
11
+ * / Pick now) — the first two short-circuit straight to the next step. When
12
+ * the user opens "pick now", they get per-segment tabs and a per-passenger
13
+ * row showing each pax's currently picked seat (or "select"). Clicking a
14
+ * seat assigns it to the active passenger and moves the cursor on.
15
+ */
16
+ export function FlightSeatsStep({ outboundOffer, returnOffer, passengers, passengerCounts, getSeatMap, value, onChange, mode, onModeChange, }) {
17
+ const messages = useFlightsUiMessagesOrDefault();
18
+ const segments = useMemo(() => collectSegments(outboundOffer, returnOffer, messages), [outboundOffer, returnOffer, messages]);
19
+ const paxRows = useMemo(() => buildPassengerRows(passengers, passengerCounts, messages), [passengers, passengerCounts, messages]);
20
+ const [activeSegmentIdx, setActiveSegmentIdx] = useState(0);
21
+ const [activePaxIdx, setActivePaxIdx] = useState(0);
22
+ // Auto-advance to the next pax once a seat is picked for the current one.
23
+ useEffect(() => {
24
+ if (mode !== "now")
25
+ return;
26
+ const segId = segments[activeSegmentIdx]?.segmentId;
27
+ if (!segId)
28
+ return;
29
+ const pickedAll = paxRows.length > 0 &&
30
+ paxRows.every((p) => value.some((v) => v.passengerId === p.passengerId && v.segmentId === segId));
31
+ if (pickedAll) {
32
+ // All pax picked for this segment — advance to next segment if any.
33
+ if (activeSegmentIdx < segments.length - 1) {
34
+ setActiveSegmentIdx((i) => i + 1);
35
+ setActivePaxIdx(0);
36
+ }
37
+ }
38
+ }, [value, segments, activeSegmentIdx, paxRows, mode]);
39
+ const activeSegment = segments[activeSegmentIdx] ?? null;
40
+ return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("h2", { className: "font-semibold text-base", children: messages.flightSeatsStep.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: messages.flightSeatsStep.description })] }), _jsx(ModePicker, { mode: mode, onChange: onModeChange, messages: messages }), mode === "now" && activeSegment && (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(SegmentTabs, { segments: segments, activeIdx: activeSegmentIdx, paxCount: paxRows.length, picks: value, onChange: (idx) => {
41
+ setActiveSegmentIdx(idx);
42
+ setActivePaxIdx(0);
43
+ } }), _jsx(PaxBar, { paxRows: paxRows, activeIdx: activePaxIdx, picks: value, segmentId: activeSegment.segmentId, onActivate: setActivePaxIdx, onClear: (passengerId) => {
44
+ onChange(value.filter((v) => !(v.passengerId === passengerId && v.segmentId === activeSegment.segmentId)));
45
+ } }), _jsx(SeatMapPanel, { slot: getSeatMap({
46
+ offerId: activeSegment.offerId,
47
+ segmentId: activeSegment.segmentId,
48
+ }), paxRows: paxRows, activePaxIdx: activePaxIdx, picks: value, segmentId: activeSegment.segmentId, messages: messages, onPick: (seat) => {
49
+ const pax = paxRows[activePaxIdx];
50
+ if (!pax)
51
+ return;
52
+ const filtered = value.filter((v) => !(v.passengerId === pax.passengerId && v.segmentId === activeSegment.segmentId));
53
+ onChange([
54
+ ...filtered,
55
+ {
56
+ passengerId: pax.passengerId,
57
+ segmentId: activeSegment.segmentId,
58
+ seatNumber: seat.seatNumber,
59
+ },
60
+ ]);
61
+ } })] }))] }));
62
+ }
63
+ const PAX_SWATCHES = [
64
+ "bg-primary text-primary-foreground border-primary",
65
+ "bg-violet-600 text-white border-violet-700",
66
+ "bg-amber-600 text-white border-amber-700",
67
+ "bg-rose-600 text-white border-rose-700",
68
+ "bg-teal-600 text-white border-teal-700",
69
+ "bg-indigo-600 text-white border-indigo-700",
70
+ ];
71
+ function ModePicker({ mode, onChange, messages, }) {
72
+ return (_jsxs("div", { className: "grid gap-2 md:grid-cols-3", children: [_jsx(ModeCard, { active: mode === "skip", onClick: () => onChange("skip"), title: messages.flightSeatsStep.modes.skip.title, body: messages.flightSeatsStep.modes.skip.body }), _jsx(ModeCard, { active: mode === "auto", onClick: () => onChange("auto"), title: messages.flightSeatsStep.modes.auto.title, body: messages.flightSeatsStep.modes.auto.body, recommended: true, messages: messages }), _jsx(ModeCard, { active: mode === "now", onClick: () => onChange("now"), title: messages.flightSeatsStep.modes.now.title, body: messages.flightSeatsStep.modes.now.body })] }));
73
+ }
74
+ function ModeCard({ active, onClick, title, body, recommended, messages, }) {
75
+ return (_jsxs("button", { type: "button", onClick: onClick, className: cn("relative flex flex-col items-start gap-1 rounded-lg border bg-card p-4 text-left transition-colors", active ? "border-primary ring-2 ring-primary/20" : "hover:border-primary/40"), children: [recommended && (_jsx("span", { className: "absolute top-2 right-2 rounded-full bg-primary/10 px-2 py-0.5 font-medium text-[9px] text-primary uppercase tracking-wider", children: messages?.common.recommended })), active && _jsx(CheckCircle2, { className: "absolute top-3 right-3 h-4 w-4 text-primary" }), _jsx("span", { className: "font-medium text-sm", children: title }), _jsx("span", { className: "text-muted-foreground text-xs", children: body })] }));
76
+ }
77
+ function SegmentTabs({ segments, activeIdx, paxCount, picks, onChange, }) {
78
+ return (_jsx("div", { className: "flex items-center gap-2 overflow-x-auto rounded-md border bg-muted/20 p-1", children: segments.map((seg, idx) => {
79
+ const segPicks = picks.filter((p) => p.segmentId === seg.segmentId).length;
80
+ const complete = segPicks === paxCount && paxCount > 0;
81
+ const active = idx === activeIdx;
82
+ return (_jsxs("button", { type: "button", onClick: () => onChange(idx), className: cn("flex shrink-0 items-center gap-2 rounded px-3 py-1.5 text-sm transition-colors", active && "bg-background shadow-sm", !active && "text-muted-foreground hover:bg-background/50"), children: [complete && _jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-emerald-600" }), _jsxs("span", { className: "font-medium", children: [seg.origin, " \u2192 ", seg.destination] }), _jsxs("span", { className: "font-mono text-[10px] text-muted-foreground", children: [seg.carrier, seg.flightNumber] }), _jsxs("span", { className: "text-[11px] text-muted-foreground tabular-nums", children: [segPicks, "/", paxCount] })] }, seg.segmentId));
83
+ }) }));
84
+ }
85
+ function PaxBar({ paxRows, activeIdx, picks, segmentId, onActivate, onClear, }) {
86
+ return (_jsx("ul", { className: "flex flex-wrap items-center gap-2", children: paxRows.map((pax, idx) => {
87
+ const pick = picks.find((p) => p.passengerId === pax.passengerId && p.segmentId === segmentId);
88
+ const active = idx === activeIdx;
89
+ return (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onActivate(idx), className: cn("flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs transition-colors", active ? "border-primary bg-primary/5" : "hover:border-primary/40"), children: [_jsx("span", { className: cn("flex h-5 w-5 shrink-0 items-center justify-center rounded-full border font-mono text-[10px] font-semibold", pax.swatch), children: pax.short }), _jsx("span", { className: "font-medium", children: pax.label }), pick ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-mono text-[11px] text-foreground", children: pick.seatNumber }), _jsx(X, { className: "h-3 w-3 cursor-pointer text-muted-foreground hover:text-destructive", onClick: (e) => {
90
+ e.stopPropagation();
91
+ onClear(pax.passengerId);
92
+ } })] })) : (_jsx("span", { className: "text-muted-foreground", children: "\u2014" }))] }) }, pax.passengerId));
93
+ }) }));
94
+ }
95
+ function SeatMapPanel({ slot, paxRows, activePaxIdx, picks, segmentId, onPick, messages, }) {
96
+ if (slot.loading) {
97
+ return _jsx("div", { className: "h-72 animate-pulse rounded-2xl bg-muted/40" });
98
+ }
99
+ if (slot.error) {
100
+ return (_jsx("div", { className: "rounded-xl border border-destructive/40 bg-destructive/5 p-4 text-destructive text-sm", children: slot.error }));
101
+ }
102
+ if (!slot.seatMap) {
103
+ return (_jsx("div", { className: "rounded-xl border border-dashed p-6 text-center text-muted-foreground text-sm", children: messages.flightSeatsStep.seatMapUnavailable }));
104
+ }
105
+ const markers = picks
106
+ .filter((p) => p.segmentId === segmentId)
107
+ .map((p) => {
108
+ const paxIdx = paxRows.findIndex((r) => r.passengerId === p.passengerId);
109
+ const pax = paxRows[paxIdx];
110
+ if (!pax)
111
+ return null;
112
+ return { seatNumber: p.seatNumber, label: pax.short, swatch: pax.swatch };
113
+ })
114
+ .filter((p) => p !== null);
115
+ return (_jsx(FlightSeatMap, { seatMap: slot.seatMap, picks: markers, onSeatClick: (seat) => {
116
+ if (seat.status !== "available" && seat.status !== "selected")
117
+ return;
118
+ onPick(seat);
119
+ }, highlightedPaxLabel: paxRows[activePaxIdx]?.label }));
120
+ }
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+ // Helpers
123
+ // ─────────────────────────────────────────────────────────────────────────────
124
+ function collectSegments(outbound, returnLeg, messages) {
125
+ const out = [];
126
+ for (const seg of itinerarySegments(outbound)) {
127
+ out.push({
128
+ offerId: outbound.offerId,
129
+ segmentId: seg.segmentId,
130
+ legLabel: messages.common.legLabels.outbound,
131
+ origin: seg.departure.iataCode,
132
+ destination: seg.arrival.iataCode,
133
+ carrier: seg.carrierCode,
134
+ flightNumber: seg.flightNumber,
135
+ });
136
+ }
137
+ if (returnLeg) {
138
+ for (const seg of itinerarySegments(returnLeg)) {
139
+ out.push({
140
+ offerId: returnLeg.offerId,
141
+ segmentId: seg.segmentId,
142
+ legLabel: messages.common.legLabels.return,
143
+ origin: seg.departure.iataCode,
144
+ destination: seg.arrival.iataCode,
145
+ carrier: seg.carrierCode,
146
+ flightNumber: seg.flightNumber,
147
+ });
148
+ }
149
+ }
150
+ return out;
151
+ }
152
+ function itinerarySegments(offer) {
153
+ const out = [];
154
+ for (const itin of offer.itineraries) {
155
+ for (const seg of itin.segments)
156
+ out.push(seg);
157
+ }
158
+ return out;
159
+ }
160
+ function buildPassengerRows(passengers, counts, messages) {
161
+ const rows = [];
162
+ const total = passengers.length > 0
163
+ ? passengers.length
164
+ : counts.adults + (counts.children ?? 0) + (counts.infants ?? 0);
165
+ for (let i = 0; i < total; i++) {
166
+ const p = passengers[i];
167
+ if (p) {
168
+ rows.push({
169
+ passengerId: p.passengerId,
170
+ label: nameOrFallback(p, i, messages),
171
+ short: shortLabel(p, i),
172
+ swatch: PAX_SWATCHES[i % PAX_SWATCHES.length] ?? PAX_SWATCHES[0],
173
+ });
174
+ }
175
+ else {
176
+ rows.push({
177
+ passengerId: synthPaxId(i, counts),
178
+ label: synthPaxLabel(i, counts, messages),
179
+ short: String(i + 1),
180
+ swatch: PAX_SWATCHES[i % PAX_SWATCHES.length] ?? PAX_SWATCHES[0],
181
+ });
182
+ }
183
+ }
184
+ return rows;
185
+ }
186
+ function nameOrFallback(p, idx, messages) {
187
+ const full = `${p.firstName} ${p.lastName}`.trim();
188
+ if (full)
189
+ return full;
190
+ return `${messages.common.passengerTypeLabels[p.type]} ${idx + 1}`;
191
+ }
192
+ function shortLabel(p, idx) {
193
+ const initials = `${p.firstName[0] ?? ""}${p.lastName[0] ?? ""}`.trim();
194
+ if (initials)
195
+ return initials.toUpperCase();
196
+ return String(idx + 1);
197
+ }
198
+ function synthPaxId(i, counts) {
199
+ if (i < counts.adults)
200
+ return `pax_adult_${i + 1}`;
201
+ const afterAdults = i - counts.adults;
202
+ if (afterAdults < (counts.children ?? 0))
203
+ return `pax_child_${afterAdults + 1}`;
204
+ return `pax_infant_${i - counts.adults - (counts.children ?? 0) + 1}`;
205
+ }
206
+ function synthPaxLabel(i, counts, messages) {
207
+ if (i < counts.adults)
208
+ return `${messages.common.passengerTypeLabels.adult} ${i + 1}`;
209
+ const afterAdults = i - counts.adults;
210
+ if (afterAdults < (counts.children ?? 0)) {
211
+ return `${messages.common.passengerTypeLabels.child} ${afterAdults + 1}`;
212
+ }
213
+ return `${messages.common.passengerTypeLabels.infant} ${i - counts.adults - (counts.children ?? 0) + 1}`;
214
+ }