dashboard for org

This commit is contained in:
root
2026-01-12 11:58:12 +03:00
parent d27f66953c
commit 246ca93307
30 changed files with 4362 additions and 125 deletions
+599 -45
View File
@@ -4,17 +4,25 @@ namespace App\Controllers;
use App\Models\OrganizationModel;
use App\Models\OrganizationUserModel;
use App\Models\UserModel;
use App\Services\AccessService;
class Organizations extends BaseController
{
protected OrganizationUserModel $orgUserModel;
public function __construct()
{
$this->orgUserModel = new OrganizationUserModel();
}
public function index()
{
$orgModel = new OrganizationModel();
$orgUserModel = new OrganizationUserModel();
$userId = session()->get('user_id');
// Получаем организации пользователя через связующую таблицу
$userOrgLinks = $orgUserModel->where('user_id', $userId)->findAll();
$userOrgLinks = $this->orgUserModel->where('user_id', $userId)->findAll();
// Нам нужно получить сами данные организаций
$orgIds = array_column($userOrgLinks, 'organization_id');
@@ -24,12 +32,6 @@ class Organizations extends BaseController
$organizations = $orgModel->whereIn('id', $orgIds)->findAll();
}
// Логика автоперехода (как в Auth)
// if (count($organizations) === 1) {
// session()->set('active_org_id', $organizations[0]['id']);
// return redirect()->to('/');
// }
// Если больше 1 или 0, показываем список
return $this->renderTwig('organizations/index', [
'organizations' => $organizations,
@@ -41,7 +43,6 @@ class Organizations extends BaseController
{
if ($this->request->getMethod() === 'POST') {
$orgModel = new OrganizationModel();
$orgUserModel = new OrganizationUserModel();
$rules = [
'name' => 'required|min_length[2]',
@@ -77,7 +78,7 @@ class Organizations extends BaseController
]);
// Привязываем владельца
$orgUserModel->insert([
$this->orgUserModel->insert([
'organization_id' => $orgId,
'user_id' => session()->get('user_id'),
'role' => 'owner',
@@ -96,30 +97,66 @@ class Organizations extends BaseController
return $this->renderTwig('organizations/create');
}
/**
* Дашборд управления организацией
*/
public function dashboard($orgId)
{
$orgId = (int) $orgId;
$membership = $this->getMembership($orgId);
if (!$membership) {
return $this->redirectWithError('Доступ запрещен', '/organizations');
}
// Получаем данные организации
$orgModel = new OrganizationModel();
$organization = $orgModel->find($orgId);
if (!$organization) {
return $this->redirectWithError('Организация не найдена', '/organizations');
}
// Получаем статистику организации
$stats = [
'users_total' => $this->orgUserModel->where('organization_id', $orgId)->countAllResults(),
'users_active' => $this->orgUserModel->where('organization_id', $orgId)->where('status', 'active')->countAllResults(),
'users_blocked' => $this->orgUserModel->where('organization_id', $orgId)->where('status', 'blocked')->countAllResults(),
];
// Проверяем права для отображения пунктов меню
$canManageUsers = $this->access->canManageUsers();
$canEditOrg = true; // Все члены организации могут видеть настройки (редактировать могут только admin+)
return $this->renderTwig('organizations/dashboard', [
'organization' => $organization,
'organization_id' => $orgId,
'stats' => $stats,
'current_role' => $membership['role'],
'can_manage_users' => $canManageUsers,
'can_edit_org' => $canEditOrg,
]);
}
/**
* Редактирование организации
*/
public function edit($orgId)
{
$orgModel = new OrganizationModel();
$orgUserModel = new OrganizationUserModel();
$userId = session()->get('user_id');
// Проверяем: имеет ли пользователь доступ к этой организации?
$membership = $orgUserModel->where('organization_id', $orgId)
->where('user_id', $userId)
->first();
// Проверяем доступ через AccessService
$orgId = (int) $orgId;
$membership = $this->getMembership($orgId);
if (!$membership) {
session()->setFlashdata('error', 'Доступ запрещен');
return redirect()->to('/organizations');
return $this->redirectWithError('Доступ запрещен', '/organizations');
}
// Получаем организацию
// Проверяем права на редактирование (все роли могут редактировать)
$orgModel = new OrganizationModel();
$organization = $orgModel->find($orgId);
if (!$organization) {
session()->setFlashdata('error', 'Организация не найдена');
return redirect()->to('/organizations');
return $this->redirectWithError('Организация не найдена', '/organizations');
}
// Декодируем requisites для формы
@@ -173,37 +210,29 @@ class Organizations extends BaseController
*/
public function delete($orgId)
{
$orgModel = new OrganizationModel();
$orgUserModel = new OrganizationUserModel();
$userId = session()->get('user_id');
// Проверяем: имеет ли пользователь доступ к этой организации?
$membership = $orgUserModel->where('organization_id', $orgId)
->where('user_id', $userId)
->first();
$orgId = (int) $orgId;
$membership = $this->getMembership($orgId);
if (!$membership) {
session()->setFlashdata('error', 'Доступ запрещен');
return redirect()->to('/organizations');
return $this->redirectWithError('Доступ запрещен', '/organizations');
}
// Проверяем, что пользователь — владелец
if ($membership['role'] !== 'owner') {
session()->setFlashdata('error', 'Только владелец может удалить организацию');
return redirect()->to('/organizations');
// Проверяем права: только владелец может удалить
if (!$this->access->canDeleteOrganization()) {
return $this->redirectWithError('Только владелец может удалить организацию', '/organizations');
}
// Получаем организацию
$orgModel = new OrganizationModel();
$organization = $orgModel->find($orgId);
if (!$organization) {
session()->setFlashdata('error', 'Организация не найдена');
return redirect()->to('/organizations');
return $this->redirectWithError('Организация не найдена', '/organizations');
}
// Если это POST с подтверждением — удаляем
if ($this->request->getMethod() === 'POST') {
// Удаляем связи с пользователями
$orgUserModel->where('organization_id', $orgId)->delete();
// Удаляем связи с пользователями через forCurrentOrg()
$this->orgUserModel->forCurrentOrg()->delete();
// Мягкое удаление организации
$orgModel->delete($orgId);
@@ -226,14 +255,18 @@ class Organizations extends BaseController
public function switch($orgId)
{
$userId = session()->get('user_id');
$orgUserModel = new OrganizationUserModel();
$orgId = (int) $orgId;
// Проверяем: имеет ли пользователь доступ к этой организации?
$membership = $orgUserModel->where('organization_id', $orgId)
// Проверяем доступ
$membership = $this->orgUserModel
->where('organization_id', $orgId)
->where('user_id', $userId)
->first();
if ($membership) {
// Сбрасываем кэш AccessService при смене организации
$this->access->resetCache();
session()->set('active_org_id', $orgId);
session()->setFlashdata('success', 'Организация изменена');
return redirect()->to('/');
@@ -242,4 +275,525 @@ class Organizations extends BaseController
return redirect()->to('/organizations');
}
}
// ========================================
// Управление пользователями организации
// ========================================
/**
* Список пользователей организации
*/
public function users($orgId)
{
$orgId = (int) $orgId;
$membership = $this->getMembership($orgId);
if (!$membership) {
return $this->redirectWithError('Доступ запрещен', '/organizations');
}
// Получаем организацию
$orgModel = new OrganizationModel();
$organization = $orgModel->find($orgId);
if (!$organization) {
return $this->redirectWithError('Организация не найдена', '/organizations');
}
// Проверяем права через единое место в AccessService (включает проверку типа организации)
if (!$this->access->canManageUsers()) {
return $this->redirectWithError('У вас нет прав для управления пользователями', '/organizations/' . $orgId . '/dashboard');
}
// Рендерим таблицу через универсальный компонент
$tableHtml = $this->renderTable($this->getUsersTableConfig($orgId));
// Получаем данные пользователей для статистики
$users = $this->orgUserModel->getOrganizationUsers($orgId);
return $this->renderTwig('organizations/users', [
'organization' => $organization,
'organization_id' => $orgId,
'tableHtml' => $tableHtml,
'users' => $users,
'current_user_id' => session()->get('user_id'),
'can_manage_users' => $this->access->canManageUsers(),
'current_role' => $membership['role'],
]);
}
/**
* Конфигурация таблицы пользователей
*/
protected function getUsersTableConfig(int $orgId): array
{
// Проверяем права для кнопок действий
$canManage = $this->access->canManageUsers();
return [
'id' => 'users-table',
'url' => '/organizations/' . $orgId . '/users/table',
'model' => $this->orgUserModel,
'columns' => [
'user_email' => [
'label' => 'Пользователь',
'width' => '35%',
'type' => 'user_display',
],
'role' => [
'label' => 'Роль',
'width' => '15%',
'type' => 'role_badge',
],
'status' => [
'label' => 'Статус',
'width' => '15%',
'type' => 'status_badge',
],
'joined_at' => [
'label' => 'Дата входа',
'width' => '20%',
'type' => 'datetime',
'default' => '—',
],
],
'searchable' => ['user_email', 'user_name'],
'sortable' => ['joined_at', 'role', 'status'],
'defaultSort' => 'joined_at',
'order' => 'desc',
'actions' => true, // Включаем колонку действий
'actionsConfig' => [
// Изменение роли
[
'label' => 'Изменить роль',
'url' => '/organizations/users/' . $orgId . '/role/{user_id}',
'icon' => 'fa-solid fa-user-gear',
'class' => 'btn-outline-primary btn-sm',
'type' => 'edit',
],
// Блокировка
[
'label' => 'Заблокировать',
'url' => '/organizations/users/' . $orgId . '/block/{user_id}',
'icon' => 'fa-solid fa-ban',
'class' => 'btn-outline-warning btn-sm',
'type' => 'block',
],
// Разблокировка
[
'label' => 'Разблокировать',
'url' => '/organizations/users/' . $orgId . '/unblock/{user_id}',
'icon' => 'fa-solid fa-check',
'class' => 'btn-outline-success btn-sm',
'type' => 'unblock',
],
// Удаление
[
'label' => 'Удалить',
'url' => '/organizations/users/' . $orgId . '/remove/{user_id}',
'icon' => 'fa-solid fa-user-xmark',
'class' => 'btn-outline-danger btn-sm',
'type' => 'delete',
],
],
'can_edit' => $canManage,
'can_delete' => $canManage,
'emptyMessage' => 'В организации пока нет участников',
'emptyIcon' => 'fa-solid fa-users',
'emptyActionUrl' => '',
'emptyActionLabel'=> '',
'emptyActionIcon' => '',
'scope' => function ($builder) use ($orgId) {
$builder->select('ou.*, u.email as user_email, u.name as user_name, u.avatar as user_avatar')
->from('organization_users ou')
->join('users u', 'u.id = ou.user_id', 'left')
->where('ou.organization_id', $orgId);
},
// Ключи фильтров (совпадают с ключами columns)
'searchable' => ['user_email', 'user_name'],
// Маппинг ключей фильтров к реальным колонкам БД
'fieldMap' => [
'user_email' => 'u.email',
'user_name' => 'u.name',
],
];
}
/**
* Таблица пользователей (AJAX)
*/
public function usersTable($orgId)
{
$orgId = (int) $orgId;
$membership = $this->getMembership($orgId);
if (!$membership) {
return $this->forbiddenResponse('Доступ запрещён');
}
// Проверяем права через единое место в AccessService (включает проверку типа организации)
if (!$this->access->canManageUsers()) {
return $this->forbiddenResponse('Управление пользователями недоступно');
}
return $this->table(
$this->getUsersTableConfig($orgId),
'/organizations/' . $orgId . '/users' // URL для редиректа
);
}
/**
* Приглашение пользователя (AJAX)
*/
public function inviteUser($orgId)
{
$orgId = (int) $orgId;
$membership = $this->getMembership($orgId);
if (!$membership) {
return $this->response->setJSON([
'success' => false,
'message' => 'Доступ запрещен',
]);
}
// Проверяем права на управление пользователями
if (!$this->access->canManageUsers()) {
return $this->response->setJSON([
'success' => false,
'message' => 'У вас нет прав для приглашения пользователей',
]);
}
if (!$this->request->isAJAX()) {
return redirect()->to("/organizations/users/{$orgId}");
}
$email = $this->request->getPost('email');
$role = $this->request->getPost('role');
// Валидация
if (empty($email) || empty($role)) {
return $this->response->setJSON([
'success' => false,
'message' => 'Email и роль обязательны',
]);
}
// Проверяем валидность роли
$availableRoles = $this->access->getAvailableRolesForAssignment($membership['role']);
if (!in_array($role, $availableRoles)) {
return $this->response->setJSON([
'success' => false,
'message' => 'Недопустимая роль',
]);
}
// Создаем приглашение через сервис
$invitationService = new \App\Services\InvitationService();
$result = $invitationService->createInvitation(
$orgId,
$email,
$role,
session()->get('user_id')
);
return $this->response->setJSON($result);
}
/**
* Блокировка пользователя
*/
public function blockUser($orgId, $userId)
{
$orgId = (int) $orgId;
$userId = (int) $userId;
$membership = $this->getMembership($orgId);
if (!$membership) {
return $this->redirectWithError('Доступ запрещен', '/organizations');
}
if (!$this->access->canManageUsers()) {
return $this->redirectWithError('У вас нет прав для блокировки', "/organizations/users/{$orgId}");
}
// Нельзя заблокировать владельца
$targetMembership = $this->orgUserModel
->where('organization_id', $orgId)
->where('user_id', $userId)
->first();
if (!$targetMembership) {
return $this->redirectWithError('Пользователь не найден', "/organizations/users/{$orgId}");
}
if ($targetMembership['role'] === 'owner') {
return $this->redirectWithError('Нельзя заблокировать владельца', "/organizations/users/{$orgId}");
}
$this->orgUserModel->blockUser($targetMembership['id']);
session()->setFlashdata('success', 'Пользователь заблокирован');
return redirect()->to("/organizations/users/{$orgId}");
}
/**
* Разблокировка пользователя
*/
public function unblockUser($orgId, $userId)
{
$orgId = (int) $orgId;
$userId = (int) $userId;
$membership = $this->getMembership($orgId);
if (!$membership || !$this->access->canManageUsers()) {
return $this->redirectWithError('Доступ запрещен', '/organizations');
}
$targetMembership = $this->orgUserModel
->where('organization_id', $orgId)
->where('user_id', $userId)
->first();
if (!$targetMembership) {
return $this->redirectWithError('Пользователь не найден', "/organizations/users/{$orgId}");
}
$this->orgUserModel->unblockUser($targetMembership['id']);
session()->setFlashdata('success', 'Пользователь разблокирован');
return redirect()->to("/organizations/users/{$orgId}");
}
/**
* Выход пользователя из организации
*/
public function leaveOrganization($orgId)
{
$orgId = (int) $orgId;
$membership = $this->getMembership($orgId);
if (!$membership) {
return $this->redirectWithError('Вы не состоите в этой организации', '/organizations');
}
// Владелец не может покинуть организацию
if ($membership['role'] === 'owner') {
return $this->redirectWithError('Владелец не может покинуть организацию. Передайте права другому администратору.', "/organizations/users/{$orgId}");
}
// Удаляем из организации
$this->orgUserModel->delete($membership['id']);
// Если это была активная организация - переключаем на другую
if (session()->get('active_org_id') == $orgId) {
$userId = session()->get('user_id');
$otherOrgs = $this->orgUserModel->where('user_id', $userId)->where('status', 'active')->findAll();
if (!empty($otherOrgs)) {
session()->set('active_org_id', $otherOrgs[0]['organization_id']);
} else {
session()->remove('active_org_id');
}
}
session()->setFlashdata('success', 'Вы покинули организацию');
return redirect()->to('/organizations');
}
/**
* Повторная отправка приглашения
*/
public function resendInvite($orgId, $invitationId)
{
$orgId = (int) $orgId;
$membership = $this->getMembership($orgId);
if (!$membership || !$this->access->canManageUsers()) {
return $this->redirectWithError('Доступ запрещен', '/organizations');
}
$invitationService = new \App\Services\InvitationService();
$result = $invitationService->resendInvitation($invitationId, $orgId);
if ($result['success']) {
session()->setFlashdata('success', 'Приглашение отправлено повторно');
} else {
session()->setFlashdata('error', $result['message']);
}
return redirect()->to("/organizations/users/{$orgId}");
}
/**
* Отзыв приглашения
*/
public function cancelInvite($orgId, $invitationId)
{
$orgId = (int) $orgId;
$membership = $this->getMembership($orgId);
if (!$membership || !$this->access->canManageUsers()) {
return $this->redirectWithError('Доступ запрещен', '/organizations');
}
$invitationService = new \App\Services\InvitationService();
$result = $invitationService->cancelInvitation($invitationId, $orgId);
if ($result['success']) {
session()->setFlashdata('success', 'Приглашение отозвано');
} else {
session()->setFlashdata('error', $result['message']);
}
return redirect()->to("/organizations/users/{$orgId}");
}
/**
* Изменение роли пользователя
*/
public function updateUserRole($orgId, $userId)
{
$orgId = (int) $orgId;
$userId = (int) $userId;
$membership = $this->getMembership($orgId);
if (!$membership) {
return $this->redirectWithError('Доступ запрещен', '/organizations');
}
// Проверяем права
if (!$this->access->canManageUsers()) {
return $this->redirectWithError('У вас нет прав для изменения ролей', "/organizations/users/{$orgId}");
}
// Нельзя изменить роль владельца
$targetMembership = $this->orgUserModel
->where('organization_id', $orgId)
->where('user_id', $userId)
->first();
if (!$targetMembership) {
return $this->redirectWithError('Пользователь не найден в организации', "/organizations/users/{$orgId}");
}
if ($targetMembership['role'] === 'owner') {
return $this->redirectWithError('Нельзя изменить роль владельца', "/organizations/users/{$orgId}");
}
if ($this->request->getMethod() === 'POST') {
$newRole = $this->request->getPost('role');
// Проверяем валидность роли
$availableRoles = $this->access->getAvailableRolesForAssignment($membership['role']);
if (!in_array($newRole, $availableRoles)) {
return redirect()->back()->withInput()->with('error', 'Недопустимая роль');
}
$this->orgUserModel->update($targetMembership['id'], [
'role' => $newRole,
]);
session()->setFlashdata('success', 'Роль изменена');
return redirect()->to("/organizations/users/{$orgId}");
}
$userModel = new UserModel();
$user = $userModel->find($userId);
return $this->renderTwig('organizations/edit_user_role', [
'organization_id' => $orgId,
'user' => $user,
'current_role' => $targetMembership['role'],
'available_roles' => $availableRoles,
]);
}
/**
* Удаление пользователя из организации
*/
public function removeUser($orgId, $userId)
{
$orgId = (int) $orgId;
$userId = (int) $userId;
$currentUserId = session()->get('user_id');
$membership = $this->getMembership($orgId);
if (!$membership) {
return $this->redirectWithError('Доступ запрещен', '/organizations');
}
// Проверяем права
if (!$this->access->canManageUsers()) {
return $this->redirectWithError('У вас нет прав для удаления пользователей', "/organizations/users/{$orgId}");
}
// Нельзя удалить владельца
$targetMembership = $this->orgUserModel
->where('organization_id', $orgId)
->where('user_id', $userId)
->first();
if (!$targetMembership) {
return $this->redirectWithError('Пользователь не найден', "/organizations/users/{$orgId}");
}
if ($targetMembership['role'] === 'owner') {
return $this->redirectWithError('Нельзя удалить владельца организации', "/organizations/users/{$orgId}");
}
// Нельзя удалить самого себя (если ты admin)
if ($userId === $currentUserId) {
return $this->redirectWithError('Нельзя удалить себя из организации', "/organizations/users/{$orgId}");
}
// Удаляем пользователя из организации
$this->orgUserModel->delete($targetMembership['id']);
session()->setFlashdata('success', 'Пользователь удалён из организации');
return redirect()->to("/organizations/users/{$orgId}");
}
// ========================================
// Вспомогательные методы
// ========================================
/**
* Получение membership пользователя для организации
*
* @param int $orgId
* @return array|null
*/
protected function getMembership(int $orgId): ?array
{
$userId = session()->get('user_id');
if (!$userId || !$orgId) {
return null;
}
return $this->orgUserModel
->where('organization_id', $orgId)
->where('user_id', $userId)
->first();
}
/**
* Редирект с сообщением об ошибке
*
* @param string $message
* @param string $redirectUrl
* @return ResponseInterface
*/
protected function redirectWithError(string $message, string $redirectUrl)
{
if ($this->request->isAJAX()) {
return service('response')
->setStatusCode(403)
->setJSON(['error' => $message]);
}
session()->setFlashdata('error', $message);
return redirect()->to($redirectUrl);
}
}