This commit is contained in:
2026-01-27 00:32:06 +05:00
parent 30aa35357f
commit 0fe8f05b73
7 changed files with 2837 additions and 102 deletions

543
public/admin-groups.html Normal file
View File

@@ -0,0 +1,543 @@
<!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>

View File

@@ -272,6 +272,7 @@
<div class="user-info">
<span id="current-user"></span>
<button onclick="window.location.href = '/'">Назад к задачам</button>
<li class="nav-item"><a class="nav-link" href="/admin/groups">Управление группами</a></li>
<button onclick="logout()">Выйти</button>
</div>
</div>