Root cause of search not working: globalThis.fetch stored as a class field loses its Window binding, causing 'Illegal invocation'. Fixed with fetch.bind(globalThis). Also fix calendar days endpoint date format from yyyyMMdd to yyyy-MM-ddT00:00:00 matching Angular's ApiFormatterService.
23 KiB
Пользовательские истории: Поиск расписания полётов
Важное примечание: Функции этого раздела (обратный рейс, фильтры по времени, прямые рейсы) специфичны для раздела "Расписание" и не дублируют функции Онлайн-Табло.
US-23: Переход на вкладку "Расписание"
Цель: Пользователь переходит на страницу поиска расписания, чтобы искать рейсы на определённую неделю с расширенными параметрами.
Путь клиента:
- Исходное состояние — пользователь находится на странице
/onlineboard, в навигации видны вкладки: Онлайн-Табло, Расписание, Карта полётов. - Клик по вкладке "Расписание" — пользователь кликает по вкладке "Расписание" в основной навигации.
- Переход по URL — роутер меняет адрес на
/schedule, соответствующий маршруту изschedule-routing.module.ts. - Активация вкладки — вкладка "Расписание" подсвечивается как активная, остальные — неактивные.
- Загрузка формы фильтра — рендерится
ScheduleFilterComponentс полями: города отправления/прибытия, неделя вылета, время вылета, чекбоксы "Только прямые рейсы" и "Обратный рейс". - Автозаполнение по геолокации — если
UserLocationServiceвернул локацию и поле отправления пусто, оно заполняется кодом станции,dateRangeставится в текущую неделю.
Критерии приёмки:
- ✅ Клик по вкладке меняет URL на
/schedule. - ✅ Вкладка "Расписание" визуально отмечена как активная.
- ✅ Форма фильтра отображает все поля из Angular-шаблона.
- ✅ При разрешённой геолокации поле отправления предзаполняется.
- ✅ При наличии
urlParams.routeв резолвере форма инициализируется из URL.
Примечание: Инициализация также поддерживает hand-off через StateService (schedulein/scheduleout) — если параметры заданы, поиск запускается автоматически.
US-24: Ввод города отправления
Цель: Пользователь вводит город или аэропорт отправления для поиска расписания.
Путь клиента:
- Фокус на поле — пользователь кликает в поле "Город отправления" (
city-autocomplete,data-testid="schedule-departure-city-input"). - Ввод текста — пользователь вводит название города или IATA-код (например, "Мос" или "SVO").
- Автодополнение — компонент
city-autocompleteзапрашивает справочник и показывает выпадающий список совпадений. В компонентеcity-select, если у города несколько аэропортов, каждый аэропорт сопровождаетсяpTooltipс полным названием (airport.name, позицияtop). - Выбор значения — пользователь выбирает пункт списка, поле привязывается к
state.departureчерезngModel. - Валидация кода — сеттер
departureвызываетupdateCalendar(), который черезvalidationService.validateCode()проверяет код станции. - Обновление календаря — если оба кода валидны, вызывается
apiService.getFlightDaysByRoute()и формируется массивdisabledDates.
Критерии приёмки:
- ✅ Поле отображает плейсхолдер
SHARED.CITY_PLACEHOLDER. - ✅ Автодополнение возвращает совпадения при вводе не менее 2 символов.
- ✅ Выбранное значение сохраняется в
ScheduleFiltersStateService.departure. - ✅ Невалидный код приводит к ошибке
departureErrorпри сабмите. - ✅ После валидного выбора обновляется список
disabledDatesв календаре вылета.
Примечание: Компонент city-autocomplete и валидатор StationCodeValidationService общие для всех фич проекта.
US-25: Ввод города прибытия
Цель: Пользователь вводит город или аэропорт прибытия, чтобы задать маршрут поиска.
Путь клиента:
- Фокус на поле — пользователь кликает в поле "Город прибытия" (
data-testid="schedule-arrival-city-input"). - Ввод текста — вводит название или IATA-код города назначения.
- Автодополнение —
city-autocompleteподгружает и показывает список подходящих станций. - Выбор значения — выбранное значение записывается в
state.arrivalчерез двустороннюю привязку. - Пересчёт календаря — сеттер вызывает
updateCalendar(), который проверяет оба кода и обновляетdisabledDates(иdisabledDatesReturnпри активномwithReturn). - Проверка "город A ≠ город B" — при сабмите валидация отклоняет одинаковые города с ошибкой
SHARED.ARRIVAL-EQUALS-DEPARTURE-ERROR.
Критерии приёмки:
- ✅ Поле использует тот же компонент
city-autocomplete, что и отправление. - ✅ Выбранное значение сохраняется в
state.arrival. - ✅ Одинаковые коды отправления и прибытия блокируют сабмит с ошибкой.
- ✅ После валидного выбора обновляется
disabledDatesобоих календарей (еслиwithReturn). - ✅ Невалидный код вызывает
arrivalErrorс сообщениемSHARED.ARRIVAL-CITY-ERROR.
US-26: Кнопка "Обменять" (swap)
Цель: Пользователь одним кликом меняет местами города отправления и прибытия.
Путь клиента:
- Заполненные поля — пользователь ввёл города отправления и/или прибытия.
- Клик по кнопке ⇄ — кликает кнопку
button-changeмежду полями городов (SVGchangeCity). - Сброс ошибок — метод
exchange()обнуляетdepartureErrorиarrivalError. - Обмен значений — деструктуризацией
[departure, arrival] = [arrival, departure]значения меняются местами. - Каскадное обновление — сеттеры вызывают
updateCalendar(), что перерасчитываетdisabledDatesдля нового направления. - Отображение — поля автокомплита отображают поменянные значения.
Критерии приёмки:
- ✅ Клик по кнопке меняет значения
state.departureиstate.arrival. - ✅ Ошибки валидации городов сбрасываются.
- ✅ Календарь
disabledDatesпересчитывается под новое направление. - ✅ Работает корректно если одно из полей пустое (пустота и значение меняются).
- ✅ Кнопка доступна всегда (не disabled).
US-27: Выбор недели вылета
Цель: Пользователь выбирает неделю, для которой хочет посмотреть расписание рейсов.
Путь клиента:
- Клик по календарю — пользователь кликает
calendar-input-weekсlabel="SHARED.SCHEDULES_DATE"(data-testid="schedule-calendar"). - Показ календаря — открывается read-only календарь с ограничениями
minDate=settings.scheduleMinDate,maxDate=maxScheduleDate. - Отображение disabled дат — дни без рейсов подсвечиваются серым согласно
disabledDates, рассчитанному изgetFlightDaysByRoute. - Выбор недели — пользователь кликает на день; компонент выбирает всю неделю (пн–вс) и сохраняет её в
state.dateRange. - Сброс ошибки —
ngModelChangeвызываетresetDateRangeError(). - Каскад на обратный рейс — если включён
withReturn, сеттер пересчитываетminReturnScheduleDateдля календаря возврата.
Критерии приёмки:
- ✅ Календарь работает в режиме выбора недели, а не одного дня.
- ✅ Даты за пределами
scheduleMinDate/scheduleMaxDateнедоступны. - ✅ Дни без рейсов отображаются как disabled.
- ✅ Выбранный диапазон сохраняется как
[Date, Date]вstate.dateRange. - ✅ Предыдущая ошибка
dateRangeErrorсбрасывается при новом выборе.
Примечание: scheduleMinDate/scheduleMaxDate задаются в AppSettings; maxScheduleDate дополнительно ограничен датой возврата при withReturn.
US-28: Поиск туров (туда и обратно)
Цель: Пользователь включает режим "Обратный рейс" для поиска туров с указанием даты возврата.
Путь клиента:
- Клик по чекбоксу — пользователь отмечает чекбокс
SHARED.RETURN_FLIGHT_VIEW, что выставляетstate.withReturn = true. - Отображение блока возврата —
*ngIf="withReturn"показывает блок с календарёмSHARED.RETURN_FLIGHT_DATEи селектором времениSHARED.RETURN_FLIGHT_TIME. - Обновление календаря возврата — сеттер
withReturnвызываетupdateCalendarReturn(), который запрашиваетgetFlightDaysByRouteдля обратного направления (arrival → departure). - Выбор недели возврата — пользователь выбирает диапазон в календаре возврата, значение сохраняется в
state.returnDateRange. - Сброс при отключении — при снятии чекбокса
resetReturnDateRange()очищаетreturnDateRangeв[]. - Сабмит с inbound — при клике "Показать" формируется
inboundParamsс обращённым направлением и передаётся в URL.
Критерии приёмки:
- ✅ Чекбокс не отмечен по умолчанию.
- ✅ Блок возврата появляется только при
withReturn === true. - ✅
disabledDatesReturnрассчитывается для обратного направления. - ✅ Снятие чекбокса очищает
returnDateRange. - ✅ URL содержит второй сегмент с параметрами inbound при сабмите.
Примечание: minReturnScheduleDate не может быть раньше выбранной даты вылета; maxScheduleDate подстраивается под дату возврата.
US-29: Фильтр "Только прямые рейсы"
Цель: Пользователь ограничивает поиск только прямыми рейсами, исключая стыковки.
Путь клиента:
- Клик по чекбоксу — пользователь отмечает
SHARED.DIRECT_FLIGHT_ONLY, это выставляетstate.directOnly = true. - Пересчёт disabledDates — сеттер вызывает
updateCalendar()с аргументом!directOnly === false, ограничивая запросgetFlightDaysByRouteтолько прямыми рейсами. - Обновление календаря — календарь помечает дни без прямых рейсов как disabled.
- Формирование параметров — в
getOutboundParams()добавляетсяconnections: 0(иначеundefined). - Сабмит — пользователь нажимает "Показать", URL получает суффикс
-C0. - Отображение результатов — страница результатов показывает только прямые рейсы.
Критерии приёмки:
- ✅ Чекбокс не отмечен по умолчанию.
- ✅ При включении флага параметр
connectionsравен0. - ✅ URL маршрута содержит
-C0в сегменте параметров. - ✅
disabledDatesпересчитывается под режим "только прямые". - ✅ Флаг применяется и к outbound, и к inbound (если
withReturn).
US-30: Фильтр по времени вылета
Цель: Пользователь ограничивает поиск рейсами, вылетающими в заданное время суток.
Путь клиента:
- Открытие селектора — пользователь кликает
time-selectorс лейбломSHARED.DEPARTURE_TIME(режимfullView=false). - Выбор диапазона — компонент позволяет выбрать
timeFromиtimeTo(например, утро/день/вечер/ночь или вручную). - Сохранение в state — выбранный
IUrlTimeRangeсохраняется вstate.timeRange. - Формирование параметров —
getOutboundParams()добавляетtimeFrom/timeToчерез spread...this.timeRange. - Построение URL —
url-builder.formatRouteParams()добавляет к сегменту суффикс-{timeFrom}{timeTo}при наличии обоих значений. - Сабмит — пользователь нажимает "Показать", фильтр применяется к результатам.
Критерии приёмки:
- ✅ Селектор отображается в компактном режиме (
fullView=false). - ✅ Значение сохраняется как
{timeFrom, timeTo}вstate.timeRange. - ✅ Оба значения обязательны для добавления в URL.
- ✅ URL содержит временной суффикс в сегменте параметров outbound.
- ✅ При отсутствии выбора фильтр не применяется (параметры опущены).
US-31: Фильтр по времени прибытия (обратный рейс)
Цель: Пользователь задаёт время вылета для обратного сегмента при поиске туров.
Путь клиента:
- Условие отображения — селектор времени возврата виден только при
withReturn === true. - Открытие селектора — пользователь кликает
time-selectorс лейбломSHARED.RETURN_FLIGHT_TIME. - Выбор диапазона — выбирается диапазон
timeFrom/timeToдля обратного рейса. - Сохранение — значение записывается в
state.returnTimeRangeкакIUrlTimeRange. - Формирование inbound —
getInboundParams()добавляет...this.returnTimeRangeк параметрам обратного направления. - URL — URL получает второй сегмент с собственным временным суффиксом для inbound.
Критерии приёмки:
- ✅ Селектор недоступен, пока не включён
withReturn. - ✅ Значение сохраняется в
state.returnTimeRange. - ✅ Время применяется только к inbound-сегменту, не к outbound.
- ✅ URL содержит отдельные временные суффиксы для каждого направления.
- ✅ Отключение
withReturnне влияет на сохранённыйtimeRangeoutbound.
Примечание: Название "время прибытия" условно — в Angular это SHARED.RETURN_FLIGHT_TIME, фактически время вылета обратного рейса.
US-32: Валидация параметров поиска
Цель: Система проверяет корректность параметров перед переходом на страницу результатов.
Путь клиента:
- Клик "Показать" — пользователь нажимает кнопку
SHARED.SCHEDULES_VIEW(data-testid="schedule-search-button"). - Сбор параметров —
viewSchedule()собираетoutboundиinboundпараметры из state. - Вызов validate —
ScheduleFilterValidationService.validate({inbound, outbound})очищает предыдущие ошибки и запускает проверку. - Проверка outbound — проверяются: валидность
departure, валидностьarrival, различие городов, валидностьdateFrom/dateToчерезmoment. - Проверка inbound — при наличии inbound проверяются
dateFrom/dateTo. - Навигация или ошибка — при успехе URL строится и вызывается
router.navigateByUrl(); при ошибке соответствующее поле показывает сообщение.
Критерии приёмки:
- ✅ Пустое поле отправления → ошибка
SHARED.DEPARTURE-CITY-ERROR. - ✅ Пустое поле прибытия → ошибка
SHARED.ARRIVAL-CITY-ERROR. - ✅ Совпадение городов → ошибка
SHARED.ARRIVAL-EQUALS-DEPARTURE-ERROR. - ✅ Невалидная дата → ошибка
SHARED.WEEK_FORMAT-WRONGна соответствующем поле. - ✅ При ошибке переход не выполняется, форма остаётся активной.
Примечание: Валидация кодов станций делегируется StationCodeValidationService и выполняется асинхронно.
US-33: URL-параметры поиска расписания
Цель: Параметры поиска сериализуются в URL, чтобы ссылки можно было шарить и перезагружать страницу без потери состояния.
Путь клиента:
- Построение URL — после валидации
ScheduleUrlBuilderService.getRoutePageUrl(outbound, inbound?)формирует путь${base}/route/{outboundSeg}[/{inboundSeg}]. - Форматирование сегмента —
formatRouteParams()собирает строку{departure}-{arrival}-{dateFrom}-{dateTo}[-{timeFrom}{timeTo}][-C{connections}]. - Навигация —
router.navigateByUrl(url), а при успехеSearchHistoryService.add()сохраняет запись типаschedule-route. - Разбор URL — при прямом открытии ссылки резолвер использует
url-parser.serviceдля преобразования сегментов вIScheduleRouteParams. - Инициализация формы —
setState(routeParams)заполняетstateзначениями из URL (включаяwithReturn = trueпри наличии inbound). - Построение запроса к API —
request-builder.serviceпревращаетIScheduleRouteDirectionParamsв HTTP-запрос к бэкенду.
Критерии приёмки:
- ✅ URL содержит сегмент outbound в формате
DEP-ARR-dateFrom-dateTo[-timeTimeTo][-C0]. - ✅ При
withReturnдобавляется второй сегмент с inbound параметрами. - ✅ Прямое открытие URL восстанавливает все поля формы.
- ✅ Отсутствие inbound сегмента выставляет
withReturn = false. - ✅ История поиска получает запись при успешной навигации.
Примечание: Базовый путь фичи задаётся через токен SCHEDULE_URL_BASE; даты форматируются методом formatDate базового класса UrlBuilderService.