Дилеры (менеджеры)
Модуль "Дилеры (менеджеры)"
Версия: 1.0
Дата: 2025-11-22
Статус: Реализован
Содержание
- Общее описание
- Текущая реализация
- Права доступа
- Описание полей
- UI/UX
- Бизнес-логика
- Выявленные проблемы
- Рекомендации
1. Общее описание
1.1 Предназначение модуля
Модуль "Дилеры (менеджеры)" предназначен для управления пользователями-дилерами — физическими лицами, представляющими дилерские компании. Дилеры могут авторизоваться в системе и создавать заказы от имени своей компании.
Важно: Название "Дилеры (менеджеры)" может вводить в заблуждение. Это не менеджеры компании, а именно дилеры — сотрудники дилерских организаций (group_id = 2).
Основные функции:
- Ведение реестра пользователей-дилеров
- Привязка дилера к дилерской компании
- Управление доступом (активация/деактивация)
- Авторизация под учётной записью дилера (impersonate)
- Экспорт списка дилеров в Excel
1.2 Связь с другими модулями
| Модуль | Связь |
|---|---|
| Дилеры (компании) | Дилер принадлежит компании через company_id |
| Заказы | Дилер создаёт заказы (dealer_id в заказе) |
| Менеджеры | Менеджер курирует дилера через manager_id |
| Календарь дилера | Отдельный календарь для дилера |
1.3 Статистика
| Метрика | Значение |
|---|---|
| Всего дилеров | 13 |
| Активных | 13 |
| Неактивных | 0 |
| С привязкой к компании | 13 |
| С Telegram ID | 0 |
| С назначенным менеджером | 13 |
2. Текущая реализация
2.1 Архитектура
┌─────────────────────────────────────────────────────────────────────────────┐
│ МОДУЛЬ "ДИЛЕРЫ (МЕНЕДЖЕРЫ)" │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ РОУТЫ (routes/web.php) │
│ ├── GET /dealers → DealerController@index → Список дилеров │
│ ├── GET /dealers/add → DealerController@create → Создание дилера │
│ └── GET /dealers/edit/{id} → DealerController@show → Редактирование │
│ │
│ КОНТРОЛЛЕР (DealerController.php) │
│ └── Проверка прав: dealer_can_view OR dealer_manager_can_view │
│ │
│ LIVEWIRE КОМПОНЕНТЫ │
│ ├── Dealers/Listing.php — Filament таблица с экспортом в Excel │
│ ├── Dealers/Add.php — Форма создания дилера │
│ └── Dealers/Edit.php — Форма редактирования + удаление │
│ │
│ МОДЕЛЬ │
│ └── User (group_id = 2) — дилеры хранятся в общей таблице users │
│ │
└─────────────────────────────────────────────────────────────────────────────┘2.2 Структура данных
Дилеры хранятся в таблице users с group_id = 2.
Поля дилера в таблице users
| Поле | Тип | Описание |
|---|---|---|
id | bigint | PK |
group_id | int | 2 для дилеров |
company_id | FK | Привязка к компании |
manager_id | FK | Куратор-менеджер |
name | string | Имя |
lastname | string | Фамилия |
middlename | string | Отчество |
email | string | Email (уникальный) |
password | string | Хэш пароля |
phone | string | Телефон |
tg_id | string | ID Telegram |
active | boolean | Статус активности |
created_at | timestamp | — |
updated_at | timestamp | — |
2.3 Файлы модуля
app/
├── Http/Controllers/
│ └── DealerController.php
├── Livewire/Dealers/
│ ├── Add.php
│ ├── Edit.php
│ └── Listing.php
└── Models/
└── User.php (group_id = 2)
resources/views/templates/dealers/
├── table.blade.php
├── add.blade.php
└── edit.blade.php3. Права доступа
3.1 Политики
| Политика | Описание |
|---|---|
dealer_can_view | Просмотр ВСЕХ дилеров |
dealer_can_add | Создание дилера |
dealer_can_edit | Редактирование ЛЮБОГО дилера |
dealer_can_delete | Удаление любого дилера |
dealer_can_auth | Авторизация под дилером (impersonate) |
dealer_manager_can_view | Просмотр дилеров СВОИХ компаний |
dealer_manager_can_edit | Редактирование дилеров СВОИХ компаний |
dealer_manager_can_delete | Удаление дилеров СВОИХ компаний |
3.2 Логика разграничения
Фильтрация списка (Listing.php)
if ($this->authUser->check_access('dealer_can_view')) {
// Видит ВСЕХ дилеров
$query = User::query()->where('group_id', 2);
} elseif ($this->authUser->check_access('dealer_manager_can_view')) {
// Видит дилеров только тех компаний, где он менеджер
$query = User::query()
->where('group_id', 2)
->whereIn('company_id', function ($q) {
$q->select('id')->from('companies')
->where('manager_id', $this->authUser->id);
});
}Проверка доступа к редактированию (Edit.php)
if ($this->authUser->check_access('dealer_can_edit')) {
$disableEdit = false; // Может редактировать любого
} elseif ($this->authUser->check_access('dealer_manager_can_edit')) {
// Проверяем: это дилер напрямую назначен или принадлежит компании менеджера
$isOwnDealer = ($this->record->manager_id == $this->authUser->id);
$isDealerOfOwnCompany = ($this->record->company->manager_id == $this->authUser->id);
if ($isOwnDealer || $isDealerOfOwnCompany) {
$disableEdit = false;
}
}3.3 Матрица доступа по ролям
| Роль | Просмотр | Создание | Редактир. | Удаление | Impersonate |
|---|---|---|---|---|---|
| Администратор | ✅ Все | ✅ | ✅ Все | ✅ | ✅ |
| Менеджер | ⚠️ Своих | ❌ | ⚠️ Своих | ⚠️ Своих | ❌ |
| Дилер | ❌ | ❌ | ❌ | ❌ | ❌ |
| Производство | ❌ | ❌ | ❌ | ❌ | ❌ |
4. Описание полей
4.1 Форма создания (Add.php)
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
company_id | Select | ❌ | Выбор организации (searchable) |
lastname | TextInput | ❌ | Фамилия |
name | TextInput | ✅ | Имя |
middlename | TextInput | ❌ | Отчество |
email | TextInput | ✅ | Email (unique) |
password | Password | ✅ | Пароль |
При создании автоматически:
group_id = 2(дилер)manager_id = текущий пользователь
4.2 Форма редактирования (Edit.php)
| Поле | Тип | Особенности |
|---|---|---|
company_id | Select | Searchable |
lastname | TextInput | — |
name | TextInput | — |
middlename | TextInput | — |
phone | TextInput | Маска +7(999) 999 99 99 |
email | TextInput | Unique, ignoreRecord |
password | Password | Hidden если нет прав, хэшируется |
4.3 Обработка пароля
TextInput::make('password')
->hidden($disableEdit) // Скрыт если нет прав
->dehydrateStateUsing(fn ($state) => $state ? Hash::make($state) : null)
->dehydrated(fn ($state) => filled($state)) // Сохраняется только если заполнен
->password()
->revealable()5. UI/UX
5.1 Список дилеров
┌─────────────────────────────────────────────────────────────────────────────┐
│ Дилеры (менеджеры) [+ Создать] [Экспорт] │
├─────────────────────────────────────────────────────────────────────────────┤
│ [Поиск...] [Столбцы ▾] │
├──────┬────────────────────┬──────────┬──────────┬──────────┬────────┬───────┤
│ ☐ │ Компания ▾ │ Имя │ Фамилия │ Telegram │Менеджер│Активен│
├──────┼────────────────────┼──────────┼──────────┼──────────┼────────┼───────┤
│ ☐ │ ООО "ЭКОРУСЬ" │ Иван │ Петров │ │ Дубр.В.│ [●] │
│ ☐ │ ООО "ЯРТОРГ" │ Анна │ Сидорова │ │ Марк.С.│ [●] │
└──────┴────────────────────┴──────────┴──────────┴──────────┴────────┴───────┘
│ Показано с 1 по 13 из 13 [25][50][Все] │
└─────────────────────────────────────────────────────────────────────────────┘
Действия в строке:
✅ [Авторизоваться] — войти под учёткой дилера (impersonate)
✅ [Удалить] — удаление с подтверждением
Функции списка:
✅ Поиск по компании, имени, фамилии, Telegram ID, ФИО менеджера
✅ Сортировка по компании, дате создания
✅ Inline-переключатель "Активен" (ToggleColumn)
✅ Массовое удаление
✅ Экспорт в Excel
✅ Скрытые колонки (created_at, updated_at)5.2 Карточка дилера
┌─────────────────────────────────────────────────────────────────────────────┐
│ [← Назад] Редактирование дилера [Удалить] [Сохранить] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Организация [ООО "Компания" ] [×] ▾ │ │
│ │ │ │
│ │ Фамилия [Петров ] │ │
│ │ │ │
│ │ Имя [Иван ] │ │
│ │ │ │
│ │ Отчество [Сергеевич ] │ │
│ │ │ │
│ │ Телефон [+7(999) 123 45 67 ] │ │
│ │ │ │
│ │ Email [ivan@company.ru ] │ │
│ │ │ │
│ │ Пароль [•••••••• ] [👁] │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘5.3 Экспорт в Excel
Кнопка [Экспорт] в шапке таблицы:
- Учитывает текущие фильтры и поиск
- Экспортирует колонки: ID, Компания, Имя, Фамилия, Менеджер, Активен, Создан
- Файл:
dealers_export_YYYYMMDD_HHmmss.xlsx
$filename = 'dealers_export_'.now()->format('Ymd_His').'.xlsx';
$path = storage_path('app/tmp');
(new Xlsx($spreadsheet))->save($path.DIRECTORY_SEPARATOR.$filename);
return response()->download($full)->deleteFileAfterSend(true);6. Бизнес-логика
6.1 Иерархия связей
┌──────────────────┐
│ Менеджер │ (group_id = 3)
│ (User) │
└────────┬─────────┘
│ manager_id
▼
┌──────────────────┐ ┌──────────────────┐
│ Компания │◄──────│ Дилер │ (group_id = 2)
│ (Company) │ │ (User) │
│ │ │ │
│ manager_id ─────┼───────┤ company_id │
└──────────────────┘ │ manager_id │
└──────────────────┘Два пути привязки дилера к менеджеру:
- Напрямую:
dealer.manager_id = manager.id - Через компанию:
dealer.company.manager_id = manager.id
6.2 Impersonate (авторизация под дилером)
Функция позволяет администратору войти в систему под учётной записью дилера:
// В Listing.php
Action::make('auth')
->label('Авторизоваться')
->icon('heroicon-o-user')
->url(fn (User $record): string => route('impersonate', $record->id))Требует право: dealer_can_auth
6.3 Автоматическое назначение при создании
// Add.php — save()
$data['group_id'] = 2; // Группа "Дилер"
$data['manager_id'] = $this->authUser->id; // Текущий пользователь становится менеджером
$record = User::create($data);6.4 Проверка принадлежности при удалении
// Проверка для manager_can_delete
if ($this->authUser->check_access('dealer_manager_can_delete')) {
// Дилер напрямую назначен менеджеру ИЛИ принадлежит компании менеджера
if ($record->manager_id == $this->authUser->id ||
($record->company && $record->company->manager_id == $this->authUser->id)) {
$record->delete();
}
}6.5 Статус активности
В списке дилеров есть ToggleColumn для быстрого включения/выключения:
Tables\Columns\ToggleColumn::make('active')
->disabled(! $disableEdit) // Только с правами редактирования
->onColor('info')7. Выявленные проблемы
7.1 Критические
| # | Проблема | Файл | Влияние |
|---|---|---|---|
| — | Критических проблем не обнаружено | — | — |
7.2 Средние
| # | Проблема | Описание | Решение |
|---|---|---|---|
| 1 | Несогласованность проверки в контроллере | DealerController::show() проверяет manager_id, но дилер может принадлежать компании менеджера | Добавить проверку company.manager_id |
| 2 | Нет телефона в форме создания | Телефон можно добавить только при редактировании | Добавить в Add.php |
| 3 | Нет валидации телефона | Маска есть, но нет серверной валидации | Добавить regex правило |
| 4 | Опечатка в modalHeading | ->modalHeading('Удалить менеджера') — должен быть "дилера" | Исправить текст |
7.3 Низкие
| # | Проблема | Описание |
|---|---|---|
| 5 | Нет истории изменений | Кто и когда менял данные дилера |
| 6 | Нет фильтрации по активности | Только поиск, нет фильтра "Показать только активных" |
| 7 | Путаница в названии | "Дилеры (менеджеры)" — непонятное название модуля |
| 8 | Нет поля tg_id в формах | Telegram ID только в списке, нельзя редактировать |
8. Рекомендации
8.1 Приоритет: Высокий
Исправить проверку в контроллере
// Было (DealerController.php:30-32):
if (!$user->check_access('dealer_can_view')) {
if (!$user->check_access('dealer_manager_can_view')) abort(403);
if ($user->check_access('dealer_manager_can_view') and $dealer->manager_id != $user->id) abort(403);
}
// Стало:
if (!$user->check_access('dealer_can_view')) {
if (!$user->check_access('dealer_manager_can_view')) abort(403);
$isOwnDealer = ($dealer->manager_id == $user->id);
$isDealerOfOwnCompany = ($dealer->company && $dealer->company->manager_id == $user->id);
if (!$isOwnDealer && !$isDealerOfOwnCompany) abort(403);
}8.2 Приоритет: Средний
Добавить телефон в форму создания
// Add.php — добавить в schema после middlename:
TextInput::make('phone')
->mask('+7(999) 999 99 99')
->columnSpanFull(),Исправить опечатку
// Edit.php:137
->modalHeading('Удалить дилера') // Было: 'Удалить менеджера'8.3 Приоритет: Низкий
Добавить фильтр по активности
->filters([
Tables\Filters\TernaryFilter::make('active')
->label('Активность')
->boolean()
->trueLabel('Только активные')
->falseLabel('Только неактивные'),
])Добавить поле Telegram ID
TextInput::make('tg_id')
->label('Telegram ID')
->disabled($disableEdit)
->maxLength(255),Приложение: SQL для аналитики
Дилеры без заказов
SELECT u.id, u.name, u.lastname, c.org
FROM users u
LEFT JOIN companies c ON u.company_id = c.id
WHERE u.group_id = 2
AND NOT EXISTS (SELECT 1 FROM orders o WHERE o.dealer_id = u.id);Дилеры с количеством заказов
SELECT u.id, u.name, u.lastname, c.org, COUNT(o.id) as orders_count
FROM users u
LEFT JOIN companies c ON u.company_id = c.id
LEFT JOIN orders o ON o.dealer_id = u.id
WHERE u.group_id = 2
GROUP BY u.id, u.name, u.lastname, c.org
ORDER BY orders_count DESC;Неактивные дилеры
SELECT u.id, u.name, u.lastname, u.email, c.org
FROM users u
LEFT JOIN companies c ON u.company_id = c.id
WHERE u.group_id = 2 AND u.active = 0;Документ подготовлен на основе анализа исходного кода проекта