diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.scss b/src/features/online-board/components/OnlineBoardDetailsPage.scss
index 1638c7f6..9d47dee9 100644
--- a/src/features/online-board/components/OnlineBoardDetailsPage.scss
+++ b/src/features/online-board/components/OnlineBoardDetailsPage.scss
@@ -3,6 +3,171 @@
@use "../../../styles/fonts" as fonts;
@use "../../../styles/screen" as screen;
+// ---------------------------------------------------------------------------
+// Leg route strip — Angular's flight-details-wrapper layout.
+// ---------------------------------------------------------------------------
+
+.leg-route {
+ padding: vars.$space-xl;
+
+ &__main {
+ display: grid;
+ grid-template-columns: auto 1fr 1fr auto 1fr;
+ gap: 24px;
+ align-items: center;
+ }
+
+ &__times {
+ display: flex;
+ flex-direction: column;
+ line-height: 1;
+ }
+
+ &__time {
+ font-size: 32px;
+ font-weight: 500;
+ color: #222;
+
+ &-strike {
+ font-size: 14px;
+ color: colors.$orange;
+ text-decoration: line-through;
+ margin-top: 4px;
+ }
+ }
+
+ &__day-change {
+ font-size: 12px;
+ color: colors.$blue;
+ vertical-align: super;
+ margin-left: 4px;
+ }
+
+ &__station {
+ min-width: 0;
+ }
+
+ &__city {
+ font-size: 20px;
+ font-weight: 500;
+ color: #222;
+ }
+
+ &__airport {
+ font-size: 12px;
+ color: #8a8a8a;
+ margin-top: 4px;
+ }
+
+ &__center {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-width: 180px;
+ gap: 4px;
+ }
+
+ &__status-text {
+ font-size: 14px;
+ font-weight: 500;
+ color: colors.$blue;
+ }
+
+ &__bar {
+ width: 100%;
+ height: 2px;
+ background: #d0d5dd;
+ position: relative;
+ }
+
+ &__bar-inner {
+ height: 100%;
+ background: colors.$blue;
+ width: 0;
+ transition: width 150ms ease;
+ }
+
+ &__center--finished {
+ .leg-route__status-text { color: #6da244; }
+ .leg-route__bar-inner { width: 100%; background: #6da244; }
+ }
+
+ &__center--progress {
+ .leg-route__status-text { color: colors.$blue; }
+ .leg-route__bar-inner { width: 50%; }
+ }
+
+ &__center--cancelled {
+ .leg-route__status-text { color: #e55353; }
+ .leg-route__bar { background: #fbd4d4; }
+ .leg-route__bar-inner { background: #e55353; }
+ }
+
+ &__duration {
+ font-size: 12px;
+ color: #8a8a8a;
+ }
+
+ &__details {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 40px;
+ margin-top: vars.$space-xl;
+ padding-top: vars.$space-xl;
+ border-top: 1px dashed #e0e6f0;
+ }
+
+ &__details-side {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: vars.$space-xl;
+
+ &--arrival {
+ justify-self: end;
+ }
+ }
+
+ &__detail {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+
+ &-label {
+ font-size: 12px;
+ color: #8a8a8a;
+ }
+
+ &-value {
+ font-weight: 600;
+ color: #222;
+ }
+
+ &-offset {
+ font-weight: normal;
+ font-size: 11px;
+ color: #8a8a8a;
+ margin-left: 6px;
+ }
+
+ &-date {
+ font-size: 12px;
+ color: colors.$blue;
+ }
+ }
+
+ @include screen.mobile {
+ &__main {
+ grid-template-columns: 1fr;
+ gap: vars.$space-m;
+ }
+
+ &__details {
+ grid-template-columns: 1fr;
+ gap: vars.$space-m;
+ }
+ }
+}
+
.flight-details {
&--error,
&--not-found {
diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx
index dd87f7e5..9ef331da 100644
--- a/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx
+++ b/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx
@@ -171,9 +171,10 @@ describe("OnlineBoardDetailsPage", () => {
it("displays departure and arrival stations", () => {
render();
- // SVO and JFK appear in both FlightCard and FlightLegs sections
- expect(screen.getAllByText("SVO").length).toBeGreaterThanOrEqual(1);
- expect(screen.getAllByText("JFK").length).toBeGreaterThanOrEqual(1);
+ // The new LegRoute layout promotes city + airport name instead of
+ // IATA codes; assert those render at least once each.
+ expect(screen.getAllByText("Moscow").length).toBeGreaterThanOrEqual(1);
+ expect(screen.getAllByText("New York").length).toBeGreaterThanOrEqual(1);
});
it("displays aircraft info", () => {
diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.tsx
index d27f4787..b9ecf514 100644
--- a/src/features/online-board/components/OnlineBoardDetailsPage.tsx
+++ b/src/features/online-board/components/OnlineBoardDetailsPage.tsx
@@ -30,7 +30,7 @@ import { DetailsBackButton } from "./DetailsBackButton/index.js";
import { FlightSchedule } from "./FlightSchedule/index.js";
import { FullRouteTimeline } from "./FullRouteTimeline/index.js";
import { TransferBar } from "./TransferBar/index.js";
-import type { IParsedFlightId, IFlightLeg } from "../types.js";
+import type { IParsedFlightId, IFlightLeg, FlightStatus as FlightStatusType } from "../types.js";
import { operatingCarrier } from "../types.js";
import {
formatLocalTime,
@@ -68,67 +68,169 @@ export interface OnlineBoardDetailsPageProps {
* + terminal, plus scheduled / expected / actual times formatted as
* "23:30 UTC+03:00". Matches Angular's flight-details-wrapper rows.
*/
-function LegStation({
- side,
- code,
- airport,
- city,
- terminal,
- gate,
- bagBelt,
- scheduledIso,
- actualIso,
+/**
+ * Route strip for a single leg — mirrors Angular's flight-details-wrapper:
+ * [big time] City / Airport-Terminal [status + progress] [big time] City / Airport-Terminal
+ * with scheduled strike-through under the actual time if they differ, and
+ * a detailed time table beneath (По расписанию / Фактическое / Ожидаемое).
+ */
+function LegRoute({
+ leg,
+ status,
}: {
- side: "departure" | "arrival";
- code: string;
- airport: string | undefined;
- city: string | undefined;
- terminal: string | undefined;
- gate?: string | undefined;
- bagBelt?: string | undefined;
- scheduledIso: string;
- actualIso: string | undefined;
+ leg: IFlightLeg;
+ status: FlightStatusType;
}): JSX.Element {
const { t } = useTranslation();
- const scheduledTime = formatLocalTime(scheduledIso);
- const scheduledOffset = formatUtcOffset(scheduledIso);
- const scheduledDate = formatDayMonthYear(scheduledIso);
- const actualTime = actualIso ? formatLocalTime(actualIso) : null;
- const actualOffset = actualIso ? formatUtcOffset(actualIso) : "";
+ const dep = leg.departure;
+ const arr = leg.arrival;
+ const depSched = dep.times.scheduledDeparture;
+ const arrSched = arr.times.scheduledArrival;
+ const depActual = dep.times.actualBlockOff;
+ const arrActual = arr.times.actualBlockOn;
+
+ const depMainTime = formatLocalTime(depActual?.local ?? depSched.local);
+ const depScheduledTime = formatLocalTime(depSched.local);
+ const depShowStrike = Boolean(depActual?.local) && depMainTime !== depScheduledTime;
+ const arrMainTime = formatLocalTime(arrActual?.local ?? arrSched.local);
+ const arrScheduledTime = formatLocalTime(arrSched.local);
+ const arrShowStrike = Boolean(arrActual?.local) && arrMainTime !== arrScheduledTime;
+
+ const arrDayChange =
+ arrActual?.dayChange?.value ?? arrSched.dayChange?.value ?? 0;
+
+ const depDetail = (iso: string | undefined) => {
+ if (!iso) return null;
+ return {
+ time: formatLocalTime(iso),
+ offset: formatUtcOffset(iso),
+ date: formatDayMonthYear(iso),
+ };
+ };
+
+ const depScheduledDetail = depDetail(depSched.local);
+ const depActualDetail = depActual?.local ? depDetail(depActual.local) : null;
+ const arrScheduledDetail = depDetail(arrSched.local);
+ const arrActualDetail = arrActual?.local ? depDetail(arrActual.local) : null;
+
+ const isFinished = status === "Arrived" || status === "Landed";
+ const isCancelled = status === "Cancelled";
return (
-
-
{code}
- {airport &&
{airport}}
- {city &&
{city}}
- {terminal && (
-
- {t("SHARED.TERMINAL")} {terminal}
-
- )}
- {gate &&
{t("SHARED.GATE")} {gate}}
- {bagBelt && (
-
- {t("DETAILS.BAG_BELT")} {bagBelt}
-
- )}
-
- {t("SHARED.SCHEDULED")}
- {scheduledTime}
- {scheduledOffset && (
- {scheduledOffset}
- )}
- {scheduledDate}
-
- {actualTime && (
-
-
{t("SHARED.ACTUAL")}
-
{actualTime}
- {actualOffset && (
-
{actualOffset}
+
+
+
+
{depMainTime}
+ {depShowStrike && (
+
{depScheduledTime}
)}
- )}
+
+
+
{dep.scheduled.city}
+
+ {dep.scheduled.airport}
+ {dep.terminal ? ` - ${dep.terminal}` : ""}
+
+
+
+
+
+ {t(`FLIGHT-STATUSES.${status}`)}
+
+
+
+ {humanizeFlyingTime(leg.flyingTime, "ru")}
+
+
+
+
+
+ {arrMainTime}
+ {arrDayChange > 0 && (
+ +{arrDayChange}
+ )}
+
+ {arrShowStrike && (
+
{arrScheduledTime}
+ )}
+
+
+
+
{arr.scheduled.city}
+
+ {arr.scheduled.airport}
+ {arr.terminal ? ` - ${arr.terminal}` : ""}
+
+
+
+
+
+
+ {depScheduledDetail && (
+
+
{t("SHARED.SCHEDULED")}
+
+ {depScheduledDetail.time}
+ {depScheduledDetail.offset && (
+
+ {depScheduledDetail.offset}
+
+ )}
+
+
{depScheduledDetail.date}
+
+ )}
+ {depActualDetail && (
+
+
{t("SHARED.ACTUAL")}
+
+ {depActualDetail.time}
+ {depActualDetail.offset && (
+
+ {depActualDetail.offset}
+
+ )}
+
+
{depActualDetail.date}
+
+ )}
+
+
+ {arrScheduledDetail && (
+
+
{t("SHARED.SCHEDULED")}
+
+ {arrScheduledDetail.time}
+ {arrScheduledDetail.offset && (
+
+ {arrScheduledDetail.offset}
+
+ )}
+
+
{arrScheduledDetail.date}
+
+ )}
+ {arrActualDetail && (
+
+
+ {isFinished ? t("SHARED.ACTUAL") : t("DETAILS.STATUS_EXPECTED")}
+
+
+ {arrActualDetail.time}
+ {arrActualDetail.offset && (
+
+ {arrActualDetail.offset}
+
+ )}
+
+
{arrActualDetail.date}
+
+ )}
+
+
);
}
@@ -160,33 +262,7 @@ function FlightLegs({
)}
-
-
-
-
- {humanizeFlyingTime(leg.flyingTime, "ru")}
-
-
-
-
+
{leg.equipment.name && (
diff --git a/tests/integration/online-board/flight-details.test.tsx b/tests/integration/online-board/flight-details.test.tsx
index 113db33e..4769d209 100644
--- a/tests/integration/online-board/flight-details.test.tsx
+++ b/tests/integration/online-board/flight-details.test.tsx
@@ -276,7 +276,7 @@ describe("Flight details page integration", () => {
expect(screen.getByTestId("flight-details-not-found")).toBeTruthy();
});
- it("renders airport codes in leg stations", () => {
+ it("renders departure and arrival stations", () => {
setupWithFlight();
render(
{
canonicalOrigin="https://www.aeroflot.ru"
/>,
);
- // Airport codes appear in both FlightCard and FlightLegs
- expect(screen.getAllByText("SVO").length).toBeGreaterThanOrEqual(1);
- expect(screen.getAllByText("JFK").length).toBeGreaterThanOrEqual(1);
+ // LegRoute promotes the city name now (matches Angular), and the
+ // airport name appears as the secondary line. Assert both city names.
+ expect(screen.getAllByText("Moscow").length).toBeGreaterThanOrEqual(1);
+ expect(screen.getAllByText("New York").length).toBeGreaterThanOrEqual(1);
});
});