first commit

This commit is contained in:
root
2026-01-10 16:16:39 +03:00
commit 5f5a28e292
154 changed files with 21454 additions and 0 deletions
+233
View File
@@ -0,0 +1,233 @@
<?php
namespace App\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
use App\Models\OrganizationModel;
/**
* BaseController provides a convenient place for loading components
* and performing functions that are needed by all your controllers.
*
* Extend this class in any new controllers:
* ```
* class Home extends BaseController
* ```
*
* For security, be sure to declare any new methods as protected or private.
*/
abstract class BaseController extends Controller
{
/**
* Be sure to declare properties for any property fetch you initialized.
* The creation of dynamic property is deprecated in PHP 8.2.
*/
protected $session;
/**
* @return void
*/
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{
// Load here all helpers you want to be available in your controllers that extend BaseController.
// Caution: Do not put the this below the parent::initController() call below.
// $this->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);
}
}