Каталог — Товары
Два экрана: Список товаров и Карточка товара (просмотр / создание / редактирование).
Список товаров
Роут: /catalog/products
API: GET /api/v1/products
Что видит пользователь
Страница со списком товаров каталога в виде таблицы. Вверху — заголовок “Товары”, строка поиска и фильтры. Справа от заголовка — кнопка “Добавить товар” (только Franchise). Под заголовком — табы: “Активные” (default) и “Удалённые” (только Franchise).
Таблица
Колонки
| Колонка | Данные | Примечание |
|---|---|---|
| Фото | image_url | 36×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 | |
| Цвет POS | color | Цветной квадратик + 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_rate | Select: Без НДС / 0% / 10% / 20% / 10/110 / 20/120. Default 20% |
| Предмет расчёта | payment_subject | Select: Товар / Услуга / Работа / Подакцизный / Плата / Агентский / Составной / Иное / Работа/услуга. Default Товар |
| Способ расчёта | payment_type | Select: Полный расчёт / Предоплата / Аванс / Частичная предоплата / Кредит / Оплата кредита / Частичный расчёт. 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)
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 | Групповой / Простой |
| Min | override_min_amount или min_amount | Inline-редактирование |
| Max | override_max_amount или max_amount | Inline-редактирование |
| Опций | options.length | Количество опций в группе |
| Действия | — | “Убрать” |
Override
Если
override_min_amount/override_max_amountзаданы — показываются с подсветкой (отличаются от значений группы). При наведении — tooltip “Значение группы: min=0, max=3”.
”Добавить модификатор” → модалка
- Выбрать группу из справочника (поиск по
GET /api/v1/modifier-groups) - Override min/max (опционально)
- 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)
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)
- При открытии модалки — сразу загрузить первые 20 ингредиентов (без ввода в поиск). Если > 20 — кнопка “Показать ещё” для подгрузки. При вводе текста — фильтрация, сброс пагинации.
- Поиск по ингредиентам:
GET /api/v1/ingredients?search=...(Warehouse Service) (BR 1.11) - Если ингредиент не найден — кнопка “Создать ингредиент”:
- Быстрая форма: название, единица измерения
- API:
POST /api/v1/ingredients(Warehouse Service) (BR 1.11)
- Выбрав ингредиент — заполнить поля: брутто, нетто, единица, % потерь
- 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 | — |
| Цвет POS | Color input | — |
Свободная цена (is_open_price) | Checkbox | false |
Продажа на развес (is_by_weight) | Checkbox | false |
Исключить из акций (is_exclude_from_promo) | Checkbox | false |
Запрет ручных скидок (is_manual_discount_banned) | Checkbox | false |
Только администратор (is_admin_only) | Checkbox | false |
Алкоголь (is_alcohol) | Checkbox | false |
Табак (is_tobacco) | Checkbox | false |
Сахаросодержащий напиток (is_sugary_drink) | Checkbox | false |
Маркированный товар ЧЗ (is_marked) | Checkbox | false |
Кнопки: “Сохранить” / “Отмена” (возврат к списку).
После успешного создания: редирект на /catalog/products/:id, toast “Товар создан”.
Редактирование
Роут: /catalog/products/:id/edit
Те же поля, что и при создании (включая все секции BR 2.1), предзаполненные текущими значениями.
Изменения применяются мгновенно
Сохранение изменений применяется сразу.
Кнопки: “Сохранить” / “Отмена” (возврат к карточке просмотра).
После успешного сохранения: редирект на /catalog/products/:id, toast “Товар обновлён”.
Ошибки
| Код | HTTP | Что показываем |
|---|---|---|
NAME_DUPLICATE | 409 | ”Товар с таким названием уже существует” (inline под полем “Название”) |
VALIDATION_ERROR | 400 | Подсветка полей с ошибками + текст из details |
Состояния карточки
| Состояние | Что показываем |
|---|---|
| Загрузка | Skeleton-форма |
| Не найден | ”Товар не найден” + кнопка “Назад к списку” |
| Несохранённые изменения | При попытке покинуть: модалка “Есть несохранённые изменения. Покинуть страницу?” — “Остаться” / “Покинуть” |
Переходы
| Откуда | Куда | Триггер |
|---|---|---|
| Список | Карточка просмотра | Клик по названию |
| Список | Форма создания | Кнопка “Добавить товар” |
| Список | Форма редактирования | Действие “Редактировать” |
| Список | Карточка нового товара | Действие “Дублировать” (POST → navigate) |
| Карточка просмотра | Форма редактирования | Кнопка “Редактировать” |
| Карточка просмотра | Карточка нового товара | Кнопка “Дублировать” |
| Форма создания | Карточка просмотра | После успешного сохранения |
| Форма редактирования | Карточка просмотра | После успешного сохранения / “Отмена” |