cbced8d4b6
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).
119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { useApiClient } from "@/shared/api/provider.js";
|
||
import { getAppSettings } from "@/shared/api/appSettings.js";
|
||
|
||
const DAYS_PATTERN = /^(\d+)d$/;
|
||
const HOURS_PATTERN = /^(\d+)h$/;
|
||
|
||
// Mirrors Angular's `AppSettings` default payload
|
||
// (ClientApp/src/app/shared/models-legacy/app-settings.model.ts):
|
||
// boardSearchFrom: 1, boardSearchTo: 7,
|
||
// scheduleSearchFrom: 1, scheduleSearchTo: 330,
|
||
// flightStatusAvailableFrom: 2 (hours), buyPeriod.min: 2h, buyPeriod.max: 330d
|
||
// `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,
|
||
scheduleSearchFrom: 1,
|
||
scheduleSearchTo: 330,
|
||
flightStatusAvailableFromHours: 2,
|
||
buyTicketMinHours: 2,
|
||
buyTicketMaxHours: 330 * 24,
|
||
} as const;
|
||
|
||
function parsePattern(
|
||
value: string | undefined,
|
||
pattern: RegExp,
|
||
fallback: number,
|
||
): number {
|
||
if (!value) return fallback;
|
||
const match = pattern.exec(value);
|
||
if (!match?.[1]) return fallback;
|
||
return parseInt(match[1], 10);
|
||
}
|
||
|
||
function parseDays(value: string | undefined, fallback: number): number {
|
||
return parsePattern(value, DAYS_PATTERN, fallback);
|
||
}
|
||
|
||
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;
|
||
scheduleSearchFrom: number;
|
||
scheduleSearchTo: number;
|
||
flightStatusAvailableFromHours: number;
|
||
buyTicketMinHours: number;
|
||
buyTicketMaxHours: number;
|
||
loading: boolean;
|
||
error: Error | null;
|
||
}
|
||
|
||
/**
|
||
* Fetches the global app settings and exposes day-range and button-config numbers.
|
||
* On error or parse failure, returns defaults.
|
||
*/
|
||
export function useAppSettings(): UseAppSettingsResult {
|
||
const client = useApiClient();
|
||
const [state, setState] = useState<Omit<UseAppSettingsResult, "loading" | "error">>(DEFAULTS);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<Error | null>(null);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
|
||
getAppSettings(client)
|
||
.then((response) => {
|
||
if (cancelled) return;
|
||
const ob = response.uiOptions?.filter?.onlineboard;
|
||
const sc = response.uiOptions?.filter?.schedule;
|
||
const fs = response.uiOptions?.buttons?.flightStatus;
|
||
const bt = response.uiOptions?.buttons?.buyTicket;
|
||
|
||
setState({
|
||
onlineboardSearchFrom: parseDays(ob?.searchFrom, DEFAULTS.onlineboardSearchFrom),
|
||
onlineboardSearchTo: parseDays(ob?.searchTo, DEFAULTS.onlineboardSearchTo),
|
||
scheduleSearchFrom: parseDays(sc?.searchFrom, DEFAULTS.scheduleSearchFrom),
|
||
scheduleSearchTo: parseDays(sc?.searchTo, DEFAULTS.scheduleSearchTo),
|
||
flightStatusAvailableFromHours: parseHours(
|
||
fs?.availableFrom,
|
||
DEFAULTS.flightStatusAvailableFromHours,
|
||
),
|
||
buyTicketMinHours: parseHoursOrDays(bt?.period?.min, DEFAULTS.buyTicketMinHours),
|
||
buyTicketMaxHours: parseHoursOrDays(bt?.period?.max, DEFAULTS.buyTicketMaxHours),
|
||
});
|
||
setLoading(false);
|
||
})
|
||
.catch((err: Error) => {
|
||
if (cancelled) return;
|
||
setError(err);
|
||
setLoading(false);
|
||
});
|
||
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [client]);
|
||
|
||
return { ...state, loading, error };
|
||
}
|