Files
bp/app/Controllers/Superadmin.php
T

559 lines
21 KiB
PHP

<?php
namespace App\Controllers;
/**
* Superadmin - Панель суперадмина
*
* Управление системой: тарифы, организации, пользователи.
*/
class Superadmin extends BaseController
{
protected $organizationModel;
protected $userModel;
protected $planModel;
public function __construct()
{
$this->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'));
}
}