71d0c983fd
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.
260 lines
28 KiB
Markdown
260 lines
28 KiB
Markdown
# Пользовательские истории: Навигация и элементы интерфейса
|
||
|
||
## 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 и необработанных промисах.
|