bp/app/Controllers/Superadmin.php

559 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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'));
}
}