From 19ae50af80c028ab1f5f4024744875ddcc44d19b Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 6 May 2026 00:10:59 +0300 Subject: [PATCH] Fix online board details date selection --- .../FlightsMiniList/FlightsMiniListItem.tsx | 3 +- .../components/OnlineBoardDetailsPage.tsx | 5 +- .../components/OnlineBoardSearchPage.tsx | 5 +- .../online-board/flightSearchDate.test.ts | 113 ++++++++++++++++++ src/features/online-board/flightSearchDate.ts | 34 ++++++ 5 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 src/features/online-board/flightSearchDate.test.ts create mode 100644 src/features/online-board/flightSearchDate.ts diff --git a/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx b/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx index 61a5f00f..f8771fb3 100644 --- a/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx +++ b/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx @@ -12,6 +12,7 @@ import { Link } from "@modern-js/runtime/router"; import { useTranslation } from "@/i18n/provider.js"; import type { ISimpleFlight, IFlightLeg } from "../../types.js"; import { buildOnlineBoardUrl } from "../../url.js"; +import { getFlightSearchDate } from "../../flightSearchDate.js"; import { formatLocalTime, formatDayMonthYear, @@ -51,7 +52,7 @@ export const FlightsMiniListItem = forwardRef = ({ // Pick the flight matching the URL's flightId (date-based match). The API // response may contain multiple flights with the same flight number on - // different dates; we need the one the user actually navigated to. + // different dates; match Angular's dateToSearchBy, not backend flightId.date. const flight = - allFlights.find((f) => f.flightId.date === flightId.date) ?? firstFlight; + allFlights.find((f) => getFlightSearchDate(f) === flightId.date) ?? firstFlight; // Live updates via SignalR const { flight: liveFlight, connectionStatus } = useLiveFlightDetails( diff --git a/src/features/online-board/components/OnlineBoardSearchPage.tsx b/src/features/online-board/components/OnlineBoardSearchPage.tsx index 9887067e..5dfd0a8e 100644 --- a/src/features/online-board/components/OnlineBoardSearchPage.tsx +++ b/src/features/online-board/components/OnlineBoardSearchPage.tsx @@ -36,6 +36,7 @@ import { useCalendarDays } from "../hooks/useCalendarDays.js"; import { buildOnlineBoardUrl } from "../url.js"; import { buildFlightListJsonLd } from "../json-ld.js"; import { sortFlights } from "../sortFlights.js"; +import { getFlightSearchDate } from "../flightSearchDate.js"; import { PobedaAuroraBanner, shouldShowPobedaAuroraBanner, @@ -388,13 +389,13 @@ export const OnlineBoardSearchPage: FC = ({ carrier: flight.flightId.carrier, flightNumber: flight.flightId.flightNumber, suffix: flight.flightId.suffix, - date: flight.flightId.date, + date: getFlightSearchDate(flight), } : { type: "details", carrier: flight.flightId.carrier, flightNumber: flight.flightId.flightNumber, - date: flight.flightId.date, + date: getFlightSearchDate(flight), }; const detailsUrl = buildOnlineBoardUrl(detailsParams); void navigate(`/${locale}/${detailsUrl}`); diff --git a/src/features/online-board/flightSearchDate.test.ts b/src/features/online-board/flightSearchDate.test.ts new file mode 100644 index 00000000..f12db967 --- /dev/null +++ b/src/features/online-board/flightSearchDate.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "vitest"; +import { getFlightSearchDate } from "./flightSearchDate.js"; +import type { IDirectFlight, IMultiLegFlight, IFlightLeg } from "./types.js"; + +function leg(local: string): IFlightLeg { + return { + arrival: { + scheduled: { airport: "", airportCode: "", city: "", cityCode: "", countryCode: "" }, + times: { + scheduledArrival: { + dayChange: { value: 0, title: "" }, + local, + localTime: "", + tzOffset: 0, + utc: "", + }, + }, + }, + dayChange: 0, + departure: { + scheduled: { airport: "", airportCode: "", city: "", cityCode: "", countryCode: "" }, + checkingStatus: "Scheduled", + times: { + scheduledDeparture: { + dayChange: { value: 0, title: "" }, + local, + localTime: "", + tzOffset: 0, + utc: "", + }, + }, + }, + equipment: {}, + flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false }, + flyingTime: "", + index: 0, + operatingBy: {}, + status: "Scheduled", + updated: "", + }; +} + +describe("getFlightSearchDate", () => { + it("uses scheduled local departure date instead of backend flightId.date", () => { + const flight: IDirectFlight = { + id: "SU6951", + routeType: "Direct", + flyingTime: "", + operatingBy: {}, + status: "Arrived", + flightId: { + carrier: "SU", + flightNumber: "6951", + suffix: "", + date: "2026-05-04", + dateLT: "2026-05-05", + }, + leg: leg("2026-05-05T00:30:00+03:00"), + }; + + expect(getFlightSearchDate(flight)).toBe("20260505"); + }); + + it("uses the first leg for multi-leg flights", () => { + const flight: IMultiLegFlight = { + id: "SU100", + routeType: "MultiLeg", + flyingTime: "", + operatingBy: {}, + status: "Scheduled", + flightId: { + carrier: "SU", + flightNumber: "0100", + suffix: "", + date: "20260504", + }, + legs: [ + leg("2026-05-05T23:30:00+03:00"), + leg("2026-05-06T01:30:00+03:00"), + ], + }; + + expect(getFlightSearchDate(flight)).toBe("20260505"); + }); + + it("falls back to dateLT and then flightId.date when leg date is unavailable", () => { + const flight: IDirectFlight = { + id: "SU42", + routeType: "Direct", + flyingTime: "", + operatingBy: {}, + status: "Scheduled", + flightId: { + carrier: "SU", + flightNumber: "0042", + suffix: "", + date: "2026-05-04", + dateLT: "2026-05-05", + }, + leg: leg("10:00"), + }; + + expect(getFlightSearchDate(flight)).toBe("20260505"); + + const { dateLT: _dateLT, ...flightIdWithoutDateLt } = flight.flightId; + const withoutDateLt: IDirectFlight = { + ...flight, + flightId: flightIdWithoutDateLt, + }; + + expect(getFlightSearchDate(withoutDateLt)).toBe("20260504"); + }); +}); diff --git a/src/features/online-board/flightSearchDate.ts b/src/features/online-board/flightSearchDate.ts new file mode 100644 index 00000000..b8a3e92d --- /dev/null +++ b/src/features/online-board/flightSearchDate.ts @@ -0,0 +1,34 @@ +import type { IFlightLeg, ISimpleFlight } from "./types.js"; + +function compactDate(value: string | undefined): string | null { + if (!value) return null; + if (/^\d{8}$/.test(value)) return value; + + const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(value); + if (!match) return null; + + return `${match[1]}${match[2]}${match[3]}`; +} + +function getFirstLeg(flight: ISimpleFlight): IFlightLeg | null { + if (flight.routeType === "Direct") return flight.leg; + return flight.legs[0] ?? null; +} + +/** + * Angular's FlightDateUtils.getFlightDate derives the details URL date from + * the first leg's scheduled local departure date, not from flightId.date. + * Overnight flights can have flightId.date set to the previous backend + * service day while dateLT / scheduled local departure belongs to the board + * day the user clicked. + */ +export function getFlightSearchDate(flight: ISimpleFlight): string { + const firstLeg = getFirstLeg(flight); + + return ( + compactDate(firstLeg?.departure.times.scheduledDeparture.local) ?? + compactDate(flight.flightId.dateLT) ?? + compactDate(flight.flightId.date) ?? + flight.flightId.date.replace(/-/g, "") + ); +}