diff --git a/src/features/online-board/components/FlightSchedule/FlightSchedule.scss b/src/features/online-board/components/FlightSchedule/FlightSchedule.scss new file mode 100644 index 00000000..b4013f0d --- /dev/null +++ b/src/features/online-board/components/FlightSchedule/FlightSchedule.scss @@ -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); + } + } +} diff --git a/src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx b/src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx new file mode 100644 index 00000000..aed86113 --- /dev/null +++ b/src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx @@ -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(); + expect(container.firstChild).toBeNull(); + }); + + it("returns null when daysOfWeek.flight is empty string", () => { + const flight = makeFlight({ daysOfWeek: { current: "1111111", flight: "" } }); + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders with data-testid=flight-schedule when daysOfWeek present", () => { + const flight = makeFlight({ + daysOfWeek: { current: "1111111", flight: "1111111" }, + }); + render(); + expect(screen.getByTestId("flight-schedule")).toBeTruthy(); + }); + + it("renders 7 day boxes from daysOfWeek.flight", () => { + const flight = makeFlight({ + daysOfWeek: { current: "1110000", flight: "1110000" }, + }); + render(); + 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(); + 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(); + expect(screen.getByText("10:00")).toBeTruthy(); + expect(screen.getByText("12:30")).toBeTruthy(); + expect(screen.getByText("2h 30m")).toBeTruthy(); + }); +}); diff --git a/src/features/online-board/components/FlightSchedule/FlightSchedule.tsx b/src/features/online-board/components/FlightSchedule/FlightSchedule.tsx new file mode 100644 index 00000000..260d02e2 --- /dev/null +++ b/src/features/online-board/components/FlightSchedule/FlightSchedule.tsx @@ -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 = ({ 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 ( +
+ + +
+ {t("SHARED.DEPARTURE-SCHEDULED")} + {formatLocalTime(depLocal)} +
+
+ {t("SHARED.ARRIVAL-SCHEDULED")} + {formatLocalTime(arrLocal)} +
+
+ {t("SHARED.PATH-TIME")} + {flight.flyingTime} +
+
+
+ +
+
+ {t("SHARED.DAYS-EXECUTE-FLIGHT")} +
+ +
+ {note} +
+
+
+ ); +}; diff --git a/src/features/online-board/components/FlightSchedule/index.ts b/src/features/online-board/components/FlightSchedule/index.ts new file mode 100644 index 00000000..56e4b713 --- /dev/null +++ b/src/features/online-board/components/FlightSchedule/index.ts @@ -0,0 +1,2 @@ +export { FlightSchedule } from "./FlightSchedule.js"; +export type { FlightScheduleProps } from "./FlightSchedule.js";