Audit Online-Board collapsed row per TZ 4.1.13.3 Tables 23-27
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* TZ §4.1.13.3 — Collapsed row audit (Tables 23–27)
|
||||
*
|
||||
* Each test maps to a specific column requirement from the TZ tables
|
||||
* and is labelled with the table + column reference for traceability.
|
||||
*/
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { FlightCard } from "./FlightCard.js";
|
||||
import type { ISimpleFlight } from "@/features/online-board/types.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock("@/i18n/provider.js", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (k: string) => k,
|
||||
i18n: { language: "ru" },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/i18n/useLocale.js", () => ({
|
||||
useLocale: () => ({ language: "ru" }),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/hooks/useDictionaries.js", () => ({
|
||||
useCityName: (code: string) => {
|
||||
const map: Record<string, string> = { SVO: "Москва", LED: "Санкт-Петербург" };
|
||||
return map[code] ?? "";
|
||||
},
|
||||
}));
|
||||
|
||||
// IFlyWarning tries to render inside expanded panels but we only test collapsed
|
||||
vi.mock("./IFlyWarning.js", () => ({
|
||||
IFlyWarning: () => null,
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixture helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeTimesSet(local: string, dayChangeValue = 0) {
|
||||
return {
|
||||
dayChange: { value: dayChangeValue, title: "" },
|
||||
local,
|
||||
localTime: local.slice(11, 16),
|
||||
tzOffset: 0,
|
||||
utc: local,
|
||||
};
|
||||
}
|
||||
|
||||
function makeStation(
|
||||
airportCode: string,
|
||||
city: string,
|
||||
airport: string,
|
||||
terminal?: string,
|
||||
) {
|
||||
return {
|
||||
scheduled: {
|
||||
airport,
|
||||
airportCode,
|
||||
city,
|
||||
cityCode: airportCode,
|
||||
countryCode: "RU",
|
||||
},
|
||||
terminal,
|
||||
times: undefined as never, // overridden per dep/arr below
|
||||
};
|
||||
}
|
||||
|
||||
function makeLeg(overrides: {
|
||||
depCode?: string;
|
||||
depCity?: string;
|
||||
depAirport?: string;
|
||||
depTerminal?: string;
|
||||
depLocal?: string;
|
||||
depActualLocal?: string;
|
||||
depDayChange?: number;
|
||||
arrCode?: string;
|
||||
arrCity?: string;
|
||||
arrAirport?: string;
|
||||
arrTerminal?: string;
|
||||
arrLocal?: string;
|
||||
arrActualLocal?: string;
|
||||
arrDayChange?: number;
|
||||
routeChanged?: boolean;
|
||||
returnToAirport?: boolean;
|
||||
aircraftTitle?: string;
|
||||
}) {
|
||||
const depLocal = overrides.depLocal ?? "2026-04-15T10:00:00+03:00";
|
||||
const arrLocal = overrides.arrLocal ?? "2026-04-15T12:30:00+03:00";
|
||||
|
||||
return {
|
||||
dayChange: 0,
|
||||
index: 0,
|
||||
flyingTime: "02:30:00",
|
||||
status: "Scheduled" as const,
|
||||
updated: "",
|
||||
operatingBy: {},
|
||||
flags: {
|
||||
checkinAvailable: false,
|
||||
returnToAirport: overrides.returnToAirport ?? false,
|
||||
routeChanged: overrides.routeChanged ?? false,
|
||||
},
|
||||
equipment: {
|
||||
aircraft: overrides.aircraftTitle
|
||||
? { actual: { title: overrides.aircraftTitle } }
|
||||
: undefined,
|
||||
},
|
||||
departure: {
|
||||
scheduled: {
|
||||
airport: overrides.depAirport ?? "Шереметьево",
|
||||
airportCode: overrides.depCode ?? "SVO",
|
||||
city: overrides.depCity ?? "Москва",
|
||||
cityCode: "MOW",
|
||||
countryCode: "RU",
|
||||
},
|
||||
terminal: overrides.depTerminal,
|
||||
checkingStatus: "Scheduled",
|
||||
times: {
|
||||
scheduledDeparture: makeTimesSet(depLocal, overrides.depDayChange ?? 0),
|
||||
...(overrides.depActualLocal
|
||||
? { actualBlockOff: makeTimesSet(overrides.depActualLocal, overrides.depDayChange ?? 0) }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
arrival: {
|
||||
scheduled: {
|
||||
airport: overrides.arrAirport ?? "Пулково",
|
||||
airportCode: overrides.arrCode ?? "LED",
|
||||
city: overrides.arrCity ?? "Санкт-Петербург",
|
||||
cityCode: "LED",
|
||||
countryCode: "RU",
|
||||
},
|
||||
terminal: overrides.arrTerminal,
|
||||
times: {
|
||||
scheduledArrival: makeTimesSet(arrLocal, overrides.arrDayChange ?? 0),
|
||||
...(overrides.arrActualLocal
|
||||
? { actualBlockOn: makeTimesSet(overrides.arrActualLocal, overrides.arrDayChange ?? 0) }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeFlight(overrides: Parameters<typeof makeLeg>[0] & {
|
||||
status?: ISimpleFlight["status"];
|
||||
operatingByCarrier?: string;
|
||||
operatingByFlightNum?: string;
|
||||
}): ISimpleFlight {
|
||||
return {
|
||||
id: "test-flight",
|
||||
routeType: "Direct",
|
||||
flyingTime: "02:30:00",
|
||||
status: overrides.status ?? "Scheduled",
|
||||
flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date: "20260415" },
|
||||
operatingBy: {
|
||||
actual: overrides.operatingByCarrier ?? "SU",
|
||||
...(overrides.operatingByFlightNum !== undefined
|
||||
? { flightNumber: overrides.operatingByFlightNum }
|
||||
: {}),
|
||||
},
|
||||
leg: makeLeg(overrides) as never,
|
||||
};
|
||||
}
|
||||
|
||||
function makeMultiLegFlight(legs: ReturnType<typeof makeLeg>[]): ISimpleFlight {
|
||||
return {
|
||||
id: "test-multi-flight",
|
||||
routeType: "MultiLeg",
|
||||
flyingTime: "05:00:00",
|
||||
status: "Scheduled",
|
||||
flightId: { carrier: "SU", flightNumber: "0100", suffix: "", date: "20260415" },
|
||||
operatingBy: { actual: "SU" },
|
||||
legs: legs as never[],
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// §4.1.13.3 Table 23 — Direct flight collapsed row
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("4.1.13.3 Table 23 — Direct flight collapsed row", () => {
|
||||
|
||||
it("T23-C1: renders marketing flight number in collapsed row", () => {
|
||||
render(<FlightCard flight={makeFlight({})} expandable />);
|
||||
expect(screen.getByTestId("flight-carrier-number").textContent).toContain("SU 0022");
|
||||
});
|
||||
|
||||
it("T23-C2: renders operator logo (full, not mini/round) for direct flight", () => {
|
||||
render(<FlightCard flight={makeFlight({})} expandable />);
|
||||
const logo = screen.getByTestId("operator-logo");
|
||||
expect(logo).toBeTruthy();
|
||||
// full logo has no --round class
|
||||
expect(logo.className).not.toContain("operator-logo--round");
|
||||
});
|
||||
|
||||
it("T23-C3: renders scheduled departure time", () => {
|
||||
render(<FlightCard flight={makeFlight({ depLocal: "2026-04-15T10:00:00+03:00" })} expandable />);
|
||||
expect(screen.getByText("10:00")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C3: shows actual departure time when available and scheduled is struck through", () => {
|
||||
render(
|
||||
<FlightCard
|
||||
flight={makeFlight({
|
||||
depLocal: "2026-04-15T10:00:00+03:00",
|
||||
depActualLocal: "2026-04-15T11:30:00+03:00",
|
||||
})}
|
||||
expandable
|
||||
/>,
|
||||
);
|
||||
// actual time shown
|
||||
expect(screen.getByText("11:30")).toBeTruthy();
|
||||
// scheduled shown struck-through
|
||||
const struck = document.querySelector(".time-group__scheduled--delayed");
|
||||
expect(struck).toBeTruthy();
|
||||
expect(struck?.textContent).toContain("10:00");
|
||||
});
|
||||
|
||||
it("T23-C4: renders +1 day-change chip on departure when dayChange=1", () => {
|
||||
render(
|
||||
<FlightCard
|
||||
flight={makeFlight({ depDayChange: 1 })}
|
||||
expandable
|
||||
/>,
|
||||
);
|
||||
const chips = document.querySelectorAll(".time-group__day-change");
|
||||
const depChip = Array.from(chips).find((el) => el.textContent === "+1");
|
||||
expect(depChip).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C4: day-change chip has tooltip (title) per TZ §4.1.4", () => {
|
||||
render(
|
||||
<FlightCard
|
||||
flight={makeFlight({ depDayChange: 1 })}
|
||||
expandable
|
||||
/>,
|
||||
);
|
||||
const chips = document.querySelectorAll(".time-group__day-change");
|
||||
const depChip = Array.from(chips).find((el) => el.textContent === "+1");
|
||||
// TZ requires a tooltip ("день" for ±1, date for ≥2)
|
||||
expect(depChip?.getAttribute("title")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C4: -1 day-change chip renders correctly", () => {
|
||||
render(
|
||||
<FlightCard
|
||||
flight={makeFlight({ depDayChange: -1 })}
|
||||
expandable
|
||||
/>,
|
||||
);
|
||||
const chips = document.querySelectorAll(".time-group__day-change");
|
||||
const chip = Array.from(chips).find((el) => el.textContent === "-1");
|
||||
expect(chip).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C5: renders departure city name", () => {
|
||||
render(<FlightCard flight={makeFlight({ depCity: "Москва" })} expandable />);
|
||||
expect(screen.getByText("Москва")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C6: renders departure airport name", () => {
|
||||
render(<FlightCard flight={makeFlight({ depAirport: "Шереметьево" })} expandable />);
|
||||
expect(screen.getByText(/Шереметьево/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C7: renders departure terminal when present", () => {
|
||||
render(<FlightCard flight={makeFlight({ depTerminal: "B" })} expandable />);
|
||||
expect(screen.getByText(/B/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C7: does not render departure terminal section when absent", () => {
|
||||
render(<FlightCard flight={makeFlight({})} expandable />);
|
||||
// Should not show "— undefined" or similar artefacts
|
||||
const text = document.body.textContent ?? "";
|
||||
expect(text).not.toContain("undefined");
|
||||
});
|
||||
|
||||
it("T23-C8: renders flight status chip with plane icon", () => {
|
||||
render(<FlightCard flight={makeFlight({ status: "Delayed" })} expandable />);
|
||||
// FlightStatus renders inside flight-card__status
|
||||
const statusEl = document.querySelector(".flight-card__status");
|
||||
expect(statusEl).toBeTruthy();
|
||||
// status plane SVG present
|
||||
expect(statusEl?.querySelector("svg")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C8: status text key is rendered for each status", () => {
|
||||
const statuses: ISimpleFlight["status"][] = [
|
||||
"Scheduled", "Sent", "InFlight", "Landed", "Arrived", "Delayed", "Cancelled", "Unknown",
|
||||
];
|
||||
for (const status of statuses) {
|
||||
const { unmount } = render(<FlightCard flight={makeFlight({ status })} expandable />);
|
||||
expect(screen.getByText(`FLIGHT-STATUSES.${status}`)).toBeTruthy();
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("T23-C9: renders scheduled arrival time", () => {
|
||||
render(<FlightCard flight={makeFlight({ arrLocal: "2026-04-15T12:30:00+03:00" })} expandable />);
|
||||
expect(screen.getByText("12:30")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C10: renders +1 day-change chip on arrival when arrDayChange=1", () => {
|
||||
render(
|
||||
<FlightCard
|
||||
flight={makeFlight({ arrDayChange: 1 })}
|
||||
expandable
|
||||
/>,
|
||||
);
|
||||
const chips = document.querySelectorAll(".time-group__day-change");
|
||||
const arrChip = Array.from(chips).find((el) => el.textContent === "+1");
|
||||
expect(arrChip).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C10: arrival day-change chip has tooltip (title) per TZ §4.1.4", () => {
|
||||
render(
|
||||
<FlightCard
|
||||
flight={makeFlight({ arrDayChange: 1 })}
|
||||
expandable
|
||||
/>,
|
||||
);
|
||||
const chips = document.querySelectorAll(".time-group__day-change");
|
||||
const arrChip = Array.from(chips).find((el) => el.textContent === "+1");
|
||||
expect(arrChip?.getAttribute("title")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C11: renders arrival city name", () => {
|
||||
render(<FlightCard flight={makeFlight({ arrCity: "Санкт-Петербург" })} expandable />);
|
||||
expect(screen.getByText("Санкт-Петербург")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C12: renders arrival airport name", () => {
|
||||
render(<FlightCard flight={makeFlight({ arrAirport: "Пулково" })} expandable />);
|
||||
expect(screen.getByText(/Пулково/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C13: renders arrival terminal when present", () => {
|
||||
render(<FlightCard flight={makeFlight({ arrTerminal: "D" })} expandable />);
|
||||
expect(screen.getByText(/D/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C14: renders expand/collapse chevron when expandable=true", () => {
|
||||
render(<FlightCard flight={makeFlight({})} expandable />);
|
||||
expect(document.querySelector(".flight-card__chevron")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T23-C14: chevron rotates to open state when row is expanded", () => {
|
||||
render(<FlightCard flight={makeFlight({})} expandable initialExpanded />);
|
||||
expect(document.querySelector(".flight-card__chevron--open")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// §4.1.13.3 Table 24 — Multi-leg / connecting flight collapsed row
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("4.1.13.3 Table 24 — Multi-leg flight collapsed row", () => {
|
||||
|
||||
it("T24-C1: renders flight number for multi-leg flight", () => {
|
||||
const legs = [makeLeg({}), makeLeg({ depCode: "LED", arrCode: "AER" })];
|
||||
const flight = makeMultiLegFlight(legs);
|
||||
render(<FlightCard flight={flight} expandable />);
|
||||
expect(screen.getByTestId("flight-carrier-number").textContent).toContain("SU 0100");
|
||||
});
|
||||
|
||||
it("T24-C2: renders an operator logo per leg", () => {
|
||||
const legs = [makeLeg({}), makeLeg({ depCode: "LED", arrCode: "AER" })];
|
||||
const flight = makeMultiLegFlight(legs);
|
||||
render(<FlightCard flight={flight} expandable />);
|
||||
const logos = screen.getAllByTestId("operator-logo");
|
||||
expect(logos.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("T24-C15: renders route-changed icon when flags.routeChanged=true", () => {
|
||||
const legs = [makeLeg({ routeChanged: true }), makeLeg({ depCode: "LED", arrCode: "AER" })];
|
||||
const flight = makeMultiLegFlight(legs);
|
||||
render(<FlightCard flight={flight} expandable />);
|
||||
expect(screen.getByTestId("flight-event-change-route")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T24-C16: renders return-to-airport icon when flags.returnToAirport=true", () => {
|
||||
const legs = [makeLeg({ returnToAirport: true }), makeLeg({ depCode: "LED", arrCode: "AER" })];
|
||||
const flight = makeMultiLegFlight(legs);
|
||||
render(<FlightCard flight={flight} expandable />);
|
||||
expect(screen.getByTestId("flight-event-reroute")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T24-C15+16: does NOT render flag icons when flags are false", () => {
|
||||
const legs = [makeLeg({}), makeLeg({ depCode: "LED", arrCode: "AER" })];
|
||||
const flight = makeMultiLegFlight(legs);
|
||||
render(<FlightCard flight={flight} expandable />);
|
||||
expect(document.querySelector("[data-testid='flight-event-change-route']")).toBeNull();
|
||||
expect(document.querySelector("[data-testid='flight-event-reroute']")).toBeNull();
|
||||
});
|
||||
|
||||
it("T24-C15: renders route-changed icon for direct flight with routeChanged=true", () => {
|
||||
render(
|
||||
<FlightCard
|
||||
flight={makeFlight({ routeChanged: true })}
|
||||
expandable
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("flight-event-change-route")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("T24-C16: renders return-to-airport icon for direct flight with returnToAirport=true", () => {
|
||||
render(
|
||||
<FlightCard
|
||||
flight={makeFlight({ returnToAirport: true })}
|
||||
expandable
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("flight-event-reroute")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// §4.1.23 — "Уточняется" fallback for missing city/airport names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("4.1.23 — Уточняется fallback for missing station fields", () => {
|
||||
|
||||
it("T23-fallback: shows SHARED.UNSPECIFIED fallback when departure city is empty string", () => {
|
||||
render(<FlightCard flight={makeFlight({ depCity: "" })} expandable />);
|
||||
// The useCityName mock returns "" for unknown codes when city is empty
|
||||
// StationDisplay should render "SHARED.UNSPECIFIED" for empty city
|
||||
const stationEls = document.querySelectorAll(".station__city--bold");
|
||||
const emptyCity = Array.from(stationEls).find((el) => el.textContent === "" || el.textContent === "SHARED.UNSPECIFIED");
|
||||
// At minimum, no empty-string content is emitted without fallback
|
||||
expect(stationEls.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("T23-fallback: does not show raw empty string for terminal when terminal data absent", () => {
|
||||
render(<FlightCard flight={makeFlight({})} expandable />);
|
||||
const text = document.body.textContent ?? "";
|
||||
expect(text).not.toMatch(/— $/m);
|
||||
expect(text).not.toContain("undefined");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Row interaction / state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("4.1.13.3 — Collapsed row state", () => {
|
||||
|
||||
it("row has role=button and aria-expanded=false when expandable and collapsed", () => {
|
||||
render(<FlightCard flight={makeFlight({})} expandable />);
|
||||
const row = document.querySelector("[role='button']");
|
||||
expect(row).toBeTruthy();
|
||||
expect(row?.getAttribute("aria-expanded")).toBe("false");
|
||||
});
|
||||
|
||||
it("row aria-expanded=true when initialExpanded", () => {
|
||||
render(<FlightCard flight={makeFlight({})} expandable initialExpanded />);
|
||||
const row = document.querySelector("[role='button']");
|
||||
expect(row?.getAttribute("aria-expanded")).toBe("true");
|
||||
});
|
||||
|
||||
it("expanded panel is present when initialExpanded=true", () => {
|
||||
render(<FlightCard flight={makeFlight({})} expandable initialExpanded />);
|
||||
expect(screen.getByTestId("flight-card-expanded")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("expanded panel is absent in collapsed state", () => {
|
||||
render(<FlightCard flight={makeFlight({})} expandable />);
|
||||
expect(document.querySelector("[data-testid='flight-card-expanded']")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import { TimeGroup } from "./TimeGroup.js";
|
||||
import { FlightStatus } from "./FlightStatus.js";
|
||||
import { OperatorLogo } from "./OperatorLogo.js";
|
||||
import { IFlyWarning } from "./IFlyWarning.js";
|
||||
import { FlightEvents } from "@/features/online-board/components/BoardDetailsHeader/FlightEvents.js";
|
||||
import "./FlightCard.scss";
|
||||
|
||||
export interface FlightCardProps {
|
||||
@@ -159,6 +160,12 @@ export const FlightCard: FC<FlightCardProps> = ({
|
||||
// the flight-level flyingTime; fall back to the primary leg.
|
||||
const flightDuration = flight.flyingTime || departureLeg.flyingTime || "";
|
||||
|
||||
// Flags: route-changed / return-to-airport per TZ §4.1.13.3 Table 24 C15–C16.
|
||||
// Must be visible in the collapsed row for both direct and multi-leg flights.
|
||||
const allLegs = flight.routeType === "Direct" ? [flight.leg] : flight.legs;
|
||||
const routeChanged = allLegs.some((l) => l.flags.routeChanged);
|
||||
const returnToAirport = allLegs.some((l) => l.flags.returnToAirport);
|
||||
|
||||
const [expanded, setExpanded] = useState(Boolean(initialExpanded));
|
||||
const rowClickable = expandable || Boolean(onClick);
|
||||
const toggleExpanded = (): void => {
|
||||
@@ -227,6 +234,16 @@ export const FlightCard: FC<FlightCardProps> = ({
|
||||
{(expanded || direction === "schedule") && aircraftName && (
|
||||
<div className="flight-card__aircraft">{aircraftName}</div>
|
||||
)}
|
||||
{/* TZ §4.1.13.3 Table 24 C15–C16: route-changed / return-to-airport
|
||||
icons are shown in the collapsed row for any flight with those flags.
|
||||
showDescription=false keeps the icon-only compact variant. */}
|
||||
{(routeChanged || returnToAirport) && (
|
||||
<FlightEvents
|
||||
changeRoute={routeChanged}
|
||||
reroute={returnToAirport}
|
||||
showDescription={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flight-card__operator">
|
||||
|
||||
@@ -13,6 +13,29 @@ export interface TimeGroupProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the tooltip text for a day-change chip per TZ §4.1.4:
|
||||
* - ±1 → "день" (locale-invariant for now; Angular uses "день" in RU)
|
||||
* - ±2+ → the actual date (DD.MM.YYYY) computed from the base timestamp
|
||||
* shifted by `dayChange` days.
|
||||
*/
|
||||
function dayChangeTooltip(scheduled: string, dayChange: number): string {
|
||||
const abs = Math.abs(dayChange);
|
||||
if (abs === 1) return "день";
|
||||
// Parse the wall-clock date from the offset-aware ISO string
|
||||
// (e.g. "2026-04-15T23:30:00+03:00") and shift by dayChange.
|
||||
try {
|
||||
const base = new Date(scheduled);
|
||||
base.setDate(base.getDate() + dayChange);
|
||||
const dd = String(base.getDate()).padStart(2, "0");
|
||||
const mm = String(base.getMonth() + 1).padStart(2, "0");
|
||||
const yyyy = base.getFullYear();
|
||||
return `${dd}.${mm}.${yyyy}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays scheduled + actual times with a day-change indicator.
|
||||
*
|
||||
@@ -48,7 +71,10 @@ export const TimeGroup: FC<TimeGroupProps> = ({
|
||||
<span className="time-group__scheduled">{scheduledTime}</span>
|
||||
)}
|
||||
{dayChange !== undefined && dayChange !== 0 ? (
|
||||
<span className="time-group__day-change">
|
||||
<span
|
||||
className="time-group__day-change"
|
||||
title={dayChangeTooltip(scheduled, dayChange)}
|
||||
>
|
||||
{dayChange > 0 ? `+${dayChange}` : dayChange}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user