# Outer-customer saved directions (carrier)

<!-- wb-memory.v1
{
  "schemaVersion": "wb-memory.v1",
  "id": "screen.tender-outer-customer-saved-directions",
  "kind": "screen",
  "title": "Outer-customer saved directions (carrier)",
  "lifecycle": "active",
  "authority": "canonical",
  "areaId": "area.tenders",
  "routes": ["/tenders/outer-customer"],
  "links": [
    {
      "type": "related-to",
      "targetId": "screen.tender-outer-customer",
      "sourceRefIds": ["S1"]
    },
    {
      "type": "uses-component",
      "targetId": "component.empty-state",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.tag",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.switch",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.button",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.button-flat",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.divider",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.tooltip",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.input-search",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.chip",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.badge",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.popup",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.toast",
      "sourceRefIds": ["S2"]
    },
    {
      "type": "uses-component",
      "targetId": "component.map-pin",
      "sourceRefIds": ["S3"]
    },
    {
      "type": "uses-component",
      "targetId": "component.map-zoom",
      "sourceRefIds": ["S3"]
    },
    {
      "type": "uses-component",
      "targetId": "component.map-control-button",
      "sourceRefIds": ["S3"]
    }
  ],
  "sourceRefs": [
    {
      "id": "S1",
      "kind": "spec",
      "authority": "canonical",
      "path": "specs/ux/screens/tender-outer-customer.md",
      "status": "verified"
    },
    {
      "id": "S2",
      "kind": "code",
      "authority": "implementation",
      "path": "src/app/features/outer-customer-tenders/saved-directions/saved-directions.component.ts",
      "status": "verified"
    },
    {
      "id": "S3",
      "kind": "code",
      "authority": "implementation",
      "path": "src/app/features/outer-customer-tenders/saved-directions/russia-map/russia-map.component.ts",
      "status": "verified"
    }
  ]
}
-->

Tasks: A-006 — https://github.com/logisticsdesign/logistics-2/issues/34;
A-008 — https://github.com/logisticsdesign/logistics-2/issues/48 (states and
edge-case alerts) · member `anastasia-ermoshina` · agent: Claude Code.

## Readiness

| Dimension | Status and evidence |
| --- | --- |
| Specification | This document. Carrier only; the logist has no such tab. |
| Figma | File `jwmCEMZZjn9cieKYqxI6FL` (read-only): empty `18061:19217`, new direction `18066:41988`, departure focus `18066:44818`, search `18066:45830`, departure chosen `18066:47303`, arrival tags `18066:48168`, several arrivals `18066:49012`, saved `18726:16935`, list `18066:49516`, map hover `18158:15162`, map Specs sheet `18288:41309`, zoom to a direction `18232:51490`, region card with directions `18227:50538`, grouped pins `18885:20228`, cities `18890:20851`; A-008: delete `18731:18448`, `18731:20286`, `18731:20487`, undo toast `18066:51347`, «Любой» and map hover `18232:52161`, edge-case alerts `18729:17718`, `19083:61352`. The layouts show the «Магистраль» section; only the tab content is taken from them. |
| Local prototype | Tab «Сохранённые направления» of the carrier on `/tenders/outer-customer`, static fixtures. [S2] [S3] |
| Verification | Focused unit tests pass (models, store, point field, tab, map centres; screen spec). Browser check at 1440 px: empty state, search with groups, keyboard selection, map picking, tags, save and toast, list with selection and arc, filters, reverse direction, region popups, zoom. No `verify:local` run yet. |

## Purpose

The carrier subscribes to directions (departure → arrival) to get notified
about matching tenders. Saved directions exist for every tender type; this
prototype builds them on the outer-customer section as the first example.
Directions are created from the tender list (bookmark on a card, not wired
yet) or here — by typing or by clicking regions on the map.

## Source and Route

- Route `/tenders/outer-customer`, role «Перевозчик», tab «Сохранённые
  направления» (`screen.tender-outer-customer`). The tab has no route of its
  own; switching tabs keeps the saved directions.

## Layout

- Under the tabs, 16 px down: one bordered box (radius 16 px) filling the
  rest of the height. Left — a 429 px panel with a right border; right — the
  map on `--color-background-dropdown-active`.
- The tab «Сохранённые направления» carries a counter badge with the number of
  saved directions (0 in the empty state).

## Primary Scenario

1. **Empty state (18061:19217).** «0 из 50 направлений», a grey card with
   `wbl-empty-state`: «Направлений пока нет», «Подпишитесь на направление,
   чтобы вам приходили уведомления о подходящих тендерах», action «Добавить
   направление». Below — «Предложенные направления / По вашим тендерам за
   последние 90 дней» with four suggestion tags «Москва → Омская обл.»,
   «Омская обл. → Любой», «Новосибирская обл. → Москва», «Санкт-Петербург →
   Любой»; «+» on a tag saves that direction at once.
2. **New direction (18066:41988).** «‹ Новое направление», «Введите точки
   вручную или выберите с помощью карты», the departure field «Город или
   регион отправления» with a «Любой» switch, a divider with the swap button,
   the arrival field «Город или регион прибытия» with its own «Любой», a
   divider, the switch «Получать уведомления о тендерах» (on) and «Сохранить»
   (size M, disabled while nothing would be created).
3. **Search (18066:45830).** Typing opens suggestions in two groups «Город» and
   «Регионы», up to five each; a place matches when a word in its name starts
   with the query, names starting with it come first, the matched part is
   medium weight. Arrow keys move, Enter chooses, Escape restores the value.
4. **Departure is one place, arrival is several (18066:47303, 18066:48168,
   18066:49012).** The chosen departure shows as «Отправление / value»;
   arrivals show as removable tags with «Добавить ещё»; Backspace in an empty
   arrival input removes the last tag. «Будет создано направлений: N» appears
   above «Сохранить» — one direction per departure × arrival pair not yet
   saved. «Любой» turns its field grey with a «Любой» tag; «Любой → Любой» is
   never created.
5. **Swap.** Exchanges departure and arrival (including «Любой»). It is
   disabled while there are several arrivals, with the tooltip «Поменять
   местами можно, когда прибытие одно».
6. **Save (18726:16935).** New pairs go on top of the list with the chosen
   notification setting; toast «Добавлено N направлений» (declined by number);
   the form stays open and empty for the next direction; the map keeps the
   saved regions filled. «‹» returns to the list.
7. **My directions (18066:49516).** «Мои направления» with «Добавить»,
   «N из 50 направлений», full-width search «Поиск» (by either place or the whole label),
   sort icon and chips «Все · С тендерами · С уведомлениями». «С тендерами»
   keeps directions with more than 0 tenders, «С уведомлениями» — those with
   notifications on; both can be on at once and combine with «И»; «Все» is on
   when no chip is and turns the others off. The map shows only what the list
   shows (chips and search): fills, pins, cities. Each row: tender
   count badge (accent when > 0, grey at 0), «Откуда → Куда» with short names
   («обл.», «Р.»), and 16 px icon buttons in `--color-text-icon-primary-gray`,
   12 px apart, with tooltips — «Создать обратное направление»
   (disabled with «Обратное направление уже есть» when it exists), the bell
   and «Удалить».
   - **Notifications.** A crossed bell means notifications are off: tooltip
     «Включить уведомления», a click turns them on, the icon becomes the filled
     bell and a toast «Уведомления включены» shows for 3 s. A filled bell means
     on: tooltip «Выключить уведомления», toast «Уведомления выключены».
   - **Delete (18731:18448 → 18731:20286 → 18731:20487).** «Удалить» opens
     `wbl-modal` S «Удалить направление?» with the direction and «Не удалять» /
     «Удалить» (danger). After deleting — toast «Направление удалено» with
     «Вернуть» (18066:51347, 5 s); «Вернуть» puts the direction back in its
     place in the list.
   - Hovering a region or a city on the map outlines the rows it takes part in
     (18232:52161). Hover on a row
   shows the direction on the map. A click selects the row (accent background
   and a 4 px accent bar on the left) and zooms the map as close as both
   regions still fit (18232:51490); a second click on the selected row clears
   the selection and returns the map to the default view (zoom ×1, nothing
   selected).

## Map (Specs 18288:41309, 18158:15162)

- Only the outline of Russia by region, no base map: 83 regions from the SVG
  «russia 1» exported from Figma (`public/assets/maps/russia-regions.svg`),
  each path keyed by its ISO code (`RU-KYA`, `RU-NVS` …). Moscow and Saint
  Petersburg are map regions but cities in search.
- **Every region reacts to hover:** a dark outline on top of its neighbours and
  a tooltip next to the cursor:
  - list or empty state, region without directions — «Регион / Нажмите, чтобы
    добавить направление»; region with directions — «Регион / Участвует в N
    направлениях. Нажмите» and a stronger fill with an accent outline;
  - form with a focused departure — «Регион / Выбрать как отправление», with an
    arrival target — «Регион / Выбрать как прибытие», crosshair cursor.
- **Click:** in the form with a target, the region goes straight into that
  field (after the departure the target moves to the arrival); otherwise a
  `wbl-popup` opens at the click: for a region without directions — «Выбрать
  как отправление» / «Выбрать как прибытие» (opens the form with it); for a
  region with directions — «N направлений», the list with tender badges (a
  click opens that direction: selects the row and zooms to it, 18227:50538)
  and «Добавить направление» (form with this region as departure). The popup closes on its cross, Escape or a click on empty map.
- **States:** saved regions — light accent fill; the shown direction — stronger
  fill and accent outline plus an arc between the two regions; regions chosen
  in the form — accent outline; cities chosen in the form — a 6 px accent dot
  in their region.
- **Pins (list only):** `wbl-map-pin` S in the centre of every region with
  directions — «✓» for one, the number for several. A region whose only
  direction goes to «Любой» shows an arrow out of it (`arrow-upright`); one
  whose only direction comes from «Любой» — an arrow into it
  (`arrow-downleft`, 18232:52161). Pins closer than 22 px on
  screen merge into a group pin with the number of regions; a click on the
  group zooms as close as those regions fit (18885:20228).
- Zoom `+`/`−` in whole steps ×1 … ×8 and drag to pan when zoomed; pins, dots
  and line widths keep their size. «ⓘ» is visual only.
- **Cities («Показать города» / «Скрыть города», 18890:20851).** Cities appear
  by population, not all at once: from ×2.5 — the 16 cities over a million;
  from ×3.5 — plus 24 cities over 500 000; from ×4.5 — the other regional
  centres (86 in total). A caption under the zoom says what is shown now and
  what comes next. A city in a saved direction (in the form — a chosen one) is
  a filled dot and is shown even with cities off; the rest are outlined dots.
  Every dot has its name; labels never overlap — on a conflict the label of
  the smaller city is hidden (the dot stays). A click on a city opens the same
  card as a region; the type is written in the same line as the name — «Г.
  Тула» — without a separate line. A city with listed directions shows them
  like a region; otherwise «Выбрать как отправление / прибытие». In the form
  with a target field the city goes straight into it. The city tooltip is
  «Г. Тула» plus the same action line as for a region.
- City points are fixtures: latitude/longitude projected onto the Figma SVG
  with a projection fitted to the region centres (mean error about one SVG
  unit); four cities near a region edge are pulled inside their region.
- The direction arc ends in a city point for a city and in the region centre
  for a region.

## States, Errors, and Edge Cases

- **Form alerts (A-008, 18729:17718).** `wbl-alert` S above «Будет создано
  направлений», danger first, then warning, then info. With any warning the
  button reads «Всё равно сохранить» and the summary adds «из N» (19083:61352).
  - warning «N направления уже покрыты правилом «X → (Любой)»» / «Уведомления
    могут дублироваться» / «Удалить дубликаты» — removes those arrivals; their
    tags carry a warning icon;
  - warning «Правило «X → (Любой)» шире N сохранённых направлений из …» /
    «Они станут лишними» / «Удалить лишние сохранённые ранее» — deletes them;
  - danger «Направление «A → B» уже сохранено» / «Показать в списке» — opens
    the list with that row selected;
  - info «Будет создано N направлений» / «… уже сохранены и будут пропущены»;
  - info «Отправление и прибытие совпадают» / «Будут приходить уведомления о
    перевозках внутри …»;
  - info «Обратное направление «B → A» уже сохранено» / «Кругорейс будет закрыт
    полностью»;
  - warning «Тула входит в Тульскую область, а направление «…» уже сохранено»;
  - info «Осталось N направлений из 50» (5 or fewer);
  - danger «Сохранено 50 из 50 направлений» / «К списку направлений»;
  - warning «Выбрано N прибытий, свободно M слота» / «Оставить первые M»;
  - info «За последние 90 дней по направлению … не было тендеров» (one or
    several, by the tender fixture);
  - warning «Список городов и регионов не загрузился» / «Выбор с карты пока
    недоступен» / «Повторить» — reloads the map.

- Implemented: empty, form (empty, filled, «Любой»), list (all / filtered /
  «Ничего не найдено»), map hover, popups, selection, toast.
- The limit is 50: «Добавить» and the popup action are disabled at 50; a save
  creates at most the remaining number of pairs (see the limit alerts).
- Suggestions are shown only in the empty state.
- Not designed: loading, errors, an empty search in the fields (the layout
  18066:44818 shows the placeholder «Показываем тут примеры?»; the prototype
  opens suggestions only after typing).

## Data and Integration Boundary

- Directions live in `SavedDirectionsStore`, provided by the outer-customer
  screen: they survive tab and role switches while the screen is open and are
  lost on reload. No API, persistence, permissions or notifications.
- Region names, the 30 search cities, suggestions and tender counts are
  fixtures in `saved-directions.models.ts`; tender counts for the layout's
  directions repeat 18066:49516 (12, 9, 9, 4, 0), others are a stable pseudo
  random fixture.
- Visual only: sort, «ⓘ».

## Components and Accessibility

- DS: `wbl-empty-state` (standard composition), `wbl-tag`, `wbl-switch`,
  `wbl-button`, `wbl-button-flat`, `wbl-divider`, `wbl-tooltip`,
  `wbl-input-search`, `wbl-chip`, `wbl-badge`, `wbl-popup`, `wbl-toast`,
  `wbl-map-pin`, `wbl-map-zoom`, `wbl-map-control-button`.
- Feature code (not in the design system yet):
  - `app-direction-point-field` — a combobox (`role="combobox"`,
    `aria-expanded`, `aria-activedescendant`) with a grouped listbox. The DS
    `wbl-multiselect` is a checkbox list without typing groups, so it does not
    cover this field.
  - `app-russia-map` — the SVG map. MapLibre `wbl-map` is not used: the layout
    asks for region outlines only. The cursor tooltip is drawn by the map
    (tooltip tokens), because `wbl-tooltip` anchors to an element with a delay.
  - New icon `icons/arrows-swap.svg` from the layout's row action.
- Keyboard path: all actions are reachable from the panel — fields, switches,
  buttons, row toggle (`aria-pressed`) and row actions with accessible names.
  The map is a pointer shortcut; its regions are not in the tab order. Popup
  buttons are regular buttons; Escape closes the popup.
- Each «Любой» switch sits in a group named «Отправление» / «Прибытие».
- Motion is off with `prefers-reduced-motion`.

## Related Flows

- `screen.tender-outer-customer` — the tab lives on this screen (carrier).
- Proposed: saving a direction from a lot card bookmark in the tender list.

## Open Questions

- `wbl-empty-state` shows a grey title and a secondary «+» action; the layout
  shows a dark title and a primary button. Kept the DS composition — the DS
  owner decides whether the empty state needs this variant.
- What the empty departure field should suggest before typing
  («Показываем тут примеры?» in 18066:44818).
- Meaning of the «✓» pin: the prototype uses it for a region in exactly one
  direction (Specs sheet); confirm.
- «Вся Россия» (a «Любой → Любой» rule) from 18729:17718 is not created: the
  form never saves «Любой → Любой»; its alert is not implemented.
- The city tooltip in 18232:52161 reads «Направление: 1. Нажмите»; the
  prototype keeps the region wording «Участвует в N направлениях. Нажмите».
- The undo toast stays 5 s (other toasts 3 s) — confirm.
- What «ⓘ» and sort do.
- The cities layout 18890:20851 shows millionaire cities at the default view;
  the prototype follows the written thresholds (×2.5 / ×3.5 / ×4.5).
- The chip «Регионы × 1» next to the zoom in 18227:50538 is not explained —
  not implemented.
- Real coordinates of cities (the prototype uses a fitted projection).
- Region with several departures (the field allows one departure; several
  departures would need its own layout).
