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
+21 -1
View File
@@ -12,7 +12,27 @@
{# Колонка действий #}
{% if actionsConfig is defined and actionsConfig|length > 0 %}
<td class="actions-cell text-end">
{{ render_actions(item, actionsConfig)|raw }}
{# Фильтруем действия на основе прав доступа #}
{% set visibleActions = [] %}
{% for action in actionsConfig %}
{% set showAction = true %}
{% if action.type is defined %}
{% if action.type == 'edit' and not (can_edit|default(true)) %}
{% set showAction = false %}
{% elseif action.type == 'delete' and not (can_delete|default(true)) %}
{% set showAction = false %}
{% endif %}
{% endif %}
{% if showAction %}
{% set visibleActions = visibleActions|merge([action]) %}
{% endif %}
{% endfor %}
{% if visibleActions|length > 0 %}
{{ render_actions(item, visibleActions)|raw }}
{% else %}
<span class="text-muted small">—</span>
{% endif %}
</td>
{% endif %}
</tr>
+23 -1
View File
@@ -18,6 +18,8 @@
{ label: 'Ред.', url: '/clients/edit/{id}', icon: 'bi bi-pencil', class: 'btn-outline-primary' },
{ label: 'Удалить', url: '/clients/delete/{id}', icon: 'bi bi-trash', class: 'btn-outline-danger' }
]
- can_edit: Разрешено ли редактирование (для фильтрации действий)
- can_delete: Разрешено ли удаление (для фильтрации действий)
- emptyMessage: Сообщение при отсутствии данных
- emptyActionUrl: URL для кнопки действия
- emptyActionLabel: Текст кнопки
@@ -50,7 +52,27 @@
{# Колонка действий #}
{% if actionsConfig is defined and actionsConfig|length > 0 %}
<td class="actions-cell text-end">
{{ render_actions(item, actionsConfig)|raw }}
{# Фильтруем действия на основе прав доступа #}
{% set visibleActions = [] %}
{% for action in actionsConfig %}
{% set showAction = true %}
{% if action.type is defined %}
{% if action.type == 'edit' and not (can_edit|default(true)) %}
{% set showAction = false %}
{% elseif action.type == 'delete' and not (can_delete|default(true)) %}
{% set showAction = false %}
{% endif %}
{% endif %}
{% if showAction %}
{% set visibleActions = visibleActions|merge([action]) %}
{% endif %}
{% endfor %}
{% if visibleActions|length > 0 %}
{{ render_actions(item, visibleActions)|raw }}
{% else %}
<span class="text-muted small">—</span>
{% endif %}
</td>
{% endif %}
</tr>
+3 -3
View File
@@ -106,10 +106,10 @@
<li><hr class="dropdown-divider"></li>
<!-- Ссылка: Настройки этой организации (будущий функционал) -->
<!-- Ссылка: Дашборд управления организацией -->
<li>
<a class="dropdown-item text-muted" href="#">
<i class="fa-solid fa-gear me-2"></i> Настройки текущей
<a class="dropdown-item" href="{{ base_url('/organizations/' ~ current_org.id ~ '/dashboard') }}">
<i class="fa-solid fa-sliders text-primary me-2"></i> Управление организацией
</a>
</li>
</ul>
@@ -0,0 +1,20 @@
{#
organizations/confirm_modal.twig - Модальное окно подтверждения действия
#}
<div class="modal fade" id="confirmModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="confirmModalTitle">Подтверждение действия</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p id="confirmModalMessage" class="mb-0">Вы уверены?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="confirmModalBtn">Подтвердить</button>
</div>
</div>
</div>
</div>
+181
View File
@@ -0,0 +1,181 @@
{% extends 'layouts/base.twig' %}
{% block title %}Управление организацией - {{ organization.name }} - {{ parent() }}{% endblock %}
{% block content %}
<div class="container-fluid py-4">
{# Заголовок #}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">Управление организацией</h1>
<h3 class="text-muted mb-0">{{ organization.name }}</h3>
</div>
<div class="text-end">
{{ role_badge(current_role) }}
</div>
</div>
{# Статистика #}
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="display-6 fw-bold text-primary">{{ stats.users_total }}</div>
<div class="text-muted">Всего участников</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="display-6 fw-bold text-success">{{ stats.users_active }}</div>
<div class="text-muted">Активных</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="display-6 fw-bold text-warning">{{ stats.users_blocked }}</div>
<div class="text-muted">Заблокировано</div>
</div>
</div>
</div>
</div>
{# Карточки управления #}
<div class="row g-4">
{# Управление командой #}
{% if can_manage_users %}
<div class="col-md-6 col-lg-4">
<a href="{{ base_url('/organizations/'~ organization.id ~ '/users' ) }}" class="card h-100 text-decoration-none border-0 shadow-sm card-hover">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="bg-primary bg-opacity-10 rounded-3 p-3 me-3">
<i class="fa-solid fa-users-gear fs-3 text-primary"></i>
</div>
<div>
<h5 class="card-title mb-1 text-dark">Управление командой</h5>
<p class="card-text text-muted small mb-0">Приглашайте, блокируйте и управляйте ролями участников организации</p>
</div>
</div>
</div>
<div class="card-footer bg-transparent border-0 pt-0">
<span class="text-primary fw-medium">Перейти <i class="fa-solid fa-arrow-right ms-1"></i></span>
</div>
</a>
</div>
{% endif %}
{# Редактирование организации #}
<div class="col-md-6 col-lg-4">
<a href="{{ base_url('/organizations/edit/' ~ organization.id) }}" class="card h-100 text-decoration-none border-0 shadow-sm card-hover">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="bg-info bg-opacity-10 rounded-3 p-3 me-3">
<i class="fa-solid fa-building fs-3 text-info"></i>
</div>
<div>
<h5 class="card-title mb-1 text-dark">Реквизиты организации</h5>
<p class="card-text text-muted small mb-0">Измените название, адрес, банковские реквизиты и другую информацию</p>
</div>
</div>
</div>
<div class="card-footer bg-transparent border-0 pt-0">
<span class="text-info fw-medium">Редактировать <i class="fa-solid fa-arrow-right ms-1"></i></span>
</div>
</a>
</div>
{# Модули организации - заглушка #}
<div class="col-md-6 col-lg-4">
<div class="card h-100 text-decoration-none border-0 shadow-sm bg-light">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="bg-secondary bg-opacity-10 rounded-3 p-3 me-3">
<i class="fa-solid fa-puzzle fs-3 text-secondary"></i>
</div>
<div>
<h5 class="card-title mb-1 text-dark">Модули</h5>
<p class="card-text text-muted small mb-0">Управление подключёнными модулями и функционалом организации</p>
</div>
</div>
</div>
<div class="card-footer bg-transparent border-0 pt-0">
<span class="text-muted fw-medium">Скоро <i class="fa-solid fa-clock ms-1"></i></span>
</div>
</div>
</div>
{# Биллинг - заглушка #}
<div class="col-md-6 col-lg-4">
<div class="card h-100 text-decoration-none border-0 shadow-sm bg-light">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="bg-success bg-opacity-10 rounded-3 p-3 me-3">
<i class="fa-solid fa-credit-card fs-3 text-success"></i>
</div>
<div>
<h5 class="card-title mb-1 text-dark">Биллинг и оплата</h5>
<p class="card-text text-muted small mb-0">Просмотр счетов, история платежей и управление подпиской</p>
</div>
</div>
</div>
<div class="card-footer bg-transparent border-0 pt-0">
<span class="text-muted fw-medium">Скоро <i class="fa-solid fa-clock ms-1"></i></span>
</div>
</div>
</div>
{# Приглашения - заглушка #}
<div class="col-md-6 col-lg-4">
<div class="card h-100 text-decoration-none border-0 shadow-sm bg-light">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="bg-warning bg-opacity-10 rounded-3 p-3 me-3">
<i class="fa-solid fa-envelope-open-text fs-3 text-warning"></i>
</div>
<div>
<h5 class="card-title mb-1 text-dark">История приглашений</h5>
<p class="card-text text-muted small mb-0">Просмотр отправленных и отклонённых приглашений</p>
</div>
</div>
</div>
<div class="card-footer bg-transparent border-0 pt-0">
<span class="text-muted fw-medium">Скоро <i class="fa-solid fa-clock ms-1"></i></span>
</div>
</div>
</div>
{# Настройки безопасности - заглушка #}
<div class="col-md-6 col-lg-4">
<div class="card h-100 text-decoration-none border-0 shadow-sm bg-light">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="bg-danger bg-opacity-10 rounded-3 p-3 me-3">
<i class="fa-solid fa-shield-halved fs-3 text-danger"></i>
</div>
<div>
<h5 class="card-title mb-1 text-dark">Безопасность</h5>
<p class="card-text text-muted small mb-0">Настройки безопасности, двухфакторная аутентификация, логи</p>
</div>
</div>
</div>
<div class="card-footer bg-transparent border-0 pt-0">
<span class="text-muted fw-medium">Скоро <i class="fa-solid fa-clock ms-1"></i></span>
</div>
</div>
</div>
</div>
</div>
<style>
.card-hover {
transition: all 0.2s ease;
}
.card-hover:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
</style>
{% endblock %}
@@ -0,0 +1,55 @@
{#
organizations/edit_role_modal.twig - Модальное окно изменения роли пользователя
#}
<div class="modal fade" id="editRoleModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fa-solid fa-user-tag me-2"></i>Изменить роль
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Пользователь: <strong id="editRoleUserEmail"></strong></p>
<div class="mb-3">
<label for="editRoleSelect" class="form-label">Роль</label>
<select class="form-select" id="editRoleSelect">
{% for role_value, role_info in get_all_roles() %}
{% if role_value != 'owner' %}
<option value="{{ role_value }}">{{ role_info.label }}</option>
{% endif %}
{% endfor %}
</select>
<div class="form-text" id="roleDescription"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" onclick="saveRole()">
<i class="fa-solid fa-save me-2"></i>Сохранить
</button>
</div>
</div>
</div>
</div>
<script>
const roleDescriptions = {
'admin': 'Администратор может управлять пользователями и модулями организации',
'manager': 'Менеджер имеет полный доступ к функционалу модулей',
'guest': 'Гость может только просматривать данные'
};
document.getElementById('editRoleSelect').addEventListener('change', function() {
const desc = roleDescriptions[this.value] || '';
document.getElementById('roleDescription').textContent = desc;
});
// Инициализация при открытии
document.getElementById('editRoleModal').addEventListener('shown.bs.modal', function() {
const role = document.getElementById('editRoleSelect').value;
document.getElementById('roleDescription').textContent = roleDescriptions[role] || '';
});
</script>
@@ -0,0 +1,107 @@
{#
organizations/invitation_accept.twig - Страница принятия/отклонения приглашения
#}
{% extends 'layouts/landing.twig' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="min-vh-100 d-flex align-items-center justify-content-center py-5" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow-lg border-0">
<div class="card-body p-5">
{# Логотип организации #}
<div class="text-center mb-4">
{% if organization.logo %}
<img src="{{ organization.logo }}" alt="{{ organization.name }}" style="max-height: 60px;">
{% else %}
<div class="bg-primary text-white rounded-3 d-inline-flex align-items-center justify-content-center" style="width: 60px; height: 60px;">
<i class="fa-solid fa-building fs-3"></i>
</div>
{% endif %}
</div>
<h2 class="text-center mb-4">Приглашение в организацию</h2>
{# Информация об организации #}
<div class="text-center mb-4">
<h4 class="mb-1">{{ organization.name }}</h4>
<span class="badge bg-primary fs-6">{{ role_label }}</span>
</div>
<p class="text-center text-muted mb-4">
Вас пригласили присоединиться к организации "{{ organization.name }}"
</p>
{% if invited_by %}
<p class="text-center text-muted small mb-4">
Приглашение отправил: {{ invited_by.name|default(invited_by.email) }}
</p>
{% endif %}
{# Если пользователь не авторизован #}
{% if not is_logged_in %}
<div class="alert alert-warning">
<i class="fa-solid fa-triangle-exclamation me-2"></i>
Для принятия приглашения необходимо войти в аккаунт или зарегистрироваться
</div>
{% endif %}
{# Если авторизован, но email не совпадает #}
{% if is_logged_in and not email_matches %}
<div class="alert alert-warning">
<i class="fa-solid fa-triangle-exclamation me-2"></i>
Внимание! Вы вошли как <strong>{{ get_session('email') }}</strong>,
а приглашение отправлено на другой email.
</div>
{% endif %}
{# Форма принятия/отклонения #}
<form action="/invitation/accept/{{ token }}" method="POST">
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>">
<input type="hidden" name="action" value="accept">
<div class="d-flex gap-3">
<button type="submit" class="btn btn-primary flex-grow-1">
<i class="fa-solid fa-check me-2"></i>Принять
</button>
<button type="submit"
name="action"
value="decline"
class="btn btn-outline-danger flex-grow-1">
<i class="fa-solid fa-xmark me-2"></i>Отклонить
</button>
</div>
</form>
{# Ссылка на вход/регистрацию #}
{% if not is_logged_in %}
<div class="text-center mt-4">
<a href="/login?redirect={{ url_encode('/invitation/accept/' ~ token) }}" class="text-muted">
<i class="fa-solid fa-sign-in-alt me-1"></i>Войти в аккаунт
</a>
<span class="text-muted mx-2">|</span>
<a href="/register?redirect={{ url_encode('/invitation/accept/' ~ token) }}" class="text-muted">
<i class="fa-solid fa-user-plus me-1"></i>Регистрация
</a>
</div>
{% endif %}
<div class="text-center mt-4 text-muted small">
<i class="fa-regular fa-clock me-1"></i>
Приглашение действительно 48 часов
</div>
</div>
</div>
{# Футер #}
<div class="text-center mt-4 text-white-50 small">
&copy; {{ "now"|date("Y") }} Бизнес.Точка
</div>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,114 @@
{#
organizations/invitation_complete.twig - Страница завершения регистрации нового пользователя
#}
{% extends 'layouts/landing.twig' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="min-vh-100 d-flex align-items-center justify-content-center py-5" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow-lg border-0">
<div class="card-body p-5">
{# Логотип организации #}
<div class="text-center mb-4">
{% if organization.logo %}
<img src="{{ organization.logo }}" alt="{{ organization.name }}" style="max-height: 60px;">
{% else %}
<div class="bg-primary text-white rounded-3 d-inline-flex align-items-center justify-content-center" style="width: 60px; height: 60px;">
<i class="fa-solid fa-building fs-3"></i>
</div>
{% endif %}
</div>
<h2 class="text-center mb-3">Завершение регистрации</h2>
<p class="text-center text-muted mb-4">
Вы приняли приглашение в организацию "{{ organization.name }}"
<br>
в роли <span class="badge bg-primary">{{ role_label }}</span>
</p>
<p class="text-center text-muted mb-4">
Пожалуйста, создайте пароль для вашего аккаунта
</p>
{# Ошибки валидации #}
{% if get_alerts()|filter(a => a.type == 'error')|length > 0 %}
<div class="alert alert-danger">
{% for alert in get_alerts() %}
{% if alert.type == 'error' %}
<div>{{ alert.message }}</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{# Форма регистрации #}
<form action="/invitation/complete/{{ token }}" method="POST">
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>">
<div class="mb-3">
<label for="completeName" class="form-label">Ваше имя</label>
<input type="text"
class="form-control {% if old.name is defined and old.name is empty %}is-invalid{% endif %}"
id="completeName"
name="name"
value="{{ old.name|default('') }}"
placeholder="Иван Иванов"
required>
{% if old.name is defined and old.name is empty %}
<div class="invalid-feedback">Имя обязательно</div>
{% endif %}
</div>
<div class="mb-3">
<label for="completeEmail" class="form-label">Email</label>
<input type="email"
class="form-control"
id="completeEmail"
value="{{ email }}"
disabled>
<div class="form-text">Email подтверждён через приглашение</div>
</div>
<div class="mb-3">
<label for="completePassword" class="form-label">Пароль</label>
<input type="password"
class="form-control"
id="completePassword"
name="password"
minlength="8"
required>
<div class="form-text">Минимум 8 символов</div>
</div>
<div class="mb-4">
<label for="completePasswordConfirm" class="form-label">Подтвердите пароль</label>
<input type="password"
class="form-control"
id="completePasswordConfirm"
name="password_confirm"
required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fa-solid fa-check me-2"></i>Завершить регистрацию
</button>
</div>
</form>
</div>
</div>
{# Футер #}
<div class="text-center mt-4 text-white-50 small">
&copy; {{ "now"|date("Y") }} Бизнес.Точка
</div>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,45 @@
{#
organizations/invitation_expired.twig - Страница истёкшего/недействительного приглашения
#}
{% extends 'layouts/landing.twig' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="min-vh-100 d-flex align-items-center justify-content-center py-5" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow-lg border-0">
<div class="card-body p-5 text-center">
<div class="mb-4">
<i class="fa-solid fa-triangle-exclamation text-warning" style="font-size: 4rem;"></i>
</div>
<h3 class="mb-3">{{ title }}</h3>
<p class="text-muted mb-4">
Это приглашение недействительно или уже было обработано.<br>
Возможно, оно истекло или было отозвано отправителем.
</p>
<div class="d-grid gap-2">
<a href="/" class="btn btn-primary">
<i class="fa-solid fa-home me-2"></i>На главную
</a>
<a href="/login" class="btn btn-outline-secondary">
<i class="fa-solid fa-sign-in-alt me-2"></i>Войти в аккаунт
</a>
</div>
</div>
</div>
{# Футер #}
<div class="text-center mt-4 text-white-50 small">
&copy; {{ "now"|date("Y") }} Бизнес.Точка
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+101
View File
@@ -0,0 +1,101 @@
{#
organizations/invite_modal.twig - Модальное окно приглашения пользователя
#}
<div class="modal fade" id="inviteUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fa-solid fa-user-plus me-2"></i>Пригласить пользователя
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="inviteUserForm" action="/organizations/users/{{ organization_id }}/invite" method="POST">
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>">
<div class="mb-3">
<label for="inviteEmail" class="form-label">Email адрес</label>
<input type="email"
class="form-control"
id="inviteEmail"
name="email"
placeholder="user@example.com"
required>
<div class="form-text">На этот адрес будет отправлено приглашение</div>
</div>
<div class="mb-3">
<label for="inviteRole" class="form-label">Роль</label>
<select class="form-select" id="inviteRole" name="role" required>
{% for role_value, role_info in get_all_roles() %}
{% if role_value != 'owner' %}
<option value="{{ role_value }}">{{ role_info.label }}</option>
{% endif %}
{% endfor %}
</select>
<div class="form-text">{{ get_all_roles()[current_role].description|default('') }}</div>
</div>
<div class="d-flex justify-content-end gap-2">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-paper-plane me-2"></i>Отправить приглашение
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{# Модалка с ссылкой приглашения #}
<div class="modal fade" id="inviteLinkModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fa-solid fa-check-circle text-success me-2"></i>Приглашение отправлено
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Приглашение успешно отправлено на <strong id="inviteEmailDisplay"></strong></p>
<div class="mb-3">
<label class="form-label">Ссылка для приглашения</label>
<div class="input-group">
<input type="text"
class="form-control"
id="inviteLinkInput"
readonly>
<button type="button"
class="btn btn-outline-secondary"
id="copyLinkBtn"
onclick="copyInviteLink()"
title="Копировать">
<i class="fa-regular fa-copy"></i>
</button>
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<button type="button" class="btn btn-outline-primary" onclick="shareViaWebShare()">
<i class="fa-solid fa-share-nodes me-1"></i>Поделиться
</button>
<a href="#" onclick="shareToTelegram(); return false;" class="btn btn-outline-primary">
<i class="fa-brands fa-telegram me-1"></i>Telegram
</a>
</div>
<div class="alert alert-info mt-3 mb-0">
<i class="fa-solid fa-circle-info me-2"></i>
Если email не дошёл, можно скопировать ссылку и отправить другим способом
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Готово</button>
</div>
</div>
</div>
</div>
+328
View File
@@ -0,0 +1,328 @@
{#
organizations/users.twig - Страница управления пользователями организации
#}
{% extends 'layouts/base.twig' %}
{% block title %}Участники организации - {{ parent() }}{% endblock %}
{% block content %}
<div class="container-fluid py-4">
{# Заголовок #}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">Участники организации</h1>
<h3 class="text-muted mb-0">{{ organization.name }}</h3>
</div>
{% if can_manage_users %}
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#inviteUserModal">
<i class="fa-solid fa-user-plus me-2"></i>Пригласить пользователя
</button>
{% endif %}
</div>
{# Статистика #}
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="small opacity-75">Всего участников</div>
<div class="fs-4 fw-bold">{{ users|length }}</div>
</div>
<i class="fa-solid fa-users fs-1 opacity-25"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="small opacity-75">Активных</div>
<div class="fs-4 fw-bold">{{ users|filter(u => u.status == 'active')|length }}</div>
</div>
<i class="fa-solid fa-check-circle fs-1 opacity-25"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-dark">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="small opacity-75">Ожидают ответа</div>
<div class="fs-4 fw-bold">{{ users|filter(u => u.status == 'pending')|length }}</div>
</div>
<i class="fa-solid fa-clock fs-1 opacity-25"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-danger text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="small opacity-75">Заблокировано</div>
<div class="fs-4 fw-bold">{{ users|filter(u => u.status == 'blocked')|length }}</div>
</div>
<i class="fa-solid fa-ban fs-1 opacity-25"></i>
</div>
</div>
</div>
</div>
</div>
{# Таблица пользователей #}
{{ tableHtml|raw }}
{# CSRF токен для AJAX запросов #}
{{ csrf_field()|raw }}
{# Кнопка выхода из организации #}
{% if current_role != 'owner' %}
<div class="mt-4">
<button type="button" class="btn btn-outline-danger" onclick="leaveOrganization()">
<i class="fa-solid fa-sign-out-alt me-2"></i>Покинуть организацию
</button>
</div>
{% endif %}
</div>
{# Модалка приглашения #}
{% include 'organizations/invite_modal.twig' %}
{# Модалка подтверждения действий #}
{% include 'organizations/confirm_modal.twig' %}
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="/assets/css/modules/data-table.css">
{% endblock %}
{% block scripts %}
{{ parent() }}
<script src="/assets/js/modules/DataTable.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.data-table').forEach(function(container) {
const id = container.id;
const url = container.dataset.url;
const perPage = parseInt(container.dataset.perPage) || 10;
if (window.dataTables && window.dataTables[id]) {
return;
}
const table = new DataTable(id, {
url: url,
perPage: perPage
});
window.dataTables = window.dataTables || {};
window.dataTables[id] = table;
});
});
</script>
<script>
// Функции для действий с пользователями
function openEditRoleModal(userId, email, currentRole) {
document.getElementById('editRoleUserId').value = userId;
document.getElementById('editRoleUserEmail').textContent = email;
document.getElementById('editRoleSelect').value = currentRole;
const modal = new bootstrap.Modal(document.getElementById('editRoleModal'));
modal.show();
}
function saveRole() {
const userId = document.getElementById('editRoleUserId').value;
const role = document.getElementById('editRoleSelect').value;
const form = document.createElement('form');
form.method = 'POST';
form.action = '/organizations/users/' + {{ organization_id }} + '/role';
const tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = '<?= csrf_token() ?>';
tokenInput.value = '<?= csrf_hash() ?>';
form.appendChild(tokenInput);
const userIdInput = document.createElement('input');
userIdInput.type = 'hidden';
userIdInput.name = 'user_id';
userIdInput.value = userId;
form.appendChild(userIdInput);
const roleInput = document.createElement('input');
roleInput.type = 'hidden';
roleInput.name = 'role';
roleInput.value = role;
form.appendChild(roleInput);
document.body.appendChild(form);
form.submit();
}
function blockUser(userId) {
showConfirmModal(
'Блокировка пользователя',
'Вы уверены, что хотите заблокировать этого пользователя? Он потеряет доступ к организации.',
'/organizations/users/' + {{ organization_id }} + '/block/' + userId
);
}
function unblockUser(userId) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '/organizations/users/' + {{ organization_id }} + '/unblock/' + userId;
const tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = '<?= csrf_token() ?>';
tokenInput.value = '<?= csrf_hash() ?>';
form.appendChild(tokenInput);
document.body.appendChild(form);
form.submit();
}
function removeUser(userId) {
showConfirmModal(
'Удаление пользователя',
'Вы уверены, что хотите удалить этого пользователя из организации? Это действие нельзя отменить.',
'/organizations/users/' + {{ organization_id }} + '/remove/' + userId
);
}
function leaveOrganization() {
showConfirmModal(
'Покинуть организацию',
'Вы уверены, что хотите покинуть эту организацию?',
'/organizations/users/' + {{ organization_id }} + '/leave',
true
);
}
// Функция показа модалки подтверждения
function showConfirmModal(title, message, actionUrl, isDanger) {
document.getElementById('confirmModalTitle').textContent = title;
document.getElementById('confirmModalMessage').textContent = message;
const btn = document.getElementById('confirmModalBtn');
btn.onclick = function() {
const form = document.createElement('form');
form.method = 'POST';
form.action = actionUrl;
const tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = '<?= csrf_token() ?>';
tokenInput.value = '<?= csrf_hash() ?>';
form.appendChild(tokenInput);
document.body.appendChild(form);
form.submit();
};
if (isDanger) {
btn.className = 'btn btn-danger';
} else {
btn.className = 'btn btn-primary';
}
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
modal.show();
}
// Обработка отправки приглашения
document.getElementById('inviteUserForm').addEventListener('submit', function(e) {
e.preventDefault();
const form = this;
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin me-2"></i>Отправка...';
submitBtn.disabled = true;
const formData = new FormData(form);
fetch(form.action, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Показываем ссылку приглашения
showInviteLinkModal(data.invite_link, data.email);
form.reset();
// Перезагружаем страницу чтобы увидеть нового пользователя
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
alert(data.message || 'Ошибка при отправке приглашения');
}
})
.catch(error => {
console.error('Error:', error);
alert('Произошла ошибка');
})
.finally(() => {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
});
});
function showInviteLinkModal(link, email) {
document.getElementById('inviteLinkInput').value = link;
document.getElementById('inviteEmailDisplay').textContent = email;
// Скрываем модалку приглашения
bootstrap.Modal.getInstance(document.getElementById('inviteUserModal')).hide();
// Показываем модалку со ссылкой
const modal = new bootstrap.Modal(document.getElementById('inviteLinkModal'));
modal.show();
}
function copyInviteLink() {
const input = document.getElementById('inviteLinkInput');
input.select();
input.setSelectionRange(0, 99999);
navigator.clipboard.writeText(input.value).then(() => {
const btn = document.getElementById('copyLinkBtn');
const originalIcon = btn.innerHTML;
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
setTimeout(() => {
btn.innerHTML = originalIcon;
}, 2000);
});
}
function shareToTelegram() {
const link = document.getElementById('inviteLinkInput').value;
const text = encodeURIComponent('Присоединяйся к нашей организации!');
window.open('https://t.me/share/url?url=' + encodeURIComponent(link) + '&text=' + text, '_blank');
}
function shareViaWebShare() {
const link = document.getElementById('inviteLinkInput').value;
if (navigator.share) {
navigator.share({
title: 'Приглашение в Бизнес.Точка',
text: 'Присоединяйся к нашей организации!',
url: link
});
}
}
</script>
{% endblock %}