543 lines
25 KiB
HTML
543 lines
25 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Управление группами пользователей</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
|
||
<style>
|
||
.group-color {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 4px;
|
||
display: inline-block;
|
||
margin-right: 8px;
|
||
vertical-align: middle;
|
||
}
|
||
.badge-custom {
|
||
font-size: 0.8em;
|
||
padding: 4px 8px;
|
||
}
|
||
.table-hover tbody tr:hover {
|
||
background-color: rgba(0, 0, 0, 0.075);
|
||
}
|
||
.cursor-pointer {
|
||
cursor: pointer;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||
<div class="container-fluid">
|
||
<a class="navbar-brand" href="/admin">CRM Админ-панель</a>
|
||
<div class="collapse navbar-collapse">
|
||
<ul class="navbar-nav me-auto">
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="/admin">Главная</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="/admin/profiles">Пользователи</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link active" href="/admin/groups">Группы</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="/admin-doc">Документы</a>
|
||
</li>
|
||
</ul>
|
||
<div class="navbar-text">
|
||
<span id="currentUser"></span>
|
||
<button class="btn btn-sm btn-outline-light ms-2" onclick="logout()">Выйти</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="container-fluid mt-4">
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">Управление группами пользователей</h5>
|
||
<button class="btn btn-primary btn-sm" onclick="openCreateGroupModal()">
|
||
<i class="bi bi-plus-circle"></i> Создать группу
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>Название</th>
|
||
<th>Описание</th>
|
||
<th>Участников</th>
|
||
<th>Может согласовывать</th>
|
||
<th>Цвет</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="groupsTableBody">
|
||
<!-- Группы будут загружены сюда -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row mt-4">
|
||
<div class="col-12">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">Пользователи и их группы</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>Пользователь</th>
|
||
<th>Роль</th>
|
||
<th>Группы</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="usersTableBody">
|
||
<!-- Пользователи будут загружены сюда -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно создания/редактирования группы -->
|
||
<div class="modal fade" id="groupModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="groupModalTitle">Создать группу</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="groupForm">
|
||
<input type="hidden" id="groupId">
|
||
<div class="mb-3">
|
||
<label class="form-label">Название группы *</label>
|
||
<input type="text" class="form-control" id="groupName" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Описание</label>
|
||
<textarea class="form-control" id="groupDescription" rows="3"></textarea>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Цвет группы</label>
|
||
<input type="color" class="form-control form-control-color" id="groupColor" value="#3498db">
|
||
</div>
|
||
<div class="mb-3 form-check">
|
||
<input type="checkbox" class="form-check-input" id="canApproveDocuments">
|
||
<label class="form-check-label">Может согласовывать документы</label>
|
||
<small class="form-text text-muted d-block">
|
||
Пользователи этой группы будут доступны для выбора при создании задач согласования документов
|
||
</small>
|
||
</div>
|
||
</form>
|
||
</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="saveGroup()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно управления группами пользователя -->
|
||
<div class="modal fade" id="userGroupsModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">Группы пользователя: <span id="userName"></span></h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div id="userGroupsList">
|
||
<!-- Группы пользователя будут загружены сюда -->
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
let currentUser = null;
|
||
let groups = [];
|
||
let users = [];
|
||
|
||
// Загрузка данных при загрузке страницы
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
await checkAuth();
|
||
await loadGroups();
|
||
await loadUsersWithGroups();
|
||
});
|
||
|
||
// Проверка авторизации
|
||
async function checkAuth() {
|
||
try {
|
||
const response = await fetch('/api/user');
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
currentUser = data.user;
|
||
|
||
if (currentUser.role !== 'admin') {
|
||
window.location.href = '/';
|
||
return;
|
||
}
|
||
|
||
document.getElementById('currentUser').textContent =
|
||
`${currentUser.name} (${currentUser.role})`;
|
||
} else {
|
||
window.location.href = '/';
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка проверки авторизации:', error);
|
||
window.location.href = '/';
|
||
}
|
||
}
|
||
|
||
// Выход из системы
|
||
function logout() {
|
||
fetch('/api/logout', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' }
|
||
})
|
||
.then(() => {
|
||
window.location.href = '/';
|
||
});
|
||
}
|
||
|
||
// Загрузка групп
|
||
async function loadGroups() {
|
||
try {
|
||
const response = await fetch('/api/groups');
|
||
if (response.ok) {
|
||
groups = await response.json();
|
||
renderGroupsTable();
|
||
} else {
|
||
showError('Ошибка загрузки групп');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки групп:', error);
|
||
showError('Ошибка загрузки групп');
|
||
}
|
||
}
|
||
|
||
// Загрузка пользователей с группами
|
||
async function loadUsersWithGroups() {
|
||
try {
|
||
const response = await fetch('/api/users-with-groups');
|
||
if (response.ok) {
|
||
users = await response.json();
|
||
renderUsersTable();
|
||
} else {
|
||
showError('Ошибка загрузки пользователей');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки пользователей:', error);
|
||
showError('Ошибка загрузки пользователей');
|
||
}
|
||
}
|
||
|
||
// Отображение таблицы групп
|
||
function renderGroupsTable() {
|
||
const tbody = document.getElementById('groupsTableBody');
|
||
tbody.innerHTML = '';
|
||
|
||
if (groups.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="6" class="text-center text-muted">
|
||
Нет созданных групп
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
groups.forEach(group => {
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td>
|
||
<span class="group-color" style="background-color: ${group.color}"></span>
|
||
<strong>${group.name}</strong>
|
||
</td>
|
||
<td>${group.description || '-'}</td>
|
||
<td>
|
||
<span class="badge bg-primary">${group.member_count || 0}</span>
|
||
</td>
|
||
<td>
|
||
${group.can_approve_documents
|
||
? '<span class="badge bg-success">Да</span>'
|
||
: '<span class="badge bg-secondary">Нет</span>'}
|
||
</td>
|
||
<td>
|
||
<span class="group-color" style="background-color: ${group.color}"></span>
|
||
${group.color}
|
||
</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-outline-primary me-1" onclick="editGroup(${group.id})">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="deleteGroup(${group.id})">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// Отображение таблицы пользователей
|
||
function renderUsersTable() {
|
||
const tbody = document.getElementById('usersTableBody');
|
||
tbody.innerHTML = '';
|
||
|
||
if (users.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="4" class="text-center text-muted">
|
||
Нет пользователей
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
users.forEach(user => {
|
||
const groupsHtml = user.group_names.length > 0
|
||
? user.group_names.map(name =>
|
||
`<span class="badge bg-info me-1">${name}</span>`
|
||
).join('')
|
||
: '<span class="text-muted">Нет групп</span>';
|
||
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td>
|
||
<div><strong>${user.name}</strong></div>
|
||
<small class="text-muted">${user.login} • ${user.email}</small>
|
||
</td>
|
||
<td>
|
||
<span class="badge ${user.role === 'admin' ? 'bg-danger' : 'bg-secondary'}">
|
||
${user.role === 'admin' ? 'Администратор' : 'Учитель'}
|
||
</span>
|
||
</td>
|
||
<td>${groupsHtml}</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-outline-primary" onclick="manageUserGroups(${user.id}, '${user.name}')">
|
||
<i class="bi bi-person-gear"></i> Управление группами
|
||
</button>
|
||
</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// Открытие модального окна создания группы
|
||
function openCreateGroupModal() {
|
||
document.getElementById('groupModalTitle').textContent = 'Создать группу';
|
||
document.getElementById('groupId').value = '';
|
||
document.getElementById('groupName').value = '';
|
||
document.getElementById('groupDescription').value = '';
|
||
document.getElementById('groupColor').value = '#3498db';
|
||
document.getElementById('canApproveDocuments').checked = false;
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('groupModal'));
|
||
modal.show();
|
||
}
|
||
|
||
// Редактирование группы
|
||
async function editGroup(groupId) {
|
||
const group = groups.find(g => g.id === groupId);
|
||
if (!group) return;
|
||
|
||
document.getElementById('groupModalTitle').textContent = 'Редактировать группу';
|
||
document.getElementById('groupId').value = group.id;
|
||
document.getElementById('groupName').value = group.name;
|
||
document.getElementById('groupDescription').value = group.description || '';
|
||
document.getElementById('groupColor').value = group.color;
|
||
document.getElementById('canApproveDocuments').checked = !!group.can_approve_documents;
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('groupModal'));
|
||
modal.show();
|
||
}
|
||
|
||
// Сохранение группы
|
||
async function saveGroup() {
|
||
const groupId = document.getElementById('groupId').value;
|
||
const groupData = {
|
||
name: document.getElementById('groupName').value.trim(),
|
||
description: document.getElementById('groupDescription').value.trim(),
|
||
color: document.getElementById('groupColor').value,
|
||
can_approve_documents: document.getElementById('canApproveDocuments').checked
|
||
};
|
||
|
||
if (!groupData.name) {
|
||
showError('Введите название группы');
|
||
return;
|
||
}
|
||
|
||
const url = groupId
|
||
? `/api/groups/${groupId}`
|
||
: '/api/groups';
|
||
const method = groupId ? 'PUT' : 'POST';
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(groupData)
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
showSuccess(data.message || 'Группа сохранена');
|
||
|
||
// Закрываем модальное окно
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('groupModal'));
|
||
modal.hide();
|
||
|
||
// Перезагружаем данные
|
||
await loadGroups();
|
||
await loadUsersWithGroups();
|
||
} else {
|
||
const errorData = await response.json();
|
||
showError(errorData.error || 'Ошибка сохранения группы');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка сохранения группы:', error);
|
||
showError('Ошибка сохранения группы');
|
||
}
|
||
}
|
||
|
||
// Удаление группы
|
||
async function deleteGroup(groupId) {
|
||
if (!confirm('Вы уверены, что хотите удалить эту группу? Все связи с пользователями будут удалены.')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/groups/${groupId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
showSuccess(data.message || 'Группа удалена');
|
||
|
||
// Перезагружаем данные
|
||
await loadGroups();
|
||
await loadUsersWithGroups();
|
||
} else {
|
||
const errorData = await response.json();
|
||
showError(errorData.error || 'Ошибка удаления группы');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка удаления группы:', error);
|
||
showError('Ошибка удаления группы');
|
||
}
|
||
}
|
||
|
||
// Управление группами пользователя
|
||
async function manageUserGroups(userId, userName) {
|
||
document.getElementById('userName').textContent = userName;
|
||
|
||
try {
|
||
// Получаем доступные группы для пользователя
|
||
const response = await fetch(`/api/users/${userId}/available-groups`);
|
||
if (response.ok) {
|
||
const availableGroups = await response.json();
|
||
|
||
// Создаем список чекбоксов
|
||
let groupsHtml = '';
|
||
availableGroups.forEach(group => {
|
||
groupsHtml += `
|
||
<div class="form-check mb-2">
|
||
<input class="form-check-input" type="checkbox"
|
||
id="group_${group.id}"
|
||
${group.is_member ? 'checked' : ''}
|
||
onchange="toggleUserGroup(${userId}, ${group.id}, this.checked)">
|
||
<label class="form-check-label" for="group_${group.id}">
|
||
<span class="group-color" style="background-color: ${group.color}"></span>
|
||
${group.name}
|
||
${group.can_approve_documents
|
||
? '<span class="badge bg-success badge-custom ms-1">Согласование</span>'
|
||
: ''}
|
||
</label>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
document.getElementById('userGroupsList').innerHTML = groupsHtml ||
|
||
'<p class="text-muted">Нет доступных групп</p>';
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('userGroupsModal'));
|
||
modal.show();
|
||
} else {
|
||
showError('Ошибка загрузки групп пользователя');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки групп:', error);
|
||
showError('Ошибка загрузки групп');
|
||
}
|
||
}
|
||
|
||
// Переключение группы пользователя
|
||
async function toggleUserGroup(userId, groupId, isChecked) {
|
||
try {
|
||
const response = await fetch(`/api/users/${userId}/groups`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
groupIds: isChecked
|
||
? [...document.querySelectorAll('#userGroupsList input:checked')]
|
||
.map(input => parseInt(input.id.split('_')[1]))
|
||
: [...document.querySelectorAll('#userGroupsList input:checked')]
|
||
.filter(input => parseInt(input.id.split('_')[1]) !== groupId)
|
||
.map(input => parseInt(input.id.split('_')[1]))
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
showError(errorData.error || 'Ошибка обновления групп');
|
||
// Возвращаем чекбокс в предыдущее состояние
|
||
const checkbox = document.getElementById(`group_${groupId}`);
|
||
checkbox.checked = !isChecked;
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка обновления групп:', error);
|
||
showError('Ошибка обновления групп');
|
||
// Возвращаем чекбокс в предыдущее состояние
|
||
const checkbox = document.getElementById(`group_${groupId}`);
|
||
checkbox.checked = !isChecked;
|
||
}
|
||
}
|
||
|
||
// Вспомогательные функции для уведомлений
|
||
function showSuccess(message) {
|
||
alert(message); // Можно заменить на красивый toast
|
||
}
|
||
|
||
function showError(message) {
|
||
alert('Ошибка: ' + message); // Можно заменить на красивый toast
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |