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

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

---

## 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()`, который проверяет оба кода и обновляет `disabledDates` (и `disabledDatesReturn` при активном `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. **Построение URL** — `url-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. **Формирование inbound** — `getInboundParams()` добавляет `...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. **Вызов validate** — `ScheduleFilterValidationService.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. **Построение запроса к 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`.

---
