Модули
Каталог
Версия: 1.0
Дата: 2025-11-21
Статус: Реализован
Общее описание
Текущая реализация
Описание полей
Права доступа
UI/UX по ролям
Бизнес-логика
Выявленные проблемы
Рекомендации по улучшению
Модуль "Каталог" предназначен для:
Управления каталогом товаров (продукции компании)
Организации товаров по категориям и подкатегориям
Отображения товаров дилерам для оформления заказов
Редактирования цен, характеристик, изображений товаров
Сущность Таблица Описание Категория categoriesГруппировка товаров (ТВЕРЬ LITE, КЕССОНЫ и т.д.) Товар productsКонкретный продукт (КЕССОН Тверь 0,95) Связь категория-товар category_productMany-to-Many связь Характеристика property_productСвойства товара (объём, глубина) Опция optionsДополнительные опции товара (входы, доп.опции)
Тип Флаг additional Описание Основные товары falseСептики, кессоны, погреба — требуют производственный слот Дополнительные товары trueНасосы, компрессоры, химия — не занимают слот
┌─────────────────────────────────────────────────────────────────────────────┐
│ МОДУЛЬ КАТАЛОГ │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ РОУТЫ (routes/web.php) │
│ ├── /catalog → CatalogController@index → Список категорий │
│ ├── /category/{id} → CatalogController@category → Страница категории │
│ ├── /category/{cat}/product/edit/{id} → CatalogController@product → Товар │
│ └── /products → CatalogController@products → Все товары списком │
│ │
│ КОНТРОЛЛЕР (CatalogController.php) │
│ └── Роутинг по group_id: │
│ ├── Дилер (2) → Карточки категорий/товаров (только active=1) │
│ └── Остальные → Filament таблицы с редактированием │
│ │
│ LIVEWIRE КОМПОНЕНТЫ │
│ ├── Category/ │
│ │ ├── Listing.php — Filament таблица категорий │
│ │ └── Control.php — Кнопки Создать/Изменить/Удалить │
│ └── Product/ │
│ ├── Listing.php — Filament таблица товаров в категории │
│ ├── Control.php — Форма редактирования товара (2 вкладки) │
│ ├── All.php — Все товары списком (только для админа) │
│ └── Buy.php — Кнопка "Новый заказ" для дилера │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Поле Тип Описание idbigint PK namestring(255) Название категории imagestring Путь к изображению category_idFK nullable Родительская категория (для подкатегорий) additionalboolean Флаг "Дополнительные товары" indexint Порядок сортировки created_attimestamp — updated_attimestamp —
Поле Тип Описание idbigint PK namestring(255) Название товара typestring Тип товара (Септик, Кессон, Погреб, Другое) add_typestring nullable Подтип для допников (pump, compressor, box...) pricedecimal(8,2) Розничная цена dealer_pricedecimal(8,2) nullable Фиксированная дилерская цена (для допников) price_installdecimal(8,2) Стоимость монтажа step_sizeint Шаг наращивания горловины (мм) step_pricedecimal(8,2) Цена за шаг (розничная) step_price_dealerdecimal(8,2) Цена за шаг (дилерская) timeint Время производства (единиц) activeboolean Активен/скрыт descriptiontext Описание (HTML) imagesjson Массив путей к изображениям filesjson Массив прикреплённых файлов [{name, file}] indexint Порядок сортировки
-- category_product (Many-to-Many)
category_id, product_id
-- manufacture_product (Many-to-Many) — товар на каких заводах
manufacture_id, product_id
-- property_product — характеристики товара
id, product_id, property_id, value
Поле Название в UI Тип ввода Описание nameНазвание TextInput Обязательное imageИзображение FileUpload Картинка категории category_idРодительская категория Select Для подкатегорий additionalДоп.товары Toggle Флаг дополнительных товаров
Поле Название в UI Тип ввода Описание nameНазвание* TextInput Обязательное, max 255 typeТип* Select Септик, Кессон, Погреб, Другое add_typeДополнительный тип Select pump, compressor, box, ruff... priceСтоимость розничная* Number Префикс ₽ dealer_priceДилерская цена Number Фикс. цена для допников price_installСтоимость монтажа Number Цена монтажа step_sizeШаг наращивания Number В мм step_priceСтоимость шага наращивания Number Розничная step_price_dealerСтоимость шага для дилера Number Дилерская activeАктивен* Toggle Показывать ли дилерам manufacturesДоступно на производствах MultiSelect Заводы descriptionОписание RichEditor HTML categoriesКатегории MultiSelect Привязка к категориям timeВремя Number Время производства filesФайлы Repeater [{name, file}]optionsОпции Repeater Входы и доп.опции imagesЗагрузите изображения FileUpload Multiple Галерея
Поле Тип Описание productPropertiesRepeater property_id + value
┌──────────────────────────────────────────────────────────────────┐
│ Опции товара (таблица options) │
├──────────────────────────────────────────────────────────────────┤
│ name — название опции │
│ price — цена │
│ group — тип: 'enter' (вход) или 'single' (доп.опция) │
│ type — для входов: 'end' (торец), 'left', 'right' │
└──────────────────────────────────────────────────────────────────┘
Политика Описание category_can_viewМожет смотреть категории category_can_addМожет добавлять категории category_can_editМожет редактировать категории category_can_deleteМожет удалять категории
Политика Описание product_can_viewМожет смотреть товары product_can_addМожет добавлять товары product_can_editМожет редактировать товары product_can_deleteМожет удалять товары
Роль Просмотр Создание Редактирование Удаление Инлайн-редакт. Администратор ✅ ✅ ✅ ✅ ✅ Менеджер ✅ ⚠️ ⚠️ ⚠️ ⚠️ Дилер ✅ (active=1) ❌ ❌ ❌ ❌ Производство ✅ ❌ ❌ ❌ ❌
┌─────────────────────────────────────────────────────────────────────────────┐
│ Каталог [+ Создать] │
├─────────────────────────────────────────────────────────────────────────────┤
│ [↕ Изменить порядок] [Поиск...] [Столбцы ▾] │
├──────┬────────────┬──────────────────────┬───────────┬──────────┬───────────┤
│ ☐ │ Изображение│ Название │ Доп.товары│ Кол-во │ Действия │
├──────┼────────────┼──────────────────────┼───────────┼──────────┼───────────┤
│ ☐ │ [img] │ ТВЕРЬ CLASSIC │ [ ] │ 0 │ [Удалить] │
│ ☐ │ [img] │ ТВЕРЬ LITE │ [ ] │ 0 │ [Удалить] │
│ ☐ │ [img] │ КЕССОНЫ │ [ ] │ 6 │ [Удалить] │
│ ☐ │ [img] │ Дополнительные товары│ [●] │ 35 │ [Удалить] │
└──────┴────────────┴──────────────────────┴───────────┴──────────┴───────────┘
Функции:
✅ Drag&Drop сортировка (reorderable)
✅ Toggle "Доп.товары" прямо в таблице
✅ Поиск по названию
✅ Удаление (одиночное и массовое)
✅ Переход к категории по клику
┌─────────────────────────────────────────────────────────────────────────────┐
│ [← Назад] Категория КЕССОНЫ [Изменить] [Удалить] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Подкатегории [+ Создать] │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Не найдено categories │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ Продукты [+ Создать] │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ ☐ │ Превью │ Название* │ Тип* │ Цена* │ Монтаж │ Время│Акт.│ │
│ │───│────────│────────────────│───────│──────────│─────────│──────│────│ │
│ │ ☐ │ [img] │[КЕССОН 0,95 ]│[Кессон]│[66100.00]│[46900.00]│[100] │[●] │ │
│ │ ☐ │ [img] │[КЕССОН 0,95У ]│[Кессон]│[79900.00]│[46900.00]│[100] │[●] │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Функции:
✅ Инлайн-редактирование (TextInputColumn, SelectColumn, ToggleColumn)
✅ Создание подкатегорий
✅ Создание товаров с автопривязкой к категории
✅ Drag&Drop сортировка
┌─────────────────────────────────────────────────────────────────────────────┐
│ [← Назад] Продукт КЕССОН Тверь 0,95 [Удалить] [Сохранить] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ [Общая информация] [Характеристики] │
│ ┌──────────────────────────────────────────────┬───────────────────────────┐│
│ │ Название* [КЕССОН Тверь 0,95 ] │ Загрузите изображения ││
│ │ Тип* [Кессон ▾] │ ┌─────────────────────┐ ││
│ │ Дополнительный тип [Выбрать вариант ▾] │ │ [img1] [img2] [img3]│ ││
│ │ │ │ Перетащите файлы │ ││
│ │ Стоимость розничная* [₽ 66100.00 ] │ └─────────────────────┘ ││
│ │ Дилерская цена [₽ ] │ ││
│ │ Стоимость монтажа [₽ 46900.00 ] │ ││
│ │ │ ││
│ │ Шаг наращивания [мм 200 ] │ ││
│ │ Стоимость шага [₽ 5500.00 ] │ ││
│ │ Стоимость для дилера [₽ 4500.00 ] │ ││
│ │ │ ││
│ │ Активен* [●] │ ││
│ │ Доступно на производствах [Выбрать... ▾] │ ││
│ │ │ ││
│ │ Описание │ ││
│ │ [B][I][S][🔗][H2][H3][❝][</>][≡][#][📎] │ ││
│ │ ┌────────────────────────────────────────┐ │ ││
│ │ │ Inventore est in omnis hic nesciunt. │ │ ││
│ │ └────────────────────────────────────────┘ │ ││
│ │ │ ││
│ │ Категории [КЕССОНЫ ×] [+ Добавить] │ ││
│ │ Время [100] │ ││
│ │ │ ││
│ │ Файлы [+ Добавить] │ ││
│ │ Опции [+ Добавить] │ ││
│ └──────────────────────────────────────────────┴───────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Каталог │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ [img] │ │ [img] │ │ [img] │ │
│ │ │ │ │ │ │ │
│ │ ТВЕРЬ LITE │ │ТВЕРЬ CLASSIC │ │ ТВЕРЬ AERO │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ [img] │ │ [img] │ │ [img] │ │
│ │ │ │ │ │ │ │
│ │ КЕССОНЫ │ │ ПОГРЕБА │ │ ТВЕРЬ PRO │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Особенности:
- Только категории с активными товарами
- Карточки с изображениями
- Красивый дизайн (синие заголовки)
┌─────────────────────────────────────────────────────────────────────────────┐
│ [← Назад] КЕССОНЫ │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ [Подкатегория 1] [Подкатегория 2] ← кнопки подкатегорий │
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ [img] КЕССОН Тверь 0,95 │ │ [img] КЕССОН Тверь 0,95У │ │
│ │ │ │ │ │
│ │ Цена 66 100 руб. │ │ Цена 79 900 руб. │ │
│ └──────────────────────────┘ └──────────────────────────┘ │
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ [img] КЕССОН Тверь 1,27 │ │ [img] КЕССОН Тверь 1,27У │ │
│ │ │ │ │ │
│ │ Цена 93 000 руб. │ │ Цена 111 500 руб. │ │
│ └──────────────────────────┘ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Особенности:
- Только активные товары (active=1)
- Отображение цены и характеристик
- Товары из подкатегорий тоже показываются
┌─────────────────────────────────────────────────────────────────────────────┐
│ [← Назад] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ КЕССОН Тверь 0,95 │
│ │ │ Цена 66 100 руб. │
│ │ [IMAGE] │ │
│ │ │ [🛒 Новый заказ] │
│ │ │ │
│ └────────────────┘ │
│ │
│ [Описание] [Файлы] │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Inventore est in omnis hic nesciunt. │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Функции:
✅ Просмотр изображения (Fancybox галерея)
✅ Вкладки Описание / Файлы
✅ Кнопка "Новый заказ" — переход к оформлению заказа
✅ Скачивание файлов
// Product::getDealerPrice($dealer)
if ( $this -> isAdditionalProduct ()) {
// Для допников — фиксированная дилерская цена
return $this -> dealer_price;
}
// Для основных товаров — цена со скидкой дилера
$basePrice = $this -> price;
$topCategory = $this -> categories -> first () -> getTopLevelParent ();
$discount = $dealer -> discount ($topCategory -> id); // % скидки на категорию
$dealerPrice = $basePrice * ( 1 - $discount / 100 );
// Product::isAdditionalProduct()
// Товар является дополнительным, если его корневая категория
// имеет флаг additional = true
foreach ( $this -> categories as $category) {
$parent = $category -> getTopLevelParent ();
if ($parent && $parent -> additional) {
return true ;
}
}
return false ;
Поле Описание step_sizeШаг в мм (обычно 200мм) step_priceРозничная цена за шаг step_price_dealerДилерская цена за шаг
Итоговая цена = базовая_цена + (количество_шагов × цена_шага)
Поле time определяет, сколько производственных единиц занимает товар
Используется в календаре для расчёта свободного времени
Допники (additional=true) не занимают производственный слот
# Проблема Влияние Решение 1 Нет проверки прав category_can_add Безопасность Добавить в Control.php 2 Нет валидации при инлайн-редактировании Целостность данных Добавить afterStateUpdated
# Проблема Влияние Решение 3 Дилер видит розничную цену Неверное отображение Показывать getDealerPrice() 4 Нет кеширования категорий Производительность Добавить Cache 5 N+1 в cat_list.blade.php Производительность Eager loading 6 Дублирование price_install в Listing.phpКод Удалить дубль
# Проблема Влияние Решение 7 Неиспользуемый Log::emergency Засорение логов Удалить или заменить 8 Хардкод add_type Поддержка Вынести в конфиг или БД 9 Нет превью файлов UX Добавить иконки типов файлов
Файл: resources/views/components/catalog/product.blade.php
// Было:
@ formatPrice ($product -> price) руб .
// Стало:
@ php
$dealer = auth () -> user ();
$displayPrice = $product -> getDealerPrice ($dealer);
@ endphp
@ formatPrice ($displayPrice) руб .
Файл: app/Livewire/Category/Control.php
public function createAction () : CreateAction
{
$canAdd = $this -> authUser ?-> check_access ( 'category_can_add' ) ?? false ;
return CreateAction :: make ( 'create' )
-> visible ($canAdd) // Скрыть кнопку если нет прав
// ...
}
// В контроллере
$categories = Cache :: remember ( 'catalog_categories' , 3600 , function () {
return Category :: whereNull ( 'category_id' )
-> whereHas ( 'products' , fn ($q) => $q -> where ( 'active' , 1 ))
-> get ();
});
// CatalogController::category()
$category -> load ([
'products' => fn ($q) => $q -> where ( 'active' , 1 ) -> with ( 'productProperties.property' ),
'children.products' => fn ($q) => $q -> where ( 'active' , 1 ) -> with ( 'productProperties.property' ),
]);
// Создать таблицу product_subtypes
Schema :: create ( 'product_subtypes' , function ( Blueprint $table) {
$table -> id ();
$table -> string ( 'key' ); // pump, compressor...
$table -> string ( 'name' ); // Насос, Компрессор...
$table -> timestamps ();
});
app/
├── Http/Controllers/
│ └── CatalogController.php
├── Livewire/
│ ├── Category/
│ │ ├── Control.php
│ │ └── Listing.php
│ └── Product/
│ ├── All.php
│ ├── Buy.php
│ ├── Control.php
│ └── Listing.php
└── Models/
├── Category.php
├── Product.php
└── Option.php
database/migrations/
├── 2024_03_07_104535_create_categories_table.php
├── 2024_03_07_104550_create_products_table.php
├── 2024_03_07_113802_create_category_product_table.php
├── 2024_04_23_123318_add_field_to_categories_table.php # additional
├── 2024_04_26_165816_add_field_to_products_table.php # dealer_price
└── 2024_05_14_170055_add_step_price_dealer_to_products_table.php
resources/views/
├── components/catalog/
│ ├── category.blade.php # Карточка категории для дилера
│ ├── subcategory.blade.php # Кнопка подкатегории
│ ├── product.blade.php # Карточка товара для дилера
│ └── gallery.blade.php # Галерея изображений
├── livewire/product/
│ └── buy.blade.php # Кнопка "Новый заказ"
└── templates/
├── categories/
│ ├── catalog.blade.php # Список категорий (админ)
│ ├── category.blade.php # Страница категории (админ)
│ ├── list.blade.php # Список категорий (дилер)
│ └── cat_list.blade.php # Страница категории (дилер)
└── products/
├── all.blade.php # Все товары (только админ)
├── edit.blade.php # Редактирование (админ)
└── show.blade.php # Просмотр (дилер)
SELECT
c . name as category,
COUNT ( cp . product_id ) as products_count,
SUM ( CASE WHEN p . active = 1 THEN 1 ELSE 0 END ) as active_count
FROM categories c
LEFT JOIN category_product cp ON c . id = cp . category_id
LEFT JOIN products p ON cp . product_id = p . id
WHERE c . category_id IS NULL
GROUP BY c . id
ORDER BY c . index ;
SELECT p . id , p . name , p . active
FROM products p
LEFT JOIN category_product cp ON p . id = cp . product_id
WHERE cp . category_id IS NULL ;
Документ подготовлен на основе анализа исходного кода проекта