Документация ЛК
Модули

Дилеры (менеджеры)

Модуль "Дилеры (менеджеры)"

Версия: 1.0
Дата: 2025-11-22
Статус: Реализован


Содержание

  1. Общее описание
  2. Текущая реализация
  3. Права доступа
  4. Описание полей
  5. UI/UX
  6. Бизнес-логика
  7. Выявленные проблемы
  8. Рекомендации

1. Общее описание

1.1 Предназначение модуля

Модуль "Дилеры (менеджеры)" предназначен для управления пользователями-дилерами — физическими лицами, представляющими дилерские компании. Дилеры могут авторизоваться в системе и создавать заказы от имени своей компании.

Важно: Название "Дилеры (менеджеры)" может вводить в заблуждение. Это не менеджеры компании, а именно дилеры — сотрудники дилерских организаций (group_id = 2).

Основные функции:

  • Ведение реестра пользователей-дилеров
  • Привязка дилера к дилерской компании
  • Управление доступом (активация/деактивация)
  • Авторизация под учётной записью дилера (impersonate)
  • Экспорт списка дилеров в Excel

1.2 Связь с другими модулями

МодульСвязь
Дилеры (компании)Дилер принадлежит компании через company_id
ЗаказыДилер создаёт заказы (dealer_id в заказе)
МенеджерыМенеджер курирует дилера через manager_id
Календарь дилераОтдельный календарь для дилера

1.3 Статистика

МетрикаЗначение
Всего дилеров13
Активных13
Неактивных0
С привязкой к компании13
С Telegram ID0
С назначенным менеджером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

ПолеТипОписание
idbigintPK
group_idint2 для дилеров
company_idFKПривязка к компании
manager_idFKКуратор-менеджер
namestringИмя
lastnamestringФамилия
middlenamestringОтчество
emailstringEmail (уникальный)
passwordstringХэш пароля
phonestringТелефон
tg_idstringID Telegram
activebooleanСтатус активности
created_attimestamp
updated_attimestamp

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.php

3. Права доступа

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_idSelectВыбор организации (searchable)
lastnameTextInputФамилия
nameTextInputИмя
middlenameTextInputОтчество
emailTextInputEmail (unique)
passwordPasswordПароль

При создании автоматически:

  • group_id = 2 (дилер)
  • manager_id = текущий пользователь

4.2 Форма редактирования (Edit.php)

ПолеТипОсобенности
company_idSelectSearchable
lastnameTextInput
nameTextInput
middlenameTextInput
phoneTextInputМаска +7(999) 999 99 99
emailTextInputUnique, ignoreRecord
passwordPasswordHidden если нет прав, хэшируется

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      │
                           └──────────────────┘

Два пути привязки дилера к менеджеру:

  1. Напрямую: dealer.manager_id = manager.id
  2. Через компанию: 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;

Документ подготовлен на основе анализа исходного кода проекта

On this page

Модуль "Дилеры (менеджеры)"Содержание1. Общее описание1.1 Предназначение модуля1.2 Связь с другими модулями1.3 Статистика2. Текущая реализация2.1 Архитектура2.2 Структура данныхПоля дилера в таблице users2.3 Файлы модуля3. Права доступа3.1 Политики3.2 Логика разграниченияФильтрация списка (Listing.php)Проверка доступа к редактированию (Edit.php)3.3 Матрица доступа по ролям4. Описание полей4.1 Форма создания (Add.php)4.2 Форма редактирования (Edit.php)4.3 Обработка пароля5. UI/UX5.1 Список дилеров5.2 Карточка дилера5.3 Экспорт в Excel6. Бизнес-логика6.1 Иерархия связей6.2 Impersonate (авторизация под дилером)6.3 Автоматическое назначение при создании6.4 Проверка принадлежности при удалении6.5 Статус активности7. Выявленные проблемы7.1 Критические7.2 Средние7.3 Низкие8. Рекомендации8.1 Приоритет: ВысокийИсправить проверку в контроллере8.2 Приоритет: СреднийДобавить телефон в форму созданияИсправить опечатку8.3 Приоритет: НизкийДобавить фильтр по активностиДобавить поле Telegram IDПриложение: SQL для аналитикиДилеры без заказовДилеры с количеством заказовНеактивные дилеры