Модули
Товары списком
Версия: 1.0
Дата: 2025-11-21
Статус: Реализован
Общее описание
Текущая реализация
Права доступа
Функции и возможности
UI/UX
Выявленные проблемы
Сравнение с модулем Каталог
Рекомендации по улучшению
Модуль "Товары списком" предоставляет плоский список всех товаров системы без группировки по категориям.
Основные цели:
Быстрый поиск товара по названию
Массовое редактирование товаров (инлайн)
Просмотр и редактирование товаров без категории
Быстрая сортировка и фильтрация всех товаров
Критерий Каталог Товары списком Структура Иерархия категорий → товары Плоский список всех товаров Доступ Все пользователи с category_can_view Только администраторы (group_id == 1) Создание товаров ✅ Да (с привязкой к категории) ❌ Нет Товары без категории ❌ Не видны ✅ Видны Назначение Основная работа с каталогом Административный инструмент
Метрика Значение Всего товаров 145 С категориями 142 Без категорий 3 Типы Септик (69), Другое (63), Погреб (7), Кессон (6)
┌─────────────────────────────────────────────────────────────────────────────┐
│ МОДУЛЬ "ТОВАРЫ СПИСКОМ" │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ РОУТ (routes/web.php) │
│ └── GET /products → CatalogController@products │
│ │
│ КОНТРОЛЛЕР (CatalogController.php) │
│ └── products() — проверка group_id == 1, возврат view │
│ │
│ LIVEWIRE КОМПОНЕНТ │
│ └── Product/All.php — Filament Table со всеми товарами │
│ │
│ BLADE ШАБЛОН │
│ └── templates/products/all.blade.php │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Файл Описание routes/web.phpРоут /products app/Http/Controllers/CatalogController.phpМетод products() app/Livewire/Product/All.phpLivewire компонент с Filament Table resources/views/templates/products/all.blade.phpBlade шаблон app/View/Components/Sidebar.phpПункт меню (строка 88-89)
// CatalogController.php
public function products ( Request $request)
{
$user = $request -> user ();
if ( ! $user or $user -> group_id != 1 ) // ТОЛЬКО АДМИНИСТРАТОР
abort ( 403 );
return view ( 'templates.products.all' );
}
// Sidebar.php (строки 88-89)
if ( $this -> authUser -> group_id == 1 ) {
$this -> items[ 'products' ] = 'Товары списком' ;
}
Проверка Где Описание group_id == 1CatalogController@productsДоступ только администраторам group_id == 1Sidebar.phpОтображение пункта меню ❌ Нет проверки прав Product/All.phpУдаление без проверки
Роль Доступ к модулю Просмотр Инлайн-редактирование Удаление Администратор ✅ ✅ ✅ ✅ Менеджер ❌ 403 — — — Дилер ❌ 403 — — — Производство ❌ 403 — — —
В Product/All.php нет проверки прав на удаление (product_can_delete):
// Product/All.php (строки 47-57)
-> actions ([
Action :: make ( 'delete' ) -> label ( 'Удалить' )
-> icon ( 'heroicon-o-trash' )
-> requiresConfirmation ()
-> action ( fn ( Product $record) => $record -> delete ()) // ⚠️ Нет проверки!
])
-> bulkActions ([
BulkAction :: make ( 'delete' ) -> label ( 'Удалить отмеченные' )
-> requiresConfirmation ()
-> action ( fn ( Collection $records) => $records -> each -> delete ()) // ⚠️ Нет проверки!
])
Функция Описание Реализация Просмотр списка Все товары в плоском списке Filament Table Поиск По названию и типу searchable()Сортировка По цене, времени, монтажу sortable()Инлайн-редактирование Прямо в таблице TextInputColumn, ToggleColumnDrag&Drop сортировка Изменение порядка reorderable('index')Удаление Одиночное и массовое Actions + BulkActions Переход к карточке Клик по строке recordUrl()Скрытие столбцов Toggle колонок toggleable()Пагинация 5/10/25/50/Все Filament defaults
Функция Причина Рекомендация Создание товара Не реализовано Добавить CreateAction Фильтры Закомментированы Добавить фильтр по типу, активности Экспорт Не реализован Добавить ExportAction Просмотр категорий Не отображается Добавить колонку категорий
┌─────────────────────────────────────────────────────────────────────────────┐
│ Товары списком │
├─────────────────────────────────────────────────────────────────────────────┤
│ [↕ Изменить порядок] [Поиск...] [Столбцы ▾] │
├──────┬────────┬──────────────────┬───────┬───────┬────────┬─────┬─────┬─────┤
│ ☐ │Превью │Название │Тип │Цена ↕ │Монтаж ↕│Время│Акт. │ ··· │
├──────┼────────┼──────────────────┼───────┼───────┼────────┼─────┼─────┼─────┤
│ ☐ │[img] │[КЕССОН Тверь 0,95]│[Кессон]│[66100]│[46900] │[100]│ [●] │[Уд] │
│ ☐ │[img] │[Тверь CLASSIC 2П]│[Септик]│[296900│[49500] │[150]│ [●] │[Уд] │
│ ... │ │ │ │ │ │ │ │ │
└──────┴────────┴──────────────────┴───────┴───────┴────────┴─────┴─────┴─────┘
│ Показано с 1 по 10 из 145 [5][10][25][50][Все] [1][2]...[15][→]│
└─────────────────────────────────────────────────────────────────────────────┘
Колонка Тип Инлайн-редакт. Сортировка Поиск Превью (images) ImageColumn ❌ ❌ ❌ Название (name) TextInputColumn ✅ ❌ ✅ Тип (type) TextInputColumn ✅ ❌ ✅ Цена (price) TextInputColumn ✅ ✅ ❌ Стоимость монтажа TextInputColumn ✅ ✅ ❌ Время (time) TextInputColumn ✅ ✅ ❌ Активен (active) ToggleColumn ✅ ❌ ❌
Колонка Описание Дилерская цена (dealer_price) Фикс. цена для допников Шаг наращивания (step_size) В мм Стоимость шага (step_price) Розничная Стоимость шага для дилера Дилерская Дата создания created_at Дата обновления updated_at
TextInputColumn — изменение сохраняется при потере фокуса
ToggleColumn — мгновенное переключение активности
Нет валидации — можно ввести невалидные значения
Нет уведомлений — нет обратной связи после сохранения
# Проблема Файл Строки Влияние 1 Нет проверки прав на удаление Product/All.php47-57 Безопасность 2 Нет валидации инлайн-ввода Product/All.php79-106 Целостность данных
# Проблема Описание Решение 3 Избыточная проверка authUser $this->authUser and $this->authUser (строка 72)Упростить 4 Нет отображения категории Товар может быть в нескольких категориях Добавить колонку 5 Нет фильтров Закомментированы Раскомментировать и добавить 6 Товары без категории Ссылка на /category/none/product/edit/{id} Исправить fallback
# Проблема Описание 7 Нет кнопки создания Нельзя создать товар из этого модуля 8 Дублирование кода Похожий код в Product/Listing.php 9 Нет экспорта Полезно для отчётов
Компонент Product/All.php Product/Listing.php Источник данных Product::query()$category->products()Проверка прав удаления ❌ Нет ✅ product_can_delete Колонка type TextInputColumn SelectColumn Дублирование price_install ❌ Нет ⚠️ Да (строки 111-112, 125-126)
// Product/All.php — нет проверки прав
-> actions ([
Action :: make ( 'delete' )
-> action ( fn ( Product $record) => $record -> delete ())
])
// Product/Listing.php — есть проверка
$can_delete = $this -> authUser -> check_access ( 'product_can_delete' );
// ...
if ($can_delete)
$out[] = Action :: make ( 'delete' ) ...
Файл: app/Livewire/Product/All.php
public function table ( Table $table) : Table
{
$can_delete = $this -> authUser -> check_access ( 'product_can_delete' );
return $table
-> query ( Product :: query ())
// ...
-> actions (
$can_delete ? [
Action :: make ( 'delete' ) -> label ( 'Удалить' )
-> icon ( 'heroicon-o-trash' )
-> requiresConfirmation ()
-> action ( fn ( Product $record) => $record -> delete ())
] : []
)
-> bulkActions (
$can_delete ? [
BulkAction :: make ( 'delete' ) -> label ( 'Удалить отмеченные' )
-> requiresConfirmation ()
-> action ( fn ( Collection $records) => $records -> each -> delete ())
-> deselectRecordsAfterCompletion ()
] : []
);
}
Tables\Columns\TextInputColumn :: make ( 'price' )
-> rules ([ 'required' , 'numeric' , 'min:0' ])
-> afterStateUpdated ( function ($record, $state) {
Notification :: make ()
-> title ( 'Цена обновлена' )
-> success ()
-> send ();
})
-> sortable (),
Tables\Columns\TextColumn :: make ( 'categories.name' )
-> label ( 'Категории' )
-> badge ()
-> separator ( ',' )
-> toggleable (),
-> filters ([
Tables\Filters\SelectFilter :: make ( 'type' )
-> options ( Type :: all () -> pluck ( 'name' , 'name' )),
Tables\Filters\TernaryFilter :: make ( 'active' )
-> label ( 'Активен' ),
Tables\Filters\Filter :: make ( 'no_category' )
-> label ( 'Без категории' )
-> query ( fn ( Builder $query) => $query -> doesntHave ( 'categories' )),
])
-> recordUrl ( fn ($record) =>
$record -> categories -> count ()
? route ( 'product-edit' , [
'category_id' => $record -> categories -> first () -> id,
'id' => $record -> id
])
: route ( 'products' ) // Остаёмся на странице списка
);
// Было (строка 72):
if ( $this -> authUser and $this -> authUser) {
// Стало:
if ( $this -> authUser) {
-> headerActions ([
Tables\Actions\CreateAction :: make ()
-> model ( Product ::class )
-> form ([
TextInput :: make ( 'name' ) -> required (),
Select :: make ( 'type' ) -> options ( Type :: all () -> pluck ( 'name' , 'name' )) -> required (),
// ...
])
])
<? php
namespace App\Livewire\Product ;
use App\Models\Product ;
use App\Models\User ;
use Filament\Tables ;
use Filament\Tables\Table ;
use Filament\Tables\Actions\Action ;
use Filament\Tables\Actions\BulkAction ;
use Livewire\Component ;
use Illuminate\Database\Eloquent\Collection ;
class All extends Component implements HasForms , HasTable
{
use InteractsWithForms , InteractsWithTable ;
public User $authUser;
public function mount ()
{
$this -> authUser = Request :: user ();
}
public function table ( Table $table) : Table
{
return $table
-> query ( Product :: query ())
-> inverseRelationship ( 'categories' )
-> columns ( $this -> getTableColumns ())
-> reorderable ( 'index' )
-> defaultSort ( 'index' , 'asc' )
-> filters ([ /* пусто */ ])
-> actions ([
Action :: make ( 'delete' ) -> label ( 'Удалить' )
-> icon ( 'heroicon-o-trash' )
-> requiresConfirmation ()
-> action ( fn ( Product $record) => $record -> delete ())
])
-> bulkActions ([
BulkAction :: make ( 'delete' ) -> label ( 'Удалить отмеченные' )
-> requiresConfirmation ()
-> action ( fn ( Collection $records) => $records -> each -> delete ())
-> deselectRecordsAfterCompletion ()
])
-> recordUrl ( fn ($record) =>
$record -> categories -> count ()
? route ( 'product-edit' , [ 'category_id' => $record -> categories -> first () -> id, 'id' => $record -> id])
: route ( 'product-edit' , [ 'category_id' => 'none' , 'id' => $record -> id])
);
}
public function getTableColumns ()
{
// Возвращает колонки с инлайн-редактированием
// TextInputColumn, ToggleColumn, toggleable() для скрытых
}
}
При клике на товар открывается карточка редактирования (Product/Control.php) с двумя вкладками:
Поле Тип Обязательность Описание name TextInput ✅ required Название товара type Select ✅ required Тип (Септик, Кессон, Погреб, Другое) add_type Select ❌ Подтип для допников price TextInput (numeric) ✅ required Розничная цена dealer_price TextInput (numeric) ❌ Дилерская цена price_install TextInput (numeric) ❌ Стоимость монтажа step_size TextInput (numeric) ❌ Шаг наращивания (мм) step_price TextInput (numeric) ❌ Цена за шаг step_price_dealer TextInput (numeric) ❌ Дилерская цена за шаг active Toggle ✅ required Активность manufactures MultiSelect ❌ Заводы description RichEditor ❌ Описание HTML categories MultiSelect ❌ Категории time TextInput (numeric) ❌ Время производства files Repeater ❌ Файлы [{name, file}] options Repeater ❌ Опции [{name, price, group, type}] images FileUpload multiple ❌ Изображения
Поле Тип Обязательность Описание productProperties Repeater ❌ Связь property_id + value
SELECT p . id , p . name , p . type , p . active
FROM products p
LEFT JOIN category_product cp ON p . id = cp . product_id
WHERE cp . category_id IS NULL ;
SELECT type , COUNT ( * ) as cnt,
SUM ( CASE WHEN active = 1 THEN 1 ELSE 0 END ) as active_cnt
FROM products
GROUP BY type
ORDER BY cnt DESC ;
Документ подготовлен на основе анализа исходного кода проекта