diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 8c4ba51..a8a01d7 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -45,10 +45,20 @@ $routes->group('', ['filter' => 'org'], static function ($routes) {
$routes->post('organizations/(:num)/users/(:num)/block', 'Organizations::blockUser/$1/$2');
$routes->post('organizations/(:num)/users/(:num)/unblock', 'Organizations::unblockUser/$1/$2');
$routes->post('organizations/(:num)/users/(:num)/remove', 'Organizations::removeUser/$1/$2');
+ $routes->post('organizations/(:num)/leave', 'Organizations::leaveOrganization/$1');
$routes->post('organizations/(:num)/users/leave', 'Organizations::leaveOrganization/$1');
$routes->post('organizations/(:num)/users/(:num)/resend', 'Organizations::resendInvite/$1/$2');
$routes->post('organizations/(:num)/users/(:num)/cancel', 'Organizations::cancelInvite/$1/$2');
});
+# Маршруты профиля
+$routes->get('profile', 'Profile::index');
+$routes->get('profile/organizations', 'Profile::organizations');
+$routes->get('profile/security', 'Profile::security');
+$routes->post('profile/update-name', 'Profile::updateName');
+$routes->post('profile/upload-avatar', 'Profile::uploadAvatar');
+$routes->post('profile/change-password', 'Profile::changePassword');
+$routes->post('profile/leave-org/(:num)', 'Profile::leaveOrganization/$1');
+
# Подключение роутов модулей
require_once APPPATH . 'Modules/Clients/Config/Routes.php';
\ No newline at end of file
diff --git a/app/Controllers/Auth.php b/app/Controllers/Auth.php
index 2bbbfc1..9f44328 100644
--- a/app/Controllers/Auth.php
+++ b/app/Controllers/Auth.php
@@ -345,6 +345,14 @@ class Auth extends BaseController
'isLoggedIn' => true
];
+ // === ЗАПОМНИТЬ МЕНЯ ===
+ $remember = $this->request->getPost('remember');
+ if ($remember) {
+ $this->createRememberToken($user['id']);
+ // Устанавливаем сессию на 30 дней
+ $this->session->setExpiry(30 * 24 * 60 * 60); // 30 дней в секундах
+ }
+
// АВТОМАТИЧЕСКИЙ ВЫБОР ОРГАНИЗАЦИИ
if (count($userOrgs) === 1) {
// Если одна организация — заходим автоматически для удобства
@@ -397,11 +405,78 @@ class Auth extends BaseController
public function logout()
{
+ $userId = session()->get('user_id');
+
+ // Удаляем все remember-токены пользователя
+ if ($userId) {
+ $db = \Config\Database::connect();
+ $db->table('remember_tokens')->where('user_id', $userId)->delete();
+ }
+
session()->destroy();
session()->remove('active_org_id');
return redirect()->to('/');
}
+ /**
+ * Создание remember-токена для автологина
+ */
+ protected function createRememberToken(int $userId): void
+ {
+ $selector = bin2hex(random_bytes(16));
+ $validator = bin2hex(random_bytes(32));
+ $tokenHash = hash('sha256', $validator);
+ $expiresAt = date('Y-m-d H:i:s', strtotime('+30 days'));
+
+ $db = \Config\Database::connect();
+ $db->table('remember_tokens')->insert([
+ 'user_id' => $userId,
+ 'selector' => $selector,
+ 'token_hash' => $tokenHash,
+ 'expires_at' => $expiresAt,
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'user_agent' => $this->request->getUserAgent()->getAgentString(),
+ 'ip_address' => $this->request->getIPAddress(),
+ ]);
+
+ // Устанавливаем cookie на 30 дней
+ $cookie = \Config\Services::response()->setCookie('remember_selector', $selector, 30 * 24 * 60 * 60);
+ $cookie = \Config\Services::response()->setCookie('remember_token', $validator, 30 * 24 * 60 * 60);
+ }
+
+ /**
+ * Проверка remember-токена (вызывается перед фильтрами авторизации)
+ * Возвращает user_id если токен валиден, иначе null
+ */
+ public static function checkRememberToken(): ?int
+ {
+ $request = \Config\Services::request();
+ $selector = $request->getCookie('remember_selector');
+ $validator = $request->getCookie('remember_token');
+
+ if (!$selector || !$validator) {
+ return null;
+ }
+
+ $db = \Config\Database::connect();
+ $token = $db->table('remember_tokens')
+ ->where('selector', $selector)
+ ->where('expires_at >', date('Y-m-d H:i:s'))
+ ->get()
+ ->getRowArray();
+
+ if (!$token) {
+ return null;
+ }
+
+ $tokenHash = hash('sha256', $validator);
+ if (!hash_equals($token['token_hash'], $tokenHash)) {
+ return null;
+ }
+
+ return (int) $token['user_id'];
+ }
+
/**
* DEBUG: Просмотр состояния rate limiting (только для разработки)
* DELETE: Убрать перед релизом!
diff --git a/app/Controllers/Organizations.php b/app/Controllers/Organizations.php
index dbb8b3c..9430ef4 100644
--- a/app/Controllers/Organizations.php
+++ b/app/Controllers/Organizations.php
@@ -261,6 +261,10 @@ class Organizations extends BaseController
$this->session->set('active_org_id', $orgId);
$this->session->setFlashdata('success', 'Организация изменена');
+ $referer = $this->request->getHeader('Referer');
+ if ($referer && strpos($referer->getValue(), '/organizations/switch') === false) {
+ return redirect()->to($referer->getValue());
+ }
return redirect()->to('/');
} else {
$this->session->setFlashdata('error', 'Доступ запрещен');
@@ -567,11 +571,23 @@ class Organizations extends BaseController
$membership = $this->getMembership($orgId);
if (!$membership) {
+ if ($this->request->isAJAX()) {
+ return $this->response->setJSON([
+ 'success' => false,
+ 'message' => 'Вы не состоите в этой организации',
+ ]);
+ }
return $this->redirectWithError('Вы не состоите в этой организации', '/organizations');
}
// Владелец не может покинуть организацию
if ($membership['role'] === 'owner') {
+ if ($this->request->isAJAX()) {
+ return $this->response->setJSON([
+ 'success' => false,
+ 'message' => 'Владелец не может покинуть организацию. Передайте права другому администратору.',
+ ]);
+ }
return $this->redirectWithError('Владелец не может покинуть организацию. Передайте права другому администратору.', "/organizations/users/{$orgId}");
}
@@ -590,6 +606,16 @@ class Organizations extends BaseController
}
}
+ // Сбрасываем кэш AccessService
+ $this->access->resetCache();
+
+ if ($this->request->isAJAX()) {
+ return $this->response->setJSON([
+ 'success' => true,
+ 'message' => 'Вы покинули организацию',
+ ]);
+ }
+
$this->session->setFlashdata('success', 'Вы покинули организацию');
return redirect()->to('/organizations');
}
diff --git a/app/Controllers/Profile.php b/app/Controllers/Profile.php
new file mode 100644
index 0000000..8c52010
--- /dev/null
+++ b/app/Controllers/Profile.php
@@ -0,0 +1,245 @@
+userModel = new UserModel();
+ $this->orgModel = new OrganizationModel();
+ }
+
+ /**
+ * Главная страница профиля
+ */
+ public function index()
+ {
+ $userId = $this->getCurrentUserId();
+ $user = $this->userModel->find($userId);
+
+ return $this->renderTwig('profile/index', [
+ 'title' => 'Профиль',
+ 'user' => $user,
+ 'active_tab' => 'general',
+ ]);
+ }
+
+ /**
+ * Вкладка "Организации"
+ */
+ public function organizations()
+ {
+ $userId = $this->getCurrentUserId();
+ $user = $this->userModel->find($userId);
+ $currentOrgId = $this->session->get('active_org_id');
+
+ // Получаем все организации пользователя
+ $orgUserModel = $this->getOrgUserModel();
+ $memberships = $orgUserModel->where('user_id', $userId)->findAll();
+
+ $orgIds = array_column($memberships, 'organization_id');
+ $organizations = [];
+
+ if (!empty($orgIds)) {
+ $organizations = $this->orgModel->whereIn('id', $orgIds)->findAll();
+ }
+
+ // Объединяем данные
+ $orgList = [];
+ foreach ($organizations as $org) {
+ // Находим соответствующий membership
+ $membership = null;
+ foreach ($memberships as $m) {
+ if ($m['organization_id'] == $org['id']) {
+ $membership = $m;
+ break;
+ }
+ }
+
+ $orgList[] = [
+ 'id' => $org['id'],
+ 'name' => $org['name'],
+ 'type' => $org['type'],
+ 'role' => $membership['role'] ?? 'guest',
+ 'status' => $membership['status'] ?? 'active',
+ 'joined_at' => $membership['joined_at'] ?? null,
+ 'is_owner' => ($membership['role'] ?? '') === 'owner',
+ 'is_current_org' => ((int) $org['id'] === (int) $currentOrgId),
+ ];
+ }
+
+ return $this->renderTwig('profile/organizations', [
+ 'title' => 'Мои организации',
+ 'user' => $user,
+ 'organizations' => $orgList,
+ 'active_tab' => 'organizations',
+ ]);
+ }
+
+ /**
+ * Вкладка "Безопасность"
+ */
+ public function security()
+ {
+ $userId = $this->getCurrentUserId();
+ $user = $this->userModel->find($userId);
+
+ return $this->renderTwig('profile/security', [
+ 'title' => 'Безопасность',
+ 'user' => $user,
+ 'active_tab' => 'security',
+ ]);
+ }
+
+ /**
+ * Обновление имени пользователя
+ */
+ public function updateName()
+ {
+ $userId = $this->getCurrentUserId();
+ $name = trim($this->request->getPost('name'));
+
+ if (empty($name)) {
+ $this->session->setFlashdata('error', 'Имя обязательно для заполнения');
+ return redirect()->to('/profile');
+ }
+
+ if (strlen($name) < 3) {
+ $this->session->setFlashdata('error', 'Имя должно содержать минимум 3 символа');
+ return redirect()->to('/profile');
+ }
+
+ $this->userModel->update($userId, ['name' => $name]);
+ $this->session->set('name', $name);
+ $this->session->setFlashdata('success', 'Имя успешно обновлено');
+
+ return redirect()->to('/profile');
+ }
+
+ /**
+ * Загрузка аватара
+ */
+ public function uploadAvatar()
+ {
+ $userId = $this->getCurrentUserId();
+ $file = $this->request->getFile('avatar');
+
+ if (!$file || !$file->isValid()) {
+ $this->session->setFlashdata('error', 'Ошибка загрузки файла');
+ return redirect()->to('/profile');
+ }
+
+ // Валидация
+ $allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
+ $maxSize = 2 * 1024 * 1024; // 2MB
+
+ if (!in_array($file->getMimeType(), $allowedTypes)) {
+ $this->session->setFlashdata('error', 'Разрешены только файлы JPG, PNG и GIF');
+ return redirect()->to('/profile');
+ }
+
+ if ($file->getSize() > $maxSize) {
+ $this->session->setFlashdata('error', 'Максимальный размер файла - 2 МБ');
+ return redirect()->to('/profile');
+ }
+
+ // Создаём директорию для аватаров если нет
+ $uploadPath = ROOTPATH . 'public/uploads/avatars';
+ if (!is_dir($uploadPath)) {
+ mkdir($uploadPath, 0755, true);
+ }
+
+ // Генерируем уникальное имя файла
+ $extension = $file->getClientExtension();
+ $newFileName = 'avatar_' . $userId . '_' . time() . '.' . $extension;
+
+ // Перемещаем файл
+ $file->move($uploadPath, $newFileName);
+
+ // Удаляем старый аватар если был
+ $user = $this->userModel->find($userId);
+ if (!empty($user['avatar']) && file_exists($uploadPath . '/' . $user['avatar'])) {
+ @unlink($uploadPath . '/' . $user['avatar']);
+ }
+
+ // Обновляем путь к аватару в базе
+ $this->userModel->update($userId, ['avatar' => $newFileName]);
+
+ $this->session->setFlashdata('success', 'Аватар успешно загружен');
+ return redirect()->to('/profile');
+ }
+
+ /**
+ * Смена пароля
+ */
+ public function changePassword()
+ {
+ $userId = $this->getCurrentUserId();
+ $user = $this->userModel->find($userId);
+
+ $currentPassword = $this->request->getPost('current_password');
+ $newPassword = $this->request->getPost('new_password');
+ $confirmPassword = $this->request->getPost('confirm_password');
+
+ // Валидация
+ if (empty($currentPassword)) {
+ $this->session->setFlashdata('error', 'Введите текущий пароль');
+ return redirect()->to('/profile/security');
+ }
+
+ if (empty($newPassword)) {
+ $this->session->setFlashdata('error', 'Введите новый пароль');
+ return redirect()->to('/profile/security');
+ }
+
+ if (strlen($newPassword) < 6) {
+ $this->session->setFlashdata('error', 'Новый пароль должен содержать минимум 6 символов');
+ return redirect()->to('/profile/security');
+ }
+
+ if ($newPassword !== $confirmPassword) {
+ $this->session->setFlashdata('error', 'Пароли не совпадают');
+ return redirect()->to('/profile/security');
+ }
+
+ // Проверяем текущий пароль
+ if (!password_verify($currentPassword, $user['password'])) {
+ $this->session->setFlashdata('error', 'Неверный текущий пароль');
+ return redirect()->to('/profile/security');
+ }
+
+ // Обновляем пароль
+ $this->userModel->update($userId, ['password' => $newPassword]);
+
+ // Завершаем все сессии пользователя (кроме текущей)
+ $this->endAllUserSessions($userId);
+
+ $this->session->setFlashdata('success', 'Пароль успешно изменён. Для безопасности вы будете разлогинены на всех устройствах.');
+ return redirect()->to('/logout');
+ }
+
+ /**
+ * Завершение всех сессий пользователя
+ */
+ private function endAllUserSessions(int $userId): void
+ {
+ // Удаляем все remember-токены пользователя
+ $db = \Config\Database::connect();
+ $db->table('remember_tokens')->where('user_id', $userId)->delete();
+
+ // Регенерируем ID текущей сессии
+ $this->session->regenerate(true);
+ }
+}
diff --git a/app/Database/Migrations/2026-01-12-000001_AddInviteFieldsToOrganizationUsers.php b/app/Database/Migrations/2026-01-12-000001_AddInviteFieldsToOrganizationUsers.php
index fecfb92..a77e62a 100644
--- a/app/Database/Migrations/2026-01-12-000001_AddInviteFieldsToOrganizationUsers.php
+++ b/app/Database/Migrations/2026-01-12-000001_AddInviteFieldsToOrganizationUsers.php
@@ -8,53 +8,68 @@ class AddInviteFieldsToOrganizationUsers extends Migration
{
public function up()
{
- // Добавление полей для системы приглашений
- $fields = [
- 'invite_token' => [
- 'type' => 'VARCHAR',
- 'constraint' => 64,
- 'null' => true,
- 'after' => 'role',
- 'comment' => 'Токен для принятия приглашения',
- ],
- 'invited_by' => [
- 'type' => 'INT',
- 'unsigned' => true,
- 'null' => true,
- 'after' => 'invite_token',
- 'comment' => 'ID пользователя, который отправил приглашение',
- ],
- 'invited_at' => [
- 'type' => 'DATETIME',
- 'null' => true,
- 'after' => 'invited_by',
- 'comment' => 'Дата отправки приглашения',
- ],
- 'status' => [
- 'type' => 'ENUM',
- 'values' => ['active', 'pending', 'blocked'],
- 'default' => 'pending',
- 'after' => 'invited_at',
- 'comment' => 'Статус участия в организации',
- ],
- ];
-
- $this->db->simpleQuery("ALTER TABLE organization_users ADD COLUMN status ENUM('active', 'pending', 'blocked') DEFAULT 'pending' AFTER invited_at");
- $this->db->simpleQuery("ALTER TABLE organization_users ADD COLUMN invite_token VARCHAR(64) NULL AFTER role");
- $this->db->simpleQuery("ALTER TABLE organization_users ADD COLUMN invited_by INT UNSIGNED NULL AFTER invite_token");
- $this->db->simpleQuery("ALTER TABLE organization_users ADD COLUMN invited_at DATETIME NULL AFTER invited_by");
-
- // Индекс для быстрого поиска по токену
- $this->db->simpleQuery("CREATE INDEX idx_org_users_token ON organization_users(invite_token)");
+ // Примечание: Таблица уже создана с полями:
+ // id, organization_id, user_id, role, status, joined_at, created_at
+ //
+ // Добавляем только недостающие поля для системы приглашений:
+ // - invite_token: токен для принятия приглашения
+ // - invited_by: кто отправил приглашение
+ // - invited_at: когда отправлено приглашение
+
+ // Проверяем, существует ли уже колонка invite_token
+ $fields = $this->db->getFieldData('organization_users');
+ $existingFields = array_column($fields, 'name');
+
+ if (!in_array('invite_token', $existingFields)) {
+ $this->db->simpleQuery("ALTER TABLE organization_users ADD COLUMN invite_token VARCHAR(64) NULL AFTER role");
+ }
+
+ if (!in_array('invited_by', $existingFields)) {
+ $this->db->simpleQuery("ALTER TABLE organization_users ADD COLUMN invited_by INT UNSIGNED NULL AFTER invite_token");
+ }
+
+ if (!in_array('invited_at', $existingFields)) {
+ $this->db->simpleQuery("ALTER TABLE organization_users ADD COLUMN invited_at DATETIME NULL AFTER invited_by");
+ }
+
+ // Индекс для быстрого поиену (еска по токсли ещё нет)
+ $indexes = $this->db->getIndexData('organization_users');
+ $hasTokenIndex = false;
+ foreach ($indexes as $index) {
+ if ($index->name === 'idx_org_users_token') {
+ $hasTokenIndex = true;
+ break;
+ }
+ }
+
+ if (!$hasTokenIndex) {
+ $this->db->simpleQuery("CREATE INDEX idx_org_users_token ON organization_users(invite_token)");
+ }
+
+ // Изменяем поле status, чтобы включить 'pending' и установить его как значение по умолчанию
+ // Используем прямой SQL для модификации ENUM
+ $this->db->simpleQuery("ALTER TABLE organization_users MODIFY COLUMN status ENUM('active', 'pending', 'invited', 'blocked') NOT NULL DEFAULT 'pending'");
}
public function down()
{
// Удаление полей и индекса при откате миграции
- $this->db->simpleQuery("DROP INDEX idx_org_users_token ON organization_users");
- $this->db->simpleQuery("ALTER TABLE organization_users DROP COLUMN invited_at");
- $this->db->simpleQuery("ALTER TABLE organization_users DROP COLUMN invited_by");
- $this->db->simpleQuery("ALTER TABLE organization_users DROP COLUMN invite_token");
- $this->db->simpleQuery("ALTER TABLE organization_users DROP COLUMN status");
+ $fields = $this->db->getFieldData('organization_users');
+ $existingFields = array_column($fields, 'name');
+
+ // Удаляем только если поля существуют
+ $this->db->simpleQuery("DROP INDEX IF EXISTS idx_org_users_token ON organization_users");
+
+ if (in_array('invited_at', $existingFields)) {
+ $this->db->simpleQuery("ALTER TABLE organization_users DROP COLUMN invited_at");
+ }
+
+ if (in_array('invited_by', $existingFields)) {
+ $this->db->simpleQuery("ALTER TABLE organization_users DROP COLUMN invited_by");
+ }
+
+ if (in_array('invite_token', $existingFields)) {
+ $this->db->simpleQuery("ALTER TABLE organization_users DROP COLUMN invite_token");
+ }
}
}
diff --git a/app/Database/Migrations/2026-01-13-000001_CreateRememberTokensTable.php b/app/Database/Migrations/2026-01-13-000001_CreateRememberTokensTable.php
new file mode 100644
index 0000000..541da2b
--- /dev/null
+++ b/app/Database/Migrations/2026-01-13-000001_CreateRememberTokensTable.php
@@ -0,0 +1,61 @@
+forge->addField([
+ 'id' => [
+ 'type' => 'INT',
+ 'constraint' => 11,
+ 'unsigned' => true,
+ 'auto_increment' => true,
+ ],
+ 'user_id' => [
+ 'type' => 'INT',
+ 'constraint' => 11,
+ 'unsigned' => true,
+ ],
+ 'selector' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 64,
+ ],
+ 'token_hash' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 128,
+ ],
+ 'expires_at' => [
+ 'type' => 'DATETIME',
+ ],
+ 'created_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'user_agent' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 500,
+ 'null' => true,
+ ],
+ 'ip_address' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 45,
+ 'null' => true,
+ ],
+ ]);
+ $this->forge->addKey('id', true);
+ $this->forge->addKey('user_id');
+ $this->forge->addKey('selector');
+ $this->forge->addKey('expires_at');
+ $this->forge->addForeignKey('user_id', 'users', 'id', false, 'CASCADE');
+ $this->forge->createTable('remember_tokens');
+ }
+
+ public function down()
+ {
+ $this->forge->dropTable('remember_tokens');
+ }
+}
diff --git a/app/Libraries/Twig/TwigGlobalsExtension.php b/app/Libraries/Twig/TwigGlobalsExtension.php
index 56ed299..cdc96cf 100644
--- a/app/Libraries/Twig/TwigGlobalsExtension.php
+++ b/app/Libraries/Twig/TwigGlobalsExtension.php
@@ -21,6 +21,8 @@ class TwigGlobalsExtension extends AbstractExtension
new TwigFunction('get_current_route', [$this, 'getCurrentRoute'], ['is_safe' => ['html']]),
new TwigFunction('render_actions', [$this, 'renderActions'], ['is_safe' => ['html']]),
new TwigFunction('render_cell', [$this, 'renderCell'], ['is_safe' => ['html']]),
+ new TwigFunction('get_avatar_url', [$this, 'getAvatarUrl'], ['is_safe' => ['html']]),
+ new TwigFunction('get_avatar', [$this, 'getAvatar'], ['is_safe' => ['html']]),
// Access functions
new TwigFunction('can', [$this, 'can'], ['is_safe' => ['html']]),
@@ -155,6 +157,44 @@ class TwigGlobalsExtension extends AbstractExtension
return '' . esc($label) . '';
}
+ public function getAvatarUrl($avatar = null, $size = 32): string
+ {
+ if (empty($avatar)) {
+ return '';
+ }
+ return base_url('/uploads/avatars/' . $avatar);
+ }
+
+ public function getAvatar($user = null, $size = 32, $class = ''): string
+ {
+ if (!$user) {
+ $session = session();
+ $userId = $session->get('user_id');
+ if (!$userId) {
+ return '';
+ }
+ $userModel = new \App\Models\UserModel();
+ $user = $userModel->find($userId);
+ }
+
+ $name = $user['name'] ?? 'U';
+ $avatar = $user['avatar'] ?? null;
+
+ if ($avatar) {
+ $url = base_url('/uploads/avatars/' . $avatar);
+ $style = "width: {$size}px; height: {$size}px; object-fit: cover; border-radius: 50%;";
+ return '';
+ }
+
+ // Генерируем фон на основе имени
+ $colors = ['667eea', '764ba2', 'f093fb', 'f5576c', '4facfe', '00f2fe'];
+ $color = $colors[crc32($name) % count($colors)];
+ $initial = strtoupper(substr($name, 0, 1));
+ $style = "width: {$size}px; height: {$size}px; background: linear-gradient(135deg, #{$color} 0%, #{$color}dd 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: " . ($size / 2) . "px;";
+
+ return '
{{ user.email }}
+{{ user.email }}
+У вас пока нет организаций
+ + Создать первую организацию + +| Организация | +Тип | +Ваша роль | +Дата входа | +Действия | +
|---|---|---|---|---|
|
+
+
+
+
+
+
+ {{ org.name }}
+ {% if org.is_current_org %}
+ Текущая
+ {% endif %}
+
+ + ID: {{ org.id }} + |
+ + {% if org.type == 'personal' %} + Личное пространство + {% else %} + Бизнес + {% endif %} + | ++ {% if org.role == 'owner' %} + Владелец + {% elseif org.role == 'admin' %} + Администратор + {% elseif org.role == 'manager' %} + Менеджер + {% else %} + Гость + {% endif %} + | +{{ org.joined_at ? org.joined_at|date('d.m.Y H:i') : '—' }} | +
+
+ {# ВЛАДЕЛЕЦ #}
+ {% if org.is_owner %}
+ {# Текущая организация #}
+ {% if org.is_current_org %}
+
+
+
+
+
+
+
+ {% else %}
+ {# Не текущая организация #}
+
+
+
+
+
+
+
+ {% endif %}
+ {% elseif org.role in ['admin', 'manager'] %}
+ {# Админ/Менеджер #}
+ {% if org.is_current_org %}
+
+
+
+
+ {% else %}
+
+
+
+
+ {% endif %}
+ {% else %}
+ {# Гость (не owner, не admin, не manager) #}
+ {% if org.is_current_org %}
+
+ {% else %}
+
+
+
+
+ {% endif %}
+ {% endif %}
+
+ |
+
{{ user.email }}
+