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.
+
Если вы получили это письмо, значит настройки уведомлений работают корректно.
+
+
Информация о тесте:
+
+ - Получатель: ${emailTo}
+ - Отправитель: Администратор
+ - Время: ${new Date().toLocaleString('ru-RU')}
+
+
+
+
+
+
+ `
+ );
+
+ 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