577 lines
21 KiB
PHP
577 lines
21 KiB
PHP
<?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'));
|
||
}
|
||
}
|