Files
flights_web/src/shared/hooks/useAppSettings.ts
T
gnezim cbced8d4b6 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).
2026-04-23 16:53:38 +03:00

119 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}