Files
minicrm/public/admin-groups.html
2026-01-27 00:32:06 +05:00

543 lines
25 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>