Files
flights_web/docs/user-stories-3-schedule-search.md
gnezim 71d0c983fd
CI / ci (push) Failing after 28s
Deploy / build-and-deploy (push) Failing after 5s
Fix API calls: bind fetch to globalThis, fix date format for calendar
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.
2026-04-15 22:32:51 +03:00

23 KiB
Raw Permalink Blame History

Пользовательские истории: Поиск расписания полётов

Важное примечание: Функции этого раздела (обратный рейс, фильтры по времени, прямые рейсы) специфичны для раздела "Расписание" и не дублируют функции Онлайн-Табло.


US-23: Переход на вкладку "Расписание"

Цель: Пользователь переходит на страницу поиска расписания, чтобы искать рейсы на определённую неделю с расширенными параметрами.

Путь клиента:

  1. Исходное состояние — пользователь находится на странице /onlineboard, в навигации видны вкладки: Онлайн-Табло, Расписание, Карта полётов.
  2. Клик по вкладке "Расписание" — пользователь кликает по вкладке "Расписание" в основной навигации.
  3. Переход по URL — роутер меняет адрес на /schedule, соответствующий маршруту из schedule-routing.module.ts.
  4. Активация вкладки — вкладка "Расписание" подсвечивается как активная, остальные — неактивные.
  5. Загрузка формы фильтра — рендерится ScheduleFilterComponent с полями: города отправления/прибытия, неделя вылета, время вылета, чекбоксы "Только прямые рейсы" и "Обратный рейс".
  6. Автозаполнение по геолокации — если UserLocationService вернул локацию и поле отправления пусто, оно заполняется кодом станции, dateRange ставится в текущую неделю.

Критерии приёмки:

  • Клик по вкладке меняет URL на /schedule.
  • Вкладка "Расписание" визуально отмечена как активная.
  • Форма фильтра отображает все поля из Angular-шаблона.
  • При разрешённой геолокации поле отправления предзаполняется.
  • При наличии urlParams.route в резолвере форма инициализируется из URL.

Примечание: Инициализация также поддерживает hand-off через StateService (schedulein/scheduleout) — если параметры заданы, поиск запускается автоматически.


US-24: Ввод города отправления

Цель: Пользователь вводит город или аэропорт отправления для поиска расписания.

Путь клиента:

  1. Фокус на поле — пользователь кликает в поле "Город отправления" (city-autocomplete, data-testid="schedule-departure-city-input").
  2. Ввод текста — пользователь вводит название города или IATA-код (например, "Мос" или "SVO").
  3. Автодополнение — компонент city-autocomplete запрашивает справочник и показывает выпадающий список совпадений. В компоненте city-select, если у города несколько аэропортов, каждый аэропорт сопровождается pTooltip с полным названием (airport.name, позиция top).
  4. Выбор значения — пользователь выбирает пункт списка, поле привязывается к state.departure через ngModel.
  5. Валидация кода — сеттер departure вызывает updateCalendar(), который через validationService.validateCode() проверяет код станции.
  6. Обновление календаря — если оба кода валидны, вызывается apiService.getFlightDaysByRoute() и формируется массив disabledDates.

Критерии приёмки:

  • Поле отображает плейсхолдер SHARED.CITY_PLACEHOLDER.
  • Автодополнение возвращает совпадения при вводе не менее 2 символов.
  • Выбранное значение сохраняется в ScheduleFiltersStateService.departure.
  • Невалидный код приводит к ошибке departureError при сабмите.
  • После валидного выбора обновляется список disabledDates в календаре вылета.

Примечание: Компонент city-autocomplete и валидатор StationCodeValidationService общие для всех фич проекта.


US-25: Ввод города прибытия

Цель: Пользователь вводит город или аэропорт прибытия, чтобы задать маршрут поиска.

Путь клиента:

  1. Фокус на поле — пользователь кликает в поле "Город прибытия" (data-testid="schedule-arrival-city-input").
  2. Ввод текста — вводит название или IATA-код города назначения.
  3. Автодополнениеcity-autocomplete подгружает и показывает список подходящих станций.
  4. Выбор значения — выбранное значение записывается в state.arrival через двустороннюю привязку.
  5. Пересчёт календаря — сеттер вызывает updateCalendar(), который проверяет оба кода и обновляет disabledDatesdisabledDatesReturn при активном withReturn).
  6. Проверка "город A ≠ город B" — при сабмите валидация отклоняет одинаковые города с ошибкой SHARED.ARRIVAL-EQUALS-DEPARTURE-ERROR.

Критерии приёмки:

  • Поле использует тот же компонент city-autocomplete, что и отправление.
  • Выбранное значение сохраняется в state.arrival.
  • Одинаковые коды отправления и прибытия блокируют сабмит с ошибкой.
  • После валидного выбора обновляется disabledDates обоих календарей (если withReturn).
  • Невалидный код вызывает arrivalError с сообщением SHARED.ARRIVAL-CITY-ERROR.

US-26: Кнопка "Обменять" (swap)

Цель: Пользователь одним кликом меняет местами города отправления и прибытия.

Путь клиента:

  1. Заполненные поля — пользователь ввёл города отправления и/или прибытия.
  2. Клик по кнопке ⇄ — кликает кнопку button-change между полями городов (SVG changeCity).
  3. Сброс ошибок — метод exchange() обнуляет departureError и arrivalError.
  4. Обмен значений — деструктуризацией [departure, arrival] = [arrival, departure] значения меняются местами.
  5. Каскадное обновление — сеттеры вызывают updateCalendar(), что перерасчитывает disabledDates для нового направления.
  6. Отображение — поля автокомплита отображают поменянные значения.

Критерии приёмки:

  • Клик по кнопке меняет значения state.departure и state.arrival.
  • Ошибки валидации городов сбрасываются.
  • Календарь disabledDates пересчитывается под новое направление.
  • Работает корректно если одно из полей пустое (пустота и значение меняются).
  • Кнопка доступна всегда (не disabled).

US-27: Выбор недели вылета

Цель: Пользователь выбирает неделю, для которой хочет посмотреть расписание рейсов.

Путь клиента:

  1. Клик по календарю — пользователь кликает calendar-input-week с label="SHARED.SCHEDULES_DATE" (data-testid="schedule-calendar").
  2. Показ календаря — открывается read-only календарь с ограничениями minDate=settings.scheduleMinDate, maxDate=maxScheduleDate.
  3. Отображение disabled дат — дни без рейсов подсвечиваются серым согласно disabledDates, рассчитанному из getFlightDaysByRoute.
  4. Выбор недели — пользователь кликает на день; компонент выбирает всю неделю (пн–вс) и сохраняет её в state.dateRange.
  5. Сброс ошибкиngModelChange вызывает resetDateRangeError().
  6. Каскад на обратный рейс — если включён withReturn, сеттер пересчитывает minReturnScheduleDate для календаря возврата.

Критерии приёмки:

  • Календарь работает в режиме выбора недели, а не одного дня.
  • Даты за пределами scheduleMinDate/scheduleMaxDate недоступны.
  • Дни без рейсов отображаются как disabled.
  • Выбранный диапазон сохраняется как [Date, Date] в state.dateRange.
  • Предыдущая ошибка dateRangeError сбрасывается при новом выборе.

Примечание: scheduleMinDate/scheduleMaxDate задаются в AppSettings; maxScheduleDate дополнительно ограничен датой возврата при withReturn.


US-28: Поиск туров (туда и обратно)

Цель: Пользователь включает режим "Обратный рейс" для поиска туров с указанием даты возврата.

Путь клиента:

  1. Клик по чекбоксу — пользователь отмечает чекбокс SHARED.RETURN_FLIGHT_VIEW, что выставляет state.withReturn = true.
  2. Отображение блока возврата*ngIf="withReturn" показывает блок с календарём SHARED.RETURN_FLIGHT_DATE и селектором времени SHARED.RETURN_FLIGHT_TIME.
  3. Обновление календаря возврата — сеттер withReturn вызывает updateCalendarReturn(), который запрашивает getFlightDaysByRoute для обратного направления (arrival → departure).
  4. Выбор недели возврата — пользователь выбирает диапазон в календаре возврата, значение сохраняется в state.returnDateRange.
  5. Сброс при отключении — при снятии чекбокса resetReturnDateRange() очищает returnDateRange в [].
  6. Сабмит с inbound — при клике "Показать" формируется inboundParams с обращённым направлением и передаётся в URL.

Критерии приёмки:

  • Чекбокс не отмечен по умолчанию.
  • Блок возврата появляется только при withReturn === true.
  • disabledDatesReturn рассчитывается для обратного направления.
  • Снятие чекбокса очищает returnDateRange.
  • URL содержит второй сегмент с параметрами inbound при сабмите.

Примечание: minReturnScheduleDate не может быть раньше выбранной даты вылета; maxScheduleDate подстраивается под дату возврата.


US-29: Фильтр "Только прямые рейсы"

Цель: Пользователь ограничивает поиск только прямыми рейсами, исключая стыковки.

Путь клиента:

  1. Клик по чекбоксу — пользователь отмечает SHARED.DIRECT_FLIGHT_ONLY, это выставляет state.directOnly = true.
  2. Пересчёт disabledDates — сеттер вызывает updateCalendar() с аргументом !directOnly === false, ограничивая запрос getFlightDaysByRoute только прямыми рейсами.
  3. Обновление календаря — календарь помечает дни без прямых рейсов как disabled.
  4. Формирование параметров — в getOutboundParams() добавляется connections: 0 (иначе undefined).
  5. Сабмит — пользователь нажимает "Показать", URL получает суффикс -C0.
  6. Отображение результатов — страница результатов показывает только прямые рейсы.

Критерии приёмки:

  • Чекбокс не отмечен по умолчанию.
  • При включении флага параметр connections равен 0.
  • URL маршрута содержит -C0 в сегменте параметров.
  • disabledDates пересчитывается под режим "только прямые".
  • Флаг применяется и к outbound, и к inbound (если withReturn).

US-30: Фильтр по времени вылета

Цель: Пользователь ограничивает поиск рейсами, вылетающими в заданное время суток.

Путь клиента:

  1. Открытие селектора — пользователь кликает time-selector с лейблом SHARED.DEPARTURE_TIME (режим fullView=false).
  2. Выбор диапазона — компонент позволяет выбрать timeFrom и timeTo (например, утро/день/вечер/ночь или вручную).
  3. Сохранение в state — выбранный IUrlTimeRange сохраняется в state.timeRange.
  4. Формирование параметровgetOutboundParams() добавляет timeFrom/timeTo через spread ...this.timeRange.
  5. Построение URLurl-builder.formatRouteParams() добавляет к сегменту суффикс -{timeFrom}{timeTo} при наличии обоих значений.
  6. Сабмит — пользователь нажимает "Показать", фильтр применяется к результатам.

Критерии приёмки:

  • Селектор отображается в компактном режиме (fullView=false).
  • Значение сохраняется как {timeFrom, timeTo} в state.timeRange.
  • Оба значения обязательны для добавления в URL.
  • URL содержит временной суффикс в сегменте параметров outbound.
  • При отсутствии выбора фильтр не применяется (параметры опущены).

US-31: Фильтр по времени прибытия (обратный рейс)

Цель: Пользователь задаёт время вылета для обратного сегмента при поиске туров.

Путь клиента:

  1. Условие отображения — селектор времени возврата виден только при withReturn === true.
  2. Открытие селектора — пользователь кликает time-selector с лейблом SHARED.RETURN_FLIGHT_TIME.
  3. Выбор диапазона — выбирается диапазон timeFrom/timeTo для обратного рейса.
  4. Сохранение — значение записывается в state.returnTimeRange как IUrlTimeRange.
  5. Формирование inboundgetInboundParams() добавляет ...this.returnTimeRange к параметрам обратного направления.
  6. URL — URL получает второй сегмент с собственным временным суффиксом для inbound.

Критерии приёмки:

  • Селектор недоступен, пока не включён withReturn.
  • Значение сохраняется в state.returnTimeRange.
  • Время применяется только к inbound-сегменту, не к outbound.
  • URL содержит отдельные временные суффиксы для каждого направления.
  • Отключение withReturn не влияет на сохранённый timeRange outbound.

Примечание: Название "время прибытия" условно — в Angular это SHARED.RETURN_FLIGHT_TIME, фактически время вылета обратного рейса.


US-32: Валидация параметров поиска

Цель: Система проверяет корректность параметров перед переходом на страницу результатов.

Путь клиента:

  1. Клик "Показать" — пользователь нажимает кнопку SHARED.SCHEDULES_VIEW (data-testid="schedule-search-button").
  2. Сбор параметровviewSchedule() собирает outbound и inbound параметры из state.
  3. Вызов validateScheduleFilterValidationService.validate({inbound, outbound}) очищает предыдущие ошибки и запускает проверку.
  4. Проверка outbound — проверяются: валидность departure, валидность arrival, различие городов, валидность dateFrom/dateTo через moment.
  5. Проверка inbound — при наличии inbound проверяются dateFrom/dateTo.
  6. Навигация или ошибка — при успехе 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, чтобы ссылки можно было шарить и перезагружать страницу без потери состояния.

Путь клиента:

  1. Построение URL — после валидации ScheduleUrlBuilderService.getRoutePageUrl(outbound, inbound?) формирует путь ${base}/route/{outboundSeg}[/{inboundSeg}].
  2. Форматирование сегментаformatRouteParams() собирает строку {departure}-{arrival}-{dateFrom}-{dateTo}[-{timeFrom}{timeTo}][-C{connections}].
  3. Навигацияrouter.navigateByUrl(url), а при успехе SearchHistoryService.add() сохраняет запись типа schedule-route.
  4. Разбор URL — при прямом открытии ссылки резолвер использует url-parser.service для преобразования сегментов в IScheduleRouteParams.
  5. Инициализация формыsetState(routeParams) заполняет state значениями из URL (включая withReturn = true при наличии inbound).
  6. Построение запроса к APIrequest-builder.service превращает IScheduleRouteDirectionParams в HTTP-запрос к бэкенду.

Критерии приёмки:

  • URL содержит сегмент outbound в формате DEP-ARR-dateFrom-dateTo[-timeTimeTo][-C0].
  • При withReturn добавляется второй сегмент с inbound параметрами.
  • Прямое открытие URL восстанавливает все поля формы.
  • Отсутствие inbound сегмента выставляет withReturn = false.
  • История поиска получает запись при успешной навигации.

Примечание: Базовый путь фичи задаётся через токен SCHEDULE_URL_BASE; даты форматируются методом formatDate базового класса UrlBuilderService.