Files
flights_web/docs/user-stories-1-navigation-ui.md
T
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

260 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Пользовательские истории: Навигация и элементы интерфейса
## 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 и необработанных промисах.