Внешние меню — Конструктор
Роут: /external-menus/{id}/edit
Источник
BR 4.1 Бизнес-спека: Внешние меню
Двухпанельный редактор меню. Слева — каталог товаров, справа — структура меню (категории + items). Drag-drop из левой в правую. Клик на item → раскрытие override-формы.
Layout
┌─ Шапка ──────────────────────────────────────────────────────────────────┐
│ ← Назад "Бар — основной экран" ✏ │ 📋 [URL] │ Меню ⚙ Опубл. │
├──────────────────────────────────────────────────────────────────────────┤
│ Каталог товаров (40%) │ Структура меню (60%) │
│ ┌─────────────────────────┐ │ ┌────────────────────────────────────┐ │
│ │ 🔍 [поиск...] │ │ │ ▾ Кофе ⚙ ☰ │ │
│ │ │ │ │ ☰ Капучино 290 ₽ ⚙ │ │
│ │ Фильтр: [Все категории▾]│ │ │ ☰ Латте 310 ₽ ⚙ │ │
│ │ │ │ │ ☰ Эспрессо 250 ₽ ⚙ │ │
│ │ ▾ Кофе │ │ │ ▾ Десерты ⚙ ☰ │ │
│ │ ⚿ Капучино 250 ₽ │ │ │ ☰ Чизкейк 350 ₽ ⚙ │ │
│ │ ⚿ Латте 280 ₽ │ │ │ │ │
│ │ ⚿ Эспрессо 200 ₽ │ │ │ + Добавить категорию │ │
│ │ ▾ Десерты │ │ │ │ │
│ │ • Чизкейк 350 ₽ │ │ │ │ │
│ │ • Тирамису 400 ₽ │ │ │ │ │
│ └─────────────────────────┘ │ └────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
Адаптивность:
- Min-width: 1280px (рекомендуется десктоп)
- На <1280px — отдельный режим «Mobile» в P1; в P0 показываем баннер «Конструктор оптимизирован для широкого экрана»
Шапка
Слева
- Кнопка «← Назад» — возврат на
/external-menus - Имя меню — inline-редактируемое (клик → input → сохранение по Enter / blur)
- Иконка ✏ → раскрывает модалку настроек (см. ниже)
Центр
- Live URL (только для
tv_screenpublished):https://erp-test.nirbi.ru/r/{slug}+ кнопка «📋 Скопировать»
Справа
- Меню ⚙ — раскрывает дроп-меню действий: Дублировать / Скачать ZIP / Скачать JSON / Превью / Удалить
- Кнопка «Опубликовать» (primary) — для
draftсо заполненным меню - Кнопка «Снять с публикации» (outline) — для
published
Модалка настроек меню
Триггер: ✏ рядом с именем
| Поле | Поведение |
|---|---|
| Имя | Inline-edit, валидация уникальности |
| Канал | Read-only (channel менять нельзя) с пометкой «Канал зафиксирован при создании» |
| Шаблон (для tv_screen) | Select: Сетка / Слайдер / Список. Изменение применяется сразу при сохранении |
| Slug (для tv_screen) | Текст-инпут с regex-валидацией. Дубликат → ошибка под полем |
| ТТ | Select: «Вся сеть» / список ТТ scope’а |
Save → PATCH /api/v1/external-menus/{id}
Левая панель — каталог товаров
Что отображается
- Дерево категорий каталога (с возможностью свернуть/раскрыть)
- Внутри категорий — товары
- Иконка перед товаром:
- ⚿ (зелёная) — товар уже в этом меню
- • (серая) — товара нет в меню, можно перетащить
Поиск
- Поле сверху, debounce 300 ms
- Ищет по
nameтоваров каталога - Query к
GET /api/v1/products?search=X&franchise_id=Y
Фильтры
- Select «Категория» — фильтр по
category_id(default: «Все») - Toggle «Скрывать уже добавленные» (default off) — скрывает товары с иконкой ⚿
Drag-drop поведение
- Курсор «grab» при наведении на товар
- При начале drag — товар становится полупрозрачным, появляется призрак рядом с курсором
- Подсветка валидных drop-зон (категории справа): голубая рамка
- Drop в категорию справа →
POST /api/v1/external-menus/{id}/itemsсproduct_id+category_id - При успехе — анимация (item «приземляется»), иконка слева меняется на ⚿
- При ошибке
ITEM_DUPLICATE(409) — toast «Товар уже в меню»
Правая панель — структура меню
Категории
- Каждая категория — раскрывающийся блок (accordion)
- Слева ▾/▸ — раскрытие
- Справа — иконки действий: ⚙ (настройки) / ☰ (drag-handle для переупорядочивания)
Действия с категорией (⚙)
| Действие | Триггер |
|---|---|
| Переименовать | Модалка inline-edit |
| Иконка | Модалка с URL-полем + preview |
| Привязать к каталог-категории | Select из активных категорий каталога (для наследования имени) |
| Отвязать от каталог-категории | Кнопка «Сделать кастомной» |
| Удалить категорию | Модалка подтверждения «Удалить категорию вместе с N товарами?» |
Drag-drop переупорядочивания
- Drag-handle ☰ → перетянуть категорию вверх/вниз
- При drop →
POST /external-menus/{id}/categories/reorderс новым порядком всех категорий
Добавление категории
- Кнопка «+ Добавить категорию» внизу
- Inline-input в самом конце списка
- Enter / blur →
POST /external-menus/{id}/categories
Items внутри категории
- Каждый item — строка с:
- Drag-handle ☰ слева (для reorder в категории и между категориями)
- Имя (с пометкой override курсивом если задан):
Капучино(наследует) или*Капучино с молоком*(override) - Цена (effective_price крупно, рядом серый:
(каталог 250 ₽)если есть override) - Toggle visibility (👁 / 👁🗨)
- Иконка ⚙ → раскрытие override-формы
Состояния item
| Статус | Визуал |
|---|---|
ok + visible=true | Обычный, чёрный текст |
ok + visible=false | Серый, иконка глаз перечёркнут |
ok + товар в стоп-листе ТТ | Жёлтый банер «В стоп-листе ТТ — на мониторе скрыт» |
orphan | Красный банер «Товар удалён в каталоге» + кнопка «Восстановить» (если в каталоге товар вернули) или «Удалить из меню» |
Override-форма (раскрытие по ⚙)
┌─ Капучино — overrides ──────────────────────────────────┐
│ │
│ Имя для канала │
│ [Капучино с молоком ] ← пусто = из каталога
│ Pre-fill placeholder: "Капучино" серым │
│ │
│ Описание │
│ [_____________________________________________] │
│ │
│ Цена │
│ [290.00] ₽ (каталог: 250 ₽, прейскурант: 270 ₽) │
│ Эффективная цена: 290 ₽ │
│ │
│ ☑ Видим на мониторе │
│ │
│ [Очистить overrides] [Сохранить] [Отмена] │
└─────────────────────────────────────────────────────────┘
Behavior:
- Inline-edit полей. Сохранение через debounce 600 ms ИЛИ по кнопке «Сохранить»
- Кнопка «Очистить overrides» обнуляет все override-поля (вернёт каталог-значения) — модалка подтверждения
- Эффективная цена обновляется в realtime при вводе override_price
- При вводе невалидной цены (отрицательная, нулевая, > 1 000 000) — красная подсказка
Drag-drop переноса item между категориями
- Drag-handle ☰ → перетащить в другую категорию
- Drop →
PATCH /external-menus/{id}/items/{itemId}с новымcategory_id
Удаление item
- Действие «Удалить» в раскрытой override-форме (или через context menu правой кнопкой)
- Модалка: «Удалить «Капучино» из меню? Связь с каталогом не нарушится — товар останется в основном каталоге.»
DELETE /external-menus/{id}/items/{itemId}
Кнопки публикации (шапка)
«Опубликовать»
| Условие | Поведение |
|---|---|
| Меню пустое (нет items) | Disabled, tooltip «Добавьте хотя бы один товар» |
status=draft AND есть items | Активна |
status=published | Скрыта (показывается «Снять с публикации») |
Click:
- Если
tv_screen— модалка «Меню будет опубликовано. Live URL:https://.... После публикации меню начнёт показываться на мониторах с этим URL.» →POST /publish - Если
json— модалка «JSON-экспорт станет доступен по запросу к API. Опубликовать?» →POST /publish - При успехе — toast «Опубликовано», шапка обновляется (live URL копируемый), кнопка меняется
«Снять с публикации»
См. модалку из Список — Снять с публикации.
Превью (модалка / новая вкладка)
Действие «Превью» в Меню ⚙ (только для tv_screen):
- Открывает
/r/{slug}?preview=1в новой вкладке - Этот endpoint работает даже для
draft(требует preview-токен в cookie — устанавливается этой страницей при клике) - Показывает текущее состояние меню в выбранном шаблоне как будет на мониторе
- Не подключается к WebSocket — статичный snapshot
Live-обновление превью внутри редактора (опционально, P1)
- В правой нижней четверти страницы — embedded iframe
/r/{slug}?preview=1(компактная версия) - При изменении меню — iframe перезагружается через WebSocket-сигнал
- Можно скрыть через toggle «Скрыть превью»
В P0 — без embedded iframe; владелец открывает превью отдельной вкладкой по кнопке.
Состояния и ошибки
| Состояние | Что показываем |
|---|---|
| Загрузка | Skeleton обеих панелей + skeleton шапки |
| Меню не найдено (404) | «Меню не найдено или удалено» + кнопка «← К списку» |
| Нет прав (403) | «Недостаточно прав для редактирования» + кнопка «← К списку» |
| Сохранение в процессе | Спиннер у кнопки «Сохранить» override-формы |
| Сохранение упало | Toast красный «Не удалось сохранить» + кнопка «Повторить» |
| Конфликт дубликата (409 ITEM_DUPLICATE) | Toast «Товар уже в меню» при попытке drag-drop |
| Конфликт slug (409 SLUG_TAKEN) | Inline-error в настройках |
| Orphan-item появился (live при работе) | Красный банер over-item, refresh данных автоматически |
Indicator «Несохранённые изменения»
Если override-форма открыта с unsaved changes и владелец пытается:
- Закрыть форму без сохранения → confirm «Изменения будут потеряны»
- Открыть другой item → confirm
- Уйти со страницы (browser tab close) → стандартный browser-prompt
Ролевой доступ
Владелец франшизы / Владелец партнёра (свои ТТ)
- Полный доступ ко всем функциям
Обычный сотрудник с external_menus.read (без .edit)
- Видит редактор в read-only режиме:
- Drag-drop отключён
- Кнопки сохранения / публикации скрыты
- Override-формы — только просмотр
- Это редкий случай — обычно у обычного сотрудника нет даже
read
Переходы
| Откуда | Куда | Триггер |
|---|---|---|
| Конструктор | Список | Кнопка «← Назад» |
| Конструктор | Превью | «Превью» в Меню ⚙ |
| Конструктор | Список (после удаления) | Удалить → confirm → /external-menus |