organizationModel = new \App\Models\OrganizationModel(); $this->userModel = new \App\Models\UserModel(); $this->planModel = new \App\Models\PlanModel(); } /** * Дашборд суперадмина */ public function index() { // Статистика $stats = [ 'total_users' => $this->userModel->countAll(), 'total_orgs' => $this->organizationModel->countAll(), 'active_today' => $this->userModel->where('created_at >=', date('Y-m-d'))->countAllResults(), 'total_plans' => $this->planModel->countAll(), ]; // Последние организации $recentOrgs = $this->organizationModel ->orderBy('created_at', 'DESC') ->findAll(5); // Последние пользователи $recentUsers = $this->userModel ->orderBy('created_at', 'DESC') ->findAll(5); return $this->renderTwig('superadmin/dashboard', compact('stats', 'recentOrgs', 'recentUsers')); } // ========================================================================= // УПРАВЛЕНИЕ ТАРИФАМИ // ========================================================================= /** * Список тарифов */ public function plans() { $plans = $this->planModel->findAll(); // Декодируем features для Twig foreach ($plans as &$plan) { $plan['features'] = json_decode($plan['features'] ?? '[]', true); } return $this->renderTwig('superadmin/plans/index', compact('plans')); } /** * Создание тарифа (форма) */ public function createPlan() { return $this->renderTwig('superadmin/plans/create'); } /** * Сохранение тарифа */ public function storePlan() { // Получаем features из текстового поля (каждая строка - отдельная возможность) $featuresText = $this->request->getPost('features_list'); $features = []; if ($featuresText) { $lines = explode("\n", trim($featuresText)); foreach ($lines as $line) { $line = trim($line); if (!empty($line)) { $features[] = $line; } } } $data = [ 'name' => $this->request->getPost('name'), 'description' => $this->request->getPost('description'), 'price' => (float) $this->request->getPost('price'), 'currency' => $this->request->getPost('currency') ?? 'RUB', 'billing_period' => $this->request->getPost('billing_period') ?? 'monthly', 'max_users' => (int) $this->request->getPost('max_users'), 'max_clients' => (int) $this->request->getPost('max_clients'), 'max_storage' => (int) $this->request->getPost('max_storage'), 'features' => json_encode($features), 'is_active' => $this->request->getPost('is_active') ?? 1, 'is_default' => $this->request->getPost('is_default') ?? 0, ]; if (!$this->planModel->insert($data)) { return redirect()->back()->withInput()->with('error', 'Ошибка создания тарифа: ' . implode(', ', $this->planModel->errors())); } return redirect()->to('/superadmin/plans')->with('success', 'Тариф успешно создан'); } /** * Редактирование тарифа (форма) */ public function editPlan($id) { $plan = $this->planModel->find($id); if (!$plan) { throw new \CodeIgniter\Exceptions\PageNotFoundException('Тариф не найден'); } // Декодируем features для отображения в textarea $plan['features'] = json_decode($plan['features'] ?? '[]', true); return $this->renderTwig('superadmin/plans/edit', compact('plan')); } /** * Обновление тарифа */ public function updatePlan($id) { // Получаем features из текстового поля (каждая строка - отдельная возможность) $featuresText = $this->request->getPost('features_list'); $features = []; if ($featuresText) { $lines = explode("\n", trim($featuresText)); foreach ($lines as $line) { $line = trim($line); if (!empty($line)) { $features[] = $line; } } } $data = [ 'name' => $this->request->getPost('name'), 'description' => $this->request->getPost('description'), 'price' => (float) $this->request->getPost('price'), 'currency' => $this->request->getPost('currency') ?? 'RUB', 'billing_period' => $this->request->getPost('billing_period') ?? 'monthly', 'max_users' => (int) $this->request->getPost('max_users'), 'max_clients' => (int) $this->request->getPost('max_clients'), 'max_storage' => (int) $this->request->getPost('max_storage'), 'features' => json_encode($features), 'is_active' => $this->request->getPost('is_active') ?? 1, 'is_default' => $this->request->getPost('is_default') ?? 0, ]; if (!$this->planModel->update($id, $data)) { return redirect()->back()->withInput()->with('error', 'Ошибка обновления тарифа: ' . implode(', ', $this->planModel->errors())); } return redirect()->to('/superadmin/plans')->with('success', 'Тариф успешно обновлён'); } /** * Удаление тарифа */ public function deletePlan($id) { if (!$this->planModel->delete($id)) { return redirect()->to('/superadmin/plans')->with('error', 'Ошибка удаления тарифа'); } return redirect()->to('/superadmin/plans')->with('success', 'Тариф успешно удалён'); } // ========================================================================= // УПРАВЛЕНИЕ ОРГАНИЗАЦИЯМИ // ========================================================================= /** * Конфигурация таблицы организаций */ protected function getOrganizationsTableConfig(): array { return [ 'id' => 'organizations-table', 'url' => '/superadmin/organizations/table', 'model' => $this->organizationModel, 'columns' => [ 'id' => ['label' => 'ID', 'width' => '60px'], 'name' => ['label' => 'Название'], 'type' => ['label' => 'Тип', 'width' => '100px'], 'user_count' => ['label' => 'Пользователей', 'width' => '100px'], 'status' => ['label' => 'Статус', 'width' => '120px'], 'created_at' => ['label' => 'Дата', 'width' => '100px'], ], 'searchable' => ['name', 'id'], 'sortable' => ['id', 'name', 'created_at'], 'defaultSort' => 'created_at', 'order' => 'desc', 'scope' => function ($builder) { // JOIN с подсчётом пользователей организации $builder->from('organizations') ->select('organizations.*, (SELECT COUNT(*) FROM organization_users WHERE organization_users.organization_id = organizations.id AND status = "active") as user_count'); }, 'actions' => ['label' => 'Действия', 'width' => '140px'], 'actionsConfig' => [ [ 'label' => '', 'url' => '/superadmin/organizations/view/{id}', 'icon' => 'fa-solid fa-eye', 'class' => 'btn-outline-primary', 'title' => 'Просмотр', ], [ 'label' => '', 'url' => '/superadmin/organizations/block/{id}', 'icon' => 'fa-solid fa-ban', 'class' => 'btn-outline-warning', 'title' => 'Заблокировать', 'confirm' => 'Заблокировать организацию?', ], [ 'label' => '', 'url' => '/superadmin/organizations/delete/{id}', 'icon' => 'fa-solid fa-trash', 'class' => 'btn-outline-danger', 'title' => 'Удалить', 'confirm' => 'Удалить организацию? Это действие нельзя отменить!', ], ], 'emptyMessage' => 'Организации не найдены', 'emptyIcon' => 'bi bi-building', ]; } /** * Список организаций */ public function organizations() { $config = $this->getOrganizationsTableConfig(); $tableHtml = $this->renderTable($config); return $this->renderTwig('superadmin/organizations/index', [ 'tableHtml' => $tableHtml, 'config' => $config, ]); } /** * AJAX таблица организаций */ public function organizationsTable() { $config = $this->getOrganizationsTableConfig(); return $this->table($config); } /** * Просмотр организации */ public function viewOrganization($id) { $organization = $this->organizationModel->find($id); if (!$organization) { throw new \CodeIgniter\Exceptions\PageNotFoundException('Организация не найдена'); } // Пользователи организации $orgUserModel = new \App\Models\OrganizationUserModel(); $users = $orgUserModel->getOrganizationUsers($id); // Список тарифов для выбора $plans = $this->planModel->where('is_active', 1)->findAll(); // Текущая подписка организации из таблицы связей $db = \Config\Database::connect(); $subscriptionTable = $db->table('organization_plan_subscriptions'); $currentSubscription = $subscriptionTable ->where('organization_id', $id) ->orderBy('id', 'DESC') ->get() ->getRowArray(); return $this->renderTwig('superadmin/organizations/view', compact('organization', 'users', 'plans', 'currentSubscription')); } /** * Блокировка организации */ public function blockOrganization($id) { $this->organizationModel->update($id, ['status' => 'blocked']); return redirect()->to("/superadmin/organizations/view/{$id}")->with('success', 'Организация заблокирована'); } /** * Разблокировка организации */ public function unblockOrganization($id) { $this->organizationModel->update($id, ['status' => 'active']); return redirect()->to("/superadmin/organizations/view/{$id}")->with('success', 'Организация разблокирована'); } /** * Удаление организации (полное удаление) */ public function deleteOrganization($id) { // Полное удаление без soft delete $this->organizationModel->delete($id, true); return redirect()->to('/superadmin/organizations')->with('success', 'Организация удалена'); } /** * Назначение тарифа организации */ public function setOrganizationPlan($id) { $planId = $this->request->getPost('plan_id'); $durationDays = (int) $this->request->getPost('duration_days'); if (!$planId) { return redirect()->back()->with('error', 'Выберите тариф'); } $plan = $this->planModel->find($planId); if (!$plan) { return redirect()->back()->with('error', 'Тариф не найден'); } $db = \Config\Database::connect(); $subscriptionsTable = $db->table('organization_plan_subscriptions'); $startDate = date('Y-m-d H:i:s'); $endDate = $durationDays > 0 ? date('Y-m-d H:i:s', strtotime("+{$durationDays} days")) : null; // Проверяем существующую подписку $existingSub = $subscriptionsTable ->where('organization_id', $id) ->where('plan_id', $planId) ->get() ->getRowArray(); if ($existingSub) { // Обновляем существующую подписку $subscriptionsTable->where('id', $existingSub['id'])->update([ 'status' => $durationDays > 0 ? 'active' : 'trial', 'trial_ends_at' => $endDate, 'expires_at' => $endDate, 'updated_at' => date('Y-m-d H:i:s'), ]); } else { // Создаём новую подписку $subscriptionsTable->insert([ 'organization_id' => $id, 'plan_id' => $planId, 'status' => $durationDays > 0 ? 'active' : 'trial', 'trial_ends_at' => $endDate, 'expires_at' => $endDate, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); } return redirect()->to("/superadmin/organizations/view/{$id}")->with('success', 'Тариф успешно назначен'); } // ========================================================================= // УПРАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯМИ // ========================================================================= /** * Конфигурация таблицы пользователей */ protected function getUsersTableConfig(): array { return [ 'id' => 'users-table', 'url' => '/superadmin/users/table', 'model' => $this->userModel, 'columns' => [ 'id' => ['label' => 'ID', 'width' => '60px'], 'name' => ['label' => 'Имя'], 'email' => ['label' => 'Email'], 'system_role' => ['label' => 'Роль', 'width' => '140px'], 'org_count' => ['label' => 'Организаций', 'width' => '100px'], 'status' => ['label' => 'Статус', 'width' => '120px'], 'created_at' => ['label' => 'Дата', 'width' => '100px'], ], 'searchable' => ['name', 'email', 'id'], 'sortable' => ['id', 'name', 'email', 'created_at'], 'defaultSort' => 'created_at', 'order' => 'desc', 'scope' => function ($builder) { // JOIN с подсчётом организаций пользователя $builder->from('users') ->select('users.*, (SELECT COUNT(*) FROM organization_users WHERE organization_users.user_id = users.id) as org_count'); }, 'actions' => ['label' => 'Действия', 'width' => '140px'], 'actionsConfig' => [ [ 'label' => '', 'url' => '/superadmin/users/block/{id}', 'icon' => 'fa-solid fa-ban', 'class' => 'btn-outline-warning', 'title' => 'Заблокировать', 'confirm' => 'Заблокировать пользователя?', ], [ 'label' => '', 'url' => '/superadmin/users/delete/{id}', 'icon' => 'fa-solid fa-trash', 'class' => 'btn-outline-danger', 'title' => 'Удалить', 'confirm' => 'Удалить пользователя? Это действие нельзя отменить!', ], ], 'emptyMessage' => 'Пользователи не найдены', 'emptyIcon' => 'bi bi-people', ]; } /** * Список пользователей */ public function users() { $config = $this->getUsersTableConfig(); $tableHtml = $this->renderTable($config); return $this->renderTwig('superadmin/users/index', [ 'tableHtml' => $tableHtml, 'config' => $config, ]); } /** * AJAX таблица пользователей */ public function usersTable() { $config = $this->getUsersTableConfig(); return $this->table($config); } /** * Изменение системной роли пользователя */ public function updateUserRole($id) { $newRole = $this->request->getPost('system_role'); $allowedRoles = ['user', 'admin', 'superadmin']; if (!in_array($newRole, $allowedRoles)) { return redirect()->back()->with('error', 'Недопустимая роль'); } $this->userModel->update($id, ['system_role' => $newRole]); return redirect()->back()->with('success', 'Роль пользователя обновлена'); } /** * Блокировка пользователя */ public function blockUser($id) { $this->userModel->update($id, ['status' => 'blocked']); return redirect()->back()->with('success', 'Пользователь заблокирован'); } /** * Разблокировка пользователя */ public function unblockUser($id) { $this->userModel->update($id, ['status' => 'active']); return redirect()->back()->with('success', 'Пользователь разблокирован'); } /** * Удаление пользователя (полное удаление) */ public function deleteUser($id) { // Полное удаление без soft delete $this->userModel->delete($id, true); return redirect()->to('/superadmin/users')->with('success', 'Пользователь удалён'); } // ========================================================================= // СТАТИСТИКА // ========================================================================= /** * Статистика использования */ public function statistics() { // Статистика по дням (последние 30 дней) $dailyStats = []; for ($i = 29; $i >= 0; $i--) { $date = date('Y-m-d', strtotime("-{$i} days")); $dailyStats[] = [ 'date' => $date, 'users' => $this->userModel->where('DATE(created_at)', $date)->countAllResults(), 'orgs' => $this->organizationModel->where('DATE(created_at)', $date)->countAllResults(), ]; } // Статистика по тарифам (через таблицу подписок) $planStats = []; $plans = $this->planModel->where('is_active', 1)->findAll(); // Проверяем существование таблицы подписок $db = \Config\Database::connect(); $tableExists = $db->tableExists('organization_plan_subscriptions'); if ($tableExists) { $subscriptionsTable = $db->table('organization_plan_subscriptions'); foreach ($plans as $plan) { $count = $subscriptionsTable->where('plan_id', $plan['id']) ->where('status', 'active') ->countAllResults(); $planStats[$plan['id']] = [ 'name' => $plan['name'], 'orgs_count' => $count, ]; } } else { // Таблица подписок ещё не создана - показываем 0 для всех тарифов foreach ($plans as $plan) { $planStats[$plan['id']] = [ 'name' => $plan['name'], 'orgs_count' => 0, ]; } } return $this->renderTwig('superadmin/statistics', compact('dailyStats', 'planStats')); } }