Files
minicrm/public/admin-doc.html
2026-01-27 15:12:27 +05:00

856 lines
32 KiB
HTML
Raw 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.
<!-- 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>