Match Angular duration format + UTC offset + scheduled path time

- formatDuration(locale='ru') now emits 'Xч. Xмин.' (and 'Xд. Xч. Xмин.')
  with trailing dots, matching Angular's DurationPipe + SHARED.SHORT-HOUR
  translations. Every 'В пути', 'До прилета', and 'Время в пути' label on
  the details page now reads identically to Angular.
- FlightSchedule shows the SCHEDULED duration (dep→arr from the timestamps)
  instead of the actual flyingTime the API reports, so the Расписание рейса
  row reads '1ч. 30мин.' for a 15:30→17:00 schedule even after an early
  landing. The Вылет / Прилет columns also surface the 'UTC+HH:MM' offset
  below each time, matching the Angular layout.
This commit is contained in:
2026-04-18 21:11:58 +03:00
parent b6920cbf60
commit f4b4c53816
6 changed files with 56 additions and 24 deletions
@@ -31,6 +31,13 @@
color: #222;
}
&__offset {
margin-left: 6px;
font-size: 11px;
font-weight: 500;
color: #8a8a8a;
}
&__days-section {
margin-top: 20px;
}
@@ -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(<FlightSchedule flight={flight} />);
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();
});
});
@@ -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<FlightScheduleProps> = ({ flight }) => {
@@ -54,16 +56,33 @@ export const FlightSchedule: FC<FlightScheduleProps> = ({ flight }) => {
<div className="flight-schedule__body">
<div className="flight-schedule__col">
<div className="flight-schedule__label">{t("SHARED.DEPARTURE-SCHEDULED")}</div>
<div className="flight-schedule__value">{formatLocalTime(depLocal)}</div>
<div className="flight-schedule__value">
{formatLocalTime(depLocal)}
{formatUtcOffset(depLocal) && (
<span className="flight-schedule__offset">
{formatUtcOffset(depLocal)}
</span>
)}
</div>
</div>
<div className="flight-schedule__col">
<div className="flight-schedule__label">{t("SHARED.ARRIVAL-SCHEDULED")}</div>
<div className="flight-schedule__value">{formatLocalTime(arrLocal)}</div>
<div className="flight-schedule__value">
{formatLocalTime(arrLocal)}
{formatUtcOffset(arrLocal) && (
<span className="flight-schedule__offset">
{formatUtcOffset(arrLocal)}
</span>
)}
</div>
</div>
<div className="flight-schedule__col">
<div className="flight-schedule__label">{t("SHARED.PATH-TIME")}</div>
<div className="flight-schedule__value">
{humanizeFlyingTime(flight.flyingTime, "ru")}
{formatDuration(
scheduledDurationMinutes(depLocal, arrLocal),
"ru",
)}
</div>
</div>
</div>
@@ -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");
});
+4 -4
View File
@@ -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д.. 12мин.");
});
it("returns 'Unknown' for negative values", () => {
+4 -2
View File
@@ -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} ` : "";