diff --git a/app/Config/Services.php b/app/Config/Services.php index 96a4e78..ede6022 100644 --- a/app/Config/Services.php +++ b/app/Config/Services.php @@ -2,6 +2,8 @@ namespace Config; +use App\Libraries\RateLimitIdentifier; +use App\Services\RateLimitService; use CodeIgniter\Config\BaseService; /** @@ -45,9 +47,30 @@ class Services extends BaseService return new \App\Services\AccessService(); } + /** + * Сервис для идентификации клиентов в rate limiting + * + * Использует комбинацию cookie token + IP + User Agent + * для уникальной идентификации браузера клиента. + * + * @param bool $getShared + * @return \App\Libraries\RateLimitIdentifier + */ + public static function rateLimitIdentifier(bool $getShared = true) + { + if ($getShared) { + return static::getSharedInstance('rateLimitIdentifier'); + } + + return new RateLimitIdentifier(); + } + /** * Сервис для rate limiting * + * Обеспечивает защиту от брутфорса и ограничение частоты запросов. + * При недоступности Redis автоматически использует файловый кэш. + * * @param bool $getShared * @return \App\Services\RateLimitService|null */ @@ -58,7 +81,7 @@ class Services extends BaseService } try { - return \App\Services\RateLimitService::getInstance(); + return RateLimitService::getInstance(); } catch (\Exception $e) { log_message('warning', 'RateLimitService unavailable: ' . $e->getMessage()); return null; diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index db1495c..3cb809c 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -259,6 +259,7 @@ abstract class BaseController extends Controller $tableData['actions'] = $config['actions'] ?? false; $tableData['actionsConfig'] = $config['actionsConfig'] ?? []; $tableData['columns'] = $config['columns'] ?? []; + $tableData['onRowClick'] = $config['onRowClick'] ?? null; // Параметры для пустого состояния $tableData['emptyMessage'] = $config['emptyMessage'] ?? 'Нет данных'; diff --git a/app/Modules/Clients/Config/Routes.php b/app/Modules/Clients/Config/Routes.php index aceb006..1f1ed52 100644 --- a/app/Modules/Clients/Config/Routes.php +++ b/app/Modules/Clients/Config/Routes.php @@ -4,9 +4,15 @@ $routes->group('clients', ['filter' => 'org', 'namespace' => 'App\Modules\Clients\Controllers'], static function ($routes) { $routes->get('/', 'Clients::index'); $routes->get('table', 'Clients::table'); // AJAX endpoint для таблицы + $routes->get('view/(:num)', 'Clients::view/$1'); // API: данные клиента $routes->get('new', 'Clients::new'); $routes->post('create', 'Clients::create'); $routes->get('edit/(:num)', 'Clients::edit/$1'); $routes->post('update/(:num)', 'Clients::update/$1'); $routes->get('delete/(:num)', 'Clients::delete/$1'); -}); + + // Экспорт и импорт + $routes->get('export', 'Clients::export'); + $routes->get('import', 'Clients::importPage'); + $routes->post('import', 'Clients::import'); +}); \ No newline at end of file diff --git a/app/Modules/Clients/Controllers/Clients.php b/app/Modules/Clients/Controllers/Clients.php index 31adf8b..35e87a9 100644 --- a/app/Modules/Clients/Controllers/Clients.php +++ b/app/Modules/Clients/Controllers/Clients.php @@ -70,6 +70,7 @@ class Clients extends BaseController 'type' => 'delete', ] ], + 'onRowClick' => 'viewClient', // Функция для открытия карточки клиента 'emptyMessage' => 'Клиентов пока нет', 'emptyIcon' => 'fa-solid fa-users', 'emptyActionUrl' => base_url('/clients/new'), @@ -237,4 +238,245 @@ class Clients extends BaseController 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 '
| ID | Имя | Телефон | Статус | Создан | Обновлён | |
|---|---|---|---|---|---|---|
| ' . $client['id'] . ' | '; + echo '' . htmlspecialchars($client['name']) . ' | '; + echo '' . htmlspecialchars($client['email'] ?? '') . ' | '; + echo '' . htmlspecialchars($client['phone'] ?? '') . ' | '; + echo '' . ($client['status'] ?? 'active') . ' | '; + echo '' . ($client['created_at'] ?? '') . ' | '; + echo '' . ($client['updated_at'] ?? '') . ' | '; + echo '
Управление клиентами вашей организации
- - Добавить клиента - +