# Пользовательские истории: Навигация и элементы интерфейса

## US-1: Навигация по основным вкладкам

**Цель:** Пользователь может переключаться между основными разделами приложения (Онлайн-Табло, Расписание, Карта полётов) для поиска информации о рейсах в разных режимах.

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

1. **Открытие приложения** — пользователь заходит на сайт, срабатывает редирект с корневого пути на `/onlineboard`, в верхней части страницы отображается компонент `page-tabs` с двумя вкладками: «Онлайн-Табло» и «Расписание».
2. **Отображение третьей вкладки** — если включён feature flag `flightsMap`, дополнительно появляется вкладка «Карта полётов» во втором ряду; без флага она не рендерится.
3. **Индикация активной вкладки** — текущая вкладка помечена CSS-классом `active`; при наведении показывается tooltip с текстом из ключей `SHARED.TAB-BOARD-TOOLTIP`, `SHARED.TAB-SCHEDULE-TOOLTIP`, `SHARED.TAB-FLIGHTS-MAP-TOOLTIP`.
4. **Клик по вкладке «Расписание»** — Angular Router выполняет переход на `/schedule`, URL обновляется, загружается `ScheduleModule`, активный класс переходит на новую вкладку.
5. **Клик по вкладке «Карта полётов»** — роутер через `FeatureFlagGuard` проверяет флаг и, если он включён, загружает `FlightsMapModule` по пути `/flights-map`.
6. **Возврат на Онлайн-Табло** — пользователь кликает первую вкладку, URL меняется на `/onlineboard`, загружается `OnlineBoardModule`, активная вкладка снова подсвечивается.

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

- ✅ На старте пользователь попадает на `/onlineboard` (редирект с `/`).
- ✅ Видны ровно две вкладки, если `flightsMap` выключен, и три — если включён.
- ✅ Клик по вкладке меняет URL на `/onlineboard`, `/schedule` или `/flights-map` без перезагрузки страницы.
- ✅ Активная вкладка визуально отличается от неактивных (класс `active`).
- ✅ При наведении появляется tooltip с локализованным текстом.
- ✅ Переход между вкладками не вызывает ошибок в консоли.

**Примечание:** Видимость вкладки «Карта полётов» управляется feature flag `flightsMap` (`FeatureFlagGuard` + `*ngIf="flightsMapEnabled"` в `page-tabs.component.html`).

---

## US-2: Переключение языка интерфейса

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

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

1. **Определение языка** — `LocalizationService` читает `APP_BASE_HREF` формата `/country-language` (например, `/ru-ru`) и извлекает код языка из позиций 4-6.
2. **Инициализация переводов** — сервис вызывает `translate.addLangs(...)` с полным списком (`ru`, `en`, `es`, `fr`, `it`, `jp`, `ko`, `zh`, `de`) и применяет `translate.use(language)`.
3. **Рендеринг интерфейса** — все строки UI проходят через пайп `| translate`, подставляются значения из соответствующих JSON-файлов переводов.
4. **Выбор календарной локали** — геттер `calendarTranslate` возвращает объект локализации PrimeNG Calendar (`calendarRu`, `calendarEn`, ...) согласно текущему языку.
5. **Смена языка** — пользователь переходит по URL с другим префиксом (например, `/en-us/onlineboard`); приложение перезагружается с новым `baseHref`, и весь интерфейс отображается на выбранном языке.
6. **Редиректы с коротких кодов** — запросы на `/ru`, `/en`, `/es`, `/fr`, `/it`, `/ja`, `/ko`, `/zh`, `/de` перенаправляются на `/onlineboard` с сохранением языка из baseHref.

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

- ✅ Поддерживаются все 9 языков из enum `Language`.
- ✅ Текущий язык определяется из `APP_BASE_HREF` при старте приложения.
- ✅ Все тексты, метки кнопок и плейсхолдеры локализованы через `ngx-translate`.
- ✅ Календарь PrimeNG отображается на выбранном языке.
- ✅ Короткие языковые пути в роутинге ведут на `/onlineboard`.

**Примечание:** Переключатель языка находится в шапке внешней площадки aeroflot.ru и не входит в состав Angular-приложения; внутри модуля язык фиксируется через `baseHref`.

---

## US-3: Навигация по хлебным крошкам

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

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

1. **Отображение компонента** — на страницах результатов поиска и деталей рейса рендерится `flights-page-breadcrumbs`, оборачивающий `<p-breadcrumb>` из PrimeNG.
2. **Формирование списка** — в `ngOnInit` компонент подписывается на `route.data` и получает массив `breadcrumbs` из `IRouteData`.
3. **Первый элемент** — в начало списка всегда подставляется `defaultMenuItem` с ярлыком `SHARED.MAIN` и ссылкой на `https://www.aeroflot.ru`.
4. **Локализация** — все элементы прогоняются через `translation.instant(item.label)`, ключи переводятся в текущий язык.
5. **Клик по элементу** — пользователь нажимает на любой уровень, PrimeNG `p-breadcrumb` выполняет навигацию по `url` элемента (внешний URL или внутренний путь).
6. **Отсутствие на главных страницах** — на стартовых страницах разделов и на `/flights-map` маршруты не содержат `breadcrumbs` в `data`, поэтому компонент не отображается.

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

- ✅ Хлебные крошки рендерятся только там, где в `route.data` есть массив `breadcrumbs`.
- ✅ Первый элемент всегда ведёт на `https://www.aeroflot.ru` с ярлыком из ключа `SHARED.MAIN`.
- ✅ Каждый элемент является кликабельной ссылкой.
- ✅ Метки переводятся на текущий язык интерфейса.
- ✅ На `/flights-map` и стартовых экранах разделов компонент отсутствует.

---

## US-4: Кнопка обратной связи

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

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

1. **Отображение кнопки** — в макете страницы присутствует компонент `feedback-button` с текстом из ключа `SHARED.FEEDBACK`.
2. **Клик по кнопке** — обработчик `showForm()` устанавливает `formVisible = true`, что активирует `*ngIf="formVisible"` и монтирует компонент `feedback-form`.
3. **Отображение формы** — на экране появляется блок `.feedback-form` с кнопкой закрытия и встроенным `<iframe>`, загружающим Microsoft Forms по фиксированному URL `https://forms.office.com/Pages/ResponsePage.aspx?...`.
4. **Заполнение формы** — пользователь взаимодействует с формой непосредственно внутри iframe; вся логика валидации и отправки обеспечивается Microsoft Forms.
5. **Закрытие формы** — клик по кнопке закрытия или клик за пределами формы (директива `clickOutside` у `feedback-form`) вызывает `hideForm()`, который снимает `formVisible`.

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

- ✅ Кнопка «Обратная связь» всегда видна на странице с локализованным текстом.
- ✅ Клик по кнопке показывает форму с iframe Microsoft Forms.
- ✅ Форма закрывается по клику вне её области или по кнопке закрытия.
- ✅ iframe загружается с тем же URL, что задан в шаблоне.
- ✅ Повторное открытие формы работает без перезагрузки страницы.

**Примечание:** Форма реализована через внешний iframe Microsoft Forms — собственной логики отправки в приложении нет.

---

## US-5: Кнопка прокрутки к началу страницы

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

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

1. **Монтирование компонента** — `scroll-up-button` рендерит скрытый маркер `#observer-target` и блок кнопки с классами `scroll-up` / `scroll-up--visible`.
2. **Наблюдение за позицией** — в `ngOnInit` создаётся `IntersectionObserver` с `threshold: [1]`, который следит за пересечением `#observer-target` с вьюпортом.
3. **Показ кнопки при скролле** — когда `entry.boundingClientRect.top < 0` (цель ушла выше вьюпорта), `buttonVisible` становится `true` и добавляется модификатор `scroll-up--visible`.
4. **Резервная проверка** — каждые 500 мс вызывается `checkVisibility()`, которая сверяет позицию `.page-layout__header` и показывает кнопку, если шапка ушла выше вьюпорта.
5. **Клик по кнопке** — `handleClick()` берёт элемент `flights-root`, вычисляет его `getBoundingClientRect().top` и вызывает `body.scrollTo(0, body.scrollTop + top)`, после чего скрывает кнопку.
6. **Уничтожение** — в `ngOnDestroy` наблюдатель и интервал очищаются.

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

- ✅ Кнопка не видна, пока шапка страницы находится во вьюпорте.
- ✅ Кнопка появляется, когда пользователь прокрутил страницу ниже шапки.
- ✅ Клик по кнопке возвращает скролл body к началу корня приложения.
- ✅ Класс `scroll-up--visible` корректно добавляется и снимается.
- ✅ Интервал и IntersectionObserver освобождаются при уничтожении компонента.

---

## US-6: Боковая панель с фильтрами

**Цель:** На странице Онлайн-Табло пользователь видит боковую панель с фильтрами поиска и может раскрывать нужные секции.

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

1. **Отображение секции** — компонент `online-board-filter` рендерит `<section class="frame">` с `<p-accordion>` от PrimeNG.
2. **Секция «Номер рейса»** — первый `p-accordionTab` (`data-testid="flight-filter"`) содержит заголовок из ключа `BOARD.FLIGHT_NUMBER`, иконку `arrow-down-icon` и вложенный `online-board-flight-number-filter`.
3. **Секция «Маршрут»** — второй `p-accordionTab` (`data-testid="route-filter"`) содержит заголовок `BOARD.ROUTE`, такую же иконку и `online-board-route-filter`.
4. **Раскрытие секции** — клик по заголовку вызывает событие `onOpen` аккордеона; обработчик `handleOpen($event)` запоминает активную секцию, иконка поворачивается (`rotated`).
5. **Сворачивание** — повторный клик вызывает `onClose` и `handleClose()`, секция сворачивается.
6. **Передача настроек** — во вложенные фильтры передаются `settings.boardMinDate` и `settings.boardMaxDate`, события `onSearch` пробрасываются через `handleFlightNumberSearch` и `handleRouteSearch`.

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

- ✅ В панели ровно две секции: «Номер рейса» и «Маршрут».
- ✅ Заголовки переведены через ключи `BOARD.FLIGHT_NUMBER` и `BOARD.ROUTE`.
- ✅ Секции раскрываются и сворачиваются кликом по заголовку.
- ✅ Активная секция отмечена повёрнутой иконкой `arrow-down-icon`.
- ✅ Каждая секция имеет `data-testid` (`flight-filter`, `route-filter`).
- ✅ При поиске из секции вызываются соответствующие обработчики.

---

## US-7: Информационные разделы на главной странице

**Цель:** На главной странице Онлайн-Табло пользователь видит блок популярных запросов и может одним кликом подставить параметры в форму поиска.

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

1. **Отображение блока** — компонент `popular-requests` рендерит контейнер `.popular-requests` с заголовком из ключа `BOARD.POPULAR-CHAPTERS`.
2. **Рендеринг карточек** — внутри контейнера последовательно выводятся до четырёх элементов `<popular-request>` с `*ngIf="requests[0..3]"` — каждая карточка показывается только при наличии данных.
3. **Содержимое карточки** — конкретный шаблон карточки выбирается компонентом `popular-request` в зависимости от типа запроса (flight-number, arrival, departure, route).
4. **Клик по карточке** — событие `onClick` поднимается в `popular-requests`, обработчик `handleRequestClick($event)` заполняет форму поиска параметрами выбранного запроса.
5. **Скрытие при активном поиске** — блок отображается только на стартовом экране раздела; после выполнения поиска родительский шаблон перестаёт его рендерить.

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

- ✅ Заголовок блока локализован через ключ `BOARD.POPULAR-CHAPTERS`.
- ✅ Максимум 4 карточки, каждая рендерится только при наличии данных.
- ✅ Клик по карточке вызывает `handleRequestClick` с данными запроса.
- ✅ Блок отсутствует на страницах результатов поиска.
- ✅ Карточки загружаются через `PopularRequestsModule` и связанные сервисы.

---

## US-8: История поиска

**Цель:** Пользователь видит свои последние поисковые запросы на главных страницах Онлайн-Табло и Расписания и может быстро повторить любой из них.

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

1. **Отображение секции** — компонент `search-history` рендерит `<section class="frame search-history">`, но только если `historyItems.length > 0`.
2. **Аккордеон** — внутри секции расположен `<p-accordion>` с одним `<p-accordionTab>`, заголовок которого содержит ключ `BOARD.YOU_SEARCH` и `arrow-down-icon`.
3. **Список элементов** — в содержимом таба через `*ngFor` выводятся `<search-history-item>` для каждой записи из `historyItems`.
4. **Иконка с тултипом типа поиска** — каждая запись начинается с иконки: для онлайн-табло (`plane-icon`) с `pTooltip` `SHARED.LAST-SEARCH-BOARD`, для расписания (`alarm-clock-icon`) с `pTooltip` `SHARED.LAST-SEARCH-SCHEDULE` (позиция `top`, стиль `afl-tooltip`).
5. **Клик по элементу** — обработчик `(click)="openHistoryUrl(item)"` выполняет переход по сохранённому URL запроса, восстанавливая параметры формы.
6. **Типы элементов** — поддерживаются разные шаблоны записей: `online-board-flight-number-history-item`, `online-board-route-history-item`, `schedule-item` — каждый отрисовывает свой набор полей.
6. **Отсутствие на карте полётов** — `search-history` не включён в шаблон страницы `/flights-map`, поэтому там секция не показывается.

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

- ✅ Секция видна только при наличии хотя бы одной записи.
- ✅ Заголовок таба переведён через ключ `BOARD.YOU_SEARCH`.
- ✅ Каждая запись — отдельный `<search-history-item>` с собственным шаблоном.
- ✅ Иконка типа записи сопровождается PrimeNG-тултипом (`SHARED.LAST-SEARCH-BOARD` или `SHARED.LAST-SEARCH-SCHEDULE`).
- ✅ Клик по записи вызывает `openHistoryUrl(item)` и выполняет переход.
- ✅ На странице «Карта полётов» секция отсутствует.

---

## US-9: Локализация заголовка страницы и мета-тегов

**Цель:** Заголовок вкладки браузера и мета-теги страницы отображаются на языке, выбранном пользователем.

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

1. **Инициализация локали** — при загрузке модуля раздела `LocalizationService` уже содержит корректный `Language`, извлечённый из `APP_BASE_HREF`.
2. **Установка заголовка** — резолвер маршрута (`SettingsResolver`) подготавливает данные, а компоненты страницы через `Title` и `Meta` сервисы Angular устанавливают `document.title` и мета-теги.
3. **Источник строк** — ключи заголовков и описаний (например, `BOARD.TITLE`, `SCHEDULE.TITLE-TAB`, `FLIGHTS-MAP.TITLE`) берутся из JSON-файлов переводов через `translate.instant`.
4. **Тултип заголовка страницы** — компонент `page-title` дублирует текст заголовка в `pTooltip` (позиция `bottom`, `hideDelay: 1000`, стиль `afl-tooltip tooltip--one-line`), чтобы длинные заголовки, усечённые CSS, были полностью доступны при наведении.
5. **Применение языка** — значения подставляются в `<title>`, `<meta name="description">` и OpenGraph-теги на языке, соответствующем `baseHref`.
5. **Смена языка через URL** — при переходе на префикс другого языка приложение перезагружается, резолвер и компоненты формируют мета-информацию уже на новом языке.

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

- ✅ `document.title` содержит переведённую строку, соответствующую текущему разделу.
- ✅ Мета-теги `description`/`keywords` формируются из ключей перевода.
- ✅ Смена языка через URL-префикс обновляет title и meta.
- ✅ Все три раздела (`onlineboard`, `schedule`, `flights-map`) имеют собственные ключи заголовков.
- ✅ Заголовок `<h1>` сопровождается `pTooltip` (bottom, hideDelay 1000) для доступности длинных усечённых заголовков.
- ✅ При отсутствии перевода подставляется fallback-значение ключа.

---

## US-10: Адаптивный дизайн

**Цель:** Интерфейс корректно отображается и используется на мобильных, планшетных и десктопных разрешениях.

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

1. **Мобильный экран** — при ширине вьюпорта меньше breakpoint'а CSS-стили перестраивают макет: боковая панель фильтров занимает всю ширину, таблица результатов переключается на компактный режим отображения карточек.
2. **Планшет** — на средних ширинах фильтры могут располагаться над списком результатов, шапка страницы и `page-tabs` сохраняют горизонтальное расположение.
3. **Десктоп** — на широких экранах фильтры отображаются в боковой колонке, таблица результатов занимает основную область, включаются tooltip-элементы PrimeNG.
4. **Изменение ширины окна** — пользователь меняет размер окна браузера, CSS media queries пересчитывают стили без перезагрузки страницы, компоненты PrimeNG адаптируются автоматически.
5. **Сохранение функциональности** — все действия (поиск, фильтрация, открытие деталей, обратная связь, прокрутка вверх) остаются доступными на любом устройстве.

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

- ✅ Макет перестраивается при пересечении CSS-breakpoint'ов без перезагрузки.
- ✅ Боковая панель фильтров адаптируется под узкие экраны.
- ✅ Вкладки `page-tabs` остаются кликабельными на мобильных устройствах.
- ✅ Таблицы/списки результатов читаемы на малых ширинах.
- ✅ Все функции приложения доступны на мобильных, планшетных и десктопных разрешениях.

---

## US-11: Отсутствие ошибок в консоли браузера

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

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

1. **Открытие приложения** — пользователь заходит на `/onlineboard`; приложение инициализирует `LocalizationService`, `SettingsResolver` и модули без ошибок.
2. **Навигация по вкладкам** — переходы между `/onlineboard`, `/schedule` и (при включённом флаге) `/flights-map` не вызывают исключений в Router и ленивой загрузке модулей.
3. **Поиск и фильтрация** — заполнение форм, применение фильтров и открытие результатов не приводят к ошибкам HTTP-слоя, интерцепторов или PrimeNG-компонентов.
4. **Открытие деталей рейса** — переход на страницу деталей, обратная связь, прокрутка вверх, раскрытие истории поиска выполняются без предупреждений и ошибок.
5. **Проверка в DevTools** — пользователь открывает вкладку Console, выполняет сценарии и не видит сообщений уровня `error` или `unhandled promise rejection`.

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

- ✅ При начальной загрузке страницы в консоли нет ошибок.
- ✅ Навигация между всеми основными разделами не генерирует ошибок.
- ✅ Поиск, фильтрация и открытие деталей рейса завершаются без исключений.
- ✅ Интерактивные элементы (обратная связь, scroll-up, история) не выбрасывают ошибок.
- ✅ Отсутствуют предупреждения об устаревших API и необработанных промисах.
