diff --git a/src/features/schedule/components/ScheduleFilter.tsx b/src/features/schedule/components/ScheduleFilter.tsx index 3b63446f..d95a573e 100644 --- a/src/features/schedule/components/ScheduleFilter.tsx +++ b/src/features/schedule/components/ScheduleFilter.tsx @@ -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 = ({ 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(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 = ({ className="input--filter" data-testid="schedule-date-input" inputId="schedule-date-input" + inputRef={dateRangeInputRef} readOnlyInput /> diff --git a/src/features/schedule/components/ScheduleStartPage.test.tsx b/src/features/schedule/components/ScheduleStartPage.test.tsx index 0481f60d..6ac02568 100644 --- a/src/features/schedule/components/ScheduleStartPage.test.tsx +++ b/src/features/schedule/components/ScheduleStartPage.test.tsx @@ -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(); + 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) // --------------------------------------------------------------------------- diff --git a/src/features/schedule/dateLabels.test.ts b/src/features/schedule/dateLabels.test.ts new file mode 100644 index 00000000..4af2b30c --- /dev/null +++ b/src/features/schedule/dateLabels.test.ts @@ -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(""); + }); +}); diff --git a/src/features/schedule/dateLabels.ts b/src/features/schedule/dateLabels.ts new file mode 100644 index 00000000..1cc33ca8 --- /dev/null +++ b/src/features/schedule/dateLabels.ts @@ -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)}`; +} diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index c2773a1e..aec15a1e 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -187,7 +187,8 @@ "SCHEDULE-BOTTOM-DESCRIPTION": "Flugplan", "SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "

Auf der Seite mit dem Flugplan von Aeroflot finden Sie alle wichtigen Informationen zu den Abflug- und Ankunftszeiten unserer Flüge.
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.

Wir bieten preisgünstige Flugtickets und einen bequemen Online-Buchungsserviceм.

Reisen Sie mit Aeroflot, wo Komfort und Zuverlässigkeit immer an erster Stelle stehen!

", "OUTBOUND": "Outbound", - "RETURN": "Return" + "RETURN": "Return", + "CURRENT-WEEK": "" }, "SEO": { "BOARD": { diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index b61587f2..13833b1a 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -225,7 +225,8 @@ "SCHEDULE-BOTTOM-DESCRIPTION": "Flight schedule", "SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "

The Aeroflot flight schedule page provides all the essential information on our flight departure and arrival times.
Select your travel date and plan your journey in advance — whether it’s a direct flight or one with stopovers.

We offer competitively priced air tickets and a convenient online booking service.

Travel with Aeroflot, where comfort and reliability are always top-flight!

", "OUTBOUND": "Outbound", - "RETURN": "Return" + "RETURN": "Return", + "CURRENT-WEEK": "Current week" }, "SEO": { "BOARD": { diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 610cc3d4..552e2ac5 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -187,7 +187,8 @@ "SCHEDULE-BOTTOM-DESCRIPTION": "Horario de vuelos", "SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "

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.
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.

Ofrecemos billetes de avión a precios competitivos y un cómodo servicio de reservas en línea.

Viaje con Aeroflot, donde el confort y la fiabilidad son siempre de primera!

", "OUTBOUND": "Outbound", - "RETURN": "Return" + "RETURN": "Return", + "CURRENT-WEEK": "" }, "SEO": { "BOARD": { diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 64e19fd2..7e6153fd 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -187,7 +187,8 @@ "SCHEDULE-BOTTOM-DESCRIPTION": "Расписание рейсов Аэрофлота", "SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "

La page des horaires des vols Aeroflot présente toutes les informations essentielles sur les heures de départ et d’arrivée de nos vols.
Sélectionnez votre date de voyage et planifiez votre voyage, qu’il s’agisse d’un vol direct ou d’un vol avec escale

Nous proposons des billets d’avion à des prix compétitifs et un service de réservation en ligne très pratique.

Voyagez avec Aeroflot, où confort et fiabilité vous accompagnent toujours !

", "OUTBOUND": "Outbound", - "RETURN": "Return" + "RETURN": "Return", + "CURRENT-WEEK": "" }, "SEO": { "BOARD": { diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 1f1fac68..d046cb1e 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -187,7 +187,8 @@ "SCHEDULE-BOTTOM-DESCRIPTION": "Orario dei voli", "SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "

La pagina degli orari dei voli di Aeroflot fornisce tutte le informazioni essenziali sugli orari di partenza e di arrivo dei nostri voli.
Seleziona la data e pianifica il tuo viaggio in anticipo, sia che si tratti di un volo diretto che di uno con scalo.

Offriamo biglietti aerei a prezzi competitivi e un comodo servizio di prenotazione online.

Viaggia con Aeroflot, dove il comfort e l'affidabilità sono sempre al top!

", "OUTBOUND": "Outbound", - "RETURN": "Return" + "RETURN": "Return", + "CURRENT-WEEK": "" }, "SEO": { "BOARD": { diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 5ccb0963..fd2f0871 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -187,7 +187,8 @@ "SCHEDULE-BOTTOM-DESCRIPTION": "フライトスケジュール", "SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "

Aeroflot・ロシア航空のフライトスケジュールページでは、アエロフロート・ロシア航空の出発・到着時刻に関する情報を提供しています
ご旅行の日程を選択し、直行便でも途中降機でも、事前にご旅行の計画を立てましょう。

当社は競争力のある価格の航空券と便利なオンライン予約サービスを提供しています。

Aeroflotの快適さと信頼性は、常にトップクラスです!

", "OUTBOUND": "Outbound", - "RETURN": "Return" + "RETURN": "Return", + "CURRENT-WEEK": "" }, "SEO": { "BOARD": { diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index e9a6183b..12a9485c 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -187,7 +187,8 @@ "SCHEDULE-BOTTOM-DESCRIPTION": "항공편 일정", "SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "

Aeroflot 항공편 스케줄 페이지에서 항공편 출발 및 도착 시간에 대한 모든 필수 정보를 확인할 수 있습니다.
여행 날짜를 선택하고 직항이든 경유지가 있는 항공편이든 미리 여행 계획을 세우세요.

경쟁력 있는 가격의 항공권과 편리한 온라인 예약 서비스를 제공합니다.

항상 편안함과 신뢰성을 최우선으로 하는 Aeroflot와 함께 여행하세요!

", "OUTBOUND": "Outbound", - "RETURN": "Return" + "RETURN": "Return", + "CURRENT-WEEK": "" }, "SEO": { "BOARD": { diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index fba7ce80..9e816097 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -225,7 +225,8 @@ "SCHEDULE-BOTTOM-DESCRIPTION": "Расписание рейсов Аэрофлота", "SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "

На странице расписания рейсов Аэрофлота представлена вся необходимая информация о времени отправления и прибытия наших рейсов.
Выбирайте дату и планируйте свое путешествие заранее: прямым рейсом или с пересадками.

Мы предлагаем билеты на самолет по конкурентным ценам с удобным онлайн-сервисом для оформления заказа.

Путешествуйте с Аэрофлотом, где комфорт и надежность всегда на высоте!

", "OUTBOUND": "Туда", - "RETURN": "Обратно" + "RETURN": "Обратно", + "CURRENT-WEEK": "Текущая неделя" }, "SEO": { "BOARD": { diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index dec1d26f..dd296f8b 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -187,7 +187,8 @@ "SCHEDULE-BOTTOM-DESCRIPTION": "航班时刻表", "SCHEDULE-BOTTOM-DESCRIPTION-TEXT": "

Aeroflot航班时刻表页面提供我们航班起飞和到达时间的所有基本信息。
选择您的旅行日期,提前计划您的行程--无论是直飞航班还是中途停留的航班

我们提供价格极具竞争力的机票和便捷的在线预订服务。

乘坐俄罗斯国际航空公司的航班,舒适性和可靠性始终是一流的!

", "OUTBOUND": "Outbound", - "RETURN": "Return" + "RETURN": "Return", + "CURRENT-WEEK": "" }, "SEO": { "BOARD": {