Enable start-page schedule date availability
ci-deploy / build-deploy-test (push) Waiting to run

This commit is contained in:
2026-05-14 14:07:44 +03:00
parent 530115d8d1
commit 7fd8faf202
2 changed files with 92 additions and 3 deletions
@@ -102,6 +102,10 @@ vi.mock("@/shared/dictionaries/index.js", () => ({
getCityCodeByAirportCode: () => undefined,
}));
vi.mock("../hooks/useScheduleCalendar.js", () => ({
useScheduleCalendar: () => ({ days: [], loading: false, loaded: false }),
}));
let geoMockEnabled = false;
vi.mock("@/shared/hooks/useGeoCityDefault.js", () => ({
@@ -1,13 +1,14 @@
/**
* Schedule start page -- search form for route-based schedule search.
*
* No API calls on load. Pure form that navigates to the appropriate
* search route on submit.
* No schedule-search API calls on load. Once both cities are selected,
* fetches route operating days so unavailable dates are greyed out before
* submit, matching Angular's schedule-filter.
*
* @module
*/
import { type FC, useState, useCallback, useEffect, useRef, type FormEvent } from "react";
import { type FC, useState, useCallback, useEffect, useMemo, useRef, type FormEvent } from "react";
import { useNavigate } from "@modern-js/runtime/router";
import { useLocale } from "@/i18n/useLocale.js";
import { Calendar } from "primereact/calendar";
@@ -36,6 +37,8 @@ import { useGeoCityDefault } from "@/shared/hooks/useGeoCityDefault.js";
import { buildScheduleUrl } from "../url.js";
import { scheduleWindowBounds } from "@/shared/dateWindow.js";
import { formatScheduleDateRangeWithCurrentWeek } from "../dateLabels.js";
import { useScheduleCalendar } from "../hooks/useScheduleCalendar.js";
import type { IScheduleCalendarParams } from "../types.js";
import "./ScheduleStartPage.scss";
function toCityCode(code: string, dictionaries: IDictionaries | null): string {
@@ -56,6 +59,33 @@ function dateToYyyymmdd(value: Date): string {
return `${y}${m}${d}`;
}
function dateToIsoYmd(value: Date): string {
const y = value.getFullYear().toString();
const m = (value.getMonth() + 1).toString().padStart(2, "0");
const d = value.getDate().toString().padStart(2, "0");
return `${y}-${m}-${d}`;
}
function computeDisabledDates(
availableYmd: string[],
minDate: Date,
maxDate: Date,
): Date[] {
const available = new Set(availableYmd);
const disabled: Date[] = [];
const cursor = new Date(minDate);
cursor.setHours(0, 0, 0, 0);
while (cursor.getTime() <= maxDate.getTime()) {
if (!available.has(dateToIsoYmd(cursor))) {
disabled.push(new Date(cursor));
}
cursor.setDate(cursor.getDate() + 1);
}
return disabled;
}
function addDays(base: Date, days: number): Date {
const result = new Date(base);
result.setDate(result.getDate() + days);
@@ -245,6 +275,59 @@ export const ScheduleStartPage: FC = () => {
const scheduleMinDate = useRef(getScheduleMinDate()).current;
const scheduleMaxDate = useRef(getScheduleMaxDate()).current;
const scheduleCalendarBaseDate = useMemo(
() => dateToIsoYmd(scheduleMinDate),
[scheduleMinDate],
);
const outboundCalendarParams = useMemo<IScheduleCalendarParams | null>(() => {
const dep = toCityCode(departureCode.trim().toUpperCase(), dictionaries);
const arr = toCityCode(arrivalCode.trim().toUpperCase(), dictionaries);
if (!dep || !arr || dep === arr) return null;
return {
date: scheduleCalendarBaseDate,
departure: dep,
arrival: arr,
connections: !directOnly,
};
}, [departureCode, arrivalCode, dictionaries, directOnly, scheduleCalendarBaseDate]);
const returnCalendarParams = useMemo<IScheduleCalendarParams | null>(() => {
if (!isRoundTrip) return null;
const dep = toCityCode(departureCode.trim().toUpperCase(), dictionaries);
const arr = toCityCode(arrivalCode.trim().toUpperCase(), dictionaries);
if (!dep || !arr || dep === arr) return null;
return {
date: scheduleCalendarBaseDate,
departure: arr,
arrival: dep,
connections: !directOnly,
};
}, [departureCode, arrivalCode, dictionaries, directOnly, isRoundTrip, scheduleCalendarBaseDate]);
const {
days: outboundAvailableDays,
loaded: outboundCalendarLoaded,
} = useScheduleCalendar(outboundCalendarParams);
const {
days: returnAvailableDays,
loaded: returnCalendarLoaded,
} = useScheduleCalendar(returnCalendarParams);
const outboundDisabledDates = useMemo(
() =>
!outboundCalendarLoaded
? []
: computeDisabledDates(outboundAvailableDays, scheduleMinDate, scheduleMaxDate),
[outboundAvailableDays, outboundCalendarLoaded, scheduleMinDate, scheduleMaxDate],
);
const returnDisabledDates = useMemo(
() =>
!returnCalendarLoaded
? []
: computeDisabledDates(returnAvailableDays, scheduleMinDate, scheduleMaxDate),
[returnAvailableDays, returnCalendarLoaded, scheduleMinDate, scheduleMaxDate],
);
// TZ §4.1.9 Table 14 / Angular CalendarInputWeekComponent: when the
// selected date range contains today, the Calendar input shows
@@ -486,6 +569,7 @@ export const ScheduleStartPage: FC = () => {
selectionMode="range"
minDate={scheduleMinDate}
maxDate={scheduleMaxDate}
disabledDates={outboundDisabledDates}
selectOtherMonths
dateFormat="dd.mm.yy"
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}
@@ -552,6 +636,7 @@ export const ScheduleStartPage: FC = () => {
selectionMode="range"
minDate={scheduleMinDate}
maxDate={scheduleMaxDate}
disabledDates={returnDisabledDates}
selectOtherMonths
dateFormat="dd.mm.yy"
placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`}