Add Current-Week label substitution to Schedule date-range picker per TZ 4.1.9 Table 14

This commit is contained in:
2026-04-21 19:59:27 +03:00
parent 04a3d9cd7c
commit 8f4d5fcaa2
13 changed files with 141 additions and 10 deletions
@@ -8,7 +8,7 @@
* search is route-only.
*/
import { type FC, useState, useCallback, useRef, type FormEvent } from "react";
import { type FC, useState, useCallback, useRef, useEffect, type FormEvent } from "react";
import { useNavigate } from "@modern-js/runtime/router";
import { Calendar } from "primereact/calendar";
import { Slider, type SliderChangeEvent } from "primereact/slider";
@@ -18,6 +18,7 @@ import { CityAutocomplete } from "@/ui/city-autocomplete/index.js";
import { useDictionaries } from "@/shared/dictionaries/index.js";
import { buildScheduleUrl } from "../url.js";
import type { ScheduleParams } from "../url.js";
import { formatScheduleDateRangeWithCurrentWeek } from "../dateLabels.js";
import { scheduleWindowBounds } from "@/shared/dateWindow.js";
import "./ScheduleFilter.scss";
@@ -131,6 +132,21 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
const scheduleMinDate = useRef(getScheduleMinDate()).current;
const scheduleMaxDate = useRef(getScheduleMaxDate()).current;
// Swap the Calendar input's displayed text to "Текущая неделя" per
// TZ §4.1.9 Table 14 when the selected range equals Mon-Sun of the
// current week. Uses inputRef + useEffect to override PrimeReact's
// own dd.mm.yy rendering without touching the state value.
const dateRangeInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const [from, to] = dateRange;
if (!dateRangeInputRef.current || !from || !to) return;
const label = formatScheduleDateRangeWithCurrentWeek(from, to, t);
const currentWeekLabel = t("SCHEDULE.CURRENT-WEEK");
if (label === currentWeekLabel) {
dateRangeInputRef.current.value = currentWeekLabel;
}
}, [dateRange, t]);
const handleSwap = useCallback(() => {
setDeparture(arrival);
setArrival(departure);
@@ -300,6 +316,7 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
className="input--filter"
data-testid="schedule-date-input"
inputId="schedule-date-input"
inputRef={dateRangeInputRef}
readOnlyInput
/>
</div>
@@ -266,6 +266,34 @@ describe("ScheduleStartPage", () => {
});
});
// ---------------------------------------------------------------------------
// TZ §4.1.9 Table 14: current-week label on Schedule date-range picker
// ---------------------------------------------------------------------------
describe("4.1.9-R: Current-Week label substitution", () => {
beforeEach(() => {
vi.clearAllMocks();
sessionStore.clear();
resetCrossSectionStore();
// Clock frozen to Fri 2026-05-15 → Mon 2026-05-11 … Sun 2026-05-17
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 4, 15, 12, 0, 0));
});
afterEach(() => {
vi.useRealTimers();
});
it("4.1.9-R: start page renders with current-week dates pre-populated in session store on Route click", () => {
render(<ScheduleStartPage />);
fireEvent.click(screen.getByTestId("popular-click-route"));
const stored = JSON.parse(sessionStore.getRaw("afl-prefill:schedule")!);
// Current week Mon-Sun for 2026-05-15
expect(stored.dateFrom).toBe("20260511");
expect(stored.dateTo).toBe("20260517");
});
});
// ---------------------------------------------------------------------------
// TZ §4.1.8: cross-section hydration tests (Board → Schedule)
// ---------------------------------------------------------------------------
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { formatScheduleDateRangeWithCurrentWeek } from "./dateLabels.js";
describe("4.1.9-R: formatScheduleDateRangeWithCurrentWeek", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 4, 15, 12, 0, 0)); // Fri 2026-05-15
});
afterEach(() => { vi.useRealTimers(); });
it("returns 'Текущая неделя' when range = Mon-Sun of current week", () => {
const t = (k: string) => (k === "SCHEDULE.CURRENT-WEEK" ? "Текущая неделя" : k);
expect(
formatScheduleDateRangeWithCurrentWeek(new Date(2026, 4, 11), new Date(2026, 4, 17), t),
).toBe("Текущая неделя");
});
it("returns dd.MM.yyyy-dd.MM.yyyy for other ranges", () => {
const t = (k: string) => k;
expect(
formatScheduleDateRangeWithCurrentWeek(new Date(2026, 4, 18), new Date(2026, 4, 24), t),
).toBe("18.05.2026-24.05.2026");
});
it("returns dd.MM.yyyy-dd.MM.yyyy for partial current week", () => {
const t = (k: string) => k;
expect(
formatScheduleDateRangeWithCurrentWeek(new Date(2026, 4, 13), new Date(2026, 4, 17), t),
).toBe("13.05.2026-17.05.2026");
});
it("returns empty string for null inputs", () => {
expect(formatScheduleDateRangeWithCurrentWeek(null, null, () => "")).toBe("");
});
});
+42
View File
@@ -0,0 +1,42 @@
/**
* Schedule range-calendar label substitution per TZ §4.1.9 Table 14.
* Current week Mon-Sun → "Текущая неделя", otherwise dd.MM.yyyy-dd.MM.yyyy.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TFunction = (key: string, opts?: any) => string;
function toYmd(d: Date): string {
const day = String(d.getDate()).padStart(2, "0");
const month = String(d.getMonth() + 1).padStart(2, "0");
return `${day}.${month}.${d.getFullYear()}`;
}
function mondayOfWeek(base: Date): Date {
const d = new Date(base);
d.setHours(0, 0, 0, 0);
const offset = (d.getDay() + 6) % 7;
d.setDate(d.getDate() - offset);
return d;
}
export function formatScheduleDateRangeWithCurrentWeek(
dateFrom: Date | null | undefined,
dateTo: Date | null | undefined,
t: TFunction,
): string {
if (!dateFrom || !dateTo) return "";
const today = new Date();
today.setHours(0, 0, 0, 0);
const thisMon = mondayOfWeek(today);
const thisSun = new Date(thisMon);
thisSun.setDate(thisSun.getDate() + 6);
const from = new Date(dateFrom);
from.setHours(0, 0, 0, 0);
const to = new Date(dateTo);
to.setHours(0, 0, 0, 0);
if (from.getTime() === thisMon.getTime() && to.getTime() === thisSun.getTime()) {
return t("SCHEDULE.CURRENT-WEEK");
}
return `${toYmd(from)}-${toYmd(to)}`;
}
+2 -1
View File
@@ -187,7 +187,8 @@
"SCHEDULE-BOTTOM-DESCRIPTION": "Flugplan",
"SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "<p> Auf der Seite mit dem Flugplan von Aeroflot finden Sie alle wichtigen Informationen zu den Abflug- und Ankunftszeiten unserer Flüge. <br> Wählen Sie Ihr Reisedatum und planen Sie Ihre Reise im Voraus - egal ob es sich um einen Direktflug oder einen Flug mit Zwischenstopps handelt.</p> <p>Wir bieten preisgünstige Flugtickets und einen bequemen <a target='_blank' href='https://www.aeroflot.ru/de/booking'>Online-Buchungsserviceм</a>.</p> <p>Reisen Sie mit Aeroflot, wo Komfort und Zuverlässigkeit immer an erster Stelle stehen!</p>",
"OUTBOUND": "Outbound",
"RETURN": "Return"
"RETURN": "Return",
"CURRENT-WEEK": ""
},
"SEO": {
"BOARD": {
+2 -1
View File
@@ -225,7 +225,8 @@
"SCHEDULE-BOTTOM-DESCRIPTION": "Flight schedule",
"SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "<p>The Aeroflot flight schedule page provides all the essential information on our flight departure and arrival times. <br> Select your travel date and plan your journey in advance — whether its a direct flight or one with stopovers.</p> <p>We offer competitively priced air tickets and a convenient <a target='_blank' href='https://www.aeroflot.ru/en/booking'>online booking service</a>.</p> <p>Travel with Aeroflot, where comfort and reliability are always top-flight!</p>",
"OUTBOUND": "Outbound",
"RETURN": "Return"
"RETURN": "Return",
"CURRENT-WEEK": "Current week"
},
"SEO": {
"BOARD": {
+2 -1
View File
@@ -187,7 +187,8 @@
"SCHEDULE-BOTTOM-DESCRIPTION": "Horario de vuelos",
"SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "<p>En la página de horarios de vuelos de Aeroflot encontrará toda la información esencial sobre los horarios de salida y llegada de nuestros vuelos. <br>Seleccione la fecha de su viaje y planifíquelo con antelación, tanto si se trata de un vuelo directo como de uno con escalas. </p> <p>Ofrecemos billetes de avión a precios competitivos y un cómodo <a target='_blank' href='https://www.aeroflot.ru/es/booking'>servicio de reservas en línea.</a></p> <p>Viaje con Aeroflot, donde el confort y la fiabilidad son siempre de primera!</p>",
"OUTBOUND": "Outbound",
"RETURN": "Return"
"RETURN": "Return",
"CURRENT-WEEK": ""
},
"SEO": {
"BOARD": {
+2 -1
View File
@@ -187,7 +187,8 @@
"SCHEDULE-BOTTOM-DESCRIPTION": "Расписание рейсов Аэрофлота",
"SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "<p>La page des horaires des vols Aeroflot présente toutes les informations essentielles sur les heures de départ et darrivée de nos vols. <br>Sélectionnez votre date de voyage et planifiez votre voyage, quil sagisse dun vol direct ou dun vol avec escale </p> <p> Nous proposons des billets davion à des prix compétitifs et un <a target='_blank' href='https://www.aeroflot.ru/fr/booking'>service de réservation en ligne</a> très pratique. </p> <p>Voyagez avec Aeroflot, où confort et fiabilité vous accompagnent toujours !</p>",
"OUTBOUND": "Outbound",
"RETURN": "Return"
"RETURN": "Return",
"CURRENT-WEEK": ""
},
"SEO": {
"BOARD": {
+2 -1
View File
@@ -187,7 +187,8 @@
"SCHEDULE-BOTTOM-DESCRIPTION": "Orario dei voli",
"SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "<p>La pagina degli orari dei voli di Aeroflot fornisce tutte le informazioni essenziali sugli orari di partenza e di arrivo dei nostri voli. <br>Seleziona la data e pianifica il tuo viaggio in anticipo, sia che si tratti di un volo diretto che di uno con scalo. </p> <p>Offriamo biglietti aerei a prezzi competitivi e un comodo <a target='_blank' href='https://www.aeroflot.ru/it/booking'>servizio di prenotazione online</a>. </p> <p>Viaggia con Aeroflot, dove il comfort e l'affidabilità sono sempre al top!</p>",
"OUTBOUND": "Outbound",
"RETURN": "Return"
"RETURN": "Return",
"CURRENT-WEEK": ""
},
"SEO": {
"BOARD": {
+2 -1
View File
@@ -187,7 +187,8 @@
"SCHEDULE-BOTTOM-DESCRIPTION": "フライトスケジュール",
"SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "<p>Aeroflot・ロシア航空のフライトスケジュールページでは、アエロフロート・ロシア航空の出発・到着時刻に関する情報を提供しています<br>ご旅行の日程を選択し、直行便でも途中降機でも、事前にご旅行の計画を立てましょう。</p> <p>当社は競争力のある価格の航空券と便利な<a target='_blank' href='https://www.aeroflot.ru/ja/booking'>オンライン予約サービスを</a>提供しています。</p> <p>Aeroflotの快適さと信頼性は、常にトップクラスです!</p>",
"OUTBOUND": "Outbound",
"RETURN": "Return"
"RETURN": "Return",
"CURRENT-WEEK": ""
},
"SEO": {
"BOARD": {
+2 -1
View File
@@ -187,7 +187,8 @@
"SCHEDULE-BOTTOM-DESCRIPTION": "항공편 일정",
"SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "<p> Aeroflot 항공편 스케줄 페이지에서 항공편 출발 및 도착 시간에 대한 모든 필수 정보를 확인할 수 있습니다. <br> 여행 날짜를 선택하고 직항이든 경유지가 있는 항공편이든 미리 여행 계획을 세우세요.</p> <p>경쟁력 있는 가격의 항공권과 편리한 <a target='_blank' href='https://www.aeroflot.ru/ko/booking'>온라인 예약 서비스를</a> 제공합니다.</p> <p>항상 편안함과 신뢰성을 최우선으로 하는 Aeroflot와 함께 여행하세요!</p>",
"OUTBOUND": "Outbound",
"RETURN": "Return"
"RETURN": "Return",
"CURRENT-WEEK": ""
},
"SEO": {
"BOARD": {
+2 -1
View File
@@ -225,7 +225,8 @@
"SCHEDULE-BOTTOM-DESCRIPTION": "Расписание рейсов Аэрофлота",
"SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "<p>На странице расписания рейсов Аэрофлота представлена вся необходимая информация о времени отправления и прибытия наших рейсов. <br> Выбирайте дату и планируйте свое путешествие заранее: прямым рейсом или с пересадками.</p> <p>Мы предлагаем билеты на самолет по конкурентным ценам с удобным <a target='_blank' href='https://www.aeroflot.ru/ru/booking'>онлайн-сервисом</a> для оформления заказа.</p> <p>Путешествуйте с Аэрофлотом, где комфорт и надежность всегда на высоте!</p>",
"OUTBOUND": "Туда",
"RETURN": "Обратно"
"RETURN": "Обратно",
"CURRENT-WEEK": "Текущая неделя"
},
"SEO": {
"BOARD": {
+2 -1
View File
@@ -187,7 +187,8 @@
"SCHEDULE-BOTTOM-DESCRIPTION": "航班时刻表",
"SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "<p> Aeroflot航班时刻表页面提供我们航班起飞和到达时间的所有基本信息。<br> 选择您的旅行日期,提前计划您的行程--无论是直飞航班还是中途停留的航班</p> <p>我们提供价格极具竞争力的机票和便捷的<a target='_blank' href='https://www.aeroflot.ru/zh/booking'>在线预订服务。</a></p> <p>乘坐俄罗斯国际航空公司的航班,舒适性和可靠性始终是一流的!</p>",
"OUTBOUND": "Outbound",
"RETURN": "Return"
"RETURN": "Return",
"CURRENT-WEEK": ""
},
"SEO": {
"BOARD": {