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:
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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} ` : "";
|
||||
|
||||
Reference in New Issue
Block a user