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 ''; echo ''; foreach ($clients as $client) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
IDИмяEmailТелефонСтатусСозданОбновлён
' . $client['id'] . '' . htmlspecialchars($client['name']) . '' . htmlspecialchars($client['email'] ?? '') . '' . htmlspecialchars($client['phone'] ?? '') . '' . ($client['status'] ?? 'active') . '' . ($client['created_at'] ?? '') . '' . ($client['updated_at'] ?? '') . '
'; 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 ]); } }