diff --git a/src/features/online-board/components/details-panels/AircraftPanel.test.tsx b/src/features/online-board/components/details-panels/AircraftPanel.test.tsx index 138cb6a4..dbb3eef8 100644 --- a/src/features/online-board/components/details-panels/AircraftPanel.test.tsx +++ b/src/features/online-board/components/details-panels/AircraftPanel.test.tsx @@ -67,6 +67,25 @@ describe("AircraftPanel", () => { expect(screen.getByTestId("aircraft-panel")).toBeTruthy(); }); + // §4.1.15.4: tail number (registration mark) in aircraft panel + it("4.1.15.4-TailNumber: renders tail number when aircraft.registration is present", () => { + const eq: IEquipmentFull = { + aircraft: { actual: { title: "Airbus A320", registration: "VP-BQS" } }, + }; + render(); + expect(screen.getByText("VP-BQS")).toBeTruthy(); + // label key is AIRPLANE.TAIL-NUMBER (t mock returns key) + expect(screen.getByText("AIRPLANE.TAIL-NUMBER")).toBeTruthy(); + }); + + it("4.1.15.4-TailNumber-Absent: omits tail-number row when registration is not present", () => { + const eq: IEquipmentFull = { + aircraft: { actual: { title: "Airbus A320" } }, + }; + render(); + expect(screen.queryByText("AIRPLANE.TAIL-NUMBER")).toBeNull(); + }); + // §4.1.15.9: previous-flight chip must be a link (opens new tab) when the // flight's scheduled departure date is more recent than today − 2 days. describe("previous-flight link (§4.1.15.9)", () => { diff --git a/src/features/online-board/components/details-panels/AircraftPanel.tsx b/src/features/online-board/components/details-panels/AircraftPanel.tsx index 0d9c25f5..32f8d880 100644 --- a/src/features/online-board/components/details-panels/AircraftPanel.tsx +++ b/src/features/online-board/components/details-panels/AircraftPanel.tsx @@ -112,6 +112,11 @@ export const AircraftPanel: FC = ({ className?: string; }> = []; if (aircraft?.name) props.push({ label: t("AIRPLANE.NAME"), value: aircraft.name }); + // §4.1.15.4: tail number (registration mark, e.g. "VP-BQS") from the + // aircraft.actual.registration field. Shown when present. + if (aircraft?.registration) { + props.push({ label: t("AIRPLANE.TAIL-NUMBER"), value: aircraft.registration }); + } if (total > 0) props.push({ label: t("AIRPLANE.SEATS-TOTAL"), value: total }); if (economy > 0) props.push({ label: t("AIRPLANE.SEATS-ECONOMY"), value: economy }); if (comfort > 0) props.push({ label: t("AIRPLANE.SEATS-COMFORT"), value: comfort }); diff --git a/src/features/online-board/components/details-panels/BoardingPanel.test.tsx b/src/features/online-board/components/details-panels/BoardingPanel.test.tsx index 6766c25e..9da85bff 100644 --- a/src/features/online-board/components/details-panels/BoardingPanel.test.tsx +++ b/src/features/online-board/components/details-panels/BoardingPanel.test.tsx @@ -43,4 +43,31 @@ describe("BoardingPanel", () => { render(); expect(screen.getByTestId("boarding-panel")).toBeTruthy(); }); + + // §4.1.15.4: departure gate + transfer type in boarding panel (Angular parity) + it("4.1.15.4-Boarding-Gate: renders gate row when departure.gate is present", () => { + render(); + expect(screen.getByTestId("boarding-gate")).toBeTruthy(); + expect(screen.getByText("D5")).toBeTruthy(); + expect(screen.getByText("SHARED.NUMBER-EXIT")).toBeTruthy(); + }); + + it("4.1.15.4-Boarding-Dispatch: renders dispatch type row when departure.dispatch is present", () => { + render(); + expect(screen.getByTestId("boarding-dispatch")).toBeTruthy(); + // t mock returns key — dispatch key is "DISPATCH.Bus" + expect(screen.getByText("DISPATCH.Bus")).toBeTruthy(); + expect(screen.getByText("SHARED.LANDING-TRANSFER")).toBeTruthy(); + }); + + it("4.1.15.4-Boarding-NoGate: omits gate row when departure.gate is absent", () => { + render(); + expect(screen.queryByTestId("boarding-gate")).toBeNull(); + }); + + it("4.1.15.4-Boarding-NoDeparture: omits gate/dispatch rows when departure prop is not passed", () => { + render(); + expect(screen.queryByTestId("boarding-gate")).toBeNull(); + expect(screen.queryByTestId("boarding-dispatch")).toBeNull(); + }); }); diff --git a/src/features/online-board/components/details-panels/BoardingPanel.tsx b/src/features/online-board/components/details-panels/BoardingPanel.tsx index bfaa530c..01a82813 100644 --- a/src/features/online-board/components/details-panels/BoardingPanel.tsx +++ b/src/features/online-board/components/details-panels/BoardingPanel.tsx @@ -5,7 +5,7 @@ import { formatUtcOffset, formatDayMonthYear, } from "@/shared/utils/datetime/index.js"; -import type { IFlightTransitionItem, FlightTransitionStatus } from "../../types.js"; +import type { IFlightTransitionItem, IFlightLegDepartureStation, FlightTransitionStatus } from "../../types.js"; import "./panels.scss"; function formatTransitionTimestamp(iso: string): string { @@ -25,9 +25,11 @@ const STATUS_KEYS: Record = { export interface BoardingPanelProps { item: IFlightTransitionItem; + /** Departure station — used to show transfer type + gate (§4.1.15.4). */ + departure?: Pick; } -export const BoardingPanel: FC = ({ item }) => { +export const BoardingPanel: FC = ({ item, departure }) => { const { t } = useTranslation(); const hasEnd = Boolean(item.end?.local); @@ -47,6 +49,20 @@ export const BoardingPanel: FC = ({ item }) => { {formatTransitionTimestamp(item.end.local)} )} + {/* §4.1.15.4: dispatch type (Bus / Bridge) and departure gate from boarding panel + Angular parity: flight-details-boarding.component.html lines 17–24 */} + {departure?.dispatch && ( +
+ {t("SHARED.LANDING-TRANSFER")} + {t(`DISPATCH.${departure.dispatch}`)} +
+ )} + {departure?.gate && ( +
+ {t("SHARED.NUMBER-EXIT")} + {departure.gate} +
+ )} ); }; diff --git a/src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx b/src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx index c88c2225..b3fdb97d 100644 --- a/src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx +++ b/src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx @@ -40,6 +40,29 @@ function makeLeg(overrides: Partial = {}): IFlightLeg { return { ...base, ...overrides }; } +function makeLegWithBoarding(gate?: string, dispatch?: string): IFlightLeg { + return makeLeg({ + departure: { + scheduled: { airport: "JFK", airportCode: "JFK", city: "New York", cityCode: "NYC", countryCode: "US" }, + dispatch, + gate, + terminal: "", + checkingStatus: "Scheduled", + parkingStand: "", + times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: 0, utc: "" } }, + }, + transition: { + boarding: { + start: { dayChange: { value: 0, title: "" }, local: "11:00", localTime: "11:00", tzOffset: 0, utc: "" }, + end: { dayChange: { value: 0, title: "" }, local: "11:30", localTime: "11:30", tzOffset: 0, utc: "" }, + status: "InProgress", + isActual: true, + }, + }, + status: "InFlight", + }); +} + describe("FlightDetailsAccordion", () => { it("returns null when no panels should be visible", () => { const leg = makeLeg(); @@ -106,4 +129,28 @@ describe("FlightDetailsAccordion", () => { render(); expect(screen.getByText("DETAILS.ON_BOARD_SERVICES")).toBeTruthy(); }); + + // §4.1.15.4: departure gate + transfer type in boarding panel row + describe("boarding row gate + dispatch (§4.1.15.4)", () => { + it("4.1.15.4-AccordionBoarding-Gate: shows gate in boarding body when departure.gate is set", () => { + const leg = makeLegWithBoarding("D5", undefined); + render(); + expect(screen.getByTestId("boarding-gate")).toBeTruthy(); + expect(screen.getByText("D5")).toBeTruthy(); + }); + + it("4.1.15.4-AccordionBoarding-Dispatch: shows dispatch in boarding body when departure.dispatch is set", () => { + const leg = makeLegWithBoarding(undefined, "Bus"); + render(); + expect(screen.getByTestId("boarding-dispatch")).toBeTruthy(); + expect(screen.getByText("DISPATCH.Bus")).toBeTruthy(); + }); + + it("4.1.15.4-AccordionBoarding-NoFields: omits gate/dispatch rows when departure has neither", () => { + const leg = makeLegWithBoarding(undefined, undefined); + render(); + expect(screen.queryByTestId("boarding-gate")).toBeNull(); + expect(screen.queryByTestId("boarding-dispatch")).toBeNull(); + }); + }); }); diff --git a/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx b/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx index 6229aa4a..16442a60 100644 --- a/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx +++ b/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx @@ -161,7 +161,25 @@ export const FlightDetailsAccordion: FC = ({ leg, v icon: ICON_BOARDING, title: t("DETAILS.BOARDING"), statusStatus: boarding.status, - body: , + body: ( + <> + + {/* §4.1.15.4: dispatch type (Bus / Bridge) and departure gate. + Angular parity: flight-details-boarding.component.html lines 17–24 */} + {leg.departure.dispatch && ( +
+ {t("SHARED.LANDING-TRANSFER")} + {t(`DISPATCH.${leg.departure.dispatch}`)} +
+ )} + {leg.departure.gate && ( +
+ {t("SHARED.NUMBER-EXIT")} + {leg.departure.gate} +
+ )} + + ), legacyTestId: "boarding-panel", isTransition: true, }); @@ -229,6 +247,9 @@ export const FlightDetailsAccordion: FC = ({ leg, v if (row.id === "boarding" && leg.transition?.boarding) { legacyPanels.push(
+ {/* departure prop intentionally omitted — dispatch/gate already + rendered in the visible accordion row body above to avoid + duplicate data-testid="boarding-gate/dispatch" in the DOM. */}
, ); diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 8cd74d91..876dd392 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -4,7 +4,8 @@ "SEATS-BUSINESS": "", "SEATS-COMFORT": "", "SEATS-ECONOMY": "", - "SEATS-TOTAL": "" + "SEATS-TOTAL": "", + "TAIL-NUMBER": "Kennzeichen" }, "BOARD": { "ARRIVAL": "Ankunft", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 439d7bfa..d06c33cf 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -4,7 +4,8 @@ "SEATS-BUSINESS": "Business", "SEATS-COMFORT": "Comfort", "SEATS-ECONOMY": "Economy", - "SEATS-TOTAL": "Number of seats" + "SEATS-TOTAL": "Number of seats", + "TAIL-NUMBER": "Tail number" }, "BOARD": { "ARRIVAL": "Arrival", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 026eae43..e5ee919c 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -4,7 +4,8 @@ "SEATS-BUSINESS": "", "SEATS-COMFORT": "", "SEATS-ECONOMY": "", - "SEATS-TOTAL": "" + "SEATS-TOTAL": "", + "TAIL-NUMBER": "Matrícula" }, "BOARD": { "ARRIVAL": "Llegada", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 06b76000..7cb3b4a6 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -4,7 +4,8 @@ "SEATS-BUSINESS": "", "SEATS-COMFORT": "", "SEATS-ECONOMY": "", - "SEATS-TOTAL": "" + "SEATS-TOTAL": "", + "TAIL-NUMBER": "Immatriculation" }, "BOARD": { "ARRIVAL": "Arrivée", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index f862db6a..af3300fd 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -4,7 +4,8 @@ "SEATS-BUSINESS": "", "SEATS-COMFORT": "", "SEATS-ECONOMY": "", - "SEATS-TOTAL": "" + "SEATS-TOTAL": "", + "TAIL-NUMBER": "Numero di coda" }, "BOARD": { "ARRIVAL": "Arrivo", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 10f95ee4..c0aacfae 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -4,7 +4,8 @@ "SEATS-BUSINESS": "", "SEATS-COMFORT": "", "SEATS-ECONOMY": "", - "SEATS-TOTAL": "" + "SEATS-TOTAL": "", + "TAIL-NUMBER": "機体番号" }, "BOARD": { "ARRIVAL": "到着", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 6393c060..2c99f4b7 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -4,7 +4,8 @@ "SEATS-BUSINESS": "", "SEATS-COMFORT": "", "SEATS-ECONOMY": "", - "SEATS-TOTAL": "" + "SEATS-TOTAL": "", + "TAIL-NUMBER": "꼬리번호" }, "BOARD": { "ARRIVAL": "도착", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 36b02a7a..19678b0e 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -4,7 +4,8 @@ "SEATS-BUSINESS": "Бизнес", "SEATS-COMFORT": "Комфорт", "SEATS-ECONOMY": "Эконом", - "SEATS-TOTAL": "Количество мест" + "SEATS-TOTAL": "Количество мест", + "TAIL-NUMBER": "Бортовой номер" }, "BOARD": { "ARRIVAL": "Прилет", diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index 939a5d6e..c0214eef 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -4,7 +4,8 @@ "SEATS-BUSINESS": "", "SEATS-COMFORT": "", "SEATS-ECONOMY": "", - "SEATS-TOTAL": "" + "SEATS-TOTAL": "", + "TAIL-NUMBER": "机尾号" }, "BOARD": { "ARRIVAL": "到达",