Fix calendar days bitmask parsing and filter hydration from URL params

Calendar days API returns a 31-char bitmask ('1'=available, '0'=unavailable)
starting from baseDate-1. parseCalendarDays now converts this to yyyyMMdd
date strings matching Angular's search-page-base.component.ts logic.

Calendar strip buttons now show formatted day numbers instead of raw dates.

OnlineBoardFilter now accepts initial values from URL params so the
departure/arrival/date fields are populated on search results pages.
This commit is contained in:
2026-04-16 13:56:08 +03:00
parent 0da4b5e0a5
commit 65ed6c1749
3 changed files with 131 additions and 13 deletions
+59 -4
View File
@@ -99,8 +99,10 @@ export async function getFlightDetails(
* Get available calendar days for a given search context.
* Maps to: `GET /v1/days/{date}/31/{searchType}/{searchParams}/board/`
*
* The API returns `{ days: "2025-01-01,2025-01-02,..." }` — a single
* comma-separated string. This function splits it into `string[]`.
* The API returns `{ days: "1110101..." }` — a 31-char bitmask where each
* character represents a day starting from (baseDate - 1). '1' = flights
* available, '0' = no flights. This function converts enabled positions
* into yyyyMMdd date strings.
*/
export async function getCalendarDays(
client: ApiClient,
@@ -110,7 +112,7 @@ export async function getCalendarDays(
const path = `flights/v1/${client.locale}/days/${params.date}/31/${searchSegment}/board/`;
const response = await client.get<IDaysResponse>(path);
return parseCalendarDays(response.days);
return parseCalendarDays(response.days, params.date);
}
// ---------------------------------------------------------------------------
@@ -133,7 +135,60 @@ function buildCalendarSearchSegment(params: CalendarParams): string {
}
}
function parseCalendarDays(days: string): string[] {
/**
* Parse a calendar days bitmask into an array of yyyyMMdd date strings.
*
* The API returns a 31-char string of '1' and '0'. Each position maps to
* a day starting from (baseDate - 1 day). Positions with '1' are enabled.
*
* Matches Angular's search-page-base.component.ts logic:
* date.setDate(date.getDate() - 1);
* for (var i = 0; i < res.days.length; i++) { ... date.setDate(date.getDate() + 1); }
*/
function parseCalendarDays(days: string, baseDate: string): string[] {
if (!days) return [];
// If it looks like a bitmask (only 0s and 1s), convert to dates
if (/^[01]+$/.test(days)) {
return bitmaskToDates(days, baseDate);
}
// Fallback: comma-separated date list (legacy format)
return days.split(",").map((d) => d.trim()).filter(Boolean);
}
/**
* Convert a bitmask string to yyyyMMdd date strings.
* Base date is the search date; iteration starts from (baseDate - 1 day).
*/
function bitmaskToDates(bitmask: string, baseDate: string): string[] {
// Parse baseDate — could be "yyyy-MM-ddT00:00:00" or "yyyyMMdd"
let year: number, month: number, day: number;
if (baseDate.includes("-")) {
const parts = baseDate.split("T")[0]!.split("-");
year = parseInt(parts[0]!, 10);
month = parseInt(parts[1]!, 10) - 1;
day = parseInt(parts[2]!, 10);
} else {
year = parseInt(baseDate.slice(0, 4), 10);
month = parseInt(baseDate.slice(4, 6), 10) - 1;
day = parseInt(baseDate.slice(6, 8), 10);
}
// Start from baseDate - 1 day (matching Angular)
const cursor = new Date(year, month, day);
cursor.setDate(cursor.getDate() - 1);
const result: string[] = [];
for (let i = 0; i < bitmask.length; i++) {
if (bitmask[i] === "1") {
const y = cursor.getFullYear().toString();
const m = (cursor.getMonth() + 1).toString().padStart(2, "0");
const d = cursor.getDate().toString().padStart(2, "0");
result.push(`${y}${m}${d}`);
}
cursor.setDate(cursor.getDate() + 1);
}
return result;
}
@@ -45,23 +45,49 @@ function validateFlightNumber(value: string): string | null {
return null;
}
export const OnlineBoardFilter: FC = () => {
export interface OnlineBoardFilterProps {
/** Pre-populate filter from URL params on search results pages */
initialDeparture?: string;
initialArrival?: string;
initialDate?: string;
initialTab?: AccordionTab;
initialFlightNumber?: string;
}
function yyyymmddToDate(yyyymmdd: string): Date {
const y = parseInt(yyyymmdd.slice(0, 4), 10);
const m = parseInt(yyyymmdd.slice(4, 6), 10) - 1;
const d = parseInt(yyyymmdd.slice(6, 8), 10);
return new Date(y, m, d);
}
export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
initialDeparture,
initialArrival,
initialDate,
initialTab,
initialFlightNumber,
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
const routeParams = useParams<{ lang: string }>();
const lang = routeParams.lang ?? "ru";
const [activeTab, setActiveTab] = useState<AccordionTab>("route");
const [activeTab, setActiveTab] = useState<AccordionTab>(initialTab ?? "route");
// Flight number fields
const [flightNumber, setFlightNumber] = useState("");
const [flightDate, setFlightDate] = useState<Date | null>(new Date());
const [flightNumber, setFlightNumber] = useState(initialFlightNumber ?? "");
const [flightDate, setFlightDate] = useState<Date | null>(
initialTab === "flight" && initialDate ? yyyymmddToDate(initialDate) : new Date(),
);
const [flightNumberError, setFlightNumberError] = useState<string | null>(null);
// Route fields
const [routeDeparture, setRouteDeparture] = useState<CitySuggestion | string>("");
const [routeArrival, setRouteArrival] = useState<CitySuggestion | string>("");
const [routeDate, setRouteDate] = useState<Date | null>(new Date());
const [routeDeparture, setRouteDeparture] = useState<CitySuggestion | string>(initialDeparture ?? "");
const [routeArrival, setRouteArrival] = useState<CitySuggestion | string>(initialArrival ?? "");
const [routeDate, setRouteDate] = useState<Date | null>(
initialDate ? yyyymmddToDate(initialDate) : new Date(),
);
const [timeRange, setTimeRange] = useState<[number, number]>([0, 1440]);
// City autocomplete search
@@ -37,6 +37,16 @@ export interface OnlineBoardSearchPageProps {
params: OnlineBoardParams & { type: "flight" | "departure" | "arrival" | "route" };
}
/**
* Format a yyyyMMdd date string for display in the calendar strip.
* Shows the day number (e.g. "15", "16").
*/
function formatDayLabel(yyyymmdd: string): string {
if (yyyymmdd.length !== 8) return yyyymmdd;
const day = parseInt(yyyymmdd.slice(6, 8), 10);
return String(day);
}
/**
* Convert yyyyMMdd URL date to API format (yyyy-MM-ddT00:00:00).
*/
@@ -229,7 +239,34 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
]}
contentLeft={
<>
<OnlineBoardFilter />
<OnlineBoardFilter
{...(params.type === "route"
? {
initialDeparture: params.departure,
initialArrival: params.arrival,
initialDate: params.date,
initialTab: "route" as const,
}
: params.type === "departure"
? {
initialDeparture: params.station,
initialDate: params.date,
initialTab: "route" as const,
}
: params.type === "arrival"
? {
initialArrival: params.station,
initialDate: params.date,
initialTab: "route" as const,
}
: params.type === "flight"
? {
initialFlightNumber: params.flightNumber,
initialDate: params.date,
initialTab: "flight" as const,
}
: {})}
/>
<SearchHistory />
</>
}
@@ -243,7 +280,7 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
className={`calendar-day${day === params.date ? " calendar-day--active" : ""}`}
onClick={() => handleDateChange(day)}
>
{day}
{formatDayLabel(day)}
</button>
))}
</div>