['html']]), new TwigFunction('get_current_org', [$this, 'getCurrentOrg'], ['is_safe' => ['html']]), new TwigFunction('get_alerts', [$this, 'getAlerts'], ['is_safe' => ['html']]), new TwigFunction('render_pager', [$this, 'renderPager'], ['is_safe' => ['html']]), new TwigFunction('is_active_route', [$this, 'isActiveRoute'], ['is_safe' => ['html']]), new TwigFunction('get_current_route', [$this, 'getCurrentRoute'], ['is_safe' => ['html']]), new TwigFunction('render_actions', [$this, 'renderActions'], ['is_safe' => ['html']]), new TwigFunction('render_cell', [$this, 'renderCell'], ['is_safe' => ['html']]), ]; } public function getSession() { return session(); } public function getCurrentOrg() { $session = session(); $activeOrgId = $session->get('active_org_id'); if ($activeOrgId) { $orgModel = new OrganizationModel(); return $orgModel->find($activeOrgId); } return null; } public function getAlerts(): array { $session = session(); $alerts = []; $types = ['success', 'error', 'warning', 'info']; foreach ($types as $type) { if ($msg = $session->getFlashdata($type)) { $alerts[] = ['type' => $type, 'message' => $msg]; } } if ($validationErrors = $session->getFlashdata('errors')) { foreach ($validationErrors as $error) { $alerts[] = ['type' => 'error', 'message' => $error]; } } return $alerts; } public function renderPager($pager) { if (!$pager) { return ''; } return $pager->links(); } /** * Проверяет, является ли текущий маршрут активным */ public function isActiveRoute($routes, $exact = false): bool { $currentRoute = $this->getCurrentRoute(); if (is_string($routes)) { $routes = [$routes]; } foreach ($routes as $route) { if ($exact) { // Точное совпадение if ($currentRoute === $route) { return true; } } else { // Частичное совпадение (начинается с) // Исключаем пустую строку, так как она совпадает с любым маршрутом if ($route === '') { // Для пустого маршрута проверяем только корень if ($currentRoute === '') { return true; } } elseif (strpos($currentRoute, $route) === 0) { return true; } } } return false; } /** * Получает текущий маршрут без базового URL */ public function getCurrentRoute(): string { $uri = service('uri'); $route = $uri->getRoutePath(); // Убираем начальный слеш если есть return ltrim($route, '/'); } /** * Генерирует HTML для кнопок действий в строке таблицы * * @param object|array $item Данные строки (объект или массив) * @param array $actions Массив конфигураций действий * @return string HTML код кнопок */ public function renderActions($item, array $actions = []): string { if (empty($actions)) { return ''; } // Конвертируем объект в массив для доступа к свойствам $itemArray = $this->objectToArray($item); // DEBUG: логируем для отладки log_message('debug', 'renderActions: item type = ' . gettype($item)); log_message('debug', 'renderActions: item keys = ' . (is_array($itemArray) ? implode(', ', array_keys($itemArray)) : 'N/A')); log_message('debug', 'renderActions: item = ' . print_r($itemArray, true)); $html = '
'; foreach ($actions as $action) { $label = $action['label'] ?? 'Action'; $urlPattern = $action['url'] ?? '#'; $icon = $action['icon'] ?? ''; $class = $action['class'] ?? 'btn-outline-secondary'; $title = $action['title'] ?? $label; $target = $action['target'] ?? ''; // Подставляем значения из item в URL $url = $this->interpolate($urlPattern, $itemArray); log_message('debug', 'renderActions: urlPattern = ' . $urlPattern . ', url = ' . $url); // Формируем HTML кнопки/ссылки $iconHtml = $icon ? ' ' : ''; $targetAttr = $target ? ' target="' . esc($target) . '"' : ''; $html .= '' . $iconHtml . esc($label) . ''; } $html .= '
'; return $html; } /** * Конвертирует объект в массив (включая защищённые свойства) */ private function objectToArray($data): array { if (is_array($data)) { return $data; } if (is_object($data)) { // Используем json_decode/encode для надёжного извлечения всех свойств $json = json_encode($data); return json_decode($json, true); } return []; } /** * Рендерит значение ячейки таблицы * * @param object|array $item Данные строки * @param string $key Ключ поля для отображения * @param array $config Конфигурация колонки (опционально) * @return string HTML код ячейки */ public function renderCell($item, string $key, array $config = []): string { $itemArray = $this->objectToArray($item); $value = $itemArray[$key] ?? null; // Если значение пустое и есть значение по умолчанию if (($value === null || $value === '' || $value === false) && isset($config['default'])) { return $config['default']; } // Если указан шаблон, используем его для рендеринга if (isset($config['template'])) { $template = $config['template']; // Подставляем все значения из item в шаблон foreach ($itemArray as $k => $v) { $template = str_replace('{' . $k . '}', esc($v ?? ''), $template); } return $template; } // Применяем тип преобразования если указан if (isset($config['type'])) { switch ($config['type']) { case 'email': if ($value) { return '' . esc($value) . ''; } return $config['default'] ?? '—'; case 'phone': if ($value) { return '' . esc($value) . ''; } return $config['default'] ?? '—'; case 'date': if ($value) { return esc(date('d.m.Y', strtotime($value))); } return $config['default'] ?? '—'; case 'datetime': if ($value) { return esc(date('d.m.Y H:i', strtotime($value))); } return $config['default'] ?? '—'; case 'boolean': case 'bool': if ($value) { return 'Да'; } return 'Нет'; case 'uppercase': return $value ? esc(strtoupper($value)) : ($config['default'] ?? ''); case 'lowercase': return $value ? esc(strtolower($value)) : ($config['default'] ?? ''); case 'truncate': $length = $config['length'] ?? 50; if ($value && strlen($value) > $length) { return esc(substr($value, 0, $length)) . '...'; } return esc($value ?? ''); case 'currency': if ($value !== null && $value !== '') { return number_format((float) $value, 0, '.', ' ') . ' ₽'; } return $config['default'] ?? '—'; case 'percent': if ($value !== null && $value !== '') { return esc((float) $value) . '%'; } return $config['default'] ?? '—'; } } // По умолчанию просто возвращаем значение return $value !== null && $value !== '' ? esc((string) $value) : '—'; } /** * Подставляет значения из данных в шаблон строки * * @param string $pattern Шаблон с плейсхолдерами вида {field_name} * @param object|array $data Данные для подстановки * @return string Результирующая строка */ private function interpolate(string $pattern, $data): string { // Конвертируем объект в массив $data = is_object($data) ? $this->objectToArray($data) : $data; // Заменяем все {key} на значения return preg_replace_callback('/\{(\w+)\}/', function ($matches) use ($data) { $key = $matches[1]; return isset($data[$key]) ? esc($data[$key]) : $matches[0]; }, $pattern); } }