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

## US-12: Поиск по номеру рейса

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

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

1. **Открытие фильтра** — пользователь открывает аккордеон «Номер рейса» в панели фильтров онлайн-табло; активная вкладка выделяется цветом.
2. **Ввод номера** — в составном поле слева зафиксирован префикс «SU», справа — текстовое поле с плейсхолдером (пример «1234»), максимальная длина 5 символов.
3. **Валидация формата** — сервис проверяет регулярное выражение `^\d\d\d\d?[A-Za-z]?$`: 3–4 цифры и опциональный буквенный суффикс; при ошибке показывается кастомный компонент `<tooltip>` с локализованным сообщением (не PrimeNG `pTooltip`, а отдельный всплывающий блок ошибки).
4. **Выбор даты** — ниже расположен `calendar-input` с меткой «Дата вылета»; диапазон ограничен `boardMinDate`/`boardMaxDate`, недоступные даты заблокированы.
5. **Автодополнение номера** — при потере фокуса метод `addZeros()` дополняет номер нулями до 4 цифр и отделяет буквенный суффикс (например, ввод `14` → `0014`, `1402A` → `1402` + суффикс `A`).
6. **Запуск поиска** — кнопка «Найти» активна; при успешной валидации вызывается `handleFlightNumberSearch`, происходит переход на страницу результатов.
7. **Очистка поля** — кнопка-крестик справа от ввода очищает номер одним кликом.

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

- ✅ Префикс «SU» всегда отображается и не редактируется.
- ✅ Валидация отклоняет буквы в цифровой части и номера длиннее 5 символов.
- ✅ Пустой номер показывает ошибку `FLIGHT_NUMBER-ERROR-EMPTY`.
- ✅ Некорректный формат показывает ошибку `FLIGHT_NUMBER-ERROR-ONLY-NUMBER`.
- ✅ Поиск не запускается, если не выбрана валидная дата.
- ✅ При потере фокуса `addZeros()` дополняет номер до 4 цифр и выделяет буквенный суффикс.
- ✅ Кнопка очистки сбрасывает значение номера рейса.

---

## US-13: Выбор даты для поиска в онлайн-табло

**Цель:** Пользователь хочет указать дату вылета в пределах разрешённого окна онлайн-табло.

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

1. **Открытие календаря** — пользователь кликает по `calendar-input` с меткой «Дата вылета»; открывается поповер с месячной сеткой.
2. **Ограничение диапазона** — календарь принимает `minDate = boardMinDate` (вчера, −1 день) и `maxDate = boardMaxDate` (по умолчанию +7 дней от сегодня, настраивается через серверный контракт `boardSearchTo`).
3. **Блокировка недоступных дат** — массив `disabledDates` затемняет даты, по которым нет расписания, клик по ним игнорируется.
4. **Быстрые кнопки** — под календарной сеткой отображаются кнопки «Сегодня» и «Завтра» (локализованные), позволяющие выбрать дату одним кликом без навигации по месяцам.
5. **Выбор даты** — при клике на активную дату значение записывается в модель фильтра, поповер закрывается.
6. **Автообновление диапазона в полночь** — таймер отслеживает смену суток и автоматически сдвигает `boardMinDate`/`boardMaxDate` без перезагрузки страницы.
7. **Отображение ошибки** — некорректное значение (пустое или невалидное) показывает сообщение `SHARED.DATE_FORMAT-WRONG` под полем.
6. **Синхронизация с результатами** — выбранная дата передаётся в страницу поиска и синхронизируется с компонентом `day-tabs`.

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

- ✅ Недоступны даты раньше `boardMinDate` и позже `boardMaxDate`.
- ✅ Даты из `disabledDates` визуально выключены и не кликабельны.
- ✅ Невалидная дата вызывает показ сообщения об ошибке.
- ✅ Быстрые кнопки «Сегодня»/«Завтра» позволяют выбрать дату одним кликом.
- ✅ При смене суток min/max даты автоматически сдвигаются через таймер.
- ✅ Выбранная дата сохраняется в модели и передаётся в URL при поиске.
- ✅ Календарь закрывается после выбора даты.

---

## US-14: Поиск по городу отправления

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

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

1. **Открытие аккордеона «Маршрут»** — раскрывается вкладка с формой маршрутного поиска внутри `online-board-filter`.
2. **Ввод города отправления** — компонент `city-autocomplete` с меткой «Город отправления» принимает ввод и показывает выпадающий список подсказок. Поддерживается конвертация раскладки клавиатуры (`ruKeyboardLayout`): пользователь может набирать латиницей при включённой русской раскладке, и наоборот.
3. **Выбор из подсказок** — по мере набора сервис `cities-search-service` возвращает совпадения; клик по элементу подставляет город в поле. В выпадающем списке `city-select`, если у города несколько аэропортов, каждый аэропорт сопровождается `pTooltip` с полным названием аэропорта (`airport.name`, позиция `top`).
4. **Оставление поля прибытия пустым** — для поиска только по отправлению пользователь не заполняет второе поле.
5. **Выбор даты и запуск поиска** — указывается дата в `calendar-input`, клик по «Найти» переходит на страницу результатов вылетов.
6. **Обработка ошибки** — если город не выбран из списка, показывается ошибка `departureError` под полем.

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

- ✅ Автодополнение работает при вводе минимум нескольких символов.
- ✅ Поддерживается конвертация раскладки клавиатуры (русская ↔ латиница).
- ✅ При точном совпадении ввода с кодом или названием города происходит авто-выбор.
- ✅ Принимается только город, выбранный из списка подсказок.
- ✅ При наличии нескольких аэропортов у города каждый показывает `pTooltip` с полным названием.
- ✅ Ошибка отображается, если город введён, но не распознан.
- ✅ Поиск возможен только с заполненным полем отправления и валидной датой.
- ✅ Результаты содержат только рейсы с вылетом из указанного города.

---

## US-15: Поиск полётов с прибытием в конкретный город

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

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

1. **Открытие маршрутного фильтра** — раскрывается вкладка «Маршрут» в `online-board-filter`.
2. **Ввод города прибытия** — во втором поле `city-autocomplete` с меткой «Город прибытия» пользователь вводит название.
3. **Выбор из автодополнения** — подсказки формируются сервисом, клик подставляет валидный город в модель.
4. **Пустое поле отправления** — поле отправления остаётся пустым для поиска по направлению прибытия.
5. **Ввод даты и запуск** — выбирается дата, клик «Найти» инициирует поиск типа прибытия.
6. **Переход на страницу прибытий** — результаты отображаются как список рейсов, прибывающих в город.

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

- ✅ Автодополнение выдаёт подсказки для города прибытия.
- ✅ При невалидном вводе показывается ошибка `arrivalError`.
- ✅ Поиск с заполненным только полем прибытия корректно запускается.
- ✅ Результаты ограничены рейсами с прибытием в указанный город.
- ✅ Выбранная дата передаётся в запрос.

---

## US-16: Поиск маршрута между двумя городами

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

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

1. **Заполнение отправления** — в `city-autocomplete` с меткой «Город отправления» выбирается город из подсказок.
2. **Заполнение прибытия** — во втором поле аналогично выбирается город назначения.
3. **Кнопка обмена** — между полями расположена кнопка со SVG-иконкой `changeCity`; клик вызывает метод `exchange()` и меняет значения полей местами.
4. **Выбор даты** — используется `calendar-input` с ограничениями `boardMinDate`/`boardMaxDate`.
5. **Опциональный фильтр времени** — под датой компонент `time-selector` в режиме `fullView=false` позволяет ограничить диапазон времени вылета.
6. **Автозаполнение по геолокации** — если `UserLocationService` определил местоположение пользователя и поле отправления пусто, оно предзаполняется кодом ближайшей станции.
7. **Запуск поиска** — кнопка «Найти» вызывает `handleRouteSearch`, результаты открываются на маршрутной странице.

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

- ✅ Оба города должны быть выбраны из автодополнения.
- ✅ Кнопка обмена меняет значения полей отправления и прибытия местами.
- ✅ Ошибки для полей отправления/прибытия отображаются независимо.
- ✅ Результаты содержат только рейсы между выбранной парой городов.
- ✅ Значение `timeRange` передаётся в запрос вместе с датой.

---

## US-17: Переключение дня вылета в результатах

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

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

1. **Отображение дневных вкладок** — над списком результатов расположен компонент `day-tabs` с заголовком «Дата вылета».
2. **Генерация диапазона** — вкладки формируются между `boardMinDate` и `boardMaxDate` (до 16 дней), недоступные даты из `disabledDates` выключены.
3. **Выделение текущего дня** — выбранная дата (`searchDate`) визуально подсвечена как активная.
4. **Клик по другому дню** — событие `tabClick` вызывает `handleDateChanged($event)`, который перезапускает поиск для новой даты.
5. **Синхронизация с фильтром** — новая дата попадает в URL, значение в `calendar-input` фильтра обновляется одновременно.

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

- ✅ Вкладки отображаются только для дат в диапазоне `boardMinDate`..`boardMaxDate`.
- ✅ Даты из `disabledDates` отрисованы как неактивные и не кликабельны.
- ✅ Активная вкладка соответствует текущему `searchDate`.
- ✅ Клик по вкладке запускает новый поиск без полного перезагруза страницы.
- ✅ URL и состояние фильтра остаются согласованными с активной датой.

---

## US-18: Фильтр по времени вылета или прибытия

**Цель:** Пользователь хочет ограничить результаты рейсами в заданном временном окне.

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

1. **Проверка флага** — компонент `time-selector` в блоке результатов отображается только при включённом фичефлаге `RESULTS_TIME_SELECTOR`.
2. **Выбор метки** — метка динамически переключается между «Время вылета» и «Время прибытия» в зависимости от `requestType`.
3. **Режим полного отображения** — `fullView=true`: слайдер с двумя ползунками и подписями начала/конца диапазона.
4. **Перетаскивание ползунков** — пользователь задаёт `timeFrom` и `timeTo`, значение пишется в модель `timeRange`.
5. **Завершение жеста** — событие `slideComplete` вызывает `handleTimeChanged()`, список рейсов перезапрашивается с новым диапазоном.
6. **Блокировка для «рейса по номеру»** — при `requestType === Flight` селектор визуально отключён.

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

- ✅ Селектор скрыт, если фичефлаг `RESULTS_TIME_SELECTOR` выключен.
- ✅ Метка соответствует типу запроса (вылет/прибытие).
- ✅ Перетаскивание ползунков обновляет модель `timeRange`.
- ✅ Новый запрос отправляется только после отпускания ползунка (`slideComplete`).
- ✅ Для поиска по номеру рейса селектор неактивен.

---

## US-19: Расширенный просмотр рейса в результатах

**Цель:** Пользователь хочет увидеть детальную информацию по конкретному рейсу прямо в списке результатов.

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

1. **Просмотр списка** — компонент `board-search-result` отображает список рейсов, каждый элемент кликабелен.
2. **Выделение текущего рейса** — ближайший к текущему времени рейс автоматически определяется утилитой `find-closest-flight` и помечается как `currentFlight`.
3. **Клик по рейсу** — событие `changeCurrentFlight` обновляет активный рейс в списке.
4. **Переход к деталям** — событие `toDetails` вызывает `handleToDetailsEvent($event)`, который навигирует на страницу `flight-details`.
5. **Сохранение контекста** — URL содержит параметры поиска, чтобы при возврате список сохранил состояние.

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

- ✅ Ближайший по времени рейс помечен как текущий при первом рендере.
- ✅ Клик по строке рейса меняет выделенный `currentFlight`.
- ✅ Переход к деталям сохраняет параметры исходного поиска.
- ✅ При возврате назад список результатов остаётся на той же позиции.

**Примечание:** В Angular-коде детальный просмотр реализован как отдельная страница `flight-details`, а не как инлайн-расширение строки.

---

## US-20: Обработка пустых результатов поиска

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

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

1. **Завершение поиска** — компонент `online-board-search` получает пустой массив `flights` и скрывает список результатов.
2. **Показ заглушки** — вместо списка отображается `page-empty-list` со скроллом к заглушке (`scrollTo=true`).
3. **Особый случай для поиска по номеру** — если `requestType === Flight` и сервис вернул партнёров, вместо `page-empty-list` показывается `partners-redirect-note` со списком партнёров.
4. **Сохранение фильтров** — панель фильтра и дневные вкладки остаются видимыми, чтобы пользователь мог изменить критерии без перезагрузки.
5. **Повторный поиск** — изменение даты, времени или критерия автоматически инициирует новый запрос.

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

- ✅ При пустом списке рейсов и не-Flight запросе отображается `page-empty-list`.
- ✅ Для поиска по номеру рейса с партнёрами отображается `partners-redirect-note`.
- ✅ Панель фильтров и `day-tabs` остаются доступными для повторного поиска.
- ✅ Заглушка прокручивается в зону видимости пользователя.
- ✅ Изменение критерия сразу запускает новый запрос.

---

## US-22: Индикатор загрузки при поиске

**Цель:** Пользователь должен видеть чёткий индикатор процесса загрузки и иметь возможность отменить его.

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

1. **Запуск запроса** — при инициировании поиска `dataSource.loading` устанавливается в `true`.
2. **Блокировка фильтра и заголовка** — компонент `spin-lock` накладывается на `header-left` и `content-left`, блокируя взаимодействие с фильтром и вкладками.
3. **Полноэкранный лоадер** — в основной области показывается `page-loader` с кнопкой отмены запроса.
4. **Отмена поиска** — клик по кнопке отмены в `page-loader` вызывает `cancel()` → `handleRequestCancellation()`, прерывая запрос.
5. **Завершение** — при получении ответа `loading` становится `false`, лоадеры исчезают, отображается список рейсов либо пустая заглушка.

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

- ✅ Во время запроса отображаются `spin-lock` над фильтром и `page-loader` в контенте.
- ✅ Кнопка отмены в `page-loader` прерывает активный запрос.
- ✅ После отмены пользователь остаётся на странице с предыдущим результатом или пустым состоянием.
- ✅ После завершения загрузки все индикаторы скрываются.
- ✅ Повторный поиск корректно перезапускает цикл загрузки.

---
