Add FlightSchedule accordion with days-of-week strip

This commit is contained in:
2026-04-17 02:04:36 +03:00
parent 34b84fd44d
commit 00f88406db
4 changed files with 284 additions and 0 deletions
@@ -0,0 +1,59 @@
.flight-schedule {
background: #fff;
border-radius: 8px;
padding: 16px 24px;
margin-top: 16px;
.p-accordion-content {
padding: 12px 0;
}
&__row {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
&__label { color: #666; }
&__value { font-weight: 500; }
&__days-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #eee;
}
&__section-title {
font-size: 12px;
color: #666;
text-transform: uppercase;
margin-bottom: 8px;
}
&__note {
margin-top: 12px;
font-size: 12px;
color: #666;
font-style: italic;
}
}
.days-of-week-strip {
display: flex;
gap: 8px;
flex-wrap: wrap;
.day {
padding: 8px 12px;
border-radius: 6px;
background: #e6f1fb;
font-size: 14px;
font-weight: 500;
color: #1a3a5c;
&--inactive {
background: #f6f6f6;
color: rgba(102, 102, 102, 0.5);
}
}
}
@@ -0,0 +1,156 @@
// @vitest-environment jsdom
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { FlightSchedule } from "./FlightSchedule.js";
import type { ISimpleFlight } from "../../types.js";
vi.mock("@/i18n/provider.js", () => ({
useTranslation: () => ({
t: (k: string) => {
if (k === "SHARED.NOTE-TIME-SCHEDULE") {
return "Valid from {START_DATE} to {END_DATE}";
}
return k;
},
}),
}));
interface FlightOverrides {
daysOfWeek?: { current: string; flight: string };
depLocal?: string;
arrLocal?: string;
flyingTime?: string;
}
function makeFlight(overrides: FlightOverrides = {}): ISimpleFlight {
const depLocal = overrides.depLocal ?? "2026-04-15T10:00:00";
const arrLocal = overrides.arrLocal ?? "12:30";
const flyingTime = overrides.flyingTime ?? "2h 30m";
return {
id: "X",
routeType: "Direct",
flyingTime,
status: "Scheduled",
flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date: "20260415" },
operatingBy: { carrier: "SU", flightNumber: "0022" },
leg: {
arrival: {
scheduled: {
airport: "LED",
airportCode: "LED",
city: "SPB",
cityCode: "LED",
countryCode: "RU",
},
latest: {
airport: "LED",
airportCode: "LED",
city: "SPB",
cityCode: "LED",
countryCode: "RU",
},
dispatch: "",
gate: "",
terminal: "",
times: {
scheduledArrival: {
dayChange: { value: 0, title: "" },
local: arrLocal,
localTime: "",
tzOffset: 0,
utc: "",
},
},
},
dayChange: 0,
departure: {
scheduled: {
airport: "SVO",
airportCode: "SVO",
city: "MOW",
cityCode: "MOW",
countryCode: "RU",
},
latest: {
airport: "SVO",
airportCode: "SVO",
city: "MOW",
cityCode: "MOW",
countryCode: "RU",
},
dispatch: "",
gate: "",
terminal: "",
checkingStatus: "Scheduled",
parkingStand: "",
times: {
scheduledDeparture: {
dayChange: { value: 0, title: "" },
local: depLocal,
localTime: "10:00",
tzOffset: 0,
utc: "",
},
},
},
equipment: {},
flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
flyingTime,
index: 0,
operatingBy: {},
status: "Scheduled",
updated: "",
...(overrides.daysOfWeek ? { daysOfWeek: overrides.daysOfWeek } : {}),
},
} as ISimpleFlight;
}
describe("FlightSchedule", () => {
it("returns null when firstLeg has no daysOfWeek", () => {
const { container } = render(<FlightSchedule flight={makeFlight()} />);
expect(container.firstChild).toBeNull();
});
it("returns null when daysOfWeek.flight is empty string", () => {
const flight = makeFlight({ daysOfWeek: { current: "1111111", flight: "" } });
const { container } = render(<FlightSchedule flight={flight} />);
expect(container.firstChild).toBeNull();
});
it("renders with data-testid=flight-schedule when daysOfWeek present", () => {
const flight = makeFlight({
daysOfWeek: { current: "1111111", flight: "1111111" },
});
render(<FlightSchedule flight={flight} />);
expect(screen.getByTestId("flight-schedule")).toBeTruthy();
});
it("renders 7 day boxes from daysOfWeek.flight", () => {
const flight = makeFlight({
daysOfWeek: { current: "1110000", flight: "1110000" },
});
render(<FlightSchedule flight={flight} />);
const boxes = screen.getAllByTestId(/^day-of-week-\d$/);
expect(boxes).toHaveLength(7);
});
it("substitutes {START_DATE} and {END_DATE} into the note", () => {
const flight = makeFlight({
daysOfWeek: { current: "1111111", flight: "1111111" },
});
render(<FlightSchedule flight={flight} />);
const note = screen.getByTestId("flight-schedule-note");
expect(note.textContent).toContain("13.04.2026");
expect(note.textContent).toContain("19.04.2026");
});
it("renders scheduled departure/arrival times and duration", () => {
const flight = makeFlight({
daysOfWeek: { current: "1111111", flight: "1111111" },
});
render(<FlightSchedule flight={flight} />);
expect(screen.getByText("10:00")).toBeTruthy();
expect(screen.getByText("12:30")).toBeTruthy();
expect(screen.getByText("2h 30m")).toBeTruthy();
});
});
@@ -0,0 +1,67 @@
import type { FC } from "react";
import { Accordion, AccordionTab } from "primereact/accordion";
import { useTranslation } from "@/i18n/provider.js";
import type { ISimpleFlight } from "../../types.js";
import { DaysOfWeekStrip } from "./DaysOfWeekStrip.js";
import { getWeekDateRange } from "./weekDateRange.js";
import "./FlightSchedule.scss";
export interface FlightScheduleProps {
flight: ISimpleFlight;
}
function formatLocalTime(iso: string | undefined): string {
if (!iso) return "";
if (/^\d{2}:\d{2}$/.test(iso)) return iso;
const match = /T(\d{2}:\d{2})/.exec(iso);
return match ? match[1]! : iso;
}
export const FlightSchedule: FC<FlightScheduleProps> = ({ flight }) => {
const { t } = useTranslation();
const firstLeg = flight.routeType === "Direct" ? flight.leg : flight.legs[0];
const lastLeg =
flight.routeType === "Direct" ? flight.leg : flight.legs[flight.legs.length - 1];
if (!firstLeg?.daysOfWeek?.flight) return null;
if (!lastLeg) return null;
const depLocal = firstLeg.departure.times.scheduledDeparture.local;
const arrLocal = lastLeg.arrival.times.scheduledArrival.local;
const { start, end } = getWeekDateRange(depLocal);
const noteTemplate = t("SHARED.NOTE-TIME-SCHEDULE");
const note = noteTemplate.replace("{START_DATE}", start).replace("{END_DATE}", end);
return (
<section className="flight-schedule" data-testid="flight-schedule">
<Accordion multiple={false} activeIndex={0}>
<AccordionTab header={t("SHARED.SCHEDULE-FLIGHT")}>
<div className="flight-schedule__row">
<span className="flight-schedule__label">{t("SHARED.DEPARTURE-SCHEDULED")}</span>
<span className="flight-schedule__value">{formatLocalTime(depLocal)}</span>
</div>
<div className="flight-schedule__row">
<span className="flight-schedule__label">{t("SHARED.ARRIVAL-SCHEDULED")}</span>
<span className="flight-schedule__value">{formatLocalTime(arrLocal)}</span>
</div>
<div className="flight-schedule__row">
<span className="flight-schedule__label">{t("SHARED.PATH-TIME")}</span>
<span className="flight-schedule__value">{flight.flyingTime}</span>
</div>
</AccordionTab>
</Accordion>
<div className="flight-schedule__days-section">
<div className="flight-schedule__section-title">
{t("SHARED.DAYS-EXECUTE-FLIGHT")}
</div>
<DaysOfWeekStrip flightBitString={firstLeg.daysOfWeek.flight} />
<div className="flight-schedule__note" data-testid="flight-schedule-note">
{note}
</div>
</div>
</section>
);
};
@@ -0,0 +1,2 @@
export { FlightSchedule } from "./FlightSchedule.js";
export type { FlightScheduleProps } from "./FlightSchedule.js";