diff --git a/src/features/online-board/components/details-panels/FlightDetailsAccordion.scss b/src/features/online-board/components/details-panels/FlightDetailsAccordion.scss new file mode 100644 index 00000000..083c9663 --- /dev/null +++ b/src/features/online-board/components/details-panels/FlightDetailsAccordion.scss @@ -0,0 +1,29 @@ +.flight-details-accordion { + margin-top: 16px; + + .p-accordion-tab { + border: 1px solid #e0e0e0; + border-radius: 4px; + margin-bottom: 8px; + overflow: hidden; + } + + .p-accordion-header { + padding: 12px 16px; + background: #f8f9fa; + cursor: pointer; + font-weight: 500; + display: flex; + justify-content: space-between; + align-items: center; + + &:hover { + background: #eef1f4; + } + } + + .p-accordion-content { + padding: 0 16px 12px; + background: #fff; + } +} diff --git a/src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx b/src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx new file mode 100644 index 00000000..651e54a1 --- /dev/null +++ b/src/features/online-board/components/details-panels/FlightDetailsAccordion.test.tsx @@ -0,0 +1,104 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { FlightDetailsAccordion } from "./FlightDetailsAccordion.js"; +import type { IFlightLeg } from "../../types.js"; + +vi.mock("@/i18n/provider.js", () => ({ + useTranslation: () => ({ t: (k: string) => k }), +})); + +function makeLeg(overrides: Partial = {}): IFlightLeg { + const base: IFlightLeg = { + arrival: { + scheduled: { airport: "SVO", airportCode: "SVO", city: "Moscow", cityCode: "MOW", countryCode: "RU" }, + latest: { airport: "SVO", airportCode: "SVO", city: "Moscow", cityCode: "MOW", countryCode: "RU" }, + dispatch: "", + gate: "", + terminal: "", + times: { scheduledArrival: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: 0, utc: "" } }, + }, + dayChange: 0, + departure: { + scheduled: { airport: "JFK", airportCode: "JFK", city: "New York", cityCode: "NYC", countryCode: "US" }, + latest: { 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: "" } }, + }, + equipment: { name: "A320" }, + flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false }, + flyingTime: "1h 30m", + index: 0, + operatingBy: {}, + status: "Scheduled", + updated: "", + }; + return { ...base, ...overrides }; +} + +describe("FlightDetailsAccordion", () => { + it("returns null when no panels should be visible", () => { + const leg = makeLeg(); + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders registration tab when transition.registration is active", () => { + const leg = makeLeg({ + transition: { + registration: { + start: { dayChange: { value: 0, title: "" }, local: "10:00", localTime: "10:00", tzOffset: 0, utc: "" }, + end: { dayChange: { value: 0, title: "" }, local: "10:30", localTime: "10:30", tzOffset: 0, utc: "" }, + status: "InProgress", + isActual: true, + }, + }, + }); + render(); + expect(screen.getByTestId("flight-details-accordion")).toBeTruthy(); + expect(screen.getByText("DETAILS.REGISTRATION")).toBeTruthy(); + }); + + it("hides transition panels when viewType is Schedule", () => { + const leg = makeLeg({ + transition: { + registration: { + start: { dayChange: { value: 0, title: "" }, local: "10:00", localTime: "", tzOffset: 0, utc: "" }, + end: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: 0, utc: "" }, + status: "InProgress", + isActual: true, + }, + }, + }); + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders aircraft tab when equipment.aircraft has title", () => { + const leg = makeLeg({ + equipment: { name: "A320", aircraft: { actual: { title: "Airbus A320" } } }, + }); + render(); + expect(screen.getByText("DETAILS.AIRCRAFT")).toBeTruthy(); + }); + + it("renders meal tab when equipment.meal has items", () => { + const leg = makeLeg({ + equipment: { name: "A320", meal: [{ type: "Economy" }] }, + }); + render(); + expect(screen.getByText("DETAILS.MEAL")).toBeTruthy(); + }); + + it("renders services tab when aircraft.actual.onBoardServices has items", () => { + const leg = makeLeg({ + equipment: { name: "A320", aircraft: { actual: { title: "A320", onBoardServices: [{ id: 1 }] } } }, + }); + render(); + expect(screen.getByText("DETAILS.ON_BOARD_SERVICES")).toBeTruthy(); + }); +}); diff --git a/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx b/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx new file mode 100644 index 00000000..bac01635 --- /dev/null +++ b/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx @@ -0,0 +1,115 @@ +import { type FC, type JSX, useState } from "react"; +import { useTranslation } from "@/i18n/provider.js"; +import type { IFlightLeg } from "../../types.js"; +import { shouldShowTransition, shouldShowAircraft, type DetailsViewType } from "./shared.js"; +import { RegistrationPanel } from "./RegistrationPanel.js"; +import { BoardingPanel } from "./BoardingPanel.js"; +import { DeboardingPanel } from "./DeboardingPanel.js"; +import { AircraftPanel } from "./AircraftPanel.js"; +import { MealPanel } from "./MealPanel.js"; +import { ServicesPanel } from "./ServicesPanel.js"; +import "./FlightDetailsAccordion.scss"; + +export interface FlightDetailsAccordionProps { + leg: IFlightLeg; + viewType: DetailsViewType; +} + +interface PanelDef { + id: string; + header: string; + content: JSX.Element; +} + +export const FlightDetailsAccordion: FC = ({ leg, viewType }) => { + const { t } = useTranslation(); + const [openIds, setOpenIds] = useState>(new Set()); + + const panels: PanelDef[] = []; + + if (shouldShowTransition(leg.transition?.registration, leg.status, viewType)) { + panels.push({ + id: "registration", + header: t("DETAILS.REGISTRATION"), + content: , + }); + } + if (shouldShowTransition(leg.transition?.boarding, leg.status, viewType)) { + panels.push({ + id: "boarding", + header: t("DETAILS.BOARDING"), + content: , + }); + } + if (shouldShowTransition(leg.transition?.deboarding, leg.status, viewType)) { + panels.push({ + id: "deboarding", + header: t("DETAILS.DEBOARDING"), + content: , + }); + } + if (shouldShowAircraft(leg.equipment)) { + panels.push({ + id: "aircraft", + header: t("DETAILS.AIRCRAFT"), + content: , + }); + } + if ((leg.equipment.meal?.length ?? 0) > 0) { + panels.push({ + id: "meal", + header: t("DETAILS.MEAL"), + content: , + }); + } + if ((leg.equipment.aircraft?.actual?.onBoardServices?.length ?? 0) > 0) { + panels.push({ + id: "services", + header: t("DETAILS.ON_BOARD_SERVICES"), + content: , + }); + } + + if (panels.length === 0) return null; + + const toggle = (id: string) => { + setOpenIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + return ( +
+ {panels.map((panel) => { + const isOpen = openIds.has(panel.id); + return ( +
+
toggle(panel.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggle(panel.id); + } + }} + > + {panel.header} + +
+ {isOpen &&
{panel.content}
} +
+ ); + })} +
+ ); +};