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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user