483 lines
17 KiB
PHP
483 lines
17 KiB
PHP
<?php
|
||
|
||
namespace App\Modules\Clients\Controllers;
|
||
|
||
use App\Controllers\BaseController;
|
||
use App\Modules\Clients\Models\ClientModel;
|
||
use App\Services\AccessService;
|
||
|
||
class Clients extends BaseController
|
||
{
|
||
protected ClientModel $clientModel;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->clientModel = new ClientModel();
|
||
}
|
||
|
||
public function index()
|
||
{
|
||
// Проверка права на просмотр
|
||
if (!$this->access->canView('clients')) {
|
||
return $this->forbiddenResponse('У вас нет прав для просмотра клиентов');
|
||
}
|
||
|
||
$config = $this->getTableConfig();
|
||
|
||
return $this->renderTwig('@Clients/index', [
|
||
'title' => 'Клиенты',
|
||
'tableHtml' => $this->renderTable($config),
|
||
'can_create' => $this->access->canCreate('clients'),
|
||
'can_edit' => $this->access->canEdit('clients'),
|
||
'can_delete' => $this->access->canDelete('clients'),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Конфигурация таблицы клиентов
|
||
*/
|
||
protected function getTableConfig(): array
|
||
{
|
||
return [
|
||
'id' => 'clients-table',
|
||
'url' => '/clients/table',
|
||
'model' => $this->clientModel,
|
||
'columns' => [
|
||
'name' => ['label' => 'Имя / Название', 'width' => '40%'],
|
||
'email' => ['label' => 'Email', 'width' => '25%'],
|
||
'phone' => ['label' => 'Телефон', 'width' => '20%'],
|
||
],
|
||
'searchable' => ['name', 'email', 'phone'],
|
||
'sortable' => ['name', 'email', 'phone', 'created_at'],
|
||
'defaultSort' => 'name',
|
||
'order' => 'asc',
|
||
'actions' => ['label' => 'Действия', 'width' => '15%'],
|
||
'actionsConfig' => [
|
||
[
|
||
'label' => '',
|
||
'url' => '/clients/edit/{id}',
|
||
'icon' => 'fa-solid fa-pen',
|
||
'class' => 'btn-outline-primary',
|
||
'title' => 'Редактировать',
|
||
'type' => 'edit',
|
||
],
|
||
[
|
||
'label' => '',
|
||
'url' => '/clients/delete/{id}',
|
||
'icon' => 'fa-solid fa-trash',
|
||
'class' => 'btn-outline-danger',
|
||
'title' => 'Удалить',
|
||
'type' => 'delete',
|
||
]
|
||
],
|
||
'onRowClick' => 'viewClient', // Функция для открытия карточки клиента
|
||
'emptyMessage' => 'Клиентов пока нет',
|
||
'emptyIcon' => 'fa-solid fa-users',
|
||
'emptyActionUrl' => base_url('/clients/new'),
|
||
'emptyActionLabel'=> 'Добавить клиента',
|
||
'emptyActionIcon' => 'fa-solid fa-plus',
|
||
'can_edit' => $this->access->canEdit('clients'),
|
||
'can_delete' => $this->access->canDelete('clients'),
|
||
];
|
||
}
|
||
|
||
public function table(?array $config = null, ?string $pageUrl = null)
|
||
{
|
||
// Проверка права на просмотр
|
||
if (!$this->access->canView('clients')) {
|
||
return $this->forbiddenResponse('У вас нет прав для просмотра клиентов');
|
||
}
|
||
|
||
return parent::table($config, '/clients');
|
||
}
|
||
|
||
public function new()
|
||
{
|
||
// Проверка права на создание
|
||
if (!$this->access->canCreate('clients')) {
|
||
return $this->forbiddenResponse('У вас нет прав для создания клиентов');
|
||
}
|
||
|
||
$data = [
|
||
'title' => 'Добавить клиента',
|
||
'client' => null,
|
||
];
|
||
|
||
return $this->renderTwig('@Clients/form', $data);
|
||
}
|
||
|
||
public function create()
|
||
{
|
||
// Проверка права на создание
|
||
if (!$this->access->canCreate('clients')) {
|
||
return $this->forbiddenResponse('У вас нет прав для создания клиентов');
|
||
}
|
||
|
||
$organizationId = session()->get('active_org_id');
|
||
|
||
$rules = [
|
||
'name' => 'required|min_length[2]|max_length[255]',
|
||
'email' => 'permit_empty|valid_email',
|
||
'phone' => 'permit_empty|max_length[50]',
|
||
];
|
||
|
||
if (!$this->validate($rules)) {
|
||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||
}
|
||
|
||
$this->clientModel->insert([
|
||
'organization_id' => $organizationId,
|
||
'name' => $this->request->getPost('name'),
|
||
'email' => $this->request->getPost('email') ?? null,
|
||
'phone' => $this->request->getPost('phone') ?? null,
|
||
'notes' => $this->request->getPost('notes') ?? null,
|
||
]);
|
||
|
||
if ($this->clientModel->errors()) {
|
||
return redirect()->back()->withInput()->with('error', 'Ошибка при создании клиента');
|
||
}
|
||
|
||
session()->setFlashdata('success', 'Клиент успешно добавлен');
|
||
return redirect()->to('/clients');
|
||
}
|
||
|
||
public function edit($id)
|
||
{
|
||
// Проверка права на редактирование
|
||
if (!$this->access->canEdit('clients')) {
|
||
return $this->forbiddenResponse('У вас нет прав для редактирования клиентов');
|
||
}
|
||
|
||
$client = $this->clientModel->forCurrentOrg()->find($id);
|
||
|
||
if (!$client) {
|
||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Клиент не найден');
|
||
}
|
||
|
||
$data = [
|
||
'title' => 'Редактировать клиента',
|
||
'client' => $client,
|
||
];
|
||
|
||
return $this->renderTwig('@Clients/form', $data);
|
||
}
|
||
|
||
public function update($id)
|
||
{
|
||
// Проверка права на редактирование
|
||
if (!$this->access->canEdit('clients')) {
|
||
return $this->forbiddenResponse('У вас нет прав для редактирования клиентов');
|
||
}
|
||
|
||
// Проверяем что клиент принадлежит организации через forCurrentOrg()
|
||
$client = $this->clientModel->forCurrentOrg()->find($id);
|
||
|
||
if (!$client) {
|
||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Клиент не найден');
|
||
}
|
||
|
||
$rules = [
|
||
'name' => 'required|min_length[2]|max_length[255]',
|
||
'email' => 'permit_empty|valid_email',
|
||
'phone' => 'permit_empty|max_length[50]',
|
||
];
|
||
|
||
if (!$this->validate($rules)) {
|
||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||
}
|
||
|
||
$this->clientModel->update($id, [
|
||
'name' => $this->request->getPost('name'),
|
||
'email' => $this->request->getPost('email') ?? null,
|
||
'phone' => $this->request->getPost('phone') ?? null,
|
||
'notes' => $this->request->getPost('notes') ?? null,
|
||
]);
|
||
|
||
if ($this->clientModel->errors()) {
|
||
return redirect()->back()->withInput()->with('error', 'Ошибка при обновлении клиента');
|
||
}
|
||
|
||
session()->setFlashdata('success', 'Клиент успешно обновлён');
|
||
return redirect()->to('/clients');
|
||
}
|
||
|
||
public function delete($id)
|
||
{
|
||
// Проверка права на удаление
|
||
if (!$this->access->canDelete('clients')) {
|
||
return $this->forbiddenResponse('У вас нет прав для удаления клиентов');
|
||
}
|
||
|
||
// Проверяем что клиент принадлежит организации через forCurrentOrg()
|
||
$client = $this->clientModel->forCurrentOrg()->find($id);
|
||
|
||
if (!$client) {
|
||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Клиент не найден');
|
||
}
|
||
|
||
$this->clientModel->delete($id);
|
||
|
||
session()->setFlashdata('success', 'Клиент удалён');
|
||
return redirect()->to('/clients');
|
||
}
|
||
|
||
/**
|
||
* Возврат ответа "Доступ запрещён"
|
||
*
|
||
* @param string $message
|
||
* @return ResponseInterface
|
||
*/
|
||
protected function forbiddenResponse(string $message = 'Доступ запрещён')
|
||
{
|
||
if ($this->request->isAJAX()) {
|
||
return service('response')
|
||
->setStatusCode(403)
|
||
->setJSON(['error' => $message]);
|
||
}
|
||
|
||
session()->setFlashdata('error', $message);
|
||
return redirect()->to('/');
|
||
}
|
||
|
||
// ========================================
|
||
// API: Просмотр, Экспорт, Импорт
|
||
// ========================================
|
||
|
||
/**
|
||
* API: Получение данных клиента для модального окна
|
||
*/
|
||
public function view($id)
|
||
{
|
||
if (!$this->access->canView('clients')) {
|
||
return $this->response->setJSON([
|
||
'success' => false,
|
||
'error' => 'Доступ запрещён'
|
||
])->setStatusCode(403);
|
||
}
|
||
|
||
$client = $this->clientModel->forCurrentOrg()->find($id);
|
||
|
||
if (!$client) {
|
||
return $this->response->setJSON([
|
||
'success' => false,
|
||
'error' => 'Клиент не найден'
|
||
])->setStatusCode(404);
|
||
}
|
||
|
||
// Формируем данные для ответа
|
||
$data = [
|
||
'id' => $client['id'],
|
||
'name' => $client['name'],
|
||
'email' => $client['email'] ?? '',
|
||
'phone' => $client['phone'] ?? '',
|
||
'notes' => $client['notes'] ?? '',
|
||
'status' => $client['status'] ?? 'active',
|
||
'created_at' => $client['created_at'] ? date('d.m.Y H:i', strtotime($client['created_at'])) : '',
|
||
'updated_at' => $client['updated_at'] ? date('d.m.Y H:i', strtotime($client['updated_at'])) : '',
|
||
];
|
||
|
||
return $this->response->setJSON([
|
||
'success' => true,
|
||
'data' => $data
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Экспорт клиентов
|
||
*/
|
||
public function export()
|
||
{
|
||
if (!$this->access->canView('clients')) {
|
||
return $this->forbiddenResponse('Доступ запрещён');
|
||
}
|
||
|
||
$format = $this->request->getGet('format') ?? 'csv';
|
||
|
||
// Получаем всех клиентов организации
|
||
$clients = $this->clientModel->forCurrentOrg()->findAll();
|
||
|
||
// Устанавливаем заголовки для скачивания
|
||
if ($format === 'xlsx') {
|
||
$filename = 'clients_' . date('Y-m-d') . '.xlsx';
|
||
$this->exportToXlsx($clients, $filename);
|
||
} else {
|
||
$filename = 'clients_' . date('Y-m-d') . '.csv';
|
||
$this->exportToCsv($clients, $filename);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Экспорт в CSV
|
||
*/
|
||
protected function exportToCsv(array $clients, string $filename)
|
||
{
|
||
header('Content-Type: text/csv; charset=utf-8');
|
||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||
|
||
$output = fopen('php://output', 'w');
|
||
|
||
// Заголовок CSV
|
||
fputcsv($output, ['ID', 'Имя', 'Email', 'Телефон', 'Статус', 'Создан', 'Обновлён'], ';');
|
||
|
||
// Данные
|
||
foreach ($clients as $client) {
|
||
fputcsv($output, [
|
||
$client['id'],
|
||
$client['name'],
|
||
$client['email'] ?? '',
|
||
$client['phone'] ?? '',
|
||
$client['status'] ?? 'active',
|
||
$client['created_at'] ?? '',
|
||
$client['updated_at'] ?? '',
|
||
], ';');
|
||
}
|
||
|
||
fclose($output);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Экспорт в XLSX (упрощённый через HTML table)
|
||
*/
|
||
protected function exportToXlsx(array $clients, string $filename)
|
||
{
|
||
// Для упрощения используем HTML table с правильными заголовками Excel
|
||
// В продакшене рекомендуется использовать PhpSpreadsheet
|
||
|
||
header('Content-Type: application/vnd.ms-excel');
|
||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||
header('Cache-Control: max-age=0');
|
||
|
||
echo '<table border="1">';
|
||
echo '<tr><th>ID</th><th>Имя</th><th>Email</th><th>Телефон</th><th>Статус</th><th>Создан</th><th>Обновлён</th></tr>';
|
||
|
||
foreach ($clients as $client) {
|
||
echo '<tr>';
|
||
echo '<td>' . $client['id'] . '</td>';
|
||
echo '<td>' . htmlspecialchars($client['name']) . '</td>';
|
||
echo '<td>' . htmlspecialchars($client['email'] ?? '') . '</td>';
|
||
echo '<td>' . htmlspecialchars($client['phone'] ?? '') . '</td>';
|
||
echo '<td>' . ($client['status'] ?? 'active') . '</td>';
|
||
echo '<td>' . ($client['created_at'] ?? '') . '</td>';
|
||
echo '<td>' . ($client['updated_at'] ?? '') . '</td>';
|
||
echo '</tr>';
|
||
}
|
||
|
||
echo '</table>';
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Страница импорта клиентов (форма)
|
||
*/
|
||
public function importPage()
|
||
{
|
||
if (!$this->access->canCreate('clients')) {
|
||
return $this->forbiddenResponse('Доступ запрещён');
|
||
}
|
||
|
||
return $this->renderTwig('@Clients/import');
|
||
}
|
||
|
||
/**
|
||
* Импорт клиентов из файла
|
||
*/
|
||
public function import()
|
||
{
|
||
if (!$this->access->canCreate('clients')) {
|
||
return $this->forbiddenResponse('Доступ запрещён');
|
||
}
|
||
|
||
$file = $this->request->getFile('file');
|
||
|
||
if (!$file->isValid()) {
|
||
return $this->response->setJSON([
|
||
'success' => false,
|
||
'message' => 'Файл не загружен'
|
||
]);
|
||
}
|
||
|
||
$extension = strtolower($file->getClientExtension());
|
||
|
||
if (!in_array($extension, ['csv', 'xlsx', 'xls'])) {
|
||
return $this->response->setJSON([
|
||
'success' => false,
|
||
'message' => 'Неподдерживаемый формат файла. Используйте CSV или XLSX.'
|
||
]);
|
||
}
|
||
|
||
$organizationId = session()->get('active_org_id');
|
||
$imported = 0;
|
||
$errors = [];
|
||
|
||
// Парсим CSV файл
|
||
if ($extension === 'csv') {
|
||
$handle = fopen($file->getTempName(), 'r');
|
||
|
||
// Пропускаем заголовок
|
||
fgetcsv($handle, 0, ';');
|
||
|
||
$row = 1;
|
||
while (($data = fgetcsv($handle, 0, ';')) !== false) {
|
||
$row++;
|
||
|
||
$name = trim($data[1] ?? '');
|
||
$email = trim($data[2] ?? '');
|
||
|
||
// Валидация
|
||
if (empty($name)) {
|
||
$errors[] = "Строка $row: Имя обязательно";
|
||
continue;
|
||
}
|
||
|
||
if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||
$errors[] = "Строка $row: Некорректный email";
|
||
continue;
|
||
}
|
||
|
||
// Проверка на дубликат email
|
||
if (!empty($email)) {
|
||
$exists = $this->clientModel->where('organization_id', $organizationId)
|
||
->where('email', $email)
|
||
->countAllResults();
|
||
if ($exists > 0) {
|
||
$errors[] = "Строка $row: Клиент с email $email уже существует";
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Вставка
|
||
$this->clientModel->insert([
|
||
'organization_id' => $organizationId,
|
||
'name' => $name,
|
||
'email' => $email ?: null,
|
||
'phone' => trim($data[3] ?? '') ?: null,
|
||
'status' => 'active',
|
||
]);
|
||
|
||
$imported++;
|
||
}
|
||
|
||
fclose($handle);
|
||
} else {
|
||
// Для XLSX в продакшене использовать PhpSpreadsheet
|
||
return $this->response->setJSON([
|
||
'success' => false,
|
||
'message' => 'Импорт XLSX файлов временно недоступен. Пожалуйста, используйте CSV формат.'
|
||
]);
|
||
}
|
||
|
||
$message = "Импортировано клиентов: $imported";
|
||
if (!empty($errors)) {
|
||
$message .= '. Ошибок: ' . count($errors);
|
||
}
|
||
|
||
return $this->response->setJSON([
|
||
'success' => true,
|
||
'message' => $message,
|
||
'imported' => $imported,
|
||
'errors' => $errors
|
||
]);
|
||
}
|
||
}
|