Add FlightDetailsAccordion container orchestrating 6 panel components

This commit is contained in:
2026-04-16 22:39:45 +03:00
parent c125322078
commit 0c27422da7
3 changed files with 248 additions and 0 deletions
@@ -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;
}
}
@@ -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> = {}): 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(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
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(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
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(<FlightDetailsAccordion leg={leg} viewType="Schedule" />);
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(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
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(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
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(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
expect(screen.getByText("DETAILS.ON_BOARD_SERVICES")).toBeTruthy();
});
});
@@ -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<FlightDetailsAccordionProps> = ({ leg, viewType }) => {
const { t } = useTranslation();
const [openIds, setOpenIds] = useState<Set<string>>(new Set());
const panels: PanelDef[] = [];
if (shouldShowTransition(leg.transition?.registration, leg.status, viewType)) {
panels.push({
id: "registration",
header: t("DETAILS.REGISTRATION"),
content: <RegistrationPanel item={leg.transition!.registration!} />,
});
}
if (shouldShowTransition(leg.transition?.boarding, leg.status, viewType)) {
panels.push({
id: "boarding",
header: t("DETAILS.BOARDING"),
content: <BoardingPanel item={leg.transition!.boarding!} />,
});
}
if (shouldShowTransition(leg.transition?.deboarding, leg.status, viewType)) {
panels.push({
id: "deboarding",
header: t("DETAILS.DEBOARDING"),
content: <DeboardingPanel item={leg.transition!.deboarding!} arrival={leg.arrival} />,
});
}
if (shouldShowAircraft(leg.equipment)) {
panels.push({
id: "aircraft",
header: t("DETAILS.AIRCRAFT"),
content: <AircraftPanel equipment={leg.equipment} />,
});
}
if ((leg.equipment.meal?.length ?? 0) > 0) {
panels.push({
id: "meal",
header: t("DETAILS.MEAL"),
content: <MealPanel meals={leg.equipment.meal!} />,
});
}
if ((leg.equipment.aircraft?.actual?.onBoardServices?.length ?? 0) > 0) {
panels.push({
id: "services",
header: t("DETAILS.ON_BOARD_SERVICES"),
content: <ServicesPanel services={leg.equipment.aircraft!.actual!.onBoardServices!} />,
});
}
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 (
<div className="flight-details-accordion p-accordion" data-testid="flight-details-accordion">
{panels.map((panel) => {
const isOpen = openIds.has(panel.id);
return (
<div
key={panel.id}
className={`p-accordion-tab${isOpen ? " p-accordion-tab--active" : ""}`}
data-testid={`accordion-tab-${panel.id}`}
>
<div
className={`p-accordion-header${isOpen ? " p-highlight" : ""}`}
role="button"
tabIndex={0}
onClick={() => toggle(panel.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggle(panel.id);
}
}}
>
<span>{panel.header}</span>
<span aria-hidden="true">{isOpen ? "\u25B2" : "\u25BC"}</span>
</div>
{isOpen && <div className="p-accordion-content">{panel.content}</div>}
</div>
);
})}
</div>
);
};