Каталог — Товары

Два экрана: Список товаров и Карточка товара (просмотр / создание / редактирование).


Список товаров

Роут: /catalog/products API: GET /api/v1/products


Что видит пользователь

Страница со списком товаров каталога в виде таблицы. Вверху — заголовок “Товары”, строка поиска и фильтры. Справа от заголовка — кнопка “Добавить товар” (только Franchise). Под заголовком — табы: “Активные” (default) и “Удалённые” (только Franchise).


Таблица

Колонки

КолонкаДанныеПримечание
Фотоimage_url36×36 миниатюра (rounded), placeholder-иконка если нет фото
НазваниеnameКликабельное — переход в карточку
ТипtypeБейдж: Блюдо (dish) / Продукт (good)
Категорияcategory_nameНазвание или “Без категории”
Цена(BR 1.10) Цена убрана — определяется прейскурантом
Ед. изм.unit_of_measureшт / кг / г / л / мл / порция
СтатусstatusБейдж; Franchise — кликабельный для переключения active↔inactive

Отображение статусов

СтатусБейджТекст
activeЗелёныйАктивный
inactiveСерыйНеактивный

Фильтры

ФильтрТипЗначенияDefault
ТипSelectВсе / Блюдо / ПродуктВсе
СтатусSelectВсе / Активный / НеактивныйВсе
КатегорияSelectВсе / дерево категорийВсе

Фильтры применяются мгновенно (без кнопки “Применить”). При смене фильтра — сброс на page=1.

Поиск

  • Поле ввода с placeholder “Поиск по названию”
  • Поиск с debounce (300ms)
  • Query param: search

Сортировка

(Доработано в ADR-013)

  • По названию А-Я (default: sort=name_asc)
  • Клик по заголовку колонки “Название” переключает A-Я / Я-A
  • Query param: sort со значениями name_asc, name_desc
  • Фронт отправляет параметр sort — бэкенд должен поддержать (ADR-013)

Пагинация

  • 20 записей на страницу
  • Постраничная навигация внизу (номера страниц + стрелки)
  • Query params: page, per_page

Таб “Удалённые”

Только для роли Franchise

  • API: GET /api/v1/products?deleted=true
  • Те же колонки, но вместо колонки “Действия” — кнопка “Восстановить”
  • Восстановление: POST /api/v1/products/{id}/restore → toast “Товар восстановлен”, строка исчезает из таба

Действия

Кнопки в шапке

КнопкаПереходВидимость
”Добавить товар”/catalog/products/newТолько Franchise

Меню действий строки

ДействиеВидимостьЧто происходит
РедактироватьFranchiseПереход в /catalog/products/{id}/edit
ДублироватьFranchiseКопирует товар с “(копия)” в названии, переход в новую карточку
УдалитьFranchiseМодалка подтверждения

Модалки подтверждений

Удаление

Триггер: клик “Удалить” в меню строки API: DELETE /api/v1/products/{id}

  • Заголовок: “Удалить товар”
  • Текст: “Удалить товар [название]? Товар будет скрыт из каталога. Можно восстановить.”
  • Кнопки: “Отмена” / “Удалить” (красная)
  • После успеха: убрать строку из таблицы, toast “Товар удалён”

Состояния

СостояниеЧто показываем
ЗагрузкаSkeleton-таблица (placeholder строк)
Пусто (Franchise)“Товары пока не добавлены” + кнопка “Добавить товар”
Пусто (Franchisee/Manager)“Товары пока не добавлены”
Ошибка загрузки”Не удалось загрузить данные” + кнопка “Повторить”
Пустой поиск”Ничего не найдено по запросу «…»”

Ролевая видимость

Franchise

(Обновлено в BR 1.7.1)

  • Видит все товары франшизы (справочник), не привязан к версии каталога
  • Все действия: создание, редактирование, удаление, восстановление
  • Таб “Удалённые” доступен
  • Все фильтры

Franchisee

  • Видит только активные товары
  • Нет кнопки “Добавить товар”
  • Нет действий: удалить, редактировать
  • Нет таба “Удалённые”

Manager

  • Видит только активные товары
  • Нет кнопки “Добавить товар”
  • Нет действий: удалить, редактировать
  • Нет таба “Удалённые”

Cashier

  • 403 Forbidden — нет доступа к странице
  • Редирект на дашборд

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

Роуты:

  • /catalog/products/:id — просмотр
  • /catalog/products/new — создание
  • /catalog/products/:id/edit — редактирование

API:

  • GET /api/v1/products/:id — получение
  • POST /api/v1/products — создание
  • PATCH /api/v1/products/:id — обновление (создаёт новую версию)

Просмотр

Карточка с полями товара в режиме readonly.

Секция “Основные данные”

ПолеДанныеПримечание
Фотоimage_urlПревью 200×200. Кнопки “Загрузить”/“Заменить”/“Удалить” (Franchise)
Названиеname
ОписаниеdescriptionЕсли пусто — ”—“
ТипtypeБлюдо / Продукт
Категорияcategory_nameЕсли null — “Без категории”
Ед. изм.unit_of_measure
СтатусstatusБейдж
АртикулskuЕсли есть
ШтрихкодbarcodeЕсли есть
Порядок сортировкиsort_order
Цвет POScolorЦветной квадратик + hex-код

Секция “Вес и КБЖУ” (если есть данные)

ПолеДанные
Вес бруттоgross_weight кг
Вес неттоnet_weight кг
Калорииkcal ккал/100г
Белкиprotein г/100г
Жирыfat г/100г
Углеводыcarbs г/100г

Секция “Параметры и флаги” (если есть)

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

Секция “Кухня” (BR 2.5)

(Добавлено в BR 2.5)

ПолеДанныеПримечание
Требует приготовленияrequires_kitchenГалочка «Да» / «Нет»
Кухонная станцияkitchen_station_id → nameНазвание станции через lookup; если requires_kitchen=false — «—»

В форме создания/редактирования:

  • Чекбокс «Требует приготовления» (по умолчанию снят)
  • Dropdown «Кухонная станция» — показывается только если чекбокс отмечен. Список через GET /kitchen-stations. Если станций во франшизе нет — показать подсказку «Создайте станции в разделе “Кухонные станции” прежде чем отмечать этот флаг»
  • Валидация: при сохранении с отмеченным чекбоксом без выбора станции — ошибка поля «Выберите кухонную станцию»

Массовый импорт

При импорте товаров через Excel можно задать requires_kitchen и kitchen_station_name (по названию — сервер резолвит в ID). Несуществующие станции → строка отклоняется с ошибкой.

Секция “Фискальные атрибуты” (BR 3.3)

(Добавлено в BR 3.3)

Используются Paykeeper Adapter при формировании инвойса и фискального чека 54-ФЗ.

ПолеДанныеПримечание
Ставка НДСvat_rateSelect: Без НДС / 0% / 10% / 20% / 10/110 / 20/120. Default 20%
Предмет расчётаpayment_subjectSelect: Товар / Услуга / Работа / Подакцизный / Плата / Агентский / Составной / Иное / Работа/услуга. Default Товар
Способ расчётаpayment_typeSelect: Полный расчёт / Предоплата / Аванс / Частичная предоплата / Кредит / Оплата кредита / Частичный расчёт. Default Полный расчёт

В форме создания/редактирования — три dropdown’а под секцией «Кухня». Показываются всегда (независимо от активности PK-интеграции).

Hint под секцией: «Используются при формировании фискального чека 54-ФЗ через PayKeeper».

При миграции существующих товаров — default’ы проставляются автоматически (vat20 / goods / full), не блокируют сохранение.

Детали — Интеграция PayKeeper.

Секция “Связь с 1С” (BR 1.17)

(Добавлено в BR 1.17)

Маленький бейдж под другими секциями со статусом привязки товара к номенклатуре 1С:

СостояниеУсловиеБейдж
Прямая связьsku начинается с 1c:🔗 Зелёный: «1С: {КОД}» (например «1С: 00000003541»)
Виртуальный (через опции)sku пуст, есть structural-мод, у всех опций заполнен sku_1c🔗 Зелёный: «Виртуальный — {N} опций привязаны к 1С»
Виртуальный неполныйsku пуст, есть structural-мод, у части опций пуст sku_1c⚠ Жёлтый: «Виртуальный — {X} из {N} опций без 1С-кода»
Не привязанОба условия не выполнены⚠ Серый: «Не привязан к 1С» (подсказка: «Заполните Артикул в формате 1c:КОД или добавьте structural-модификатор»)

Источник данных

Клиентская логика на основе product.sku + загруженной группы модификаторов через GET /modifier-groups/{id}. Не требует нового поля в API.

Подробнее о связи с 1С — 1С Общепит.

Секция “Доступность по точкам”

  • Если available_in_all_stores = true — ”✓ Доступен во всех точках”
  • Если нет — список точек (chips с названиями)
  • Кнопка “Настроить” (Franchise) → inline-редактирование: toggle “Доступен во всех точках” + чекбокс-список магазинов
  • API сохранения: PATCH /api/v1/products/{id}/stores

Фото товара

  • API загрузки: POST /api/v1/products/{id}/image (multipart/form-data)
  • API удаления: DELETE /api/v1/products/{id}/image
  • Кнопки: “Загрузить фото” (если нет) / “Заменить” + “Удалить” (если есть) — только Franchise

Кнопки хедера: “Редактировать” (Franchise), “Дублировать” (Franchise), “Назад к списку”, “Удалить” (Franchise).

Табы карточки: [Информация] [Модификаторы] [Техкарта]

  • “Техкарта” виден только для товаров type=dish (BR 1.9)
  • Для type=good — таб скрыт
  • “Модификаторы” — все модификаторы: секция “Закреплённые” + секция “Свободные” (BR 1.9.3)
  • “Техкарта” — только рецепты, per-option вкладки определяются закреплёнными модификаторами (BR 1.9.3)

API: Модификаторы из GET /api/v1/products/{id}/modifiers. Техкарта из GET /api/v1/tech-cards?product_id={id} (Warehouse Service).

Таб “Модификаторы” (только Franchise)

(Добавлено в BR 1.8.1, обновлено в BR 1.9.3)

API: GET /api/v1/products/{id}/modifiers

Управление всеми модификаторами товара. Разделены на две визуальные секции (BR 1.9.3):

Секция “Закреплённые (структурные)”

Модификаторы с binding_type = "structural". Вверху таба. Определяют вариант блюда (размер, вид теста).

  • Кнопка “Добавить закреплённый” → модалка (как обычная, но binding_type: "structural")
  • Ошибка STRUCTURAL_MODIFIER_MIN_REQUIRED → toast “Закреплённый модификатор должен иметь min >= 1”

Секция “Свободные (дополнительные)”

Модификаторы с binding_type = "free" (или без binding_type для обратной совместимости). Под закреплёнными, с разделителем.

  • Кнопка “Добавить свободный” → модалка (как обычная, binding_type: "free").

Таблица модификаторов

КолонкаДанныеПримечание
ГруппаnameНазвание группы модификаторов
ТипtypeГрупповой / Простой
Minoverride_min_amount или min_amountInline-редактирование
Maxoverride_max_amount или max_amountInline-редактирование
Опцийoptions.lengthКоличество опций в группе
Действия“Убрать”

Override

Если override_min_amount / override_max_amount заданы — показываются с подсветкой (отличаются от значений группы). При наведении — tooltip “Значение группы: min=0, max=3”.

”Добавить модификатор” → модалка

  1. Выбрать группу из справочника (поиск по GET /api/v1/modifier-groups)
  2. Override min/max (опционально)
  3. API: POST /api/v1/products/{id}/modifiers с binding_type в зависимости от секции

Ошибка MODIFIER_ALREADY_ATTACHED: toast “Группа уже привязана к этому товару”.

”Убрать”

  • API: DELETE /api/v1/products/{id}/modifiers/{groupId}
  • Модалка: “Убрать группу [название] из товара?”
  • После успеха: строка исчезает, toast “Модификатор убран”

Обновить версию/override

  • API: PATCH /api/v1/products/{id}/modifiers/{groupId}
  • Inline: dropdown версии или поля min/max → авто-сохранение при изменении

Изменение модификаторов

Изменения применяются напрямую — без версионирования.


Таб “Техкарта” (только для type=dish)

(Добавлено в BR 1.9, обновлено в BR 1.9.3)

API (Warehouse Service):

  • GET /api/v1/tech-cards?product_id={id} — список техкарт товара
  • GET /api/v1/tech-cards/{id} — детали с ингредиентами
  • GET /api/v1/tech-cards/{id}/cost — расчёт себестоимости

Управление техкартой (рецептурой) товара. Таб виден только для товаров type=dish. Per-option вкладки (25см / 30см / 35см) определяются закреплёнными модификаторами из таба “Модификаторы”. Управление модификаторами — не здесь (BR 1.9.3).

Шапка таба

ЭлементДанныеПримечание
Выходoutput_weight + output_unit”450 г”
Себестоимостьиз /cost endpoint”187,50 ₽” или ”—” если нет данных
Статус техкартыstatusБейдж: Активная / Неактивная

Per-size вкладки

Если у товара есть size-модификатор (из product_modifiers) — внутри таба Техкарта отображаются вложенные вкладки:

[Базовая] [25 см] [30 см] [35 см]

Каждая вкладка = отдельная техкарта с modifier_option_id. “Базовая” = modifier_option_id IS NULL.

Если size-модификатора нет — вкладки не отображаются, показывается только базовая техкарта.

Таблица ингредиентов

КолонкаДанныеПримечание
Ингредиентingredient_nameКликабельное — если has_tech_card=true (полуфабрикат), открывает его техкарту
Бруттоgross_weightЧисло + единица
Неттоnet_weightЧисло + единица
% потерь (хол.)cold_loss_percent”—” если null
% потерь (гор.)hot_loss_percent”—” если null
Стоимостьиз /cost”45,00 ₽” или ”—“
Действия“Убрать” (Franchise only)

“Добавить ингредиент” → модалка

(Доработано в BUG-025)

  1. При открытии модалки — сразу загрузить первые 20 ингредиентов (без ввода в поиск). Если > 20 — кнопка “Показать ещё” для подгрузки. При вводе текста — фильтрация, сброс пагинации.
  2. Поиск по ингредиентам: GET /api/v1/ingredients?search=... (Warehouse Service) (BR 1.11)
  3. Если ингредиент не найден — кнопка “Создать ингредиент”:
    • Быстрая форма: название, единица измерения
    • API: POST /api/v1/ingredients (Warehouse Service) (BR 1.11)
  4. Выбрав ингредиент — заполнить поля: брутто, нетто, единица, % потерь
  5. API: POST /api/v1/tech-cards/{id}/items с ingredient_id

Полуфабрикаты (BUG-010)

Полуфабрикат — это ингредиент, у которого есть своя техкарта. Поиск полуфабрикатов не нужен отдельно — они отображаются в общем списке ингредиентов. Признак полуфабриката: has_tech_card=true в ответе.

Ошибка CIRCULAR_REFERENCE (422): toast “Циклическая ссылка — ингредиент-полуфабрикат создаёт цикл”.

”Убрать” ингредиент

  • Модалка: “Убрать [название] из рецептуры?”
  • API: DELETE /api/v1/tech-cards/{id}/items/{itemId}
  • Toast “Ингредиент убран”

Редактирование строки (inline)

  • Поля брутто/нетто/% потерь — inline-редактирование (click-to-edit)
  • Авто-пересчёт: при вводе брутто + нетто → % потерь вычисляется. При вводе брутто + % → нетто вычисляется
  • Auto-save: PATCH /api/v1/tech-cards/{id}/items/{itemId}

Технология приготовления

  • Textarea под таблицей ингредиентов
  • Placeholder: “Опишите шаги приготовления…”
  • Auto-save при blur: PATCH /api/v1/tech-cards/{id} с cooking_description

Создание техкарты

Если техкарты для этой версии ещё нет — вместо таблицы:

  • “Техкарта не создана” + кнопка “Создать техкарту”
  • API: POST /api/v1/tech-cards → после успеха показывается пустая техкарта для заполнения

Состояния

СостояниеЧто показываем
ЗагрузкаSkeleton
Нет техкарты”Техкарта не создана” + “Создать техкарту” (Franchise)
Пустая техкартаШапка + пустая таблица + “Добавить ингредиент”
Ошибка”Не удалось загрузить техкарту” + “Повторить”

Ролевой доступ

  • Franchise: CRUD техкарт, добавление/удаление ингредиентов, создание ингредиентов, редактирование
  • Franchisee/Manager: просмотр (таблица read-only, нет кнопок добавления/удаления)
  • Cashier: таб не виден

Создание

Роут: /catalog/products/new

Секция “Фото товара”

Загрузка/замена/удаление фото — immediate API call, не часть PATCH-формы.

Секция “Основные данные”

ПолеТипОбязательноеВалидация
НазваниеText inputДаНе пустое
ОписаниеTextareaНет
ТипSelect: Блюдо / ПродуктДа
КатегорияSelect: дерево категорийНетNullable. Пустое = “Без категории”
Ед. изм.Select: шт / кг / г / л / мл / порцияДа
Артикул (SKU)Text inputНет
ШтрихкодText inputНет
Порядок сортировкиNumber inputНетDefault: 0

Секция “Вес и КБЖУ”

ПолеТипОбязательное
Вес брутто (кг)Number inputНет
Вес нетто (кг)Number inputНет
Калории (ккал/100г)Number inputНет
Белки (г/100г)Number inputНет
Жиры (г/100г)Number inputНет
Углеводы (г/100г)Number inputНет

Секция “Параметры и флаги”

ПолеТипDefault
Время приготовления (мин)Number input
Цвет POSColor input
Свободная цена (is_open_price)Checkboxfalse
Продажа на развес (is_by_weight)Checkboxfalse
Исключить из акций (is_exclude_from_promo)Checkboxfalse
Запрет ручных скидок (is_manual_discount_banned)Checkboxfalse
Только администратор (is_admin_only)Checkboxfalse
Алкоголь (is_alcohol)Checkboxfalse
Табак (is_tobacco)Checkboxfalse
Сахаросодержащий напиток (is_sugary_drink)Checkboxfalse
Маркированный товар ЧЗ (is_marked)Checkboxfalse

Кнопки: “Сохранить” / “Отмена” (возврат к списку).

После успешного создания: редирект на /catalog/products/:id, toast “Товар создан”.


Редактирование

Роут: /catalog/products/:id/edit

Те же поля, что и при создании (включая все секции BR 2.1), предзаполненные текущими значениями.

Изменения применяются мгновенно

Сохранение изменений применяется сразу.

Кнопки: “Сохранить” / “Отмена” (возврат к карточке просмотра).

После успешного сохранения: редирект на /catalog/products/:id, toast “Товар обновлён”.


Ошибки

КодHTTPЧто показываем
NAME_DUPLICATE409”Товар с таким названием уже существует” (inline под полем “Название”)
VALIDATION_ERROR400Подсветка полей с ошибками + текст из details

Состояния карточки

СостояниеЧто показываем
ЗагрузкаSkeleton-форма
Не найден”Товар не найден” + кнопка “Назад к списку”
Несохранённые измененияПри попытке покинуть: модалка “Есть несохранённые изменения. Покинуть страницу?” — “Остаться” / “Покинуть”

Переходы

ОткудаКудаТриггер
СписокКарточка просмотраКлик по названию
СписокФорма созданияКнопка “Добавить товар”
СписокФорма редактированияДействие “Редактировать”
СписокКарточка нового товараДействие “Дублировать” (POST → navigate)
Карточка просмотраФорма редактированияКнопка “Редактировать”
Карточка просмотраКарточка нового товараКнопка “Дублировать”
Форма созданияКарточка просмотраПосле успешного сохранения
Форма редактированияКарточка просмотраПосле успешного сохранения / “Отмена”

Ссылки