['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']]), new TwigFunction('get_avatar_url', [$this, 'getAvatarUrl'], ['is_safe' => ['html']]), new TwigFunction('get_avatar', [$this, 'getAvatar'], ['is_safe' => ['html']]), // Access functions new TwigFunction('can', [$this, 'can'], ['is_safe' => ['html']]), new TwigFunction('is_role', [$this, 'isRole'], ['is_safe' => ['html']]), new TwigFunction('is_owner', [$this, 'isOwner'], ['is_safe' => ['html']]), new TwigFunction('is_admin', [$this, 'isAdmin'], ['is_safe' => ['html']]), new TwigFunction('is_manager', [$this, 'isManager'], ['is_safe' => ['html']]), new TwigFunction('current_role', [$this, 'currentRole'], ['is_safe' => ['html']]), new TwigFunction('role_label', [$this, 'roleLabel'], ['is_safe' => ['html']]), new TwigFunction('can_view', [$this, 'canView'], ['is_safe' => ['html']]), new TwigFunction('can_create', [$this, 'canCreate'], ['is_safe' => ['html']]), new TwigFunction('can_edit', [$this, 'canEdit'], ['is_safe' => ['html']]), new TwigFunction('can_delete', [$this, 'canDelete'], ['is_safe' => ['html']]), new TwigFunction('can_manage_users', [$this, 'canManageUsers'], ['is_safe' => ['html']]), // Role & Status badge functions new TwigFunction('role_badge', [$this, 'roleBadge'], ['is_safe' => ['html']]), new TwigFunction('status_badge', [$this, 'statusBadge'], ['is_safe' => ['html']]), new TwigFunction('get_all_roles', [$this, 'getAllRoles'], ['is_safe' => ['html']]), // System role functions (superadmin) new TwigFunction('is_superadmin', [$this, 'isSuperadmin'], ['is_safe' => ['html']]), new TwigFunction('is_system_admin', [$this, 'isSystemAdmin'], ['is_safe' => ['html']]), new TwigFunction('get_system_role', [$this, 'getSystemRole'], ['is_safe' => ['html']]), // Module subscription functions new TwigFunction('is_module_active', [$this, 'isModuleActive'], ['is_safe' => ['html']]), new TwigFunction('is_module_available', [$this, 'isModuleAvailable'], ['is_safe' => ['html']]), ]; } // ======================================== // Access Functions для Twig // ======================================== public function can(string $action, string $resource): bool { return service('access')->can($action, $resource); } public function isRole($roles): bool { return service('access')->isRole($roles); } public function isOwner(): bool { return service('access')->isRole(\App\Services\AccessService::ROLE_OWNER); } public function isAdmin(): bool { $role = service('access')->getCurrentRole(); return $role === \App\Services\AccessService::ROLE_ADMIN || $role === \App\Services\AccessService::ROLE_OWNER; } public function isManager(): bool { return service('access')->isManagerOrHigher(); } public function currentRole(): ?string { return service('access')->getCurrentRole(); } public function roleLabel(string $role): string { return service('access')->getRoleLabel($role); } public function canView(string $resource): bool { return service('access')->can(\App\Services\AccessService::PERMISSION_VIEW, $resource); } public function canCreate(string $resource): bool { return service('access')->can(\App\Services\AccessService::PERMISSION_CREATE, $resource); } public function canEdit(string $resource): bool { return service('access')->can(\App\Services\AccessService::PERMISSION_EDIT, $resource); } public function canDelete(string $resource): bool { return service('access')->can(\App\Services\AccessService::PERMISSION_DELETE, $resource); } public function canManageUsers(): bool { return service('access')->canManageUsers(); } // ======================================== // Role & Status Badge Functions // ======================================== public function roleBadge(string $role): string { $colors = [ 'owner' => 'bg-primary', 'admin' => 'bg-info', 'manager' => 'bg-success', 'guest' => 'bg-secondary', ]; $labels = [ 'owner' => 'Владелец', 'admin' => 'Администратор', 'manager' => 'Менеджер', 'guest' => 'Гость', ]; $color = $colors[$role] ?? 'bg-secondary'; $label = $labels[$role] ?? $role; return '' . esc($label) . ''; } // ======================================== // System Role Functions (superadmin) // ======================================== public function isSuperadmin(): bool { return service('access')->isSuperadmin(); } public function isSystemAdmin(): bool { return service('access')->isSystemAdmin(); } public function getSystemRole(): ?string { return service('access')->getSystemRole(); } // ======================================== // Module Subscription Functions // ======================================== public function isModuleActive(string $moduleCode): bool { $orgId = session()->get('active_org_id'); if (!$orgId) { return false; } $subscriptionService = new \App\Services\ModuleSubscriptionService(); return $subscriptionService->isModuleActive($moduleCode, $orgId); } public function isModuleAvailable(string $moduleCode): bool { $orgId = session()->get('active_org_id'); if (!$orgId) { return false; } $subscriptionService = new \App\Services\ModuleSubscriptionService(); return $subscriptionService->isModuleAvailable($moduleCode, $orgId); } public function statusBadge(string $status): string { $colors = [ 'active' => 'bg-success', 'pending' => 'bg-warning text-dark', 'blocked' => 'bg-danger', ]; $labels = [ 'active' => 'Активен', 'pending' => 'Ожидает', 'blocked' => 'Заблокирован', ]; $color = $colors[$status] ?? 'bg-secondary'; $label = $labels[$status] ?? $status; return '' . esc($label) . ''; } public function getAvatarUrl($avatar = null, $size = 32): string { if (empty($avatar)) { return ''; } return base_url('/uploads/avatars/' . $avatar); } public function getAvatar($user = null, $size = 32, $class = ''): string { if (!$user) { $session = session(); $userId = $session->get('user_id'); if (!$userId) { return ''; } $userModel = new \App\Models\UserModel(); $user = $userModel->find($userId); } $name = $user['name'] ?? 'U'; $avatar = $user['avatar'] ?? null; if ($avatar) { $url = base_url('/uploads/avatars/' . $avatar); $style = "width: {$size}px; height: {$size}px; object-fit: cover; border-radius: 50%;"; return '' . esc($name) . ''; } // Генерируем фон на основе имени $colors = ['667eea', '764ba2', 'f093fb', 'f5576c', '4facfe', '00f2fe']; $color = $colors[crc32($name) % count($colors)]; $initial = strtoupper(substr($name, 0, 1)); $style = "width: {$size}px; height: {$size}px; background: linear-gradient(135deg, #{$color} 0%, #{$color}dd 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: " . ($size / 2) . "px;"; return '
' . esc($initial) . '
'; } public function getAllRoles(): array { return \App\Services\AccessService::getAllRoles(); } 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')); $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'] ?? ''; $type = $action['type'] ?? ''; // Проверяем условия для показа кнопки // Владелец не может быть изменён, заблокирован или удалён if ($itemArray['role'] ?? '' === 'owner') { continue; } // Блокировка - только для активных пользователей if ($type === 'block' && ($itemArray['status'] ?? '') !== 'active') { continue; } // Разблокировка - только для заблокированных if ($type === 'unblock' && ($itemArray['status'] ?? '') !== 'blocked') { continue; } // Подставляем значения из item в URL $url = $this->interpolate($urlPattern, $itemArray); // Формируем 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 'role_badge': return $this->roleBadge((string) $value); case 'status_badge': return $this->statusBadge((string) $value); case 'user_display': // Отображение пользователя с аватаром и именем/email $name = $itemArray['user_name'] ?? ''; $email = $itemArray['user_email'] ?? $value; $avatar = $itemArray['user_avatar'] ?? ''; if (!empty($avatar)) { $avatar = '/uploads/avatars/' . $itemArray['user_avatar']; } $avatarHtml = $avatar ? '' : '
'; return '
' . $avatarHtml . '
' . esc($name ?: $email) . '
' . ($name ? '
' . esc($email) . '
' : '') . '
'; 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); } }