diff --git a/admin-server.js b/admin-server.js new file mode 100644 index 0000000..a4eee1a --- /dev/null +++ b/admin-server.js @@ -0,0 +1,1279 @@ +// admin-server.js +const express = require('express'); +const router = express.Router(); + +// Middleware для проверки прав администратора +const requireAdmin = (req, res, next) => { + if (!req.session.user || req.session.user.role !== 'admin') { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + next(); +}; + +// Статистика +router.get('/admin/stats', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + + // Получаем статистику по задачам + const tasksStats = await new Promise((resolve, reject) => { + db.get(` + SELECT + COUNT(*) as totalTasks, + COUNT(CASE WHEN status = 'active' AND closed_at IS NULL THEN 1 END) as activeTasks, + COUNT(CASE WHEN closed_at IS NOT NULL THEN 1 END) as closedTasks, + COUNT(CASE WHEN status = 'deleted' THEN 1 END) as deletedTasks + FROM tasks + `, [], (err, row) => { + if (err) reject(err); + else resolve(row || { totalTasks: 0, activeTasks: 0, closedTasks: 0, deletedTasks: 0 }); + }); + }); + + // Статистика по назначениям + const assignmentsStats = await new Promise((resolve, reject) => { + db.get(` + SELECT + COUNT(*) as totalAssignments, + COUNT(CASE WHEN status = 'assigned' THEN 1 END) as assignedCount, + COUNT(CASE WHEN status = 'in_progress' THEN 1 END) as inProgressCount, + COUNT(CASE WHEN status = 'completed' THEN 1 END) as completedCount, + COUNT(CASE WHEN status = 'overdue' THEN 1 END) as overdueCount, + COUNT(CASE WHEN status = 'rework' THEN 1 END) as reworkCount + FROM task_assignments + `, [], (err, row) => { + if (err) reject(err); + else resolve(row || { totalAssignments: 0, assignedCount: 0, inProgressCount: 0, completedCount: 0, overdueCount: 0, reworkCount: 0 }); + }); + }); + + // Статистика по пользователям + const usersStats = await new Promise((resolve, reject) => { + db.get(` + SELECT + COUNT(*) as totalUsers, + COUNT(CASE WHEN role = 'admin' THEN 1 END) as adminUsers, + COUNT(CASE WHEN role = 'teacher' THEN 1 END) as teacherUsers, + COUNT(CASE WHEN auth_type = 'ldap' THEN 1 END) as ldapUsers, + COUNT(CASE WHEN auth_type = 'local' THEN 1 END) as localUsers + FROM users + `, [], (err, row) => { + if (err) reject(err); + else resolve(row || { totalUsers: 0, adminUsers: 0, teacherUsers: 0, ldapUsers: 0, localUsers: 0 }); + }); + }); + + // Статистика по файлам + const filesStats = await new Promise((resolve, reject) => { + db.get(` + SELECT + COUNT(*) as totalFiles, + SUM(file_size) as totalFilesSize + FROM task_files + `, [], (err, row) => { + if (err) reject(err); + else resolve(row || { totalFiles: 0, totalFilesSize: 0 }); + }); + }); + + res.json({ + ...tasksStats, + ...assignmentsStats, + ...usersStats, + ...filesStats, + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('Ошибка получения статистики:', error); + res.status(500).json({ error: 'Ошибка получения статистики' }); + } +}); + +// Получение всех пользователей +router.get('/admin/users', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + + const query = ` + SELECT id, login, name, email, role, auth_type, groups, description, + created_at, last_login, updated_at + FROM users + ORDER BY name + `; + + db.all(query, [], (err, users) => { + if (err) { + console.error('Ошибка получения пользователей:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + res.json(users); + }); + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Получение конкретного пользователя +router.get('/admin/users/:id', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const userId = req.params.id; + + const query = ` + SELECT id, login, name, email, role, auth_type, groups, description, + created_at, last_login, updated_at + FROM users + WHERE id = ? + `; + + db.get(query, [userId], (err, user) => { + if (err) { + console.error('Ошибка получения пользователя:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + if (!user) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + res.json(user); + }); + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Обновление пользователя +router.put('/admin/users/:id', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const userId = req.params.id; + const { login, name, email, role, auth_type, groups, description } = req.body; + + if (!login || !name || !email) { + return res.status(400).json({ error: 'Заполните обязательные поля' }); + } + + // Проверяем, что пользователь не пытается изменить свою роль на не-админа + if (req.session.user.id == userId && role !== 'admin') { + return res.status(400).json({ error: 'Нельзя снять права администратора у себя' }); + } + + const query = ` + UPDATE users + SET login = ?, name = ?, email = ?, role = ?, auth_type = ?, + groups = ?, description = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `; + + db.run(query, [login, name, email, role, auth_type, groups || '[]', description || '', userId], function(err) { + if (err) { + console.error('Ошибка обновления пользователя:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + res.json({ success: true, message: 'Пользователь обновлен' }); + }); + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Удаление пользователя +router.delete('/admin/users/:id', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const userId = req.params.id; + + // Нельзя удалить самого себя + if (req.session.user.id == userId) { + return res.status(400).json({ error: 'Нельзя удалить самого себя' }); + } + + const query = `DELETE FROM users WHERE id = ?`; + + db.run(query, [userId], function(err) { + if (err) { + console.error('Ошибка удаления пользователя:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + res.json({ success: true, message: 'Пользователь удален' }); + }); + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Получение всех задач (для админа) +router.get('/admin/tasks', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const { + search = '', + status = 'all', + creator = '', + assignee = '', + limit = 100, + offset = 0 + } = req.query; + + let query = ` + SELECT DISTINCT + t.*, + u.name as creator_name, + u.login as creator_login, + ot.title as original_task_title, + ou.name as original_creator_name, + GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids, + GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names + FROM tasks t + LEFT JOIN users u ON t.created_by = u.id + LEFT JOIN tasks ot ON t.original_task_id = ot.id + LEFT JOIN users ou ON ot.created_by = ou.id + LEFT JOIN task_assignments ta ON t.id = ta.task_id + LEFT JOIN users u2 ON ta.user_id = u2.id + WHERE 1=1 + `; + + const params = []; + + if (status !== 'all') { + if (status === 'deleted') { + query += " AND t.status = 'deleted'"; + } else if (status === 'closed') { + query += " AND t.closed_at IS NOT NULL"; + } else if (status === 'active') { + query += " AND t.status = 'active' AND t.closed_at IS NULL"; + } + } + + if (creator) { + query += ` AND t.created_by = ?`; + params.push(creator); + } + + if (assignee) { + query += ` AND ta.user_id = ?`; + params.push(assignee); + } + + if (search) { + query += ` AND (t.title LIKE ? OR t.description LIKE ?)`; + const searchPattern = `%${search}%`; + params.push(searchPattern, searchPattern); + } + + query += " GROUP BY t.id ORDER BY t.created_at DESC LIMIT ? OFFSET ?"; + params.push(parseInt(limit), parseInt(offset)); + + db.all(query, params, (err, tasks) => { + if (err) { + console.error('Ошибка получения задач:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + res.json(tasks); + }); + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Получение всех логов активности +router.get('/admin/activity-logs', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const { + taskId = '', + userId = '', + action = '', + startDate = '', + endDate = '', + limit = 100, + offset = 0 + } = req.query; + + let query = ` + SELECT al.*, u.name as user_name, t.title as task_title + FROM activity_logs al + LEFT JOIN users u ON al.user_id = u.id + LEFT JOIN tasks t ON al.task_id = t.id + WHERE 1=1 + `; + + const params = []; + + if (taskId) { + query += ` AND al.task_id = ?`; + params.push(taskId); + } + + if (userId) { + query += ` AND al.user_id = ?`; + params.push(userId); + } + + if (action) { + query += ` AND al.action = ?`; + params.push(action); + } + + if (startDate) { + query += ` AND al.created_at >= ?`; + params.push(startDate); + } + + if (endDate) { + query += ` AND al.created_at <= ?`; + params.push(endDate); + } + + query += " ORDER BY al.created_at DESC LIMIT ? OFFSET ?"; + params.push(parseInt(limit), parseInt(offset)); + + db.all(query, params, (err, logs) => { + if (err) { + console.error('Ошибка получения логов:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + res.json(logs); + }); + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Получение логов уведомлений +router.get('/admin/notification-logs', requireAdmin, async (req, res) => { + try { + const postgresLogger = require('./postgres'); + + const { + taskId, + status, + startDate, + endDate, + limit = 100, + offset = 0 + } = req.query; + + if (!postgresLogger.pool || !postgresLogger.initialized) { + return res.json({ logs: [], total: 0, limit, offset }); + } + + let query = ` + SELECT * FROM sms_logs + WHERE 1=1 + ${taskId ? 'AND task_id = $1' : ''} + ${status ? `AND status = $${taskId ? 2 : 1}` : ''} + ${startDate ? `AND created_at >= $${taskId ? (status ? 3 : 2) : (status ? 2 : 1)}` : ''} + ${endDate ? `AND created_at <= $${taskId ? (status ? (startDate ? 4 : 3) : (startDate ? 3 : 2)) : (status ? (startDate ? 3 : 2) : (startDate ? 2 : 1))}` : ''} + ORDER BY created_at DESC + LIMIT $${taskId ? (status ? (startDate ? (endDate ? 5 : 4) : (endDate ? 4 : 3)) : (startDate ? (endDate ? 4 : 3) : (endDate ? 3 : 2))) : (status ? (startDate ? (endDate ? 4 : 3) : (endDate ? 3 : 2)) : (startDate ? (endDate ? 3 : 2) : (endDate ? 2 : 1)))} + OFFSET $${taskId ? (status ? (startDate ? (endDate ? 6 : 5) : (endDate ? 5 : 4)) : (startDate ? (endDate ? 5 : 4) : (endDate ? 4 : 3))) : (status ? (startDate ? (endDate ? 5 : 4) : (endDate ? 4 : 3)) : (startDate ? (endDate ? 4 : 3) : (endDate ? 3 : 2)))} + `; + + const params = []; + if (taskId) params.push(taskId); + if (status) params.push(status); + if (startDate) params.push(startDate); + if (endDate) params.push(endDate); + params.push(parseInt(limit), parseInt(offset)); + + const client = await postgresLogger.pool.connect(); + try { + const result = await client.query(query, params); + const logs = result.rows; + + // Получаем общее количество + const countQuery = ` + SELECT COUNT(*) as total FROM sms_logs + WHERE 1=1 + ${taskId ? 'AND task_id = $1' : ''} + ${status ? `AND status = $${taskId ? 2 : 1}` : ''} + ${startDate ? `AND created_at >= $${taskId ? (status ? 3 : 2) : (status ? 2 : 1)}` : ''} + ${endDate ? `AND created_at <= $${taskId ? (status ? (startDate ? 4 : 3) : (startDate ? 3 : 2)) : (status ? (startDate ? 3 : 2) : (startDate ? 2 : 1))}` : ''} + `; + + const countResult = await client.query(countQuery, params.slice(0, -2)); + const total = countResult.rows[0]?.total || 0; + + res.json({ + logs, + total: parseInt(total), + limit: parseInt(limit), + offset: parseInt(offset) + }); + } finally { + client.release(); + } + } catch (error) { + console.error('Ошибка получения логов уведомлений:', error); + res.status(500).json({ error: 'Ошибка получения логов' }); + } +}); + +// Получение детальной статистики +router.get('/admin/detailed-stats', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const { period = 'week' } = req.query; + + let dateFilter = ''; + let dateFormat = ''; + + switch (period) { + case 'day': + dateFilter = "created_at >= DATE('now', '-1 day')"; + dateFormat = "strftime('%H:00', created_at)"; + break; + case 'week': + dateFilter = "created_at >= DATE('now', '-7 days')"; + dateFormat = "strftime('%Y-%m-%d', created_at)"; + break; + case 'month': + dateFilter = "created_at >= DATE('now', '-30 days')"; + dateFormat = "strftime('%Y-%m-%d', created_at)"; + break; + case 'year': + dateFilter = "created_at >= DATE('now', '-365 days')"; + dateFormat = "strftime('%Y-%m', created_at)"; + break; + default: + dateFilter = "created_at >= DATE('now', '-7 days')"; + dateFormat = "strftime('%Y-%m-%d', created_at)"; + } + + // Статистика по задачам по дням + const tasksByDay = await new Promise((resolve, reject) => { + db.all(` + SELECT + ${dateFormat} as date, + COUNT(*) as total, + COUNT(CASE WHEN status = 'active' AND closed_at IS NULL THEN 1 END) as active, + COUNT(CASE WHEN closed_at IS NOT NULL THEN 1 END) as closed, + COUNT(CASE WHEN status = 'deleted' THEN 1 END) as deleted + FROM tasks + WHERE ${dateFilter} + GROUP BY ${dateFormat} + ORDER BY ${dateFormat} + `, [], (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + + // Статистика по назначениям по дням + const assignmentsByDay = await new Promise((resolve, reject) => { + db.all(` + SELECT + ${dateFormat} as date, + COUNT(*) as total, + COUNT(CASE WHEN status = 'assigned' THEN 1 END) as assigned, + COUNT(CASE WHEN status = 'in_progress' THEN 1 END) as in_progress, + COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed, + COUNT(CASE WHEN status = 'overdue' THEN 1 END) as overdue, + COUNT(CASE WHEN status = 'rework' THEN 1 END) as rework + FROM task_assignments + WHERE ${dateFilter} + GROUP BY ${dateFormat} + ORDER BY ${dateFormat} + `, [], (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + + // Статистика по пользователям (активность) + const userActivity = await new Promise((resolve, reject) => { + db.all(` + SELECT + u.name as user_name, + u.login as user_login, + u.role, + u.auth_type, + COUNT(DISTINCT t.id) as tasks_created, + COUNT(DISTINCT ta.id) as tasks_assigned, + COUNT(DISTINCT CASE WHEN ta.status = 'completed' THEN ta.id END) as tasks_completed, + MAX(u.last_login) as last_login + FROM users u + LEFT JOIN tasks t ON u.id = t.created_by + LEFT JOIN task_assignments ta ON u.id = ta.user_id + GROUP BY u.id + ORDER BY tasks_created DESC + `, [], (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + + // Статистика по файлам + const filesStats = await new Promise((resolve, reject) => { + db.all(` + SELECT + ${dateFormat} as date, + COUNT(*) as file_count, + SUM(file_size) as total_size + FROM task_files + WHERE ${dateFilter} + GROUP BY ${dateFormat} + ORDER BY ${dateFormat} + `, [], (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + + res.json({ + period, + tasksByDay, + assignmentsByDay, + userActivity, + filesStats, + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('Ошибка получения детальной статистики:', error); + res.status(500).json({ error: 'Ошибка получения статистики' }); + } +}); + +// Создание нового пользователя (только для админов) +router.post('/admin/users', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const { login, password, name, email, role = 'teacher', auth_type = 'local' } = req.body; + + if (!login || !password || !name || !email) { + return res.status(400).json({ error: 'Заполните все обязательные поля' }); + } + + // Проверяем, существует ли пользователь с таким логином + db.get("SELECT id FROM users WHERE login = ?", [login], async (err, existingUser) => { + if (err) { + console.error('Ошибка проверки пользователя:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + if (existingUser) { + return res.status(400).json({ error: 'Пользователь с таким логином уже существует' }); + } + + // Проверяем, существует ли пользователь с таким email + db.get("SELECT id FROM users WHERE email = ?", [email], async (err, existingEmail) => { + if (err) { + console.error('Ошибка проверки email:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + if (existingEmail) { + return res.status(400).json({ error: 'Пользователь с таким email уже существует' }); + } + + // Хешируем пароль + const bcrypt = require('bcryptjs'); + const hashedPassword = await bcrypt.hash(password, 10); + + // Создаем пользователя + const query = ` + INSERT INTO users (login, password, name, email, role, auth_type, created_at) + VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + `; + + db.run(query, [login, hashedPassword, name, email, role, auth_type], function(err) { + if (err) { + console.error('Ошибка создания пользователя:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + res.json({ + success: true, + message: 'Пользователь создан', + userId: this.lastID + }); + }); + }); + }); + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Сброс пароля пользователя +router.post('/admin/users/:id/reset-password', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const userId = req.params.id; + const { newPassword } = req.body; + + if (!newPassword || newPassword.length < 6) { + return res.status(400).json({ error: 'Пароль должен содержать минимум 6 символов' }); + } + + // Хешируем пароль + const bcrypt = require('bcryptjs'); + const hashedPassword = await bcrypt.hash(newPassword, 10); + + const query = `UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; + + db.run(query, [hashedPassword, userId], function(err) { + if (err) { + console.error('Ошибка сброса пароля:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + res.json({ success: true, message: 'Пароль сброшен' }); + }); + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); +// API для админ Канбан-доски +router.get('/admin/kanban-tasks', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const days = parseInt(req.query.days) || 7; + + const now = new Date(); + const futureDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000); + const futureISO = futureDate.toISOString(); + + const query = ` + SELECT DISTINCT + t.*, + u.name as creator_name, + u.login as creator_login, + GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids, + GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names + FROM tasks t + LEFT JOIN users u ON t.created_by = u.id + LEFT JOIN task_assignments ta ON t.id = ta.task_id + LEFT JOIN users u2 ON ta.user_id = u2.id + WHERE t.status = 'active' + AND t.closed_at IS NULL + AND (t.due_date IS NULL OR t.due_date <= ?) + GROUP BY t.id + ORDER BY t.due_date ASC, t.created_at DESC + `; + + db.all(query, [futureISO], async (err, tasks) => { + if (err) { + console.error('Ошибка получения задач для Канбана:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + // Добавляем статус для Канбана + const tasksWithKanban = await Promise.all(tasks.map(async (task) => { + // Получаем назначения для задачи + const assignments = await new Promise((resolve) => { + db.all(` + SELECT ta.*, u.name as user_name, u.login as user_login + FROM task_assignments ta + LEFT JOIN users u ON ta.user_id = u.id + WHERE ta.task_id = ? + `, [task.id], (err, rows) => { + resolve(rows || []); + }); + }); + + // Определяем статус для Канбана + let kanbanStatus = 'unassigned'; + + if (assignments.length === 0) { + kanbanStatus = 'unassigned'; + } else { + const hasAssigned = assignments.some(a => a.status === 'assigned'); + const hasInProgress = assignments.some(a => a.status === 'in_progress'); + const hasOverdue = assignments.some(a => a.status === 'overdue'); + const hasRework = assignments.some(a => a.status === 'rework'); + const allCompleted = assignments.every(a => a.status === 'completed'); + + if (allCompleted) { + kanbanStatus = 'completed'; + } else if (hasRework) { + kanbanStatus = 'rework'; + } else if (hasOverdue) { + kanbanStatus = 'overdue'; + } else if (hasInProgress) { + kanbanStatus = 'in_progress'; + } else if (hasAssigned) { + kanbanStatus = 'assigned'; + } + } + + return { + ...task, + kanbanStatus, + assignments + }; + })); + + res.json({ tasks: tasksWithKanban }); + }); + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); +// Экспорт данных +router.get('/admin/export', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const { format = 'json', dataType = 'all' } = req.query; + + const exportData = {}; + + if (dataType === 'all' || dataType === 'users') { + const users = await new Promise((resolve, reject) => { + db.all(` + SELECT id, login, name, email, role, auth_type, groups, description, + created_at, last_login, updated_at + FROM users + ORDER BY id + `, [], (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + exportData.users = users; + } + + if (dataType === 'all' || dataType === 'tasks') { + const tasks = await new Promise((resolve, reject) => { + db.all(` + SELECT t.*, u.name as creator_name, u.login as creator_login + FROM tasks t + LEFT JOIN users u ON t.created_by = u.id + ORDER BY t.id + `, [], (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + exportData.tasks = tasks; + } + + if (dataType === 'all' || dataType === 'assignments') { + const assignments = await new Promise((resolve, reject) => { + db.all(` + SELECT ta.*, u.name as user_name, u.login as user_login, t.title as task_title + FROM task_assignments ta + LEFT JOIN users u ON ta.user_id = u.id + LEFT JOIN tasks t ON ta.task_id = t.id + ORDER BY ta.id + `, [], (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + exportData.assignments = assignments; + } + + if (dataType === 'all' || dataType === 'logs') { + const logs = await new Promise((resolve, reject) => { + db.all(` + SELECT al.*, u.name as user_name, t.title as task_title + FROM activity_logs al + LEFT JOIN users u ON al.user_id = u.id + LEFT JOIN tasks t ON al.task_id = t.id + ORDER BY al.id + `, [], (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + exportData.activityLogs = logs; + } + + exportData.exportedAt = new Date().toISOString(); + exportData.system = 'MiniCRM'; + exportData.version = '1.0.0'; + + if (format === 'csv') { + // Простая реализация CSV экспорта (можно расширить) + const csvData = Object.entries(exportData) + .map(([key, value]) => { + if (Array.isArray(value)) { + if (value.length === 0) return `${key}: Нет данных\n`; + const headers = Object.keys(value[0]).join(','); + const rows = value.map(item => + Object.values(item).map(val => + typeof val === 'string' ? `"${val.replace(/"/g, '""')}"` : val + ).join(',') + ).join('\n'); + return `${key}:\n${headers}\n${rows}\n\n`; + } + return `${key}: ${JSON.stringify(value)}\n`; + }) + .join(''); + + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename="minicrm_export_${new Date().toISOString().split('T')[0]}.csv"`); + res.send(csvData); + } else { + res.json(exportData); + } + } catch (error) { + console.error('Ошибка экспорта данных:', error); + res.status(500).json({ error: 'Ошибка экспорта данных' }); + } +}); + +// Получение профилей пользователей с настройками уведомлений +router.get('/admin/user-profiles', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + + const query = ` + SELECT + u.id, + u.login, + u.name, + u.email as user_email, + u.role, + u.auth_type, + u.groups, + u.created_at, + u.last_login, + us.email_notifications, + us.notification_email, + us.telegram_notifications, + us.telegram_chat_id, + us.vk_notifications, + us.vk_user_id, + us.updated_at as settings_updated_at + FROM users u + LEFT JOIN user_settings us ON u.id = us.user_id + ORDER BY u.name + `; + + db.all(query, [], (err, profiles) => { + if (err) { + console.error('Ошибка получения профилей пользователей:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + res.json(profiles); + }); + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Получение конкретного профиля пользователя +router.get('/admin/user-profiles/:id', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const userId = req.params.id; + + const query = ` + SELECT + u.id, + u.login, + u.name, + u.email as user_email, + u.role, + u.auth_type, + u.groups, + u.created_at, + u.last_login, + us.email_notifications, + us.notification_email, + us.telegram_notifications, + us.telegram_chat_id, + us.vk_notifications, + us.vk_user_id, + us.created_at as settings_created_at, + us.updated_at as settings_updated_at + FROM users u + LEFT JOIN user_settings us ON u.id = us.user_id + WHERE u.id = ? + `; + + db.get(query, [userId], (err, profile) => { + if (err) { + console.error('Ошибка получения профиля пользователя:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + if (!profile) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + res.json(profile); + }); + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Обновление настроек уведомлений пользователя (для админа) +router.put('/admin/user-profiles/:id/notification-settings', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const userId = req.params.id; + const { + email_notifications, + notification_email, + telegram_notifications, + telegram_chat_id, + vk_notifications, + vk_user_id + } = req.body; + + // Валидация email + if (email_notifications && notification_email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(notification_email)) { + return res.status(400).json({ error: 'Неверный формат email' }); + } + } + + // Проверяем существование записи + db.get("SELECT id FROM user_settings WHERE user_id = ?", [userId], (err, existing) => { + if (err) { + console.error('Ошибка проверки настроек:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + if (existing) { + // Обновляем существующие настройки + db.run( + `UPDATE user_settings SET + email_notifications = ?, + notification_email = ?, + telegram_notifications = ?, + telegram_chat_id = ?, + vk_notifications = ?, + vk_user_id = ?, + updated_at = CURRENT_TIMESTAMP + WHERE user_id = ?`, + [ + email_notifications ? 1 : 0, + notification_email || '', + telegram_notifications ? 1 : 0, + telegram_chat_id || '', + vk_notifications ? 1 : 0, + vk_user_id || '', + userId + ], + function(updateErr) { + if (updateErr) { + console.error('Ошибка обновления настроек:', updateErr); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + // Логируем действие + const { logActivity } = require('./database'); + if (logActivity) { + logActivity(0, req.session.user.id, 'USER_SETTINGS_UPDATED', `Админ обновил настройки уведомлений пользователя ${userId}`); + } + + console.log(`✅ Админ обновил настройки пользователя ${userId}`); + res.json({ success: true, message: 'Настройки уведомлений обновлены' }); + } + ); + } else { + // Создаем новые настройки + db.run( + `INSERT INTO user_settings + (user_id, email_notifications, notification_email, + telegram_notifications, telegram_chat_id, + vk_notifications, vk_user_id) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + userId, + email_notifications ? 1 : 0, + notification_email || '', + telegram_notifications ? 1 : 0, + telegram_chat_id || '', + vk_notifications ? 1 : 0, + vk_user_id || '' + ], + function(insertErr) { + if (insertErr) { + console.error('Ошибка создания настроек:', insertErr); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + // Логируем действие + const { logActivity } = require('./database'); + if (logActivity) { + logActivity(null, req.session.user.id, 'USER_SETTINGS_CREATED', `Админ создал настройки уведомлений пользователя ${userId}`); + } + + console.log(`✅ Админ создал настройки пользователя ${userId}`); + res.json({ success: true, message: 'Настройки уведомлений созданы' }); + } + ); + } + }); + + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Массовое обновление email уведомлений для нескольких пользователей +router.post('/admin/bulk-email-settings', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const { users, email_notifications, notification_email } = req.body; + + if (!Array.isArray(users) || users.length === 0) { + return res.status(400).json({ error: 'Выберите пользователей' }); + } + + if (email_notifications && !notification_email) { + return res.status(400).json({ error: 'Укажите email для уведомлений' }); + } + + // Валидация email + if (email_notifications && notification_email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(notification_email)) { + return res.status(400).json({ error: 'Неверный формат email' }); + } + } + + const results = { + success: 0, + failed: 0, + errors: [] + }; + + // Обновляем каждого пользователя в транзакции + db.serialize(() => { + db.run("BEGIN TRANSACTION"); + + users.forEach(userId => { + // Проверяем существование записи + db.get("SELECT id FROM user_settings WHERE user_id = ?", [userId], (err, existing) => { + if (err) { + results.failed++; + results.errors.push({ userId, error: err.message }); + return; + } + + if (existing) { + // Обновляем существующие настройки + db.run( + `UPDATE user_settings SET + email_notifications = ?, + notification_email = ?, + updated_at = CURRENT_TIMESTAMP + WHERE user_id = ?`, + [ + email_notifications ? 1 : 0, + email_notifications ? notification_email : '', + userId + ], + function(updateErr) { + if (updateErr) { + results.failed++; + results.errors.push({ userId, error: updateErr.message }); + } else { + results.success++; + } + } + ); + } else { + // Создаем новые настройки с значениями по умолчанию + db.run( + `INSERT INTO user_settings + (user_id, email_notifications, notification_email, + telegram_notifications, telegram_chat_id, + vk_notifications, vk_user_id) + VALUES (?, ?, ?, 0, '', 0, '')`, + [ + userId, + email_notifications ? 1 : 0, + email_notifications ? notification_email : '' + ], + function(insertErr) { + if (insertErr) { + results.failed++; + results.errors.push({ userId, error: insertErr.message }); + } else { + results.success++; + } + } + ); + } + }); + }); + + setTimeout(() => { + db.run("COMMIT", (commitErr) => { + if (commitErr) { + console.error('Ошибка коммита транзакции:', commitErr); + return res.status(500).json({ error: 'Ошибка транзакции' }); + } + + // Логируем действие + const { logActivity } = require('./database'); + if (logActivity) { + logActivity(null, req.session.user.id, 'BULK_SETTINGS_UPDATED', + `Админ массово обновил настройки для ${users.length} пользователей`); + } + + console.log(`✅ Массовое обновление: успешно ${results.success}, ошибок ${results.failed}`); + res.json({ + success: true, + message: 'Настройки обновлены', + results + }); + }); + }, 1000); + }); + + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Отправка тестового уведомления пользователю +router.post('/admin/user-profiles/:id/test-notification', requireAdmin, async (req, res) => { + try { + const { getDb } = require('./database'); + const db = getDb(); + const userId = req.params.id; + const notificationType = req.body.notification_type || 'test'; + + // Получаем информацию о пользователе + db.get(` + SELECT u.id, u.name, u.email, us.email_notifications, us.notification_email + FROM users u + LEFT JOIN user_settings us ON u.id = us.user_id + WHERE u.id = ? + `, [userId], async (err, user) => { + if (err) { + console.error('Ошибка получения пользователя:', err); + return res.status(500).json({ error: 'Ошибка сервера' }); + } + + if (!user) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + // Проверяем, включены ли email уведомления + if (!user.email_notifications) { + return res.status(400).json({ error: 'Email уведомления отключены у пользователя' }); + } + + const emailTo = user.notification_email || user.email; + if (!emailTo) { + return res.status(400).json({ error: 'У пользователя не указан email' }); + } + + // Отправляем тестовое уведомление + const emailNotifications = require('./email-notifications'); + const emailSent = await emailNotifications.sendEmailNotification( + emailTo, + 'Тестовое уведомление от School CRM', + ` + + + + + + +
+
+

✅ Тестовое уведомление

+
+
+

Здравствуйте, ${user.name}!

+

Это тестовое уведомление от системы School CRM.

+

Если вы получили это письмо, значит настройки уведомлений работают корректно.

+
+

Информация о тесте:

+ +
+ +
+ + + ` + ); + + if (emailSent) { + // Логируем действие + const { logActivity } = require('./database'); + if (logActivity) { + logActivity(null, req.session.user.id, 'TEST_NOTIFICATION_SENT', + `Админ отправил тестовое уведомление пользователю ${user.name} (${emailTo})`); + } + + res.json({ + success: true, + message: 'Тестовое уведомление отправлено', + email: emailTo + }); + } else { + res.status(500).json({ error: 'Не удалось отправить уведомление' }); + } + }); + + } catch (error) { + console.error('Ошибка:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/database.js b/database.js index b974f37..486d34d 100644 --- a/database.js +++ b/database.js @@ -195,108 +195,134 @@ function createSQLiteTables() { console.log('✅ Таблица для пользовательских настроек инициализирована'); // Таблица для типов документов -db.run(`CREATE TABLE IF NOT EXISTS document_types ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - description TEXT, - template_path TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -)`); + db.run(`CREATE TABLE IF NOT EXISTS document_types ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + template_path TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`); -// Таблица для документов -db.run(`CREATE TABLE IF NOT EXISTS documents ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - description TEXT, - document_type_id INTEGER NOT NULL, - status TEXT DEFAULT 'draft', - created_by INTEGER NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - approved_at DATETIME, - approved_by INTEGER, - rejected_at DATETIME, - rejected_by INTEGER, - rejection_reason TEXT, - file_path TEXT, - file_name TEXT, - file_size INTEGER, - version INTEGER DEFAULT 1, - parent_document_id INTEGER, - FOREIGN KEY (document_type_id) REFERENCES document_types (id), - FOREIGN KEY (created_by) REFERENCES users (id), - FOREIGN KEY (approved_by) REFERENCES users (id), - FOREIGN KEY (rejected_by) REFERENCES users (id), - FOREIGN KEY (parent_document_id) REFERENCES documents (id) -)`); + // Таблица для документов + db.run(`CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + document_type_id INTEGER NOT NULL, + status TEXT DEFAULT 'draft', + created_by INTEGER NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + approved_at DATETIME, + approved_by INTEGER, + rejected_at DATETIME, + rejected_by INTEGER, + rejection_reason TEXT, + file_path TEXT, + file_name TEXT, + file_size INTEGER, + version INTEGER DEFAULT 1, + parent_document_id INTEGER, + FOREIGN KEY (document_type_id) REFERENCES document_types (id), + FOREIGN KEY (created_by) REFERENCES users (id), + FOREIGN KEY (approved_by) REFERENCES users (id), + FOREIGN KEY (rejected_by) REFERENCES users (id), + FOREIGN KEY (parent_document_id) REFERENCES documents (id) + )`); -// Таблица для этапов согласования -db.run(`CREATE TABLE IF NOT EXISTS approval_stages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - document_type_id INTEGER NOT NULL, - stage_number INTEGER NOT NULL, - stage_name TEXT NOT NULL, - approver_role TEXT, - approver_user_id INTEGER, - is_required BOOLEAN DEFAULT true, - can_edit BOOLEAN DEFAULT false, - can_comment BOOLEAN DEFAULT true, - deadline_days INTEGER, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE(document_type_id, stage_number), - FOREIGN KEY (document_type_id) REFERENCES document_types (id), - FOREIGN KEY (approver_user_id) REFERENCES users (id) -)`); + // Таблица для этапов согласования + db.run(`CREATE TABLE IF NOT EXISTS approval_stages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + document_type_id INTEGER NOT NULL, + stage_number INTEGER NOT NULL, + stage_name TEXT NOT NULL, + approver_role TEXT, + approver_user_id INTEGER, + is_required BOOLEAN DEFAULT true, + can_edit BOOLEAN DEFAULT false, + can_comment BOOLEAN DEFAULT true, + deadline_days INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(document_type_id, stage_number), + FOREIGN KEY (document_type_id) REFERENCES document_types (id), + FOREIGN KEY (approver_user_id) REFERENCES users (id) + )`); -// Таблица для согласования документов -db.run(`CREATE TABLE IF NOT EXISTS document_approvals ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id INTEGER NOT NULL, - stage_id INTEGER NOT NULL, - approver_user_id INTEGER NOT NULL, - status TEXT DEFAULT 'pending', - comments TEXT, - approved_at DATETIME, - rejected_at DATETIME, - deadline DATETIME, - notified_at DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE(document_id, stage_id, approver_user_id), - FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE, - FOREIGN KEY (stage_id) REFERENCES approval_stages (id), - FOREIGN KEY (approver_user_id) REFERENCES users (id) -)`); + // Таблица для согласования документов + db.run(`CREATE TABLE IF NOT EXISTS document_approvals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + document_id INTEGER NOT NULL, + stage_id INTEGER NOT NULL, + approver_user_id INTEGER NOT NULL, + status TEXT DEFAULT 'pending', + comments TEXT, + approved_at DATETIME, + rejected_at DATETIME, + deadline DATETIME, + notified_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(document_id, stage_id, approver_user_id), + FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE, + FOREIGN KEY (stage_id) REFERENCES approval_stages (id), + FOREIGN KEY (approver_user_id) REFERENCES users (id) + )`); -// Таблица для комментариев к документам -db.run(`CREATE TABLE IF NOT EXISTS document_comments ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - comment TEXT NOT NULL, - is_internal BOOLEAN DEFAULT false, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users (id) -)`); + // Таблица для комментариев к документам + db.run(`CREATE TABLE IF NOT EXISTS document_comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + document_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + comment TEXT NOT NULL, + is_internal BOOLEAN DEFAULT false, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) + )`); -// Таблица для истории изменений документов -db.run(`CREATE TABLE IF NOT EXISTS document_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - action TEXT NOT NULL, - changes TEXT, - version INTEGER, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users (id) -)`); + // Таблица для истории изменений документов + db.run(`CREATE TABLE IF NOT EXISTS document_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + document_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + action TEXT NOT NULL, + changes TEXT, + version INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) + )`); -console.log('✅ Таблицы для согласования документов созданы'); + console.log('✅ Таблицы для согласования документов созданы'); + + // ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ГРУПП ПОЛЬЗОВАТЕЛЕЙ ===== + + // Таблица для групп пользователей + db.run(`CREATE TABLE IF NOT EXISTS user_groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + color TEXT DEFAULT '#3498db', + can_approve_documents BOOLEAN DEFAULT false, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`); + + // Таблица для связи пользователей с группами (многие-ко-многим) + db.run(`CREATE TABLE IF NOT EXISTS user_group_memberships ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + group_id INTEGER NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, group_id), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES user_groups (id) ON DELETE CASCADE + )`); + + console.log('✅ Таблицы для групп пользователей созданы'); // Запускаем проверку и обновление структуры таблиц setTimeout(() => { @@ -339,7 +365,11 @@ function checkAndUpdateTableStructure() { { name: 'due_date', type: 'DATETIME' }, { name: 'rework_comment', type: 'TEXT' }, { name: 'closed_at', type: 'DATETIME' }, - { name: 'closed_by', type: 'INTEGER' } + { name: 'closed_by', type: 'INTEGER' }, + // Новые колонки для типа задач + { name: 'task_type', type: 'TEXT DEFAULT "regular"' }, + { name: 'approver_group_id', type: 'INTEGER' }, + { name: 'document_id', type: 'INTEGER' } ], task_assignments: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, @@ -386,6 +416,21 @@ function checkAndUpdateTableStructure() { { name: 'vk_user_id', type: 'TEXT' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } + ], + user_groups: [ + { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, + { name: 'name', type: 'TEXT NOT NULL UNIQUE' }, + { name: 'description', type: 'TEXT' }, + { name: 'color', type: 'TEXT DEFAULT "#3498db"' }, + { name: 'can_approve_documents', type: 'BOOLEAN DEFAULT false' }, + { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, + { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } + ], + user_group_memberships: [ + { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, + { name: 'user_id', type: 'INTEGER NOT NULL' }, + { name: 'group_id', type: 'INTEGER NOT NULL' }, + { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ] }; @@ -447,6 +492,46 @@ function checkAndUpdateTableStructure() { }); } }); + + // Создаем индексы для новых таблиц + setTimeout(() => { + const newIndexes = [ + "CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type)", + "CREATE INDEX IF NOT EXISTS idx_tasks_approver_group_id ON tasks(approver_group_id)", + "CREATE INDEX IF NOT EXISTS idx_user_groups_can_approve ON user_groups(can_approve_documents)", + "CREATE INDEX IF NOT EXISTS idx_user_group_memberships_user_id ON user_group_memberships(user_id)", + "CREATE INDEX IF NOT EXISTS idx_user_group_memberships_group_id ON user_group_memberships(group_id)" + ]; + + newIndexes.forEach(indexQuery => { + db.run(indexQuery, (err) => { + if (err) { + console.error(`❌ Ошибка создания индекса: ${err.message}`); + } else { + console.log(`✅ Индекс создан: ${indexQuery}`); + } + }); + }); + + // Создаем группу "Секретарь" по умолчанию, если её нет + db.get("SELECT id FROM user_groups WHERE name = 'Секретарь'", (err, group) => { + if (err || !group) { + console.log('🔧 Создаем группу "Секретарь" по умолчанию...'); + db.run( + `INSERT INTO user_groups (name, description, color, can_approve_documents) + VALUES ('Секретарь', 'Группа для согласования документов', '#e74c3c', 1)`, + (insertErr) => { + if (insertErr) { + console.error('❌ Ошибка создания группы "Секретарь":', insertErr.message); + } else { + console.log('✅ Группа "Секретарь" создана по умолчанию'); + } + } + ); + } + }); + + }, 1000); }, 1000); } @@ -621,7 +706,10 @@ async function createPostgresTables() { due_date TIMESTAMP, rework_comment TEXT, closed_at TIMESTAMP, - closed_by INTEGER REFERENCES users(id) + closed_by INTEGER REFERENCES users(id), + task_type VARCHAR(50) DEFAULT 'regular', + approver_group_id INTEGER, + document_id INTEGER ) `); @@ -687,6 +775,129 @@ async function createPostgresTables() { ) `); + // ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ГРУПП ПОЛЬЗОВАТЕЛЕЙ ===== + + // Таблица для групп пользователей + await client.query(` + CREATE TABLE IF NOT EXISTS user_groups ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + color VARCHAR(20) DEFAULT '#3498db', + can_approve_documents BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + // Таблица для связи пользователей с группами + await client.query(` + CREATE TABLE IF NOT EXISTS user_group_memberships ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + group_id INTEGER NOT NULL REFERENCES user_groups(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, group_id) + ) + `); + + // Таблицы для документов (существующие) + await client.query(` + CREATE TABLE IF NOT EXISTS document_types ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + template_path TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS documents ( + id SERIAL PRIMARY KEY, + title VARCHAR(500) NOT NULL, + description TEXT, + document_type_id INTEGER NOT NULL REFERENCES document_types(id), + status VARCHAR(50) DEFAULT 'draft', + created_by INTEGER NOT NULL REFERENCES users(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + approved_at TIMESTAMP, + approved_by INTEGER REFERENCES users(id), + rejected_at TIMESTAMP, + rejected_by INTEGER REFERENCES users(id), + rejection_reason TEXT, + file_path TEXT, + file_name TEXT, + file_size BIGINT, + version INTEGER DEFAULT 1, + parent_document_id INTEGER REFERENCES documents(id) + ) + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS approval_stages ( + id SERIAL PRIMARY KEY, + document_type_id INTEGER NOT NULL REFERENCES document_types(id), + stage_number INTEGER NOT NULL, + stage_name VARCHAR(100) NOT NULL, + approver_role VARCHAR(50), + approver_user_id INTEGER REFERENCES users(id), + is_required BOOLEAN DEFAULT true, + can_edit BOOLEAN DEFAULT false, + can_comment BOOLEAN DEFAULT true, + deadline_days INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(document_type_id, stage_number) + ) + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS document_approvals ( + id SERIAL PRIMARY KEY, + document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, + stage_id INTEGER NOT NULL REFERENCES approval_stages(id), + approver_user_id INTEGER NOT NULL REFERENCES users(id), + status VARCHAR(50) DEFAULT 'pending', + comments TEXT, + approved_at TIMESTAMP, + rejected_at TIMESTAMP, + deadline TIMESTAMP, + notified_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(document_id, stage_id, approver_user_id) + ) + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS document_comments ( + id SERIAL PRIMARY KEY, + document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id), + comment TEXT NOT NULL, + is_internal BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS document_history ( + id SERIAL PRIMARY KEY, + document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id), + action VARCHAR(100) NOT NULL, + changes TEXT, + version INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + console.log('✅ Все таблицы PostgreSQL созданы/проверены'); + // Создаем индексы const indexes = [ 'CREATE INDEX IF NOT EXISTS idx_tasks_created_by ON tasks(created_by)', @@ -698,7 +909,18 @@ async function createPostgresTables() { 'CREATE INDEX IF NOT EXISTS idx_task_files_task_id ON task_files(task_id)', 'CREATE INDEX IF NOT EXISTS idx_activity_logs_task_id ON activity_logs(task_id)', 'CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at)', - 'CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)' + 'CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)', + // Новые индексы + 'CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type)', + 'CREATE INDEX IF NOT EXISTS idx_tasks_approver_group_id ON tasks(approver_group_id)', + 'CREATE INDEX IF NOT EXISTS idx_user_groups_can_approve ON user_groups(can_approve_documents)', + 'CREATE INDEX IF NOT EXISTS idx_user_group_memberships_user_id ON user_group_memberships(user_id)', + 'CREATE INDEX IF NOT EXISTS idx_user_group_memberships_group_id ON user_group_memberships(group_id)', + // Индексы для документов + 'CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(status)', + 'CREATE INDEX IF NOT EXISTS idx_documents_created_by ON documents(created_by)', + 'CREATE INDEX IF NOT EXISTS idx_document_approvals_document_id ON document_approvals(document_id)', + 'CREATE INDEX IF NOT EXISTS idx_document_approvals_status ON document_approvals(status)' ]; for (const indexQuery of indexes) { @@ -715,6 +937,20 @@ async function createPostgresTables() { // Проверяем структуру PostgreSQL таблиц await checkPostgresTableStructure(); + // Создаем группу "Секретарь" по умолчанию + try { + const checkResult = await client.query("SELECT id FROM user_groups WHERE name = 'Секретарь'"); + if (checkResult.rows.length === 0) { + await client.query(` + INSERT INTO user_groups (name, description, color, can_approve_documents) + VALUES ('Секретарь', 'Группа для согласования документов', '#e74c3c', true) + `); + console.log('✅ Группа "Секретарь" создана по умолчанию'); + } + } catch (error) { + console.warn('⚠️ Не удалось создать группу "Секретарь":', error.message); + } + } catch (error) { console.error('❌ Ошибка создания таблиц PostgreSQL:', error.message); } @@ -742,6 +978,11 @@ async function checkPostgresTableStructure() { { name: 'vk_user_id', type: 'TEXT' }, { name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }, { name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' } + ], + tasks: [ + { name: 'task_type', type: 'VARCHAR(50) DEFAULT "regular"' }, + { name: 'approver_group_id', type: 'INTEGER' }, + { name: 'document_id', type: 'INTEGER' } ] }; @@ -799,6 +1040,96 @@ async function checkPostgresTableStructure() { } } +// Вспомогательные функции для работы с группами пользователей +function getUserGroups(userId, callback) { + const query = ` + SELECT g.* FROM user_groups g + JOIN user_group_memberships ugm ON g.id = ugm.group_id + WHERE ugm.user_id = ? + ORDER BY g.name + `; + + db.all(query, [userId], (err, groups) => { + if (err) { + console.error('❌ Ошибка получения групп пользователя:', err); + callback(err, []); + } else { + callback(null, groups || []); + } + }); +} + +function getGroupMembers(groupId, callback) { + const query = ` + SELECT u.* FROM users u + JOIN user_group_memberships ugm ON u.id = ugm.user_id + WHERE ugm.group_id = ? + ORDER BY u.name + `; + + db.all(query, [groupId], (err, members) => { + if (err) { + console.error('❌ Ошибка получения участников группы:', err); + callback(err, []); + } else { + callback(null, members || []); + } + }); +} + +function getApproverGroups(callback) { + const query = ` + SELECT g.*, COUNT(DISTINCT ugm.user_id) as member_count + FROM user_groups g + LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id + WHERE g.can_approve_documents = true + GROUP BY g.id + ORDER BY g.name + `; + + db.all(query, [], (err, groups) => { + if (err) { + console.error('❌ Ошибка получения групп согласующих:', err); + callback(err, []); + } else { + callback(null, groups || []); + } + }); +} + +function addUserToGroup(userId, groupId, callback) { + const query = ` + INSERT INTO user_group_memberships (user_id, group_id) + VALUES (?, ?) + ON CONFLICT (user_id, group_id) DO NOTHING + `; + + db.run(query, [userId, groupId], function(err) { + if (err) { + console.error('❌ Ошибка добавления пользователя в группу:', err); + callback(err); + } else { + callback(null, this.changes > 0); + } + }); +} + +function removeUserFromGroup(userId, groupId, callback) { + const query = ` + DELETE FROM user_group_memberships + WHERE user_id = ? AND group_id = ? + `; + + db.run(query, [userId, groupId], function(err) { + if (err) { + console.error('❌ Ошибка удаления пользователя из группы:', err); + callback(err); + } else { + callback(null, this.changes > 0); + } + }); +} + function createTaskFolder(taskId) { const taskFolder = path.join(tasksDir, taskId.toString()); createDirIfNotExists(taskFolder); @@ -938,7 +1269,13 @@ module.exports = { checkTaskAccess, USE_POSTGRES, getDatabaseType: () => USE_POSTGRES ? 'PostgreSQL' : 'SQLite', - checkAndUpdateTableStructure // Экспортируем для ручного запуска + checkAndUpdateTableStructure, // Экспортируем для ручного запуска + // Новые функции для работы с группами + getUserGroups, + getGroupMembers, + getApproverGroups, + addUserToGroup, + removeUserFromGroup }; // Запускаем инициализацию при экспорте (но она завершится позже) diff --git a/group-management.js b/group-management.js new file mode 100644 index 0000000..35eb5f8 --- /dev/null +++ b/group-management.js @@ -0,0 +1,383 @@ +// group-management.js +const express = require('express'); +const router = express.Router(); + +function setupGroupManagement(app, db) { + + // Middleware для проверки администратора + function requireAdmin(req, res, next) { + if (!req.session.user || req.session.user.role !== 'admin') { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + next(); + } + + // API для получения всех групп + router.get('/api/groups', requireAdmin, (req, res) => { + db.all(` + SELECT g.*, + COUNT(DISTINCT ugm.user_id) as member_count + FROM user_groups g + LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id + GROUP BY g.id + ORDER BY g.name + `, [], (err, groups) => { + if (err) { + console.error('❌ Ошибка получения групп:', err); + return res.status(500).json({ error: 'Ошибка получения групп' }); + } + res.json(groups || []); + }); + }); + + // API для получения одной группы с участниками + router.get('/api/groups/:groupId', requireAdmin, (req, res) => { + const { groupId } = req.params; + + db.get(` + SELECT g.*, + COUNT(DISTINCT ugm.user_id) as member_count + FROM user_groups g + LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id + WHERE g.id = ? + GROUP BY g.id + `, [groupId], (err, group) => { + if (err || !group) { + return res.status(404).json({ error: 'Группа не найдена' }); + } + + // Получаем участников группы + db.all(` + SELECT u.id, u.login, u.name, u.email, u.role + FROM users u + JOIN user_group_memberships ugm ON u.id = ugm.user_id + WHERE ugm.group_id = ? + ORDER BY u.name + `, [groupId], (err, members) => { + if (err) { + console.error('❌ Ошибка получения участников:', err); + return res.status(500).json({ error: 'Ошибка получения участников' }); + } + + res.json({ + ...group, + members: members || [] + }); + }); + }); + }); + + // API для создания новой группы + router.post('/api/groups', requireAdmin, (req, res) => { + const { name, description, color, can_approve_documents } = req.body; + + if (!name) { + return res.status(400).json({ error: 'Название группы обязательно' }); + } + + db.run(` + INSERT INTO user_groups (name, description, color, can_approve_documents) + VALUES (?, ?, ?, ?) + `, [ + name.trim(), + description || '', + color || '#3498db', + can_approve_documents ? 1 : 0 + ], function(err) { + if (err) { + console.error('❌ Ошибка создания группы:', err); + return res.status(500).json({ + error: 'Ошибка создания группы', + details: err.message.includes('UNIQUE') ? 'Группа с таким названием уже существует' : err.message + }); + } + + res.json({ + success: true, + groupId: this.lastID, + message: 'Группа создана успешно' + }); + }); + }); + + // API для обновления группы + router.put('/api/groups/:groupId', requireAdmin, (req, res) => { + const { groupId } = req.params; + const { name, description, color, can_approve_documents } = req.body; + + if (!name) { + return res.status(400).json({ error: 'Название группы обязательно' }); + } + + db.run(` + UPDATE user_groups + SET name = ?, description = ?, color = ?, can_approve_documents = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `, [ + name.trim(), + description || '', + color || '#3498db', + can_approve_documents ? 1 : 0, + groupId + ], function(err) { + if (err) { + console.error('❌ Ошибка обновления группы:', err); + return res.status(500).json({ + error: 'Ошибка обновления группы', + details: err.message.includes('UNIQUE') ? 'Группа с таким названием уже существует' : err.message + }); + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Группа не найдена' }); + } + + res.json({ + success: true, + message: 'Группа обновлена успешно' + }); + }); + }); + + // API для удаления группы + router.delete('/api/groups/:groupId', requireAdmin, (req, res) => { + const { groupId } = req.params; + + db.run(`DELETE FROM user_groups WHERE id = ?`, [groupId], function(err) { + if (err) { + console.error('❌ Ошибка удаления группы:', err); + return res.status(500).json({ error: 'Ошибка удаления группы' }); + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Группа не найдена' }); + } + + res.json({ + success: true, + message: 'Группа удалена успешно' + }); + }); + }); + + // API для добавления пользователя в группу + router.post('/api/groups/:groupId/members', requireAdmin, (req, res) => { + const { groupId } = req.params; + const { userId } = req.body; + + if (!userId) { + return res.status(400).json({ error: 'ID пользователя обязательно' }); + } + + // Проверяем существование группы и пользователя + db.get(`SELECT id FROM user_groups WHERE id = ?`, [groupId], (err, group) => { + if (err || !group) { + return res.status(404).json({ error: 'Группа не найдена' }); + } + + db.get(`SELECT id FROM users WHERE id = ?`, [userId], (err, user) => { + if (err || !user) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + db.run(` + INSERT OR IGNORE INTO user_group_memberships (user_id, group_id) + VALUES (?, ?) + `, [userId, groupId], function(err) { + if (err) { + console.error('❌ Ошибка добавления пользователя в группу:', err); + return res.status(500).json({ error: 'Ошибка добавления пользователя в группу' }); + } + + res.json({ + success: true, + message: 'Пользователь добавлен в группу' + }); + }); + }); + }); + }); + + // API для удаления пользователя из группы + router.delete('/api/groups/:groupId/members/:userId', requireAdmin, (req, res) => { + const { groupId, userId } = req.params; + + db.run(` + DELETE FROM user_group_memberships + WHERE user_id = ? AND group_id = ? + `, [userId, groupId], function(err) { + if (err) { + console.error('❌ Ошибка удаления пользователя из группы:', err); + return res.status(500).json({ error: 'Ошибка удаления пользователя из группы' }); + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Пользователь не найден в группе' }); + } + + res.json({ + success: true, + message: 'Пользователь удален из группы' + }); + }); + }); + + // API для получения всех пользователей с их группами + router.get('/api/users-with-groups', requireAdmin, (req, res) => { + db.all(` + SELECT u.id, u.login, u.name, u.email, u.role, + GROUP_CONCAT(DISTINCT g.name) as group_names, + GROUP_CONCAT(DISTINCT g.id) as group_ids + FROM users u + LEFT JOIN user_group_memberships ugm ON u.id = ugm.user_id + LEFT JOIN user_groups g ON ugm.group_id = g.id + WHERE u.role IN ('admin', 'teacher') + GROUP BY u.id + ORDER BY u.name + `, [], (err, users) => { + if (err) { + console.error('❌ Ошибка получения пользователей с группами:', err); + return res.status(500).json({ error: 'Ошибка получения данных' }); + } + + // Преобразуем строки в массивы + const formattedUsers = users.map(user => ({ + ...user, + group_names: user.group_names ? user.group_names.split(',') : [], + group_ids: user.group_ids ? user.group_ids.split(',').map(id => parseInt(id)) : [] + })); + + res.json(formattedUsers); + }); + }); + + // API для получения доступных групп для пользователя + router.get('/api/users/:userId/available-groups', requireAdmin, (req, res) => { + const { userId } = req.params; + + db.all(` + SELECT g.*, + CASE WHEN ugm.user_id IS NOT NULL THEN 1 ELSE 0 END as is_member + FROM user_groups g + LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id AND ugm.user_id = ? + ORDER BY g.name + `, [userId], (err, groups) => { + if (err) { + console.error('❌ Ошибка получения групп:', err); + return res.status(500).json({ error: 'Ошибка получения данных' }); + } + + res.json(groups || []); + }); + }); + + // API для массового обновления групп пользователя + router.put('/api/users/:userId/groups', requireAdmin, (req, res) => { + const { userId } = req.params; + const { groupIds } = req.body; // массив ID групп + + if (!Array.isArray(groupIds)) { + return res.status(400).json({ error: 'groupIds должен быть массивом' }); + } + + // Проверяем существование пользователя + db.get(`SELECT id FROM users WHERE id = ?`, [userId], (err, user) => { + if (err || !user) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + db.serialize(() => { + // Удаляем все текущие группы пользователя + db.run(`DELETE FROM user_group_memberships WHERE user_id = ?`, [userId], (err) => { + if (err) { + console.error('❌ Ошибка удаления старых групп:', err); + return res.status(500).json({ error: 'Ошибка обновления групп' }); + } + + // Добавляем новые группы + if (groupIds.length > 0) { + const placeholders = groupIds.map(() => '(?, ?)').join(','); + const values = groupIds.reduce((acc, groupId) => { + acc.push(userId, groupId); + return acc; + }, []); + + db.run(` + INSERT INTO user_group_memberships (user_id, group_id) + VALUES ${placeholders} + `, values, (err) => { + if (err) { + console.error('❌ Ошибка добавления новых групп:', err); + return res.status(500).json({ error: 'Ошибка обновления групп' }); + } + + res.json({ + success: true, + message: 'Группы пользователя обновлены' + }); + }); + } else { + res.json({ + success: true, + message: 'Группы пользователя обновлены' + }); + } + }); + }); + }); + }); + + // API для получения пользователей, которые могут согласовывать документы + router.get('/api/approvers', requireAdmin, (req, res) => { + db.all(` + SELECT DISTINCT u.id, u.login, u.name, u.email, u.role, + g.name as group_name, g.color as group_color + FROM users u + JOIN user_group_memberships ugm ON u.id = ugm.user_id + JOIN user_groups g ON ugm.group_id = g.id + WHERE g.can_approve_documents = 1 + ORDER BY u.name + `, [], (err, approvers) => { + if (err) { + console.error('❌ Ошибка получения согласующих:', err); + return res.status(500).json({ error: 'Ошибка получения данных' }); + } + + res.json(approvers || []); + }); + }); + + // API для получения групп, которые могут согласовывать документы + router.get('/api/approver-groups', (req, res) => { + const isAdmin = req.session.user && req.session.user.role === 'admin'; + + db.all(` + SELECT g.*, + COUNT(DISTINCT ugm.user_id) as member_count + FROM user_groups g + LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id + WHERE g.can_approve_documents = 1 + GROUP BY g.id + ORDER BY g.name + `, [], (err, groups) => { + if (err) { + console.error('❌ Ошибка получения групп согласующих:', err); + return res.status(500).json({ error: 'Ошибка получения данных' }); + } + + // Для обычных пользователей скрываем ID пользователей в группах + if (!isAdmin) { + groups = groups.map(group => ({ + ...group, + members: undefined + })); + } + + res.json(groups || []); + }); + }); + + return router; +} + +module.exports = { setupGroupManagement }; \ No newline at end of file diff --git a/public/admin-groups.html b/public/admin-groups.html new file mode 100644 index 0000000..84f295b --- /dev/null +++ b/public/admin-groups.html @@ -0,0 +1,543 @@ + + + + + + Управление группами пользователей + + + + + + + +
+
+
+
+
+
Управление группами пользователей
+ +
+
+
+ + + + + + + + + + + + + + +
НазваниеОписаниеУчастниковМожет согласовыватьЦветДействия
+
+
+
+
+
+ +
+
+
+
+
Пользователи и их группы
+
+
+
+ + + + + + + + + + + + +
ПользовательРольГруппыДействия
+
+
+
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/public/admin.html b/public/admin.html index e467589..48e2611 100644 --- a/public/admin.html +++ b/public/admin.html @@ -272,6 +272,7 @@
+
diff --git a/server.js b/server.js index 22db637..d9d360b 100644 --- a/server.js +++ b/server.js @@ -782,7 +782,13 @@ app.get('/api/email-health', requireAuth, async (req, res) => { res.status(500).json({ error: error.message }); } }); - +// Страница управления группами +app.get('/admin/groups', (req, res) => { + if (!req.session.user || req.session.user.role !== 'admin') { + return res.status(403).send('Доступ запрещен'); + } + res.sendFile(path.join(__dirname, 'public/admin-groups.html')); +}); // Инициализация сервера async function initializeServer() { console.log('🚀 Инициализация сервера...'); diff --git a/task-endpoints.js b/task-endpoints.js index 0b8a332..5281bd2 100644 --- a/task-endpoints.js +++ b/task-endpoints.js @@ -2,6 +2,19 @@ const path = require('path'); const fs = require('fs'); +function getApproverUsers(groupId) { + return new Promise((resolve) => { + db.all(` + SELECT u.id, u.login, u.name, u.email + FROM users u + JOIN user_group_memberships ugm ON u.id = ugm.user_id + WHERE ugm.group_id = ? + `, [groupId], (err, users) => { + resolve(err ? [] : users); + }); + }); +} + function setupTaskEndpoints(app, db, upload) { const { logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database'); const { sendTaskNotifications } = require('./notifications'); @@ -444,7 +457,180 @@ function setupTaskEndpoints(app, db, upload) { }); }); }); +// API для создания задачи согласования документов +app.post('/api/document-approval-tasks', requireAuth, upload.array('files', 15), (req, res) => { + const { title, description, approverGroupId, documentId, dueDate } = req.body; + const createdBy = req.session.user.id; + if (!title) { + return res.status(400).json({ error: 'Название задачи обязательно' }); + } + + if (!approverGroupId) { + return res.status(400).json({ error: 'Выберите группу для согласования' }); + } + + if (!dueDate) { + return res.status(400).json({ error: 'Дата и время выполнения обязательны' }); + } + + // Проверяем, может ли группа согласовывать документы + db.get("SELECT can_approve_documents FROM user_groups WHERE id = ?", + [approverGroupId], (err, group) => { + if (err || !group || !group.can_approve_documents) { + return res.status(400).json({ error: 'Выбранная группа не может согласовывать документы' }); + } + + db.serialize(async () => { + const startDate = new Date().toISOString(); + + // Создаем задачу с типом "document_approval" + db.run( + `INSERT INTO tasks (title, description, created_by, task_type, + approver_group_id, document_id, start_date, due_date) + VALUES (?, ?, ?, 'document_approval', ?, ?, ?, ?)`, + [title, description, createdBy, approverGroupId, documentId || null, startDate, dueDate || null], + async function(err) { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + const taskId = this.lastID; + + // Сохраняем метаданные + saveTaskMetadata(taskId, title, description, createdBy, null, startDate, dueDate); + + // Получаем всех пользователей из группы согласующих + const approvers = await getApproverUsers(approverGroupId); + + // Назначаем задачу всем согласующим в группе + if (approvers.length > 0) { + approvers.forEach(approver => { + db.run( + "INSERT INTO task_assignments (task_id, user_id, status, start_date, due_date) VALUES (?, ?, 'assigned', ?, ?)", + [taskId, approver.id, startDate, dueDate || null] + ); + }); + + // Отправляем уведомления + sendTaskNotifications('created', taskId, title, description, createdBy); + } + + logActivity(taskId, createdBy, 'DOCUMENT_APPROVAL_TASK_CREATED', + `Создана задача согласования документа для группы ${approverGroupId}`); + + // Загрузка файлов (если есть) + if (req.files && req.files.length > 0) { + const userFolder = createUserTaskFolder(taskId, req.session.user.login); + + req.files.forEach(file => { + const newPath = path.join(userFolder, path.basename(file.filename)); + fs.renameSync(file.path, newPath); + + const originalName = file.originalname; + + db.run( + "INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)", + [taskId, createdBy, path.basename(file.filename), originalName, newPath, file.size] + ); + + logActivity(taskId, createdBy, 'FILE_UPLOADED', `Загружен файл: ${originalName}`); + }); + } + + res.json({ + success: true, + taskId: taskId, + message: 'Задача согласования документа создана', + approversCount: approvers.length + }); + } + ); + }); + }); +}); + +// API для получения задач согласования документов +app.get('/api/document-approval-tasks', requireAuth, (req, res) => { + const userId = req.session.user.id; + + const query = ` + SELECT DISTINCT + t.*, + u.name as creator_name, + u.login as creator_login, + g.name as approver_group_name, + g.color as approver_group_color, + GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids, + GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names + FROM tasks t + LEFT JOIN users u ON t.created_by = u.id + LEFT JOIN user_groups g ON t.approver_group_id = g.id + LEFT JOIN task_assignments ta ON t.id = ta.task_id + LEFT JOIN users u2 ON ta.user_id = u2.id + WHERE t.task_type = 'document_approval' + AND t.status = 'active' + AND t.closed_at IS NULL + `; + + const params = []; + + if (req.session.user.role !== 'admin') { + query += ` AND (t.created_by = ? OR ta.user_id = ?)`; + params.push(userId, userId); + } + + query += " GROUP BY t.id ORDER BY t.created_at DESC"; + + db.all(query, params, (err, tasks) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + const taskPromises = tasks.map(task => { + return new Promise((resolve) => { + // Получаем назначения + db.all(` + SELECT ta.*, u.name as user_name, u.login as user_login + FROM task_assignments ta + LEFT JOIN users u ON ta.user_id = u.id + WHERE ta.task_id = ? + `, [task.id], (err, assignments) => { + if (err) { + task.assignments = []; + } else { + assignments.forEach(assignment => { + if (checkIfOverdue(assignment.due_date, assignment.status) && assignment.status !== 'completed') { + assignment.status = 'overdue'; + } + }); + task.assignments = assignments || []; + } + + // Получаем информацию о группе согласующих + if (task.approver_group_id) { + db.get(` + SELECT name, color, can_approve_documents + FROM user_groups + WHERE id = ? + `, [task.approver_group_id], (err, group) => { + task.approver_group = group || null; + resolve(task); + }); + } else { + resolve(task); + } + }); + }); + }); + + Promise.all(taskPromises).then(completedTasks => { + res.json(completedTasks); + }); + }); +}); // API для обновления статуса в Канбане app.put('/api/kanban-tasks/:taskId/status', requireAuth, (req, res) => { const { taskId } = req.params; @@ -1163,4 +1349,4 @@ function setupTaskEndpoints(app, db, upload) { }); } -module.exports = { setupTaskEndpoints }; \ No newline at end of file +module.exports = { setupTaskEndpoints,getApproverUsers }; \ No newline at end of file