diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.tsx
index 082618d9..13b8c8db 100644
--- a/src/features/online-board/components/OnlineBoardDetailsPage.tsx
+++ b/src/features/online-board/components/OnlineBoardDetailsPage.tsx
@@ -301,9 +301,11 @@ function LegRoute({
function FlightLegs({
legs,
viewType,
+ locale,
}: {
legs: IFlightLeg[];
viewType: "Onlineboard" | "Schedule";
+ locale?: string;
}): JSX.Element {
const { t } = useTranslation();
const showLegHeaders = legs.length > 1;
@@ -334,7 +336,7 @@ function FlightLegs({
)}
-
+
{(() => {
const next = legs[i + 1];
@@ -660,7 +662,7 @@ export const OnlineBoardDetailsPage: FC = ({
{/* Detailed leg information */}
-
+
{/* Angular keeps the total flying time inside the FlightSchedule
collapsible block; we used to render a separate line above it
diff --git a/src/features/online-board/components/details-panels/AircraftPanel.test.tsx b/src/features/online-board/components/details-panels/AircraftPanel.test.tsx
index 0123edaf..138cb6a4 100644
--- a/src/features/online-board/components/details-panels/AircraftPanel.test.tsx
+++ b/src/features/online-board/components/details-panels/AircraftPanel.test.tsx
@@ -8,6 +8,13 @@ vi.mock("@/i18n/provider.js", () => ({
useTranslation: () => ({ t: (k: string) => k }),
}));
+// Build an ISO date string that is N days from today (negative = past)
+function isoDateDaysFromNow(n: number): string {
+ const d = new Date();
+ d.setDate(d.getDate() + n);
+ return d.toISOString();
+}
+
describe("AircraftPanel", () => {
// The aircraft title (e.g. 'Airbus A321') is rendered by the wrapping
// FlightDetailsAccordion row caption, not by the panel itself — Angular
@@ -59,4 +66,134 @@ describe("AircraftPanel", () => {
render();
expect(screen.getByTestId("aircraft-panel")).toBeTruthy();
});
+
+ // §4.1.15.9: previous-flight chip must be a link (opens new tab) when the
+ // flight's scheduled departure date is more recent than today − 2 days.
+ describe("previous-flight link (§4.1.15.9)", () => {
+ const previousFlight = {
+ localDate: "2026-04-20",
+ carrier: "SU",
+ flightNumber: "6805",
+ date: "2026-04-20",
+ };
+
+ it("renders an link when departure is within the last 2 days", () => {
+ const eq: IEquipmentFull = {
+ aircraft: {
+ actual: { title: "A320" },
+ previousFlight,
+ },
+ };
+ // departureDateLocal set to today (within the 2-day window)
+ render(
+ ,
+ );
+ const link = screen.getByRole("link", { name: "SU 6805" });
+ expect(link).toBeTruthy();
+ expect(link.getAttribute("target")).toBe("_blank");
+ expect(link.getAttribute("href")).toContain("ru-ru/onlineboard");
+ expect(link.getAttribute("href")).toContain("SU6805");
+ });
+
+ it("includes the correct date (localDate of previousFlight) in the href", () => {
+ const eq: IEquipmentFull = {
+ aircraft: {
+ actual: { title: "A320" },
+ previousFlight: {
+ localDate: "2026-04-20",
+ carrier: "SU",
+ flightNumber: "0100",
+ date: "2026-04-20",
+ },
+ },
+ };
+ render(
+ ,
+ );
+ const link = screen.getByRole("link");
+ // Date in href must be the compact localDate of the previous flight
+ expect(link.getAttribute("href")).toContain("20260420");
+ });
+
+ it("renders plain text (no link) when departure is older than 2 days", () => {
+ const eq: IEquipmentFull = {
+ aircraft: {
+ actual: { title: "A320" },
+ previousFlight,
+ },
+ };
+ // departureDateLocal 10 days ago — outside the 2-day window
+ render(
+ ,
+ );
+ expect(screen.getByText("SU 6805")).toBeTruthy();
+ expect(screen.queryByRole("link")).toBeNull();
+ });
+
+ it("renders plain text when locale is not provided", () => {
+ const eq: IEquipmentFull = {
+ aircraft: {
+ actual: { title: "A320" },
+ previousFlight,
+ },
+ };
+ render(
+ ,
+ );
+ expect(screen.getByText("SU 6805")).toBeTruthy();
+ expect(screen.queryByRole("link")).toBeNull();
+ });
+
+ it("renders plain text when departureDateLocal is not provided", () => {
+ const eq: IEquipmentFull = {
+ aircraft: {
+ actual: { title: "A320" },
+ previousFlight,
+ },
+ };
+ render();
+ expect(screen.getByText("SU 6805")).toBeTruthy();
+ expect(screen.queryByRole("link")).toBeNull();
+ });
+
+ it("includes suffix in the link label and href when present", () => {
+ const eq: IEquipmentFull = {
+ aircraft: {
+ actual: { title: "A320" },
+ previousFlight: {
+ localDate: "2026-04-20",
+ carrier: "SU",
+ flightNumber: "1234",
+ suffix: "A",
+ date: "2026-04-20",
+ },
+ },
+ };
+ render(
+ ,
+ );
+ const link = screen.getByRole("link", { name: "SU 1234A" });
+ expect(link).toBeTruthy();
+ expect(link.getAttribute("href")).toContain("1234A");
+ });
+ });
});
diff --git a/src/features/online-board/components/details-panels/AircraftPanel.tsx b/src/features/online-board/components/details-panels/AircraftPanel.tsx
index b7f9a822..0d9c25f5 100644
--- a/src/features/online-board/components/details-panels/AircraftPanel.tsx
+++ b/src/features/online-board/components/details-panels/AircraftPanel.tsx
@@ -1,10 +1,19 @@
-import type { FC } from "react";
+import type { FC, ReactNode } from "react";
import { useTranslation } from "@/i18n/provider.js";
-import type { IEquipmentFull, ISeat, SeatType } from "../../types.js";
+import type { IEquipmentFull, ISeat, SeatType, IPreviousFlight } from "../../types.js";
+import { buildFlightUrlParams } from "../../url.js";
import "./panels.scss";
export interface AircraftPanelProps {
equipment: IEquipmentFull;
+ /** Current locale, e.g. "ru-ru". Used to build the previous-flight link. */
+ locale?: string;
+ /**
+ * Local ISO date-time string for the flight's scheduled departure.
+ * Used to gate whether the previous-flight chip is rendered as a link
+ * (§4.1.15.9: show as link when scheduled departure date > today − 2 days).
+ */
+ departureDateLocal?: string;
}
function seatsByType(seats: ISeat[] | undefined, type: SeatType): number {
@@ -19,13 +28,61 @@ function totalSeats(seats: ISeat[] | undefined): number {
return seats.reduce((acc, s) => acc + s.count, 0);
}
+/**
+ * Returns true when the previous-flight chip should be an interactive link.
+ *
+ * Angular rule (§4.1.15.9 / `shoudShowPreviosFlight`): render the chip as a
+ * link when the flight's scheduled-departure *date* is strictly after
+ * (today − 2 days). For older flights the chip shows as plain text so the
+ * stale cross-link doesn't navigate to a potentially retired board page.
+ */
+function shouldShowPreviousFlightLink(departureDateLocal: string | undefined): boolean {
+ if (!departureDateLocal) return false;
+ // Extract local calendar date from the ISO string ("2026-04-21T10:30:00+03:00")
+ const localDateStr = departureDateLocal.slice(0, 10); // "YYYY-MM-DD"
+ const scheduledDate = new Date(localDateStr);
+ if (isNaN(scheduledDate.getTime())) return false;
+
+ const now = new Date();
+ const threshold = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ threshold.setDate(threshold.getDate() - 2);
+
+ return scheduledDate > threshold;
+}
+
+/**
+ * Build the absolute URL path for the previous flight's details card.
+ * Output: `/{locale}/onlineboard/{carrier}{flightNumber}-{yyyyMMdd}`
+ * The date used is `previousFlight.localDate` (local departure date of the
+ * previous flight), matching Angular's `dateToSearchBy = new Date(prevFlight.localDate)`.
+ */
+function buildPreviousFlightHref(
+ prev: IPreviousFlight,
+ locale: string,
+): string {
+ // localDate may be "YYYY-MM-DD" or ISO; compact it to "yyyyMMdd".
+ const compactDate = prev.localDate.slice(0, 10).replace(/-/g, "");
+ const id: Parameters[0] = {
+ carrier: prev.carrier,
+ flightNumber: prev.flightNumber,
+ date: compactDate,
+ };
+ if (prev.suffix !== undefined) id.suffix = prev.suffix;
+ const urlSegment = buildFlightUrlParams(id);
+ return `/${locale}/onlineboard/${urlSegment}`;
+}
+
/**
* Aircraft panel — matches Angular's flight-details-airplane layout:
* properties arranged horizontally (Название | Количество мест | Эконом |
* Комфорт | Бизнес | Предыдущий рейс). The aircraft title/model link is
* rendered by the wrapping accordion row caption.
*/
-export const AircraftPanel: FC = ({ equipment }) => {
+export const AircraftPanel: FC = ({
+ equipment,
+ locale,
+ departureDateLocal,
+}) => {
const { t } = useTranslation();
const aircraftInfo = equipment.aircraft;
const aircraft = aircraftInfo?.actual ?? aircraftInfo?.scheduled;
@@ -39,7 +96,21 @@ export const AircraftPanel: FC = ({ equipment }) => {
? `${previous.carrier} ${previous.flightNumber}${previous.suffix ?? ""}`
: null;
- const props: Array<{ label: string; value: string | number; className?: string }> = [];
+ const showLink =
+ !!previous &&
+ !!locale &&
+ shouldShowPreviousFlightLink(departureDateLocal);
+
+ const previousHref =
+ showLink && previous && locale
+ ? buildPreviousFlightHref(previous, locale)
+ : null;
+
+ const props: Array<{
+ label: string;
+ value: ReactNode;
+ className?: string;
+ }> = [];
if (aircraft?.name) props.push({ label: t("AIRPLANE.NAME"), value: aircraft.name });
if (total > 0) props.push({ label: t("AIRPLANE.SEATS-TOTAL"), value: total });
if (economy > 0) props.push({ label: t("AIRPLANE.SEATS-ECONOMY"), value: economy });
@@ -48,8 +119,18 @@ export const AircraftPanel: FC = ({ equipment }) => {
if (previousLabel) {
props.push({
label: t("BOARD.PREVIOUS-FLIGHT"),
- value: previousLabel,
- className: "aircraft-panel__prop--link",
+ value: previousHref ? (
+
+ {previousLabel}
+
+ ) : (
+ previousLabel
+ ),
});
}
diff --git a/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx b/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx
index 0f4ce0ec..6229aa4a 100644
--- a/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx
+++ b/src/features/online-board/components/details-panels/FlightDetailsAccordion.tsx
@@ -21,6 +21,8 @@ import "./FlightDetailsAccordion.scss";
export interface FlightDetailsAccordionProps {
leg: IFlightLeg;
viewType: DetailsViewType;
+ /** Current locale (e.g. "ru-ru"). Forwarded to AircraftPanel for previous-flight link. */
+ locale?: string;
}
interface RowDef {
@@ -134,7 +136,7 @@ function TransitionTimes({
);
}
-export const FlightDetailsAccordion: FC = ({ leg, viewType }) => {
+export const FlightDetailsAccordion: FC = ({ leg, viewType, locale }) => {
const { t } = useTranslation();
const [collapsed, setCollapsed] = useState(false);
@@ -184,7 +186,13 @@ export const FlightDetailsAccordion: FC = ({ leg, v
icon: ICON_AIRCRAFT,
title: t("SHARED.PLANE"),
subtitle: title,
- body: ,
+ body: (
+
+ ),
});
}
const meals = leg.equipment.meal;