diff --git a/src/features/online-board/components/FlightSchedule/FlightSchedule.scss b/src/features/online-board/components/FlightSchedule/FlightSchedule.scss index 8da6b3d9..07341ad4 100644 --- a/src/features/online-board/components/FlightSchedule/FlightSchedule.scss +++ b/src/features/online-board/components/FlightSchedule/FlightSchedule.scss @@ -31,6 +31,13 @@ color: #222; } + &__offset { + margin-left: 6px; + font-size: 11px; + font-weight: 500; + color: #8a8a8a; + } + &__days-section { margin-top: 20px; } diff --git a/src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx b/src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx index aed86113..30377ff5 100644 --- a/src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx +++ b/src/features/online-board/components/FlightSchedule/FlightSchedule.test.tsx @@ -24,8 +24,8 @@ interface FlightOverrides { 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"; + const arrLocal = overrides.arrLocal ?? "2026-04-15T12:30:00"; + const flyingTime = overrides.flyingTime ?? "02:30:00"; return { id: "X", routeType: "Direct", @@ -149,8 +149,12 @@ describe("FlightSchedule", () => { daysOfWeek: { current: "1111111", flight: "1111111" }, }); render(); - expect(screen.getByText("10:00")).toBeTruthy(); - expect(screen.getByText("12:30")).toBeTruthy(); - expect(screen.getByText("2h 30m")).toBeTruthy(); + // The dep/arr times are rendered alongside their UTC offset suffix in + // separate spans, so use regex matchers that ignore the offset. + expect(screen.getByText(/^10:00$/)).toBeTruthy(); + expect(screen.getByText(/^12:30$/)).toBeTruthy(); + // Scheduled duration derived from dep→arr timestamps, Angular format + // '2ч. 30мин.' via formatDuration in ru locale. + expect(screen.getByText("2ч. 30мин.")).toBeTruthy(); }); }); diff --git a/src/features/online-board/components/FlightSchedule/FlightSchedule.tsx b/src/features/online-board/components/FlightSchedule/FlightSchedule.tsx index f92718a0..f31c578e 100644 --- a/src/features/online-board/components/FlightSchedule/FlightSchedule.tsx +++ b/src/features/online-board/components/FlightSchedule/FlightSchedule.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "@/i18n/provider.js"; import type { ISimpleFlight } from "../../types.js"; import { DaysOfWeekStrip } from "./DaysOfWeekStrip.js"; import { getWeekDateRange } from "./weekDateRange.js"; -import { formatDuration } from "@/shared/utils/datetime/index.js"; +import { formatDuration, formatUtcOffset } from "@/shared/utils/datetime/index.js"; import "./FlightSchedule.scss"; export interface FlightScheduleProps { @@ -18,14 +18,16 @@ function formatLocalTime(iso: string | undefined): string { return match ? match[1]! : iso; } -function humanizeFlyingTime(value: string, locale: string): string { - if (!value) return ""; - const parts = value.split(":"); - if (parts.length < 2) return value; - const h = Number(parts[0]); - const m = Number(parts[1]); - if (Number.isNaN(h) || Number.isNaN(m)) return value; - return formatDuration(h * 60 + m, locale); +/** + * Compute the scheduled dep→arr duration in minutes. Angular's Расписание + * рейса shows the scheduled time, not the actual flyingTime reported by the + * API (which reflects the real landing). + */ +function scheduledDurationMinutes(depIso: string, arrIso: string): number { + const dep = Date.parse(depIso); + const arr = Date.parse(arrIso); + if (Number.isNaN(dep) || Number.isNaN(arr) || arr <= dep) return 0; + return Math.round((arr - dep) / 60000); } export const FlightSchedule: FC = ({ flight }) => { @@ -54,16 +56,33 @@ export const FlightSchedule: FC = ({ flight }) => {
{t("SHARED.DEPARTURE-SCHEDULED")}
-
{formatLocalTime(depLocal)}
+
+ {formatLocalTime(depLocal)} + {formatUtcOffset(depLocal) && ( + + {formatUtcOffset(depLocal)} + + )} +
{t("SHARED.ARRIVAL-SCHEDULED")}
-
{formatLocalTime(arrLocal)}
+
+ {formatLocalTime(arrLocal)} + {formatUtcOffset(arrLocal) && ( + + {formatUtcOffset(arrLocal)} + + )} +
{t("SHARED.PATH-TIME")}
- {humanizeFlyingTime(flight.flyingTime, "ru")} + {formatDuration( + scheduledDurationMinutes(depLocal, arrLocal), + "ru", + )}
diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx index cccd058f..aa03debf 100644 --- a/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx +++ b/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx @@ -202,7 +202,7 @@ describe("OnlineBoardDetailsPage", () => { // value is still present inside it. const el = screen.getByTestId("flying-time"); expect(el).toBeTruthy(); - expect(el.textContent).toMatch(/10ч\s*30м/); + expect(el.textContent).toMatch(/10ч\.\s*30мин\./); expect(el.className).toContain("visually-hidden"); }); diff --git a/src/shared/utils/datetime/datetime.test.ts b/src/shared/utils/datetime/datetime.test.ts index bbcca745..07d568e1 100644 --- a/src/shared/utils/datetime/datetime.test.ts +++ b/src/shared/utils/datetime/datetime.test.ts @@ -19,12 +19,12 @@ describe("formatDuration", () => { expect(formatDuration(1572)).toBe("1d 2h 12m"); }); - it("formats in Russian locale", () => { - expect(formatDuration(150, "ru")).toBe("2ч 30м"); + it("formats in Russian locale (Angular parity: 'Xч. Xмин.')", () => { + expect(formatDuration(150, "ru")).toBe("2ч. 30мин."); }); - it("formats days in Russian locale", () => { - expect(formatDuration(1572, "ru")).toBe("1д 2ч 12м"); + it("formats days in Russian locale (Angular parity: 'Xд. Xч. Xмин.')", () => { + expect(formatDuration(1572, "ru")).toBe("1д. 2ч. 12мин."); }); it("returns 'Unknown' for negative values", () => { diff --git a/src/shared/utils/datetime/index.ts b/src/shared/utils/datetime/index.ts index d251257f..f5ed3cde 100644 --- a/src/shared/utils/datetime/index.ts +++ b/src/shared/utils/datetime/index.ts @@ -7,9 +7,11 @@ /** * Format a duration given in total minutes into a human-readable string. + * Russian units mirror Angular's DurationPipe (SHORT-DAY='д.', SHORT-HOUR='ч.', + * SHORT-MIN='мин.') so values read as '1ч. 30мин.' not '1ч 30м'. * * @example formatDuration(150) => "2h 30m" - * @example formatDuration(150, "ru") => "2ч 30м" + * @example formatDuration(150, "ru") => "2ч. 30мин." * @example formatDuration(0) => "0h 0m" */ export function formatDuration( @@ -24,7 +26,7 @@ export function formatDuration( const units = locale === "ru" - ? { d: "д", h: "ч", m: "м" } + ? { d: "д.", h: "ч.", m: "мин." } : { d: "d", h: "h", m: "m" }; const daysPart = days > 0 ? `${days}${units.d} ` : "";