refactoring
This commit is contained in:
parent
077b79b8f7
commit
c55264cf42
|
|
@ -93,45 +93,6 @@ class Auth extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирование времени блокировки для отображения
|
||||
*
|
||||
* @param int $seconds Секунды
|
||||
* @return string
|
||||
*/
|
||||
protected function formatBlockTime(int $seconds): string
|
||||
{
|
||||
if ($seconds >= 60) {
|
||||
$minutes = ceil($seconds / 60);
|
||||
return $minutes . ' ' . $this->pluralize($minutes, ['минуту', 'минуты', 'минут']);
|
||||
}
|
||||
return $seconds . ' ' . $this->pluralize($seconds, ['секунду', 'секунды', 'секунд']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Склонение окончаний для чисел
|
||||
*
|
||||
* @param int $number
|
||||
* @param array $forms [одна, две, пять]
|
||||
* @return string
|
||||
*/
|
||||
protected function pluralize(int $number, array $forms): string
|
||||
{
|
||||
$abs = abs($number);
|
||||
$mod = $abs % 10;
|
||||
|
||||
if ($abs % 100 >= 11 && $abs % 100 <= 19) {
|
||||
return $forms[2];
|
||||
}
|
||||
if ($mod === 1) {
|
||||
return $forms[0];
|
||||
}
|
||||
if ($mod >= 2 && $mod <= 4) {
|
||||
return $forms[1];
|
||||
}
|
||||
return $forms[2];
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
if ($this->request->getMethod() === 'POST') {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\OrganizationUserModel;
|
||||
use App\Services\AccessService;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use App\Models\OrganizationModel;
|
||||
use App\Services\AccessService;
|
||||
|
||||
/**
|
||||
* BaseController provides a convenient place for loading components
|
||||
|
|
@ -29,6 +29,7 @@ abstract class BaseController extends Controller
|
|||
|
||||
protected $session;
|
||||
protected AccessService $access;
|
||||
protected ?OrganizationUserModel $orgUserModel = null;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
|
|
@ -51,11 +52,54 @@ abstract class BaseController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* Проверка права на действие ( shortcut для $this->access->can() )
|
||||
*
|
||||
* @param string $action
|
||||
* @param string $resource
|
||||
* @return bool
|
||||
* Получение лениво инициализированной модели OrganizationUserModel
|
||||
*/
|
||||
protected function getOrgUserModel(): OrganizationUserModel
|
||||
{
|
||||
if ($this->orgUserModel === null) {
|
||||
$this->orgUserModel = new OrganizationUserModel();
|
||||
}
|
||||
return $this->orgUserModel;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Методы для работы с пользователем и организацией
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Получение ID текущего пользователя
|
||||
*/
|
||||
protected function getCurrentUserId(): ?int
|
||||
{
|
||||
$userId = $this->session->get('user_id');
|
||||
return $userId ? (int) $userId : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение данных текущего пользователя
|
||||
*/
|
||||
protected function getCurrentUser(): ?array
|
||||
{
|
||||
$userId = $this->getCurrentUserId();
|
||||
if (!$userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userModel = new \App\Models\UserModel();
|
||||
return $userModel->find($userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение ID активной организации
|
||||
*/
|
||||
protected function getActiveOrgId(): ?int
|
||||
{
|
||||
$orgId = $this->session->get('active_org_id');
|
||||
return $orgId ? (int) $orgId : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка права на действие (shortcut для $this->access->can())
|
||||
*/
|
||||
protected function can(string $action, string $resource): bool
|
||||
{
|
||||
|
|
@ -63,16 +107,165 @@ abstract class BaseController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* Проверка роли (shortcut для $this->access->isRole() )
|
||||
*
|
||||
* @param string|array $roles
|
||||
* @return bool
|
||||
* Проверка роли (shortcut для $this->access->isRole())
|
||||
*/
|
||||
protected function isRole($roles): bool
|
||||
{
|
||||
return $this->access->isRole($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение membership пользователя для организации
|
||||
*
|
||||
* @param int $orgId
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getMembership(int $orgId): ?array
|
||||
{
|
||||
$userId = $this->getCurrentUserId();
|
||||
if (!$userId || !$orgId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getOrgUserModel()
|
||||
->where('organization_id', $orgId)
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение membership с требованием наличия доступа
|
||||
* Бросает исключение если доступ запрещён
|
||||
*
|
||||
* @param int $orgId
|
||||
* @return array
|
||||
*/
|
||||
protected function requireMembership(int $orgId): array
|
||||
{
|
||||
$membership = $this->getMembership($orgId);
|
||||
if (!$membership) {
|
||||
throw new \RuntimeException('Доступ запрещён');
|
||||
}
|
||||
return $membership;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение ID активной организации с требованием
|
||||
* Бросает исключение если организация не выбрана
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function requireActiveOrg(): int
|
||||
{
|
||||
$orgId = $this->getActiveOrgId();
|
||||
if (!$orgId) {
|
||||
throw new \RuntimeException('Организация не выбрана');
|
||||
}
|
||||
return $orgId;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Методы для редиректов и ответов
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Редирект с сообщением об ошибке
|
||||
*/
|
||||
protected function redirectWithError(string $message, string $redirectUrl): ResponseInterface
|
||||
{
|
||||
if ($this->request->isAJAX()) {
|
||||
return service('response')
|
||||
->setStatusCode(403)
|
||||
->setJSON(['error' => $message]);
|
||||
}
|
||||
|
||||
$this->session->setFlashdata('error', $message);
|
||||
return redirect()->to($redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Редирект с сообщением об успехе
|
||||
*/
|
||||
protected function redirectWithSuccess(string $message, string $redirectUrl): ResponseInterface
|
||||
{
|
||||
if ($this->request->isAJAX()) {
|
||||
return service('response')
|
||||
->setStatusCode(200)
|
||||
->setJSON(['success' => true, 'message' => $message]);
|
||||
}
|
||||
|
||||
$this->session->setFlashdata('success', $message);
|
||||
return redirect()->to($redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ответ для AJAX запросов с ошибкой доступа
|
||||
*/
|
||||
protected function forbiddenResponse(string $message = 'Доступ запрещён'): ResponseInterface
|
||||
{
|
||||
return service('response')
|
||||
->setStatusCode(403)
|
||||
->setJSON(['error' => $message]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ответ для AJAX запросов с ошибкой валидации
|
||||
*/
|
||||
protected function validationErrorResponse(string $message = 'Ошибка валидации', array $errors = []): ResponseInterface
|
||||
{
|
||||
return service('response')
|
||||
->setStatusCode(422)
|
||||
->setJSON([
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
'errors' => $errors,
|
||||
]);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Утилиты для вывода
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Форматирование времени блокировки для отображения
|
||||
*/
|
||||
protected function formatBlockTime(int $seconds): string
|
||||
{
|
||||
if ($seconds >= 60) {
|
||||
$minutes = ceil($seconds / 60);
|
||||
return $minutes . ' ' . $this->pluralize($minutes, ['минуту', 'минуты', 'минут']);
|
||||
}
|
||||
return $seconds . ' ' . $this->pluralize($seconds, ['секунду', 'секунды', 'секунд']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Склонение окончаний для чисел
|
||||
*
|
||||
* @param int $number
|
||||
* @param array $forms [одна, две, пять]
|
||||
* @return string
|
||||
*/
|
||||
protected function pluralize(int $number, array $forms): string
|
||||
{
|
||||
$abs = abs($number);
|
||||
$mod = $abs % 10;
|
||||
|
||||
if ($abs % 100 >= 11 && $abs % 100 <= 19) {
|
||||
return $forms[2];
|
||||
}
|
||||
if ($mod === 1) {
|
||||
return $forms[0];
|
||||
}
|
||||
if ($mod >= 2 && $mod <= 4) {
|
||||
return $forms[1];
|
||||
}
|
||||
return $forms[2];
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Рендеринг Twig
|
||||
// ========================================
|
||||
|
||||
public function renderTwig($template, $data = [])
|
||||
{
|
||||
helper('csrf');
|
||||
|
|
@ -154,7 +347,7 @@ abstract class BaseController extends Controller
|
|||
// Старый способ извлечения фильтров для совместимости
|
||||
foreach ($this->request->getGet() as $key => $value) {
|
||||
if (str_starts_with($key, 'filters[') && str_ends_with($key, ']')) {
|
||||
$field = substr($key, 8, -1); // Исправлено: было 9, должно быть 8
|
||||
$field = substr($key, 8, -1);
|
||||
$filters[$field] = $value;
|
||||
}
|
||||
}
|
||||
|
|
@ -185,7 +378,7 @@ abstract class BaseController extends Controller
|
|||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Сначала проверяем fieldMap (алиасы) — они имеют приоритет
|
||||
if (isset($config['fieldMap']) && isset($config['fieldMap'][$filterKey])) {
|
||||
$realField = $config['fieldMap'][$filterKey];
|
||||
|
|
@ -222,7 +415,7 @@ abstract class BaseController extends Controller
|
|||
];
|
||||
|
||||
$data = [
|
||||
'items' => $items, // Алиас для универсального шаблона
|
||||
'items' => $items,
|
||||
'pagerDetails' => $pagerData,
|
||||
'perPage' => $perPage,
|
||||
'sort' => $sort,
|
||||
|
|
@ -259,7 +452,6 @@ abstract class BaseController extends Controller
|
|||
$tableData['actions'] = $config['actions'] ?? false;
|
||||
$tableData['actionsConfig'] = $config['actionsConfig'] ?? [];
|
||||
$tableData['columns'] = $config['columns'] ?? [];
|
||||
$tableData['onRowClick'] = $config['onRowClick'] ?? null;
|
||||
|
||||
// Параметры для пустого состояния
|
||||
$tableData['emptyMessage'] = $config['emptyMessage'] ?? 'Нет данных';
|
||||
|
|
@ -287,17 +479,17 @@ abstract class BaseController extends Controller
|
|||
public function table(?array $config = null, ?string $pageUrl = null)
|
||||
{
|
||||
$isPartial = $this->request->getGet('format') === 'partial' || $this->isAjax();
|
||||
|
||||
|
||||
// Если это частичный запрос (AJAX) — возвращаем только таблицу
|
||||
if ($isPartial) {
|
||||
return $this->renderTable($config, true);
|
||||
}
|
||||
|
||||
|
||||
// Прямой запрос к /table — редиректим на основную страницу
|
||||
// Сохраняем все параметры: page, perPage, sort, order, filters
|
||||
$params = $this->request->getGet();
|
||||
unset($params['format']); // Убираем format=partial если был
|
||||
|
||||
|
||||
if ($pageUrl) {
|
||||
$redirectUrl = $pageUrl;
|
||||
} else {
|
||||
|
|
@ -305,11 +497,11 @@ abstract class BaseController extends Controller
|
|||
$tableUrl = $config['url'] ?? '/table';
|
||||
$redirectUrl = $tableUrl;
|
||||
}
|
||||
|
||||
|
||||
if (!empty($params)) {
|
||||
$redirectUrl .= '?' . http_build_query($params);
|
||||
}
|
||||
|
||||
|
||||
return redirect()->to($redirectUrl);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,26 +3,18 @@
|
|||
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();
|
||||
$userId = session()->get('user_id');
|
||||
$userId = $this->getCurrentUserId();
|
||||
|
||||
// Получаем организации пользователя через связующую таблицу
|
||||
$userOrgLinks = $this->orgUserModel->where('user_id', $userId)->findAll();
|
||||
$userOrgLinks = $this->getOrgUserModel()->where('user_id', $userId)->findAll();
|
||||
|
||||
// Нам нужно получить сами данные организаций
|
||||
$orgIds = array_column($userOrgLinks, 'organization_id');
|
||||
|
|
@ -70,7 +62,7 @@ class Organizations extends BaseController
|
|||
|
||||
// Создаем организацию
|
||||
$orgId = $orgModel->insert([
|
||||
'owner_id' => session()->get('user_id'),
|
||||
'owner_id' => $this->getCurrentUserId(),
|
||||
'name' => $this->request->getPost('name'),
|
||||
'type' => 'business',
|
||||
'requisites' => json_encode($requisites),
|
||||
|
|
@ -78,17 +70,17 @@ class Organizations extends BaseController
|
|||
]);
|
||||
|
||||
// Привязываем владельца
|
||||
$this->orgUserModel->insert([
|
||||
$this->getOrgUserModel()->insert([
|
||||
'organization_id' => $orgId,
|
||||
'user_id' => session()->get('user_id'),
|
||||
'user_id' => $this->getCurrentUserId(),
|
||||
'role' => 'owner',
|
||||
'status' => 'active',
|
||||
'joined_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
// Сразу переключаемся на неё
|
||||
session()->set('active_org_id', $orgId);
|
||||
session()->setFlashdata('success', 'Организация успешно создана!');
|
||||
$this->session->set('active_org_id', $orgId);
|
||||
$this->session->setFlashdata('success', 'Организация успешно создана!');
|
||||
|
||||
return redirect()->to('/');
|
||||
}
|
||||
|
|
@ -119,9 +111,9 @@ class Organizations extends BaseController
|
|||
|
||||
// Получаем статистику организации
|
||||
$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(),
|
||||
'users_total' => $this->getOrgUserModel()->where('organization_id', $orgId)->countAllResults(),
|
||||
'users_active' => $this->getOrgUserModel()->where('organization_id', $orgId)->where('status', 'active')->countAllResults(),
|
||||
'users_blocked' => $this->getOrgUserModel()->where('organization_id', $orgId)->where('status', 'blocked')->countAllResults(),
|
||||
];
|
||||
|
||||
// Проверяем права для отображения пунктов меню
|
||||
|
|
@ -194,7 +186,7 @@ class Organizations extends BaseController
|
|||
'requisites' => json_encode($newRequisites),
|
||||
]);
|
||||
|
||||
session()->setFlashdata('success', 'Организация успешно обновлена!');
|
||||
$this->session->setFlashdata('success', 'Организация успешно обновлена!');
|
||||
return redirect()->to('/organizations');
|
||||
}
|
||||
|
||||
|
|
@ -232,17 +224,17 @@ class Organizations extends BaseController
|
|||
// Если это POST с подтверждением — удаляем
|
||||
if ($this->request->getMethod() === 'POST') {
|
||||
// Удаляем связи с пользователями через forCurrentOrg()
|
||||
$this->orgUserModel->forCurrentOrg()->delete();
|
||||
$this->getOrgUserModel()->forCurrentOrg()->delete();
|
||||
|
||||
// Мягкое удаление организации
|
||||
$orgModel->delete($orgId);
|
||||
|
||||
// Если удаляли активную организацию — очищаем
|
||||
if (session()->get('active_org_id') == $orgId) {
|
||||
session()->remove('active_org_id');
|
||||
if ($this->session->get('active_org_id') == $orgId) {
|
||||
$this->session->remove('active_org_id');
|
||||
}
|
||||
|
||||
session()->setFlashdata('success', 'Организация "' . $organization['name'] . '" удалена');
|
||||
$this->session->setFlashdata('success', 'Организация "' . $organization['name'] . '" удалена');
|
||||
return redirect()->to('/organizations');
|
||||
}
|
||||
|
||||
|
|
@ -254,11 +246,11 @@ class Organizations extends BaseController
|
|||
|
||||
public function switch($orgId)
|
||||
{
|
||||
$userId = session()->get('user_id');
|
||||
$userId = $this->getCurrentUserId();
|
||||
$orgId = (int) $orgId;
|
||||
|
||||
// Проверяем доступ
|
||||
$membership = $this->orgUserModel
|
||||
$membership = $this->getOrgUserModel()
|
||||
->where('organization_id', $orgId)
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
|
|
@ -267,11 +259,11 @@ class Organizations extends BaseController
|
|||
// Сбрасываем кэш AccessService при смене организации
|
||||
$this->access->resetCache();
|
||||
|
||||
session()->set('active_org_id', $orgId);
|
||||
session()->setFlashdata('success', 'Организация изменена');
|
||||
$this->session->set('active_org_id', $orgId);
|
||||
$this->session->setFlashdata('success', 'Организация изменена');
|
||||
return redirect()->to('/');
|
||||
} else {
|
||||
session()->setFlashdata('error', 'Доступ запрещен');
|
||||
$this->session->setFlashdata('error', 'Доступ запрещен');
|
||||
return redirect()->to('/organizations');
|
||||
}
|
||||
}
|
||||
|
|
@ -309,14 +301,14 @@ class Organizations extends BaseController
|
|||
$tableHtml = $this->renderTable($this->getUsersTableConfig($orgId));
|
||||
|
||||
// Получаем данные пользователей для статистики
|
||||
$users = $this->orgUserModel->getOrganizationUsers($orgId);
|
||||
$users = $this->getOrgUserModel()->getOrganizationUsers($orgId);
|
||||
|
||||
return $this->renderTwig('organizations/users', [
|
||||
'organization' => $organization,
|
||||
'organization_id' => $orgId,
|
||||
'tableHtml' => $tableHtml,
|
||||
'users' => $users,
|
||||
'current_user_id' => session()->get('user_id'),
|
||||
'current_user_id' => $this->getCurrentUserId(),
|
||||
'can_manage_users' => $this->access->canManageUsers(),
|
||||
'current_role' => $membership['role'],
|
||||
]);
|
||||
|
|
@ -329,11 +321,11 @@ class Organizations extends BaseController
|
|||
{
|
||||
// Проверяем права для кнопок действий
|
||||
$canManage = $this->access->canManageUsers();
|
||||
|
||||
|
||||
return [
|
||||
'id' => 'users-table',
|
||||
'url' => '/organizations/' . $orgId . '/users/table',
|
||||
'model' => $this->orgUserModel,
|
||||
'model' => $this->getOrgUserModel(),
|
||||
'columns' => [
|
||||
'user_email' => [
|
||||
'label' => 'Пользователь',
|
||||
|
|
@ -495,7 +487,7 @@ class Organizations extends BaseController
|
|||
$orgId,
|
||||
$email,
|
||||
$role,
|
||||
session()->get('user_id')
|
||||
$this->getCurrentUserId()
|
||||
);
|
||||
|
||||
return $this->response->setJSON($result);
|
||||
|
|
@ -519,7 +511,7 @@ class Organizations extends BaseController
|
|||
}
|
||||
|
||||
// Нельзя заблокировать владельца
|
||||
$targetMembership = $this->orgUserModel
|
||||
$targetMembership = $this->getOrgUserModel()
|
||||
->where('organization_id', $orgId)
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
|
|
@ -532,9 +524,9 @@ class Organizations extends BaseController
|
|||
return $this->redirectWithError('Нельзя заблокировать владельца', "/organizations/users/{$orgId}");
|
||||
}
|
||||
|
||||
$this->orgUserModel->blockUser($targetMembership['id']);
|
||||
$this->getOrgUserModel()->blockUser($targetMembership['id']);
|
||||
|
||||
session()->setFlashdata('success', 'Пользователь заблокирован');
|
||||
$this->session->setFlashdata('success', 'Пользователь заблокирован');
|
||||
return redirect()->to("/organizations/users/{$orgId}");
|
||||
}
|
||||
|
||||
|
|
@ -551,7 +543,7 @@ class Organizations extends BaseController
|
|||
return $this->redirectWithError('Доступ запрещен', '/organizations');
|
||||
}
|
||||
|
||||
$targetMembership = $this->orgUserModel
|
||||
$targetMembership = $this->getOrgUserModel()
|
||||
->where('organization_id', $orgId)
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
|
|
@ -560,9 +552,9 @@ class Organizations extends BaseController
|
|||
return $this->redirectWithError('Пользователь не найден', "/organizations/users/{$orgId}");
|
||||
}
|
||||
|
||||
$this->orgUserModel->unblockUser($targetMembership['id']);
|
||||
$this->getOrgUserModel()->unblockUser($targetMembership['id']);
|
||||
|
||||
session()->setFlashdata('success', 'Пользователь разблокирован');
|
||||
$this->session->setFlashdata('success', 'Пользователь разблокирован');
|
||||
return redirect()->to("/organizations/users/{$orgId}");
|
||||
}
|
||||
|
||||
|
|
@ -584,21 +576,21 @@ class Organizations extends BaseController
|
|||
}
|
||||
|
||||
// Удаляем из организации
|
||||
$this->orgUserModel->delete($membership['id']);
|
||||
$this->getOrgUserModel()->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 ($this->session->get('active_org_id') == $orgId) {
|
||||
$userId = $this->getCurrentUserId();
|
||||
$otherOrgs = $this->getOrgUserModel()->where('user_id', $userId)->where('status', 'active')->findAll();
|
||||
|
||||
if (!empty($otherOrgs)) {
|
||||
session()->set('active_org_id', $otherOrgs[0]['organization_id']);
|
||||
$this->session->set('active_org_id', $otherOrgs[0]['organization_id']);
|
||||
} else {
|
||||
session()->remove('active_org_id');
|
||||
$this->session->remove('active_org_id');
|
||||
}
|
||||
}
|
||||
|
||||
session()->setFlashdata('success', 'Вы покинули организацию');
|
||||
$this->session->setFlashdata('success', 'Вы покинули организацию');
|
||||
return redirect()->to('/organizations');
|
||||
}
|
||||
|
||||
|
|
@ -618,9 +610,9 @@ class Organizations extends BaseController
|
|||
$result = $invitationService->resendInvitation($invitationId, $orgId);
|
||||
|
||||
if ($result['success']) {
|
||||
session()->setFlashdata('success', 'Приглашение отправлено повторно');
|
||||
$this->session->setFlashdata('success', 'Приглашение отправлено повторно');
|
||||
} else {
|
||||
session()->setFlashdata('error', $result['message']);
|
||||
$this->session->setFlashdata('error', $result['message']);
|
||||
}
|
||||
|
||||
return redirect()->to("/organizations/users/{$orgId}");
|
||||
|
|
@ -642,9 +634,9 @@ class Organizations extends BaseController
|
|||
$result = $invitationService->cancelInvitation($invitationId, $orgId);
|
||||
|
||||
if ($result['success']) {
|
||||
session()->setFlashdata('success', 'Приглашение отозвано');
|
||||
$this->session->setFlashdata('success', 'Приглашение отозвано');
|
||||
} else {
|
||||
session()->setFlashdata('error', $result['message']);
|
||||
$this->session->setFlashdata('error', $result['message']);
|
||||
}
|
||||
|
||||
return redirect()->to("/organizations/users/{$orgId}");
|
||||
|
|
@ -669,7 +661,7 @@ class Organizations extends BaseController
|
|||
}
|
||||
|
||||
// Нельзя изменить роль владельца
|
||||
$targetMembership = $this->orgUserModel
|
||||
$targetMembership = $this->getOrgUserModel()
|
||||
->where('organization_id', $orgId)
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
|
|
@ -691,11 +683,11 @@ class Organizations extends BaseController
|
|||
return redirect()->back()->withInput()->with('error', 'Недопустимая роль');
|
||||
}
|
||||
|
||||
$this->orgUserModel->update($targetMembership['id'], [
|
||||
$this->getOrgUserModel()->update($targetMembership['id'], [
|
||||
'role' => $newRole,
|
||||
]);
|
||||
|
||||
session()->setFlashdata('success', 'Роль изменена');
|
||||
$this->session->setFlashdata('success', 'Роль изменена');
|
||||
return redirect()->to("/organizations/users/{$orgId}");
|
||||
}
|
||||
|
||||
|
|
@ -717,7 +709,7 @@ class Organizations extends BaseController
|
|||
{
|
||||
$orgId = (int) $orgId;
|
||||
$userId = (int) $userId;
|
||||
$currentUserId = session()->get('user_id');
|
||||
$currentUserId = $this->getCurrentUserId();
|
||||
$membership = $this->getMembership($orgId);
|
||||
|
||||
if (!$membership) {
|
||||
|
|
@ -730,7 +722,7 @@ class Organizations extends BaseController
|
|||
}
|
||||
|
||||
// Нельзя удалить владельца
|
||||
$targetMembership = $this->orgUserModel
|
||||
$targetMembership = $this->getOrgUserModel()
|
||||
->where('organization_id', $orgId)
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
|
|
@ -749,51 +741,9 @@ class Organizations extends BaseController
|
|||
}
|
||||
|
||||
// Удаляем пользователя из организации
|
||||
$this->orgUserModel->delete($targetMembership['id']);
|
||||
$this->getOrgUserModel()->delete($targetMembership['id']);
|
||||
|
||||
session()->setFlashdata('success', 'Пользователь удалён из организации');
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,19 @@ class OrganizationModel extends Model
|
|||
protected $deletedField = 'deleted_at';
|
||||
|
||||
// Получить организации конкретного пользователя
|
||||
public function getUserOrganizations(int $userId)
|
||||
public function getUserOrganizations(int $userId): array
|
||||
{
|
||||
// TODO: Здесь мы будем делать JOIN с таблицей organization_users
|
||||
// Пока упрощенная версия для владельца
|
||||
return $this->where('owner_id', $userId)->findAll();
|
||||
$db = $this->db();
|
||||
$builder = $db->newQuery();
|
||||
|
||||
return $builder->select('o.*, ou.role, ou.status as membership_status, ou.joined_at')
|
||||
->from('organizations o')
|
||||
->join('organization_users ou', 'ou.organization_id = o.id', 'inner')
|
||||
->where('ou.user_id', $userId)
|
||||
->where('ou.status', 'active')
|
||||
->where('o.deleted_at', null)
|
||||
->orderBy('ou.joined_at', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +70,6 @@ class Clients extends BaseController
|
|||
'type' => 'delete',
|
||||
]
|
||||
],
|
||||
'onRowClick' => 'viewClient', // Функция для открытия карточки клиента
|
||||
'emptyMessage' => 'Клиентов пока нет',
|
||||
'emptyIcon' => 'fa-solid fa-users',
|
||||
'emptyActionUrl' => base_url('/clients/new'),
|
||||
|
|
@ -220,263 +219,4 @@ class Clients extends BaseController
|
|||
session()->setFlashdata('success', 'Клиент удалён');
|
||||
return redirect()->to('/clients');
|
||||
}
|
||||
|
||||
/**
|
||||
* Возврат ответа "Доступ запрещён"
|
||||
*
|
||||
* @param string $message
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function forbiddenResponse(string $message = 'Доступ запрещён')
|
||||
{
|
||||
if ($this->request->isAJAX()) {
|
||||
return service('response')
|
||||
->setStatusCode(403)
|
||||
->setJSON(['error' => $message]);
|
||||
}
|
||||
|
||||
session()->setFlashdata('error', $message);
|
||||
return redirect()->to('/');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// API: Просмотр, Экспорт, Импорт
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* API: Получение данных клиента для модального окна
|
||||
*/
|
||||
public function view($id)
|
||||
{
|
||||
if (!$this->access->canView('clients')) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'error' => 'Доступ запрещён'
|
||||
])->setStatusCode(403);
|
||||
}
|
||||
|
||||
$client = $this->clientModel->forCurrentOrg()->find($id);
|
||||
|
||||
if (!$client) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'error' => 'Клиент не найден'
|
||||
])->setStatusCode(404);
|
||||
}
|
||||
|
||||
// Формируем данные для ответа
|
||||
$data = [
|
||||
'id' => $client['id'],
|
||||
'name' => $client['name'],
|
||||
'email' => $client['email'] ?? '',
|
||||
'phone' => $client['phone'] ?? '',
|
||||
'notes' => $client['notes'] ?? '',
|
||||
'status' => $client['status'] ?? 'active',
|
||||
'created_at' => $client['created_at'] ? date('d.m.Y H:i', strtotime($client['created_at'])) : '',
|
||||
'updated_at' => $client['updated_at'] ? date('d.m.Y H:i', strtotime($client['updated_at'])) : '',
|
||||
];
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'data' => $data
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Экспорт клиентов
|
||||
*/
|
||||
public function export()
|
||||
{
|
||||
if (!$this->access->canView('clients')) {
|
||||
return $this->forbiddenResponse('Доступ запрещён');
|
||||
}
|
||||
|
||||
$format = $this->request->getGet('format') ?? 'csv';
|
||||
|
||||
// Получаем всех клиентов организации
|
||||
$clients = $this->clientModel->forCurrentOrg()->findAll();
|
||||
|
||||
// Устанавливаем заголовки для скачивания
|
||||
if ($format === 'xlsx') {
|
||||
$filename = 'clients_' . date('Y-m-d') . '.xlsx';
|
||||
$this->exportToXlsx($clients, $filename);
|
||||
} else {
|
||||
$filename = 'clients_' . date('Y-m-d') . '.csv';
|
||||
$this->exportToCsv($clients, $filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Экспорт в CSV
|
||||
*/
|
||||
protected function exportToCsv(array $clients, string $filename)
|
||||
{
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
// Заголовок CSV
|
||||
fputcsv($output, ['ID', 'Имя', 'Email', 'Телефон', 'Статус', 'Создан', 'Обновлён'], ';');
|
||||
|
||||
// Данные
|
||||
foreach ($clients as $client) {
|
||||
fputcsv($output, [
|
||||
$client['id'],
|
||||
$client['name'],
|
||||
$client['email'] ?? '',
|
||||
$client['phone'] ?? '',
|
||||
$client['status'] ?? 'active',
|
||||
$client['created_at'] ?? '',
|
||||
$client['updated_at'] ?? '',
|
||||
], ';');
|
||||
}
|
||||
|
||||
fclose($output);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Экспорт в XLSX (упрощённый через HTML table)
|
||||
*/
|
||||
protected function exportToXlsx(array $clients, string $filename)
|
||||
{
|
||||
// Для упрощения используем HTML table с правильными заголовками Excel
|
||||
// В продакшене рекомендуется использовать PhpSpreadsheet
|
||||
|
||||
header('Content-Type: application/vnd.ms-excel');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Cache-Control: max-age=0');
|
||||
|
||||
echo '<table border="1">';
|
||||
echo '<tr><th>ID</th><th>Имя</th><th>Email</th><th>Телефон</th><th>Статус</th><th>Создан</th><th>Обновлён</th></tr>';
|
||||
|
||||
foreach ($clients as $client) {
|
||||
echo '<tr>';
|
||||
echo '<td>' . $client['id'] . '</td>';
|
||||
echo '<td>' . htmlspecialchars($client['name']) . '</td>';
|
||||
echo '<td>' . htmlspecialchars($client['email'] ?? '') . '</td>';
|
||||
echo '<td>' . htmlspecialchars($client['phone'] ?? '') . '</td>';
|
||||
echo '<td>' . ($client['status'] ?? 'active') . '</td>';
|
||||
echo '<td>' . ($client['created_at'] ?? '') . '</td>';
|
||||
echo '<td>' . ($client['updated_at'] ?? '') . '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
echo '</table>';
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Страница импорта клиентов (форма)
|
||||
*/
|
||||
public function importPage()
|
||||
{
|
||||
if (!$this->access->canCreate('clients')) {
|
||||
return $this->forbiddenResponse('Доступ запрещён');
|
||||
}
|
||||
|
||||
return $this->renderTwig('@Clients/import');
|
||||
}
|
||||
|
||||
/**
|
||||
* Импорт клиентов из файла
|
||||
*/
|
||||
public function import()
|
||||
{
|
||||
if (!$this->access->canCreate('clients')) {
|
||||
return $this->forbiddenResponse('Доступ запрещён');
|
||||
}
|
||||
|
||||
$file = $this->request->getFile('file');
|
||||
|
||||
if (!$file->isValid()) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => 'Файл не загружен'
|
||||
]);
|
||||
}
|
||||
|
||||
$extension = strtolower($file->getClientExtension());
|
||||
|
||||
if (!in_array($extension, ['csv', 'xlsx', 'xls'])) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => 'Неподдерживаемый формат файла. Используйте CSV или XLSX.'
|
||||
]);
|
||||
}
|
||||
|
||||
$organizationId = session()->get('active_org_id');
|
||||
$imported = 0;
|
||||
$errors = [];
|
||||
|
||||
// Парсим CSV файл
|
||||
if ($extension === 'csv') {
|
||||
$handle = fopen($file->getTempName(), 'r');
|
||||
|
||||
// Пропускаем заголовок
|
||||
fgetcsv($handle, 0, ';');
|
||||
|
||||
$row = 1;
|
||||
while (($data = fgetcsv($handle, 0, ';')) !== false) {
|
||||
$row++;
|
||||
|
||||
$name = trim($data[1] ?? '');
|
||||
$email = trim($data[2] ?? '');
|
||||
|
||||
// Валидация
|
||||
if (empty($name)) {
|
||||
$errors[] = "Строка $row: Имя обязательно";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[] = "Строка $row: Некорректный email";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверка на дубликат email
|
||||
if (!empty($email)) {
|
||||
$exists = $this->clientModel->where('organization_id', $organizationId)
|
||||
->where('email', $email)
|
||||
->countAllResults();
|
||||
if ($exists > 0) {
|
||||
$errors[] = "Строка $row: Клиент с email $email уже существует";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Вставка
|
||||
$this->clientModel->insert([
|
||||
'organization_id' => $organizationId,
|
||||
'name' => $name,
|
||||
'email' => $email ?: null,
|
||||
'phone' => trim($data[3] ?? '') ?: null,
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$imported++;
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
} else {
|
||||
// Для XLSX в продакшене использовать PhpSpreadsheet
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => 'Импорт XLSX файлов временно недоступен. Пожалуйста, используйте CSV формат.'
|
||||
]);
|
||||
}
|
||||
|
||||
$message = "Импортировано клиентов: $imported";
|
||||
if (!empty($errors)) {
|
||||
$message .= '. Ошибок: ' . count($errors);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'imported' => $imported,
|
||||
'errors' => $errors
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue