Внешние меню — Конструктор

Роут: /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_screen published): 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

Ссылки