Add FlightSchedule accordion with days-of-week strip
This commit is contained in:
@@ -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";
|
||||
Reference in New Issue
Block a user