Перейти к содержанию

04. Админка каталога — ТЗ

React 18 + TypeScript + Ant Design 5, отдельное SPA, работает с /api/admin/v1.

4.1. Принципы

  1. Разделение источника и витрины видно глазами. Всё, что пришло из iiko, отображается с пометкой «из iiko» и недоступно для ввода. Попытка «поправить цену в админке» не должна даже выглядеть возможной.
  2. Ни одного «зависшего» блюда. Любая позиция iiko находится ровно в одном из трёх состояний: привязана к товару, помечена «не для витрины», или лежит в очереди новых. Счётчик очереди — в главном меню.
  3. Работа списками, а не по одной карточке. Массовое назначение категории, массовая публикация, массовая загрузка картинок — базовые операции, а не «когда-нибудь потом».
  4. Сортировка мышью. Порядок категорий и товаров внутри категории — drag & drop, с сохранением одной кнопкой и явным индикатором несохранённых изменений.
  5. Изменения обратимы. Публикация/снятие — переключатель, история — в аудите, удаление — архивирование.

4.2. Навигация

Каталог
  ├── Категории
  ├── Товары
  ├── Новые позиции iiko           ● 3      ← бейдж с количеством NEW
  ├── Проблемы привязок            ● 2      ← бейдж с открытыми инцидентами
  ├── Модификаторы
  └── Бейджи
Данные iiko
  ├── Импорты                      ● ошибка ← бейдж при неуспешном последнем импорте
  ├── Внешние меню
  ├── Организации
  ├── Терминалы
  ├── Номенклатура iiko            (read-only справочник)
  └── Стоп-листы
Медиа
  └── Библиотека изображений
Система
  ├── Пользователи
  ├── Роли
  ├── Аудит
  └── Настройки

4.3. Роли и права

Право ADMIN CONTENT_MANAGER VIEWER
catalog.read
catalog.write (категории, товары, сортировка, медиа)
catalog.publish
iiko.read
iiko.sync.run (ручной импорт)
iiko.settings.write (внешние меню, ценовые категории)
platform.users.write, platform.audit.read

Права проверяются на бэкенде; фронт по тем же правам скрывает кнопки (GET /api/admin/v1/me возвращает список прав).

4.4. Экран «Категории»

┌─ Категории ────────────────────────────── [+ Категория] [Сохранить порядок] ─┐
│ ⣿ ▸ Пиццы                          /pizzy         42 товара   ● активна  ⋮  │
│ ⣿ ▾ Роллы                          /rolly         30 товаров  ● активна  ⋮  │
│ ⣿    ├─ Классические               /rolly/klass   18 товаров  ● активна  ⋮  │
│ ⣿    └─ Запечённые                 /rolly/zap     12 товаров  ○ скрыта   ⋮  │
│ ⣿ ▸ Напитки                        /napitki       35 товаров  ● активна  ⋮  │
│ ⣿ ▸ Соусы                          /sousy          0 товаров  ○ скрыта   ⋮  │
└──────────────────────────────────────────────────────────────────────────────┘
  • Дерево с drag & drop (перетаскивание меняет и порядок, и родителя), максимум 2 уровня.
  • Кнопка «Сохранить порядок» активна только при изменениях; уход со страницы — предупреждение.
  • В меню : редактировать, добавить подкатегорию, сортировать товары внутри, архивировать.
  • Категория с товарами не удаляется — только архивируется, с подсказкой сколько товаров осиротеет.

Форма категории: название, slug (генерируется из названия, редактируемый, проверка занятости на лету), родитель, краткое и полное описание, изображение и иконка (drag & drop, кроп), активность, «показывать в главном меню», SEO-блок (title, description, H1).

4.5. Экран «Товары»

┌─ Товары ──────────────────────────────────────────── [+ Товар] [Импорт ↻] ─┐
│ Поиск: [ панам        ]  Категория: [Пиццы ▾] Статус: [Все ▾]              │
│ Быстрые фильтры: (Без картинки 12) (Не в категории 5) (Проблемы iiko 2)    │
├───┬──────────┬──────────────┬──────────┬──────────────┬─────────┬──────────┤
│ ☐ │ фото     │ Название     │ Категория│ Варианты     │ Цена от │ Статус   │
├───┼──────────┼──────────────┼──────────┼──────────────┼─────────┼──────────┤
│ ☐ │ [img]    │ Панам        │ Пиццы    │ 25/30/35/40  │  530 ₽  │ ● Опубл. │
│ ☐ │ [img]    │ Пепперони    │ Пиццы    │ 25/30/35/40  │  530 ₽  │ ● Опубл. │
│ ☐ │  ⚠ нет   │ Бургер Пицца │ Пиццы    │ 25/30        │  610 ₽  │ ○ Черн.  │
│ ☐ │ [img]    │ Филадельфия  │ Роллы    │ —            │  690 ₽  │ ● Опубл. │
└───┴──────────┴──────────────┴──────────┴──────────────┴─────────┴──────────┘
  Выбрано 3:  [Опубликовать] [Снять] [В категорию…] [Бейджи…] [Архивировать]

Колонки настраиваются, состояние таблицы (фильтры, сортировка, страница) — в query-параметрах URL, чтобы ссылку на отфильтрованный список можно было переслать коллеге.

Фильтры: текст (по названию товара, названию блюда iiko, SKU), категория, статус, наличие изображения, наличие вариантов, бейдж, «есть расхождение с iiko», организация (где виден).

4.6. Карточка товара

Вкладки: Основное · Варианты · Изображения · Модификаторы · Размещение · Видимость · SEO · История

Правая колонка постоянно показывает панель публикации:

┌ Публикация ───────────────┐
│ Статус: Черновик          │
│                           │
│ Чтобы опубликовать:       │
│  ✓ есть вариант           │
│  ✓ вариант привязан       │
│  ✗ нет главного фото      │
│  ✓ размещён в категории   │
│                           │
│ [ Опубликовать ]  (disabled)
│ Последнее изменение:      │
│ Иванов, 12.08 14:03       │
└───────────────────────────┘

Основное

Название, подзаголовок, slug, витринное описание, состав, бейджи, «секретный товар» (доступен по прямой ссылке, не в каталоге).

Под полем описания — блок «из iiko» со значением, которое будет использовано, если поле пустое:

Описание (пусто → возьмётся из iiko)
┌──────────────────────────────────────────────────────────┐
│                                                          │
└──────────────────────────────────────────────────────────┘
  из iiko: «Креветки, моцарелла, креметта, руккола, черри, соус
  томатный, пармезан КБЖУ на 100гр: б:9г,ж:9г,у:21г,ккал: 196»
  [Подставить очищенный текст]  ← парсер отрезает КБЖУ-хвост

Варианты — ключевой экран

Здесь карточка связывается с блюдами iiko. Ось вариативности выбирается один раз («Размер»), дальше добавляются варианты.

┌ Варианты ─────── Ось: [Размер ▾]  ───────────── [+ Добавить вариант] ─┐
│ ⣿  25 см ▾   Панам 25 см           SKU 219971   530 ₽   ● вкл  ✓ по умолч. │
│ ⣿  30 см ▾   Панам 30 см           SKU 219972   730 ₽   ● вкл          ⋮  │
│ ⣿  35 см ▾   Панам 35 см           SKU 219973   930 ₽   ● вкл          ⋮  │
│ ⣿  40 см ▾   Панам 40 см           SKU 219974  1130 ₽   ● вкл          ⋮  │
└────────────────────────────────────────────────────────────────────────────┘
   Цены — из iiko, организация: [Панам Пермь, ул. Ленина ▾]

Диалог «Добавить вариант» — поиск по непривязанным позициям iiko:

┌ Привязать блюдо iiko ──────────────────────────────────────────┐
│ Поиск: [ панам                     ]  Только непривязанные ☑   │
│                                                                │
│ Подсказка: похоже, это варианты одного товара                  │
│  ☑ Панам 25 см   SKU 219971   Пицца 25 см    530 ₽             │
│  ☑ Панам 30 см   SKU 219972   Пицца 30 см    730 ₽             │
│  ☑ Панам 35 см   SKU 219973   Пицца 35 см    930 ₽             │
│  ☑ Панам 40 см   SKU 219974   Пицца 40 см   1130 ₽             │
│                                                                │
│ Все результаты                                                 │
│  ☐ Панамыч 25 см SKU 221196   Пицца 25 см    560 ₽             │
│  ☐ Панамыч 30 см …                                             │
│                                    [Отмена] [Привязать (4)]    │
└────────────────────────────────────────────────────────────────┘

Алгоритм автоподсказки (только подсказка, решение всегда за человеком):

  1. Нормализация названия: нижний регистр, отбрасывание хвоста по словарю размеров (\s*\d{2}\s*см$, \s*\d+(\.\d+)?\s*л$, \s*\d+\s*г$), схлопывание пробелов, «ё»→«е».
  2. Кандидаты — непривязанные позиции с той же нормализованной базой.
  3. Дополнительный сигнал — общий префикс SKU и одна учётная группа productCategoryId.
  4. Размер извлекается из отброшенного хвоста и сопоставляется с variant_option.code.
  5. При точном совпадении базы и различии только в размере — чекбоксы предзаполнены.

Правила экрана: - Позиция, уже привязанная к другому товару, показывается заблокированной со ссылкой на товар — ограничение unique (iiko_item_size_id) объясняется, а не выдаётся как ошибка после сохранения. - Ровно один вариант помечен «по умолчанию»; при удалении такого — метка переносится на первый. - Если позиция удалена из iiko, строка варианта краснеет: «блюдо отсутствует в меню iiko с 12.08», рядом — последнее известное состояние из снимка привязки (название, SKU, цена) и кнопка «Перепривязать» с предложенным кандидатом. Подробнее — §4.8.

Изображения

Требования к нарезке, пресетам, валидации исходников и предпросмотру — в отдельном ТЗ 07-media.md. Здесь — только состав экрана.

  • Главное фото, фото при наведении, галерея; drag & drop загрузка, сортировка мышью.
  • Кроп с фиксированными соотношениями под пресеты витрины, точка фокуса.
  • Предпросмотр во всех пресетах сразу — как картинка будет выглядеть в плитке каталога, на детальной странице и в корзине. Это главное средство против «загрузил — а на сайте обрезано».
  • Возможность задать своё фото конкретному варианту (у пиццы 25 и 40 см может быть разное фото).
  • Выбор из общей медиабиблиотеки без повторной загрузки; дедупликация по sha256.
  • Предупреждение, если исходник меньше требуемого разрешения пресета detail@2x.

Модификаторы

Read-only состав из iiko + витринные настройки:

┌ Модификаторы (из iiko, состав менять нельзя) ───────────────────────────────┐
│ ▾ Пицца Тесто 25          выбор 1 из 3, обязательно                         │
│     Отображать как: [ Тесто                    ]  Вид: [Переключатель ▾]     │
│     ├ ⣿ Традиционное      +0 ₽    [фото]  ● показывать                       │
│     ├ ⣿ Тонкое            +0 ₽    [фото]  ● показывать                       │
│     └ ⣿ Пышное           +50 ₽    [фото]  ● показывать                       │
│ ▾ Без ингредиента         0..10                                             │
│     Отображать как: [ Убрать из состава        ]  Вид: [Чекбоксы ▾]           │
│     ⚠ 24 позиции — рекомендуется свернуть по умолчанию   ☑ свернуть          │
└──────────────────────────────────────────────────────────────────────────────┘

Настройки хранятся в catalog.modifier_group_profile / catalog.modifier_profile по ключу группы/позиции iiko и применяются ко всем товарам сразу — иначе название «Пицца Тесто 25» пришлось бы переименовывать в каждой из 20 пицц.

Размещение

Список категорий с чекбоксами, одна помечена основной (участвует в URL и хлебных крошках), рядом — позиция товара в каждой категории с возможностью перейти к сортировке категории.

Видимость

Таблица организаций с переключателем «показывать». По умолчанию видно везде, где у блюда есть цена. Строки, где цены в iiko нет, показаны отдельно: «нет цены в этой организации — не будет отображаться независимо от настройки».

История

Записи аудита по товару: кто, когда, какие поля, до/после.

4.7. Экран «Новые позиции iiko»

Главный инструмент против «блюдо появилось в iiko и потерялось».

┌─ Новые позиции iiko ───────────────────── найдено при импорте 12.08 04:30 ─┐
│ ☐ │ Название            │ SKU     │ Категория iiko  │ Цена  │ Действие      │
│ ☐ │ Пицца Трюфельная 25 │ 2220401 │ Пицца 25 см     │ 690 ₽ │ [Создать][…]  │
│ ☐ │ Пицца Трюфельная 30 │ 2220402 │ Пицца 30 см     │ 890 ₽ │ [Создать][…]  │
│ ☐ │ Соус Сырный 30г     │ 2219712 │ Соусы Доставка  │  60 ₽ │ [Создать][…]  │
│ ☐ │ Коробка 25          │ 2210033 │ Прочее          │   0 ₽ │ [Не для сайта]│
└────────────────────────────────────────────────────────────────────────────┘
  Выбрано 2: [Создать один товар с вариантами] [Добавить в существующий…] [Не для сайта]

Действия: создать товар (форма предзаполнена названием без суффикса размера, категория предложена по маппингу категорий iiko), добавить вариантом к существующему товару, пометить «не для сайта» (item_triage.status = IGNORED, с комментарием — тарные позиции, полуфабрикаты, служебные блюда).

Выделение нескольких строк, отличающихся только размером, предлагает создать один товар с вариантами — это основной сценарий при появлении новой пиццы.

4.8. Экран «Проблемы привязок»

Обратная сторона экрана «Новые позиции»: там появляются блюда, здесь — пропадают. Закрывает реальный сценарий, когда сотрудник случайно удалил или пересоздал блюдо в iiko.

┌─ Проблемы привязок ──────────────────── открытых: 4   [Все ▾] [Только критичные] ─┐
│ ● КРИТИЧНО  Блюдо удалено из iiko                             12.08 04:30         │
│   Товар: Панам · вариант 30 см                                                    │
│   Было в iiko: «Панам 30 см», SKU 219972, 730 ₽ — последний раз 12.08 04:12       │
│   ▸ Найдена возможная замена: «Панам 30 см», SKU 219972, новый id  (совпал SKU)   │
│                        [Перепривязать] [Отключить вариант] [Отложить] [Не важно]  │
├───────────────────────────────────────────────────────────────────────────────────┤
│ ● КРИТИЧНО  Товар снят с публикации: не осталось живых вариантов  12.08 04:30     │
│   Товар: Соус Сырный                                                              │
│                                    [Открыть товар] [Подобрать замену] [Не важно]  │
├───────────────────────────────────────────────────────────────────────────────────┤
│ ○ ВНИМАНИЕ  Пропала цена в организации «Панам Березники»          12.08 04:30     │
│   Товар: Пепперони · вариант 40 см                                                │
├───────────────────────────────────────────────────────────────────────────────────┤
│ · ИНФО      Название в iiko изменилось                            11.08 04:30     │
│   Товар: Филадельфия. Было «Филадельфия», стало «Филадельфия классическая»        │
│                                          [Принять для витрины] [Оставить своё]    │
└───────────────────────────────────────────────────────────────────────────────────┘

Поведение:

  • Ничего не удаляется. Карточка, изображения, тексты, SEO и позиция в категории остаются на месте, даже если блюдо в iiko исчезло полностью.
  • Перепривязка в одно действие. Если блюдо пересоздали и оно пришло с новым itemId, система сама предлагает кандидата (совпадение SKU — сильный сигнал, совпадение названия и учётной группы — слабый). Нажатие «Перепривязать» подставляет новую позицию в существующий вариант; прежний id сохраняется в истории привязок.
  • Ручной подбор. Если автоматика кандидата не нашла — тот же диалог поиска по номенклатуре, что и при добавлении варианта, но с предзаполненным запросом по последнему известному названию.
  • Отложить — инцидент скрывается на сутки (частая ситуация: блюдо выключено временно).
  • Не важно — закрытие с резолюцией DISMISSED и обязательным комментарием.
  • Товар с открытым критичным инцидентом помечается в списке товаров значком, фильтр «Проблемы iiko» на экране товаров ведёт сюда же.
  • «Мигание» выгрузки (позиция пропала и вернулась в пределах окна ожидания) инцидента не создаёт — иначе экран превращается в шум и его перестают читать.

История инцидентов сохраняется после закрытия: по товару всегда видно, что с ним происходило, когда и кто принял решение.

4.9. Экран «Импорты»

┌─ Импорты ──────────────────────── [Запустить: Меню ▾] [Полный импорт] ─────┐
│ Меню 41188      ✓ SUCCESS  12.08 14:05  4.2 c  rev 128390  +3 ~240 −1      │
│ Стоп-листы      ✓ SUCCESS  12.08 14:05  0.3 c  12 позиций                  │
│ Меню 50865      ⊘ SKIPPED  12.08 14:00  ревизия не изменилась              │
│ Меню 41188      ✗ FAILED   12.08 04:30  SUSPICIOUS_SHRINK: −38% позиций    │
│                                          [Подробности] [Повторить]         │
└────────────────────────────────────────────────────────────────────────────┘

Карточка запуска: параметры запроса, статистика по сущностям, лог этапов, текст ошибки, кнопка повтора. Вверху страниц каталога — предупреждающая полоса, если последний импорт неуспешен или стоп-листы не обновлялись больше 5 минут.

4.10. Экран «Внешние меню»

Настройка того, что в legacy было хардкодом: список внешних меню, для каждого — название, ценовая категория (выбор из справочника iiko), включённость, набор организаций, ревизия, время последнего успешного импорта, кнопка ручного запуска.

4.11. Справочные экраны (read-only)

  • Организации — id, название, код, адрес, координаты, активность; редактируемых полей нет.
  • Терминалы — по организациям, статус is_alive.
  • Номенклатура iiko — плоский список всех позиций с фильтром по типу (блюдо/модификатор), учётной группе, привязанности к товару; из строки — переход к товару. Инструмент для ответа на вопрос «почему этого блюда нет на сайте».
  • Стоп-листы — по организации и терминалу, с временем последнего обновления.

4.12. Экран «Модификаторы»

Плоский список всех позиций-модификаторов с их витринными профилями: название для сайта, фото, скрытие. Позволяет один раз загрузить фото ко всем соусам и топпингам, не заходя в каждую пиццу.

4.13. Медиабиблиотека

Сетка изображений, фильтр по папке и дате, поиск по имени, показ «где используется», удаление запрещено для используемых файлов, массовая загрузка.

4.14. Общие требования к UI

  • Все списки — серверная пагинация, размер страницы 20/50/100, состояние в URL.
  • Ошибки бэкенда (problem+json) показываются полем формы, если есть field, иначе — уведомлением.
  • Оптимистичная блокировка: при сохранении отправляется version; при конфликте — диалог «запись изменена пользователем X, показать различия / перезагрузить».
  • Автосохранение не используется — только явное сохранение и предупреждение о несохранённых изменениях при уходе.
  • Локализация: интерфейс на русском; строки вынесены в словарь, чтобы не переписывать код при появлении второго языка.
  • Тёмная тема — по системной настройке (antd theme.algorithm), без отдельного бэклога.