diff --git a/admin-server.js b/admin-server.js new file mode 100644 index 0000000..b7123a9 --- /dev/null +++ b/admin-server.js @@ -0,0 +1,251 @@ +const express = require('express'); +const router = express.Router(); +const { db } = require('./database'); + +const requireAdmin = (req, res, next) => { + if (!req.session.user) { + return res.status(401).json({ error: 'Требуется аутентификация' }); + } + + if (req.session.user.role !== 'admin') { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + + next(); +}; + +router.get('/admin/users', requireAdmin, (req, res) => { + const search = req.query.search || ''; + + let query = ` + SELECT id, login, name, email, role, auth_type, groups, + description, created_at, last_login, updated_at + FROM users + WHERE 1=1 + `; + + const params = []; + + if (search) { + query += ` AND (login LIKE ? OR name LIKE ? OR email LIKE ?)`; + const searchPattern = `%${search}%`; + params.push(searchPattern, searchPattern, searchPattern); + } + + query += " ORDER BY name"; + + db.all(query, params, (err, users) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + res.json(users); + }); +}); + +router.get('/admin/users/:id', requireAdmin, (req, res) => { + const { id } = req.params; + + db.get("SELECT id, login, name, email, role, auth_type, groups, description FROM users WHERE id = ?", [id], (err, user) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + if (!user) { + res.status(404).json({ error: 'Пользователь не найден' }); + return; + } + + res.json(user); + }); +}); + +router.put('/admin/users/:id', requireAdmin, (req, res) => { + const { id } = req.params; + const { login, name, email, role, auth_type, groups, description } = req.body; + + if (!login || !name || !email) { + return res.status(400).json({ error: 'Логин, имя и email обязательны' }); + } + + if (req.session.user.id === parseInt(id) && role !== 'admin') { + return res.status(400).json({ error: 'Нельзя снять права администратора с самого себя' }); + } + + db.get("SELECT id FROM users WHERE (login = ? OR email = ?) AND id != ?", + [login, email, id], (err, existingUser) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + if (existingUser) { + return res.status(400).json({ error: 'Пользователь с таким логином или email уже существует' }); + } + + db.run( + `UPDATE users SET + login = ?, name = ?, email = ?, role = ?, auth_type = ?, + groups = ?, description = ?, updated_at = datetime('now') + WHERE id = ?`, + [ + login, + name, + email, + role || 'teacher', + auth_type || 'local', + groups || '[]', + description || '', + id + ], + function(err) { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + res.json({ + success: true, + message: 'Пользователь обновлен' + }); + } + ); + } + ); +}); + +router.delete('/admin/users/:id', requireAdmin, (req, res) => { + const { id } = req.params; + + if (req.session.user.id === parseInt(id)) { + return res.status(400).json({ error: 'Нельзя удалить самого себя' }); + } + + db.get("SELECT COUNT(*) as task_count FROM tasks WHERE created_by = ?", [id], (err, result) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + if (result.task_count > 0) { + return res.status(400).json({ + error: 'Нельзя удалить пользователя, который создавал задачи. Сначала удалите или переназначьте его задачи.' + }); + } + + db.run("DELETE FROM users WHERE id = ?", [id], function(err) { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + res.json({ + success: true, + message: 'Пользователь удален' + }); + }); + }); +}); + +router.get('/admin/stats', requireAdmin, (req, res) => { + const stats = {}; + + const queries = [ + // Общая статистика по задачам + `SELECT COUNT(*) as total_tasks FROM tasks`, + `SELECT COUNT(*) as active_tasks FROM tasks WHERE status = 'active' AND closed_at IS NULL`, + `SELECT COUNT(*) as closed_tasks FROM tasks WHERE closed_at IS NOT NULL`, + `SELECT COUNT(*) as deleted_tasks FROM tasks WHERE status = 'deleted'`, + `SELECT COUNT(DISTINCT created_by) as unique_creators FROM tasks`, + + // Статистика по статусам назначений + `SELECT COUNT(*) as total_assignments FROM task_assignments`, + `SELECT COUNT(*) as assigned_count FROM task_assignments WHERE status = 'assigned'`, + `SELECT COUNT(*) as in_progress_count FROM task_assignments WHERE status = 'in_progress'`, + `SELECT COUNT(*) as completed_count FROM task_assignments WHERE status = 'completed'`, + `SELECT COUNT(*) as overdue_count FROM task_assignments WHERE status = 'overdue'`, + `SELECT COUNT(*) as rework_count FROM task_assignments WHERE status = 'rework'`, + + // Статистика по пользователям + `SELECT COUNT(*) as total_users FROM users`, + `SELECT COUNT(*) as admin_users FROM users WHERE role = 'admin'`, + `SELECT COUNT(*) as teacher_users FROM users WHERE role = 'teacher'`, + `SELECT COUNT(*) as ldap_users FROM users WHERE auth_type = 'ldap'`, + `SELECT COUNT(*) as local_users FROM users WHERE auth_type = 'local'`, + + // Статистика по файлам + `SELECT COUNT(*) as total_files FROM task_files`, + `SELECT COALESCE(SUM(file_size), 0) as total_files_size FROM task_files`, + + // Последние созданные задачи + `SELECT t.id, t.title, t.created_at, u.name as creator_name + FROM tasks t + LEFT JOIN users u ON t.created_by = u.id + WHERE t.status = 'active' + ORDER BY t.created_at DESC LIMIT 5` + ]; + + const queryPromises = queries.map((query, index) => { + return new Promise((resolve, reject) => { + db.all(query, [], (err, rows) => { + if (err) { + reject(err); + } else { + resolve({ index, rows }); + } + }); + }); + }); + + Promise.all(queryPromises) + .then(results => { + const [ + totalTasks, activeTasks, closedTasks, deletedTasks, uniqueCreators, + totalAssignments, assignedCount, inProgressCount, completedCount, overdueCount, reworkCount, + totalUsers, adminUsers, teacherUsers, ldapUsers, localUsers, + totalFiles, totalFilesSize, + recentTasks + ] = results; + + stats.totalTasks = totalTasks.rows[0].total_tasks; + stats.activeTasks = activeTasks.rows[0].active_tasks; + stats.closedTasks = closedTasks.rows[0].closed_tasks; + stats.deletedTasks = deletedTasks.rows[0].deleted_tasks; + stats.uniqueCreators = uniqueCreators.rows[0].unique_creators; + + stats.totalAssignments = totalAssignments.rows[0].total_assignments; + stats.assignedCount = assignedCount.rows[0].assigned_count; + stats.inProgressCount = inProgressCount.rows[0].in_progress_count; + stats.completedCount = completedCount.rows[0].completed_count; + stats.overdueCount = overdueCount.rows[0].overdue_count; + stats.reworkCount = reworkCount.rows[0].rework_count; + + stats.totalUsers = totalUsers.rows[0].total_users; + stats.adminUsers = adminUsers.rows[0].admin_users; + stats.teacherUsers = teacherUsers.rows[0].teacher_users; + stats.ldapUsers = ldapUsers.rows[0].ldap_users; + stats.localUsers = localUsers.rows[0].local_users; + + stats.totalFiles = totalFiles.rows[0].total_files; + stats.totalFilesSize = totalFilesSize.rows[0].total_files_size; + + stats.recentTasks = recentTasks.rows; + + res.json(stats); + }) + .catch(err => { + res.status(500).json({ error: err.message }); + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/package.json b/package.json index 0f3398c..53dbe46 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "school-crm", - "version": "1.0.0", - "description": "CRM система для школы с управлением задачами", + "version": "1.2.0", + "description": "CRM система для школы с управлением задачами и админ-панелью", "main": "server.js", "scripts": { "start": "node server.js", diff --git a/public/admin-script.js b/public/admin-script.js new file mode 100644 index 0000000..65fccce --- /dev/null +++ b/public/admin-script.js @@ -0,0 +1,386 @@ +let currentUser = null; +let users = []; +let filteredUsers = []; + +document.addEventListener('DOMContentLoaded', function() { + checkAuth(); + setupEventListeners(); +}); + +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; + } + + showAdminInterface(); + } else { + showLoginInterface(); + } + } catch (error) { + showLoginInterface(); + } +} + +function showLoginInterface() { + document.getElementById('login-modal').style.display = 'block'; + document.querySelector('.admin-container').style.display = 'none'; +} + +function showAdminInterface() { + document.getElementById('login-modal').style.display = 'none'; + document.querySelector('.admin-container').style.display = 'block'; + + let userInfo = `Администратор: ${currentUser.name}`; + if (currentUser.auth_type === 'ldap') { + userInfo += ` (LDAP)`; + } + + document.getElementById('current-user').textContent = userInfo; + + loadUsers(); + loadDashboardStats(); +} + +function setupEventListeners() { + document.getElementById('login-form').addEventListener('submit', login); + document.getElementById('edit-user-form').addEventListener('submit', updateUser); +} + +async function login(event) { + event.preventDefault(); + + const login = document.getElementById('login').value; + const password = document.getElementById('password').value; + + try { + const response = await fetch('/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ login, password }) + }); + + if (response.ok) { + const data = await response.json(); + currentUser = data.user; + + if (currentUser.role !== 'admin') { + window.location.href = '/'; + return; + } + + showAdminInterface(); + } else { + const error = await response.json(); + alert(error.error || 'Ошибка входа'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка подключения к серверу'); + } +} + +async function logout() { + try { + await fetch('/api/logout', { method: 'POST' }); + currentUser = null; + showLoginInterface(); + } catch (error) { + console.error('Ошибка выхода:', error); + } +} + +function showAdminSection(sectionName) { + document.querySelectorAll('.admin-tab').forEach(tab => { + tab.classList.remove('active'); + }); + + document.querySelectorAll('.admin-section').forEach(section => { + section.classList.remove('active'); + }); + + document.querySelector(`.admin-tab[onclick="showAdminSection('${sectionName}')"]`).classList.add('active'); + document.getElementById(`admin-${sectionName}`).classList.add('active'); + + if (sectionName === 'users') { + loadUsers(); + } else if (sectionName === 'dashboard') { + loadDashboardStats(); + } +} + +async function loadUsers() { + try { + const response = await fetch('/admin/users'); + if (!response.ok) { + throw new Error('Ошибка загрузки пользователей'); + } + users = await response.json(); + filteredUsers = [...users]; + renderUsersTable(); + } catch (error) { + console.error('Ошибка загрузки пользователей:', error); + showError('users-table-body', 'Ошибка загрузки пользователей'); + } +} + +async function loadDashboardStats() { + try { + const response = await fetch('/admin/stats'); + if (!response.ok) { + throw new Error('Ошибка загрузки статистики'); + } + + const stats = await response.json(); + updateStatsUI(stats); + } catch (error) { + console.error('Ошибка загрузки статистики:', error); + } +} + +function updateStatsUI(stats) { + // Задачи + document.getElementById('total-tasks').textContent = stats.totalTasks; + document.getElementById('active-tasks').textContent = stats.activeTasks; + document.getElementById('closed-tasks').textContent = stats.closedTasks; + document.getElementById('deleted-tasks').textContent = stats.deletedTasks; + + // Процент активных задач + if (stats.totalTasks > 0) { + const activePercentage = Math.round((stats.activeTasks / stats.totalTasks) * 100); + document.getElementById('active-tasks-bar').style.width = `${activePercentage}%`; + } + + // Назначения + document.getElementById('total-assignments').textContent = stats.totalAssignments; + document.getElementById('assigned-count').textContent = stats.assignedCount; + document.getElementById('in-progress-count').textContent = stats.inProgressCount; + document.getElementById('completed-count').textContent = stats.completedCount; + document.getElementById('overdue-count').textContent = stats.overdueCount; + document.getElementById('rework-count').textContent = stats.reworkCount; + + // Пользователи + document.getElementById('total-users').textContent = stats.totalUsers; + document.getElementById('admin-users').textContent = stats.adminUsers; + document.getElementById('teacher-users').textContent = stats.teacherUsers; + document.getElementById('ldap-users').textContent = stats.ldapUsers; + document.getElementById('local-users').textContent = stats.localUsers; + + // Файлы + document.getElementById('total-files').textContent = stats.totalFiles; + const fileSizeMB = (stats.totalFilesSize / 1024 / 1024).toFixed(2); + document.getElementById('total-files-size').textContent = `${fileSizeMB} MB`; + + // Последние задачи + renderRecentTasks(stats.recentTasks || []); +} + +function renderRecentTasks(tasks) { + const container = document.getElementById('recent-tasks-list'); + + if (!tasks || tasks.length === 0) { + container.innerHTML = '
| ID | +Логин | +Имя | +Роль | +Тип | +Дата создания | +Последний вход | +Действия | +|
|---|---|---|---|---|---|---|---|---|
| Загрузка пользователей... | +||||||||