dashboard for org
This commit is contained in:
@@ -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>
|
||||
@@ -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">
|
||||
© {{ "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">
|
||||
© {{ "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">
|
||||
© {{ "now"|date("Y") }} Бизнес.Точка
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user