bp/app/Controllers/Superadmin.php

577 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;
use App\Models\OrganizationModel;
use App\Models\OrganizationSubscriptionModel;
use App\Models\OrganizationUserModel;
use App\Models\UserModel;
use App\Services\ModuleSubscriptionService;
/**
* Superadmin - Панель суперадмина
*
* Управление системой: модули, подписки организаций, пользователи.
*/
class Superadmin extends BaseController
{
protected $organizationModel;
protected $userModel;
protected $subscriptionModel;
protected ?OrganizationUserModel $orgUserModel = null;
protected ModuleSubscriptionService $subscriptionService;
public function __construct()
{
$this->organizationModel = new OrganizationModel();
$this->userModel = new UserModel();
$this->subscriptionModel = new OrganizationSubscriptionModel();
$this->subscriptionService = service('moduleSubscription');
}
/**
* Дашборд суперадмина
*/
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_modules' => count($this->subscriptionService->getAllModules()),
];
$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 modules()
{
$modules = $this->subscriptionService->getAllModules();
return $this->renderTwig('superadmin/modules/index', compact('modules'));
}
/**
* Обновление параметров модуля
*/
public function updateModule()
{
$moduleCode = $this->request->getPost('module_code');
$config = $this->subscriptionService->getModuleConfig($moduleCode);
if (!$moduleCode || !$config) {
return redirect()->back()->with('error', 'Модуль не найден');
}
$this->subscriptionService->saveModuleSettings(
$moduleCode,
$this->request->getPost('name'),
$this->request->getPost('description'),
(int) $this->request->getPost('price_monthly'),
(int) $this->request->getPost('price_yearly'),
(int) $this->request->getPost('trial_days')
);
return redirect()->to('/superadmin/modules')->with('success', 'Модуль успешно обновлён');
}
// =========================================================================
// УПРАВЛЕНИЕ ПОДПИСКАМИ
// =========================================================================
/**
* Конфигурация таблицы подписок
*/
protected function getSubscriptionsTableConfig(): array
{
return [
'id' => 'subscriptions-table',
'url' => '/superadmin/subscriptions/table',
'model' => $this->subscriptionModel,
'columns' => [
'id' => ['label' => 'ID', 'width' => '60px'],
'organization_name' => ['label' => 'Организация'],
'module_code' => ['label' => 'Модуль', 'width' => '100px'],
'status' => ['label' => 'Статус', 'width' => '100px'],
'expires_at' => ['label' => 'Истекает', 'width' => '120px'],
'created_at' => ['label' => 'Создана', 'width' => '120px'],
],
'searchable' => ['id', 'organization_name', 'module_code'],
'sortable' => ['id', 'created_at', 'expires_at'],
'defaultSort' => 'created_at',
'order' => 'desc',
'fieldMap' => [
'organization_name' => 'organizations.name',
'id' => 'organization_subscriptions.id',
'module_code' => 'organization_subscriptions.module_code',
'status' => 'organization_subscriptions.status',
'expires_at' => 'organization_subscriptions.expires_at',
'created_at' => 'organization_subscriptions.created_at',
],
'scope' => function ($builder) {
$builder->from('organization_subscriptions')
->select('organization_subscriptions.*, organizations.name as organization_name')
->join('organizations', 'organizations.id = organization_subscriptions.organization_id');
},
'actions' => ['label' => 'Действия', 'width' => '100px'],
'actionsConfig' => [
[
'label' => '',
'url' => '/superadmin/subscriptions/delete/{id}',
'icon' => 'fa-solid fa-trash',
'class' => 'btn-outline-danger',
'title' => 'Удалить',
'confirm' => 'Удалить подписку?',
],
],
'emptyMessage' => 'Подписки не найдены',
'emptyIcon' => 'fa-solid fa-credit-card',
];
}
/**
* Список подписок
*/
public function subscriptions()
{
$config = $this->getSubscriptionsTableConfig();
$tableHtml = $this->renderTable($config);
$modules = $this->subscriptionService->getAllModules();
$organizations = $this->organizationModel->findAll();
return $this->renderTwig('superadmin/subscriptions/index', [
'tableHtml' => $tableHtml,
'config' => $config,
'modules' => $modules,
'organizations' => $organizations,
]);
}
/**
* AJAX таблица подписок
*/
public function subscriptionsTable()
{
return parent::table($this->getSubscriptionsTableConfig(), '/superadmin/subscriptions');
}
/**
* Поиск организаций для autocomplete
*/
public function searchOrganizations()
{
$query = $this->request->getGet('q') ?? '';
$limit = 20;
$builder = $this->organizationModel->db()->table('organizations');
$builder->select('organizations.*, users.email as owner_email')
->join('organization_users', 'organization_users.organization_id = organizations.id AND organization_users.role = "owner"')
->join('users', 'users.id = organization_users.user_id')
->groupStart()
->like('organizations.name', $query)
->orLike('organizations.id', $query)
->orLike('users.email', $query)
->groupEnd()
->limit($limit);
$results = [];
foreach ($builder->get()->getResultArray() as $org) {
$results[] = [
'id' => $org['id'],
'text' => $org['name'] . ' (ID: ' . $org['id'] . ') — ' . $org['owner_email'],
];
}
return $this->response->setJSON(['results' => $results]);
}
/**
* Добавление подписки (форма)
*/
public function createSubscription()
{
$organizations = $this->organizationModel->findAll();
$modules = $this->subscriptionService->getAllModules();
return $this->renderTwig('superadmin/subscriptions/create', compact('organizations', 'modules'));
}
/**
* Сохранение подписки
*/
public function storeSubscription()
{
$organizationId = (int) $this->request->getPost('organization_id');
$moduleCode = $this->request->getPost('module_code');
$durationDays = (int) $this->request->getPost('duration_days');
$status = $this->request->getPost('status') ?? 'active';
// Валидация
$organization = $this->organizationModel->find($organizationId);
if (!$organization) {
return redirect()->back()->withInput()->with('error', 'Организация не найдена');
}
$moduleConfig = $this->subscriptionService->getModuleConfig($moduleCode);
if (!$moduleCode || !$moduleConfig) {
return redirect()->back()->withInput()->with('error', 'Модуль не найден');
}
$this->subscriptionService->upsertSubscription(
$organizationId,
$moduleCode,
$status,
$durationDays
);
return redirect()->to('/superadmin/subscriptions')->with('success', 'Подписка создана');
}
/**
* Удаление подписки
*/
public function deleteSubscription($id)
{
$this->subscriptionService->deleteSubscription($id);
return redirect()->to('/superadmin/subscriptions')->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' => 'Название'],
'owner_login' => ['label' => 'Владелец', 'width' => '150px'],
'type' => ['label' => 'Тип', 'width' => '100px'],
'user_count' => ['label' => 'Пользователей', 'width' => '100px'],
'status' => ['label' => 'Статус', 'width' => '120px'],
'created_at' => ['label' => 'Дата', 'width' => '100px'],
],
'searchable' => ['name', 'id', 'owner_login'],
'sortable' => ['id', 'name', 'created_at'],
'defaultSort' => 'created_at',
'order' => 'desc',
'scope' => function ($builder) {
$builder->resetQuery();
$builder->select('organizations.*,
(SELECT COUNT(*) FROM organization_users WHERE organization_users.organization_id = organizations.id AND status = "active") as user_count,
owner_users.email as owner_login')
->join('organization_users as ou', 'ou.organization_id = organizations.id AND ou.role = "owner"')
->join('users as owner_users', 'owner_users.id = ou.user_id', 'left');
},
'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('Организация не найдена');
}
$users = $this->getOrgUserModel()->getOrganizationUsers($id);
$subscriptions = $this->subscriptionService->getOrganizationSubscriptions($id);
$allModules = $this->subscriptionService->getAllModules();
return $this->renderTwig('superadmin/organizations/view', compact(
'organization',
'users',
'subscriptions',
'allModules'
));
}
/**
* Быстрое добавление подписки организации из просмотра организации
*/
public function addOrganizationSubscription($organizationId)
{
$moduleCode = $this->request->getPost('module_code');
$durationDays = (int) $this->request->getPost('duration_days');
$status = $this->request->getPost('status') ?? 'active';
if (!$moduleCode) {
return redirect()->back()->with('error', 'Модуль не выбран');
}
$this->subscriptionService->upsertSubscription(
$organizationId,
$moduleCode,
$status,
$durationDays
);
return redirect()->to("/superadmin/organizations/view/{$organizationId}")->with('success', 'Подписка добавлена');
}
/**
* Удаление подписки организации
*/
public function removeOrganizationSubscription($organizationId, $subscriptionId)
{
$this->subscriptionService->deleteSubscription($subscriptionId);
return redirect()->to("/superadmin/organizations/view/{$organizationId}")->with('success', 'Подписка удалена');
}
/**
* Блокировка организации
*/
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)
{
$this->organizationModel->delete($id, true);
return redirect()->to('/superadmin/organizations')->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) {
$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)
{
$this->userModel->delete($id, true);
return redirect()->to('/superadmin/users')->with('success', 'Пользователь удалён');
}
// =========================================================================
// СТАТИСТИКА
// =========================================================================
/**
* Статистика использования
*/
public function statistics()
{
$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(),
];
}
$moduleStats = $this->subscriptionService->getModuleStats();
return $this->renderTwig('superadmin/statistics', compact('dailyStats', 'moduleStats'));
}
}