Schedule details: summary header, fix mini-list duplicates, fix timeline times

The schedule details page now renders Angular's <schedule-details-header>
summary block (badges per flight + share/last-update + full-route
timeline) between the day-tabs strip and the per-leg cards, so a
connecting itinerary like SU 6188 + SU 6341 surfaces both flight
numbers and the combined Moscow→Murmansk timeline up top instead of
jumping straight from the date tabs to the first-leg detail card.

Mini-list duplicate fix: when the sibling search returned 0 matches
the fallback path used to leak the URL-parsed per-leg breakdown into
the rail, producing a first-leg-only row stacked next to the
synthesized combined row. Now the fallback is empty — the mini-list
just shows the (synthesized) current flight on its own.

FullRouteTimeline now uses the API's pre-formatted .localTime instead
of the full ISO .local, so 00:30 / 02:00 shows up instead of
2026-04-26T00:30:00+03:00.

useAppSettings.buyTicketMaxHours: parse <n>d as well as <n>h (Angular
ships 330d for buyPeriod.max). Without this the Buy button hides for
any flight more than ~3 days out.

Plumbed sortMode/onSortChange/hideColumnHeaders through DayGroupedFlightList
so the sticky ScheduleColumnHeaders and the inner list stay in sync
(removes 2 TS errors in ScheduleSearchPage).
This commit is contained in:
2026-04-23 16:53:38 +03:00
parent 7324b4c03a
commit cbced8d4b6
10 changed files with 286 additions and 96 deletions
+18 -2
View File
@@ -109,9 +109,25 @@ describe("useAppSettings", () => {
const { result } = renderHook(() => useAppSettings());
await waitFor(() => expect(result.current.loading).toBe(false));
// Defaults match Angular (flightStatusAvailableFrom: 2h, buyPeriod.min: 2h, buyPeriod.max: 72h).
// Defaults match Angular (flightStatusAvailableFrom: 2h, buyPeriod.min: 2h, buyPeriod.max: 330d).
expect(result.current.flightStatusAvailableFromHours).toBe(2);
expect(result.current.buyTicketMinHours).toBe(2);
expect(result.current.buyTicketMaxHours).toBe(72);
expect(result.current.buyTicketMaxHours).toBe(330 * 24);
});
it("parses buyPeriod.max in days (Angular API returns '330d')", async () => {
const response: AppSettingsResponse = {
uiOptions: {
buttons: {
buyTicket: { period: { min: "2h", max: "330d" } },
},
},
};
mockGetAppSettings.mockResolvedValue(response);
const { result } = renderHook(() => useAppSettings());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.buyTicketMinHours).toBe(2);
expect(result.current.buyTicketMaxHours).toBe(330 * 24);
});
});
+20 -6
View File
@@ -10,9 +10,10 @@ const HOURS_PATTERN = /^(\d+)h$/;
// boardSearchFrom: 1, boardSearchTo: 7,
// scheduleSearchFrom: 1, scheduleSearchTo: 330,
// flightStatusAvailableFrom: 2 (hours), buyPeriod.min: 2h, buyPeriod.max: 330d
// These are used when the UI-options API fails or is unreachable —
// keeping them aligned with Angular avoids a silent calendar-window
// mismatch in offline / error paths.
// `buyTicketMaxHours` mirrors Angular's `330d` → 330 * 24 = 7920 hours;
// without this the Buy button hides for any flight more than ~3 days
// out, which made the schedule details page look like 'no Купить
// button' for the typical search-2-weeks-ahead flow.
const DEFAULTS = {
onlineboardSearchFrom: 1,
onlineboardSearchTo: 7,
@@ -20,7 +21,7 @@ const DEFAULTS = {
scheduleSearchTo: 330,
flightStatusAvailableFromHours: 2,
buyTicketMinHours: 2,
buyTicketMaxHours: 72,
buyTicketMaxHours: 330 * 24,
} as const;
function parsePattern(
@@ -42,6 +43,19 @@ function parseHours(value: string | undefined, fallback: number): number {
return parsePattern(value, HOURS_PATTERN, fallback);
}
/** Parse an `<n>h` or `<n>d` duration into hours. Days expand to 24×n.
* Used for `buyPeriod.max` which Angular's settings API returns as
* `"330d"` (any other consumer in the React codebase that needs to
* accept either unit should use this rather than parseHours). */
function parseHoursOrDays(value: string | undefined, fallback: number): number {
if (!value) return fallback;
const hMatch = HOURS_PATTERN.exec(value);
if (hMatch?.[1]) return parseInt(hMatch[1], 10);
const dMatch = DAYS_PATTERN.exec(value);
if (dMatch?.[1]) return parseInt(dMatch[1], 10) * 24;
return fallback;
}
export interface UseAppSettingsResult {
onlineboardSearchFrom: number;
onlineboardSearchTo: number;
@@ -84,8 +98,8 @@ export function useAppSettings(): UseAppSettingsResult {
fs?.availableFrom,
DEFAULTS.flightStatusAvailableFromHours,
),
buyTicketMinHours: parseHours(bt?.period?.min, DEFAULTS.buyTicketMinHours),
buyTicketMaxHours: parseHours(bt?.period?.max, DEFAULTS.buyTicketMaxHours),
buyTicketMinHours: parseHoursOrDays(bt?.period?.min, DEFAULTS.buyTicketMinHours),
buyTicketMaxHours: parseHoursOrDays(bt?.period?.max, DEFAULTS.buyTicketMaxHours),
});
setLoading(false);
})