/** * Schedule API functions. * * Pure functions -- each takes an `ApiClient` as a parameter (dependency * injection). No React hooks, no context, no side effects. * * @module */ import type { ApiClient } from "@/shared/api/client.js"; import type { IScheduleSearchRequest, IScheduleResponse, IScheduleDetailsResponse, IScheduleCalendarParams, IScheduleDaysResponse, } from "./types.js"; // --------------------------------------------------------------------------- // API functions // --------------------------------------------------------------------------- /** * Search schedule flights. * Maps to: `GET schedule/1?departure=SVO&arrival=LED&dateFrom=2026-04-18&dateTo=2026-04-26&...` * * Mirrors Angular's ScheduleApiService.getFlights which: * - uses GET with query params (not POST with JSON body) * - sends dateTo = requested end + 1 day (half-open interval) * - coerces `connections` to a string ('0' / '1') * - drops empty timeFrom/timeTo */ export async function searchSchedule( client: ApiClient, params: IScheduleSearchRequest, ): Promise { const query: Record = { departure: params.departure, arrival: params.arrival, dateFrom: params.dateFrom, dateTo: addOneDayIso(params.dateTo), connections: String(params.connections ?? 1), }; if (params.timeFrom) query["timeFrom"] = params.timeFrom; if (params.timeTo) query["timeTo"] = params.timeTo; if (params.attribute) query["attribute"] = String(params.attribute); return client.get( `flights/1/${client.locale}/schedule`, query, ); } /** Return the date one day after a yyyy-MM-dd string (passthrough on bad input). */ function addOneDayIso(iso: string): string { const m = /^(\d{4})-(\d{2})-(\d{2})(.*)$/.exec(iso); if (!m) return iso; const [, y, mm, dd, tail] = m; const d = new Date(Number(y), Number(mm) - 1, Number(dd)); d.setDate(d.getDate() + 1); const ny = d.getFullYear().toString(); const nm = (d.getMonth() + 1).toString().padStart(2, "0"); const nd = d.getDate().toString().padStart(2, "0"); return `${ny}-${nm}-${nd}${tail ?? ""}`; } /** * Get multi-flight schedule details. * Maps to: `GET schedule/details?flights[0]=...&dates[0]=...&departure=...&arrival=...` * * The Angular service uses indexed query params (flights[0], flights[1], etc.). * We build the query object to match that format. */ export async function getScheduleDetails( client: ApiClient, params: { flights: string[]; dates: string[]; departure: string; arrival: string; }, ): Promise { // Omit blank departure/arrival so the upstream doesn't reject the // request. Angular's details-data-source.service falls back to // per-leg values when the page-level params are missing; the // backend accepts the request either way. const query: Record = {}; if (params.departure) query["departure"] = params.departure; if (params.arrival) query["arrival"] = params.arrival; for (let i = 0; i < params.flights.length; i++) { const flight = params.flights[i]; if (flight !== undefined) query[`flights[${i}]`] = flight; } for (let i = 0; i < params.dates.length; i++) { const date = params.dates[i]; if (date !== undefined) query[`dates[${i}]`] = date; } return client.get(`flights/v1.1/${client.locale}/schedule/details`, query); } /** * Get available calendar days for a given route. * Maps to: `GET days/{date}/382/{param}/schedule/v1` * * The API returns `{ days: "1111110001..." }` — a 382-char bitmask * where each character represents a day starting from (baseDate - 1). * '1' = available, '0' = no flights. Equivalent to the board endpoint. */ export async function getScheduleCalendarDays( client: ApiClient, params: IScheduleCalendarParams, ): Promise { const routeSegment = params.connections ? `connections/${params.departure}-${params.arrival}-1` : `route/${params.departure}-${params.arrival}`; const path = `flights/v1/${client.locale}/days/${params.date}/382/${routeSegment}/schedule/`; const response = await client.get(path); return parseCalendarDays(response.days, params.date); } // --------------------------------------------------------------------------- // Internal helpers // --------------------------------------------------------------------------- /** * Parse the /days response into an array of yyyy-MM-dd strings. * * Handles two shapes: * - legacy comma-separated ("2025-01-01,2025-01-02,…") * - bitmask ("1111000…") where each position maps to a day starting from * `baseDate - 1`. This is what the upstream actually returns today. */ function parseCalendarDays(days: string, baseDate: string): string[] { if (!days) return []; if (/^[01]+$/.test(days)) { return bitmaskToDates(days, baseDate); } return days.split(",").map((d) => d.trim()).filter(Boolean); } function bitmaskToDates(bitmask: string, baseDate: string): string[] { // baseDate is yyyy-MM-dd (possibly with Txx:xx:xx suffix). const iso = baseDate.includes("T") ? baseDate.split("T")[0] ?? baseDate : baseDate; const [y, m, d] = iso.split("-"); if (!y || !m || !d) return []; const cursor = new Date(Number(y), Number(m) - 1, Number(d)); cursor.setDate(cursor.getDate() - 1); const result: string[] = []; for (let i = 0; i < bitmask.length; i++) { if (bitmask[i] === "1") { const yy = cursor.getFullYear().toString(); const mm = (cursor.getMonth() + 1).toString().padStart(2, "0"); const dd = cursor.getDate().toString().padStart(2, "0"); result.push(`${yy}-${mm}-${dd}`); } cursor.setDate(cursor.getDate() + 1); } return result; }