This commit is contained in:
2026-01-27 15:12:27 +05:00
parent d2c530bb3a
commit 9714ac5004
6 changed files with 2499 additions and 20 deletions

856
public/admin-doc.html Normal file
View File

@@ -0,0 +1,856 @@
<!-- public/admin-doc.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Управление группами пользователей | CRM</title>
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--admin-color: #e74c3c;
--secretary-color: #3498db;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header h1 {
margin: 0;
color: #333;
display: flex;
align-items: center;
gap: 10px;
}
.header-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.tabs {
display: flex;
gap: 5px;
margin-bottom: 20px;
background: white;
padding: 5px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.tab {
padding: 12px 24px;
border: none;
background: none;
cursor: pointer;
font-size: 16px;
border-radius: 6px;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.tab:hover {
background: #f5f5f5;
}
.tab.active {
background: #3498db;
color: white;
}
.content-section {
display: none;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.content-section.active {
display: block;
}
.group-info {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 6px;
}
.group-color {
width: 20px;
height: 20px;
border-radius: 50%;
}
.group-color.admin {
background: var(--admin-color);
}
.group-color.secretary {
background: var(--secretary-color);
}
.users-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
margin-top: 20px;
}
.user-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
transition: all 0.3s;
}
.user-card:hover {
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.user-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.user-avatar {
width: 40px;
height: 40px;
background: #3498db;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.user-name {
font-weight: bold;
color: #333;
}
.user-role {
font-size: 12px;
color: #666;
margin-top: 2px;
}
.user-details {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.group-badges {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin: 10px 0;
}
.group-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
color: white;
display: flex;
align-items: center;
gap: 4px;
}
.group-badge.admin {
background: var(--admin-color);
}
.group-badge.secretary {
background: var(--secretary-color);
}
.user-actions {
display: flex;
gap: 10px;
margin-top: 10px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 5px;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover {
background: #229954;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.loading i {
font-size: 24px;
margin-bottom: 10px;
color: #3498db;
}
.search-box {
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
}
.stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
flex: 1;
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #3498db;
margin: 10px 0;
}
.stat-label {
color: #666;
font-size: 14px;
}
.no-users {
text-align: center;
padding: 40px;
color: #666;
}
@media (max-width: 768px) {
.users-container {
grid-template-columns: 1fr;
}
.stats {
flex-direction: column;
}
.user-actions {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-users-cog"></i> Управление группами пользователей</h1>
<div class="header-actions">
<button class="btn btn-primary" onclick="refreshAllData()">
<i class="fas fa-sync-alt"></i> Обновить
</button>
<button class="btn btn-success" onclick="goBack()">
<i class="fas fa-arrow-left"></i> Назад в CRM
</button>
</div>
</div>
<div class="tabs">
<button class="tab active" onclick="showTab('secretary')">
<i class="fas fa-file-signature"></i> Секретари
</button>
<button class="tab" onclick="showTab('administration')">
<i class="fas fa-user-shield"></i> Администрация
</button>
<button class="tab" onclick="showTab('all-users')">
<i class="fas fa-users"></i> Все пользователи
</button>
</div>
<!-- Секретари -->
<div id="secretary-section" class="content-section active">
<div class="group-info">
<div class="group-color secretary"></div>
<div>
<h3>Группа "Секретарь"</h3>
<p>Пользователи этой группы могут согласовывать документы в системе</p>
</div>
</div>
<div class="search-box">
<input type="text" id="secretary-search" class="search-input"
placeholder="Поиск пользователей..." onkeyup="filterUsers('secretary')">
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number" id="secretary-count">0</div>
<div class="stat-label">Всего пользователей</div>
</div>
<div class="stat-card">
<div class="stat-number" id="secretary-in-group">0</div>
<div class="stat-label">В группе "Секретарь"</div>
</div>
</div>
<div id="secretary-users" class="users-container">
<div class="loading">
<i class="fas fa-spinner fa-spin"></i>
<p>Загрузка пользователей...</p>
</div>
</div>
</div>
<!-- Администрация -->
<div id="administration-section" class="content-section">
<div class="group-info">
<div class="group-color admin"></div>
<div>
<h3>Группа "Администрация"</h3>
<p>Пользователи этой группы имеют права администратора в системе</p>
</div>
</div>
<div class="search-box">
<input type="text" id="admin-search" class="search-input"
placeholder="Поиск пользователей..." onkeyup="filterUsers('admin')">
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number" id="admin-count">0</div>
<div class="stat-label">Всего пользователей</div>
</div>
<div class="stat-card">
<div class="stat-number" id="admin-in-group">0</div>
<div class="stat-label">В группе "Администрация"</div>
</div>
</div>
<div id="admin-users" class="users-container">
<div class="loading">
<i class="fas fa-spinner fa-spin"></i>
<p>Загрузка пользователей...</p>
</div>
</div>
</div>
<!-- Все пользователи -->
<div id="all-users-section" class="content-section">
<div class="search-box">
<input type="text" id="all-search" class="search-input"
placeholder="Поиск пользователей..." onkeyup="filterUsers('all')">
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number" id="total-users">0</div>
<div class="stat-label">Всего пользователей</div>
</div>
<div class="stat-card">
<div class="stat-number" id="admins-count">0</div>
<div class="stat-label">Администраторы</div>
</div>
<div class="stat-card">
<div class="stat-number" id="secretaries-count">0</div>
<div class="stat-label">Секретари</div>
</div>
</div>
<div id="all-users" class="users-container">
<div class="loading">
<i class="fas fa-spinner fa-spin"></i>
<p>Загрузка пользователей...</p>
</div>
</div>
</div>
</div>
<script>
let currentTab = 'secretary';
let allUsers = [];
let secretaryGroupId = null;
let adminGroupId = null;
// Проверка авторизации
async function checkAuth() {
try {
const response = await fetch('/api/user');
if (response.status === 401) {
window.location.href = '/';
return false;
}
const data = await response.json();
if (data.user.role !== 'admin') {
alert('Доступ запрещен. Требуются права администратора.');
window.location.href = '/';
return false;
}
return true;
} catch (error) {
console.error('Ошибка проверки авторизации:', error);
window.location.href = '/';
return false;
}
}
// Загрузка данных
async function loadData() {
if (!await checkAuth()) return;
try {
// Загружаем группы
const groupsResponse = await fetch('/api/groups');
const groups = await groupsResponse.json();
// Находим ID групп
secretaryGroupId = groups.find(g => g.name === 'Секретарь')?.id;
adminGroupId = groups.find(g => g.name === 'Администрация')?.id;
if (!secretaryGroupId) {
console.warn('Группа "Секретарь" не найдена');
}
if (!adminGroupId) {
console.warn('Группа "Администрация" не найдена');
}
// Загружаем всех пользователей
const usersResponse = await fetch('/api/users/all');
const users = await usersResponse.json();
allUsers = users;
// Обновляем статистику
updateStats();
// Отображаем пользователей
renderUsers();
} catch (error) {
console.error('Ошибка загрузки данных:', error);
showError('Не удалось загрузить данные');
}
}
// Обновление статистики
function updateStats() {
// Статистика для секретарей
const secretaryUsers = allUsers.filter(u => u.groups?.some(g => g.group_name === 'Секретарь'));
document.getElementById('secretary-count').textContent = allUsers.length;
document.getElementById('secretary-in-group').textContent = secretaryUsers.length;
// Статистика для администрации
const adminUsers = allUsers.filter(u => u.groups?.some(g => g.group_name === 'Администрация'));
document.getElementById('admin-count').textContent = allUsers.length;
document.getElementById('admin-in-group').textContent = adminUsers.length;
// Общая статистика
document.getElementById('total-users').textContent = allUsers.length;
document.getElementById('admins-count').textContent = adminUsers.length;
document.getElementById('secretaries-count').textContent = secretaryUsers.length;
}
// Отображение пользователей
function renderUsers() {
renderSecretaryUsers();
renderAdminUsers();
renderAllUsers();
}
// Отображение пользователей для секретарей
function renderSecretaryUsers() {
const container = document.getElementById('secretary-users');
const searchTerm = document.getElementById('secretary-search').value.toLowerCase();
const filteredUsers = allUsers.filter(user =>
user.name.toLowerCase().includes(searchTerm) ||
user.login.toLowerCase().includes(searchTerm) ||
user.email.toLowerCase().includes(searchTerm)
);
if (filteredUsers.length === 0) {
container.innerHTML = `
<div class="no-users">
<i class="fas fa-users-slash" style="font-size: 48px; color: #ccc; margin-bottom: 15px;"></i>
<p>Пользователи не найдены</p>
</div>
`;
return;
}
container.innerHTML = filteredUsers.map(user => {
const isSecretary = user.groups?.some(g => g.group_name === 'Секретарь');
const isAdmin = user.groups?.some(g => g.group_name === 'Администрация');
return `
<div class="user-card">
<div class="user-header">
<div class="user-avatar">${user.name.charAt(0).toUpperCase()}</div>
<div>
<div class="user-name">${user.name}</div>
<div class="user-role">${user.role === 'admin' ? 'Администратор' : 'Учитель'}</div>
</div>
</div>
<div class="user-details">
<div><i class="fas fa-user"></i> ${user.login}</div>
<div><i class="fas fa-envelope"></i> ${user.email}</div>
</div>
<div class="group-badges">
${isSecretary ? '<span class="group-badge secretary"><i class="fas fa-file-signature"></i> Секретарь</span>' : ''}
${isAdmin ? '<span class="group-badge admin"><i class="fas fa-user-shield"></i> Администрация</span>' : ''}
</div>
<div class="user-actions">
${isSecretary ?
`<button class="btn btn-danger" onclick="removeFromGroup(${user.id}, 'secretary')">
<i class="fas fa-user-minus"></i> Убрать из секретарей
</button>` :
`<button class="btn btn-success" onclick="addToGroup(${user.id}, 'secretary')">
<i class="fas fa-user-plus"></i> Добавить в секретари
</button>`
}
</div>
</div>
`;
}).join('');
}
// Отображение пользователей для администрации
function renderAdminUsers() {
const container = document.getElementById('admin-users');
const searchTerm = document.getElementById('admin-search').value.toLowerCase();
const filteredUsers = allUsers.filter(user =>
user.name.toLowerCase().includes(searchTerm) ||
user.login.toLowerCase().includes(searchTerm) ||
user.email.toLowerCase().includes(searchTerm)
);
if (filteredUsers.length === 0) {
container.innerHTML = `
<div class="no-users">
<i class="fas fa-users-slash" style="font-size: 48px; color: #ccc; margin-bottom: 15px;"></i>
<p>Пользователи не найдены</p>
</div>
`;
return;
}
container.innerHTML = filteredUsers.map(user => {
const isSecretary = user.groups?.some(g => g.group_name === 'Секретарь');
const isAdmin = user.groups?.some(g => g.group_name === 'Администрация');
return `
<div class="user-card">
<div class="user-header">
<div class="user-avatar">${user.name.charAt(0).toUpperCase()}</div>
<div>
<div class="user-name">${user.name}</div>
<div class="user-role">${user.role === 'admin' ? 'Администратор' : 'Учитель'}</div>
</div>
</div>
<div class="user-details">
<div><i class="fas fa-user"></i> ${user.login}</div>
<div><i class="fas fa-envelope"></i> ${user.email}</div>
</div>
<div class="group-badges">
${isSecretary ? '<span class="group-badge secretary"><i class="fas fa-file-signature"></i> Секретарь</span>' : ''}
${isAdmin ? '<span class="group-badge admin"><i class="fas fa-user-shield"></i> Администрация</span>' : ''}
</div>
<div class="user-actions">
${isAdmin ?
`<button class="btn btn-danger" onclick="removeFromGroup(${user.id}, 'admin')">
<i class="fas fa-user-minus"></i> Убрать из администрации
</button>` :
`<button class="btn btn-success" onclick="addToGroup(${user.id}, 'admin')">
<i class="fas fa-user-plus"></i> Добавить в администрацию
</button>`
}
</div>
</div>
`;
}).join('');
}
// Отображение всех пользователей
function renderAllUsers() {
const container = document.getElementById('all-users');
const searchTerm = document.getElementById('all-search').value.toLowerCase();
const filteredUsers = allUsers.filter(user =>
user.name.toLowerCase().includes(searchTerm) ||
user.login.toLowerCase().includes(searchTerm) ||
user.email.toLowerCase().includes(searchTerm)
);
if (filteredUsers.length === 0) {
container.innerHTML = `
<div class="no-users">
<i class="fas fa-users-slash" style="font-size: 48px; color: #ccc; margin-bottom: 15px;"></i>
<p>Пользователи не найдены</p>
</div>
`;
return;
}
container.innerHTML = filteredUsers.map(user => {
const isSecretary = user.groups?.some(g => g.group_name === 'Секретарь');
const isAdmin = user.groups?.some(g => g.group_name === 'Администрация');
return `
<div class="user-card">
<div class="user-header">
<div class="user-avatar">${user.name.charAt(0).toUpperCase()}</div>
<div>
<div class="user-name">${user.name}</div>
<div class="user-role">${user.role === 'admin' ? 'Администратор' : 'Учитель'}</div>
</div>
</div>
<div class="user-details">
<div><i class="fas fa-user"></i> ${user.login}</div>
<div><i class="fas fa-envelope"></i> ${user.email}</div>
</div>
<div class="group-badges">
${isSecretary ? '<span class="group-badge secretary"><i class="fas fa-file-signature"></i> Секретарь</span>' : ''}
${isAdmin ? '<span class="group-badge admin"><i class="fas fa-user-shield"></i> Администрация</span>' : ''}
</div>
<div class="user-actions">
${isSecretary ?
`<button class="btn btn-danger" onclick="removeFromGroup(${user.id}, 'secretary')">
<i class="fas fa-user-minus"></i> Убрать из секретарей
</button>` :
`<button class="btn btn-success" onclick="addToGroup(${user.id}, 'secretary')">
<i class="fas fa-user-plus"></i> В секретари
</button>`
}
${isAdmin ?
`<button class="btn btn-danger" onclick="removeFromGroup(${user.id}, 'admin')">
<i class="fas fa-user-minus"></i> Убрать из администрации
</button>` :
`<button class="btn btn-primary" onclick="addToGroup(${user.id}, 'admin')">
<i class="fas fa-user-plus"></i> В администрацию
</button>`
}
</div>
</div>
`;
}).join('');
}
// Фильтрация пользователей
function filterUsers(section) {
switch(section) {
case 'secretary':
renderSecretaryUsers();
break;
case 'admin':
renderAdminUsers();
break;
case 'all':
renderAllUsers();
break;
}
}
// Показать вкладку
function showTab(tabName) {
currentTab = tabName;
// Обновляем активные вкладки
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab').forEach(tab => {
if (tab.onclick.toString().includes(tabName)) {
tab.classList.add('active');
}
});
// Обновляем активные секции
document.querySelectorAll('.content-section').forEach(section => {
section.classList.remove('active');
});
switch(tabName) {
case 'secretary':
document.getElementById('secretary-section').classList.add('active');
break;
case 'administration':
document.getElementById('administration-section').classList.add('active');
break;
case 'all-users':
document.getElementById('all-users-section').classList.add('active');
break;
}
}
// Добавить пользователя в группу
async function addToGroup(userId, groupType) {
if (!await checkAuth()) return;
const groupName = groupType === 'secretary' ? 'Секретарь' : 'Администрация';
const groupId = groupType === 'secretary' ? secretaryGroupId : adminGroupId;
if (!groupId) {
showError(`Группа "${groupName}" не найдена`);
return;
}
if (!confirm(`Добавить пользователя в группу "${groupName}"?`)) return;
try {
const response = await fetch(`/api/groups/${groupId}/users/${userId}`, {
method: 'POST'
});
if (response.ok) {
showSuccess(`Пользователь добавлен в группу "${groupName}"`);
await refreshAllData();
} else {
const error = await response.text();
showError(error);
}
} catch (error) {
console.error('Ошибка добавления в группу:', error);
showError('Не удалось добавить пользователя в группу');
}
}
// Удалить пользователя из группы
async function removeFromGroup(userId, groupType) {
if (!await checkAuth()) return;
const groupName = groupType === 'secretary' ? 'Секретарь' : 'Администрация';
const groupId = groupType === 'secretary' ? secretaryGroupId : adminGroupId;
if (!groupId) {
showError(`Группа "${groupName}" не найдена`);
return;
}
if (!confirm(`Убрать пользователя из группы "${groupName}"?`)) return;
try {
const response = await fetch(`/api/groups/${groupId}/users/${userId}`, {
method: 'DELETE'
});
if (response.ok) {
showSuccess(`Пользователь убран из группы "${groupName}"`);
await refreshAllData();
} else {
const error = await response.text();
showError(error);
}
} catch (error) {
console.error('Ошибка удаления из группы:', error);
showError('Не удалось убрать пользователя из группы');
}
}
// Обновить все данные
async function refreshAllData() {
await loadData();
showSuccess('Данные обновлены');
}
// Показать сообщение об ошибке
function showError(message) {
alert('Ошибка: ' + message);
}
// Показать сообщение об успехе
function showSuccess(message) {
// Можно заменить на более красивый toast
alert('Успех: ' + message);
}
// Вернуться в CRM
function goBack() {
window.location.href = '/admin';
}
// Инициализация
document.addEventListener('DOMContentLoaded', async () => {
if (await checkAuth()) {
await loadData();
}
});
</script>
</body>
</html>