bp/app/Modules/Clients/Controllers/Clients.php

483 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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
]);
}
}