helpers = ['form', 'url']; // Caution: Do not edit this line. parent::initController($request, $response, $logger); // Preload any models, libraries, etc, here. $this->session = service('session'); } public function renderTwig($template, $data = []) { helper('csrf'); $twig = \Config\Services::twig(); // oldInput из сессии добавляется в данные шаблона // Расширение TwigGlobalsExtension автоматически добавляет session, alerts, old, currentOrg $oldInput = $this->session->get('_ci_old_input') ?? []; $data['old'] = $data['old'] ?? $oldInput; ob_start(); $twig->display($template, $data); $content = ob_get_clean(); return $content; } // ======================================== // Методы для универсальных таблиц // ======================================== /** * Конфигурация таблицы - переопределяется в каждом контроллере */ protected function getTableConfig(): array { return [ 'model' => null, 'columns' => [], 'searchable' => [], 'sortable' => [], 'defaultSort' => 'id', 'order' => 'asc', 'viewPath' => '', 'partialPath' => '', 'itemsKey' => 'items', 'scope' => null, // callable($builder) для дополнительных модификаций ]; } /** * Проверка AJAX запроса */ protected function isAjax(): bool { $header = $this->request->header('X-Requested-With'); $value = $header ? $header->getValue() : ''; return strtolower($value) === 'xmlhttprequest'; } /** * Подготовка данных таблицы (общая логика для всех таблиц) */ protected function prepareTableData(?array $config = null): array { $config = array_merge($this->getTableConfig(), $config ?? []); $page = (int) ($this->request->getGet('page') ?? 1); $perPage = (int) ($this->request->getGet('perPage') ?? 10); $sort = $this->request->getGet('sort') ?? $config['defaultSort']; $order = $this->request->getGet('order') ?? $config['order']; // Исправление: получаем фильтры из параметра filters[] $filters = []; $rawFilters = $this->request->getGet('filters'); if ($rawFilters) { if (is_array($rawFilters)) { $filters = $rawFilters; } else { // Для обратной совместимости, если фильтры пришли в строке parse_str($rawFilters, $filters); if (isset($filters['filters'])) { $filters = $filters['filters']; } } } else { // Старый способ извлечения фильтров для совместимости foreach ($this->request->getGet() as $key => $value) { if (str_starts_with($key, 'filters[') && str_ends_with($key, ']')) { $field = substr($key, 8, -1); // Исправлено: было 9, должно быть 8 $filters[$field] = $value; } } } $model = $config['model']; $builder = $model->builder(); // Сбрасываем все предыдущие условия $builder->resetQuery(); if (isset($config['scope']) && is_callable($config['scope'])) { $config['scope']($builder); } // Применяем фильтры foreach ($filters as $field => $value) { if ($value !== '' && in_array($field, $config['searchable'])) { $builder->like($field, $value); } } // Сортировка if ($sort && in_array($sort, $config['sortable'])) { $builder->orderBy($sort, $order); } // Исправлено: countAllResults(false) вместо countAll() // Сохраняем текущее состояние builder для подсчета $countBuilder = clone $builder; $total = $countBuilder->countAllResults(false); // Получаем данные с пагинацией $builder->select('*'); $items = $builder->limit($perPage, ($page - 1) * $perPage)->get()->getResult(); $from = ($page - 1) * $perPage + 1; $to = min($page * $perPage, $total); $pagerData = [ 'currentPage' => $page, 'pageCount' => $total > 0 ? (int) ceil($total / $perPage) : 1, 'total' => $total, 'perPage' => $perPage, 'from' => $from, 'to' => $to, ]; $pagerStub = new class($pagerData) { private $data; public function __construct(array $data) { $this->data = $data; } public function getCurrentPage(): int { return $this->data['currentPage'] ?? 1; } public function getPageCount(): int { return $this->data['pageCount'] ?? 1; } public function getTotal(): int { return $this->data['total'] ?? 0; } public function getDetails(): array { return $this->data; } }; $data = [ $config['itemsKey'] => $items, 'pager' => $pagerStub, 'pagerDetails' => $pagerData, 'perPage' => $perPage, 'sort' => $sort, 'order' => $order, 'filters' => $filters, 'columns' => $config['columns'], ]; // В конце prepareTableData, перед return log_message('debug', 'Total records calculated: ' . $total); log_message('debug', 'Organization ID: ' . session()->get('active_org_id')); log_message('debug', 'SQL Query: ' . $countBuilder->getCompiledSelect()); return $data; } /** * AJAX endpoint для таблицы - возвращает partial (tbody + tfoot) */ public function table() { $config = $this->getTableConfig(); $data = $this->prepareTableData($config); if (!$this->isAjax()) { return redirect()->to('/'); } return $this->renderTwig($config['partialPath'], $data); } }