// task-endpoints.js 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'); function checkIfOverdue(dueDate, status) { if (!dueDate || status === 'completed') return false; const now = new Date(); const due = new Date(dueDate); return due < now; } // Функция проверки прав редактирования function canUserEditTask(task, user) { if (!user) return false; // Администратор может всё if (user.role === 'admin') return true; // Создатель может редактировать свою задачу только если она не назначена другим if (parseInt(task.created_by) === user.id) { if (task.assignments && task.assignments.length > 0) { // Проверяем, назначена ли задача другим пользователям const assignedToOthers = task.assignments.some(assignment => parseInt(assignment.user_id) !== user.id ); if (assignedToOthers) { // Создатель не может редактировать задачу, назначенную другим return false; } } return true; } return false; } // Middleware для аутентификации function requireAuth(req, res, next) { if (!req.session.user) { return res.status(401).json({ error: 'Требуется аутентификация' }); } next(); } // API для задач app.get('/api/tasks', requireAuth, (req, res) => { const userId = req.session.user.id; const showDeleted = req.session.user.role === 'admin' && req.query.showDeleted === 'true'; const search = req.query.search || ''; const statusFilter = req.query.status || 'active,in_progress,assigned,overdue,rework'; const creatorFilter = req.query.creator || ''; const assigneeFilter = req.query.assignee || ''; const deadlineFilter = req.query.deadline || ''; 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 (req.session.user.role !== 'admin') { query += ` AND (t.created_by = ? OR ta.user_id = ?)`; params.push(userId, userId); } if (!showDeleted) { query += " AND t.status = 'active'"; } if (statusFilter && statusFilter !== 'all') { const statuses = statusFilter.split(','); if (statuses.includes('closed')) { if (req.session.user.role !== 'admin') { query += ` AND (t.closed_at IS NOT NULL AND t.created_by = ?)`; params.push(userId); } else { query += ` AND t.closed_at IS NOT NULL`; } } else { query += ` AND t.closed_at IS NULL`; if (statuses.length > 0 && !statuses.includes('all')) { query += ` AND EXISTS ( SELECT 1 FROM task_assignments ta2 WHERE ta2.task_id = t.id AND ta2.status IN (${statuses.map(() => '?').join(',')}) )`; statuses.forEach(status => params.push(status)); } } } else { if (req.session.user.role !== 'admin') { query += ` AND (t.closed_at IS NULL OR t.created_by = ?)`; params.push(userId); } } if (creatorFilter) { query += ` AND t.created_by = ?`; params.push(creatorFilter); } if (assigneeFilter) { query += ` AND ta.user_id = ?`; params.push(assigneeFilter); } if (deadlineFilter) { const now = new Date(); let hours = 48; if (deadlineFilter === '24h') hours = 24; const deadlineTime = new Date(now.getTime() + hours * 60 * 60 * 1000); const deadlineISO = deadlineTime.toISOString(); const nowISO = now.toISOString(); query += ` AND ta.due_date IS NOT NULL AND ta.due_date > ? AND ta.due_date <= ? AND ta.status NOT IN ('completed', 'overdue')`; params.push(nowISO, deadlineISO); } 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"; 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 = []; resolve(task); return; } assignments.forEach(assignment => { if (checkIfOverdue(assignment.due_date, assignment.status) && assignment.status !== 'completed') { assignment.status = 'overdue'; } }); task.assignments = assignments || []; resolve(task); }); }); }); Promise.all(taskPromises).then(completedTasks => { res.json(completedTasks); }); }); }); app.get('/api/tasks/no-date', requireAuth, (req, res) => { const userId = req.session.user.id; const 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 t.status = 'active' AND t.closed_at IS NULL AND (t.due_date IS NULL OR t.due_date = '') AND (ta.due_date IS NULL OR ta.due_date = '') `; 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 = []; resolve(task); return; } task.assignments = assignments || []; resolve(task); }); }); }); Promise.all(taskPromises).then(completedTasks => { res.json(completedTasks); }); }); }); // API для Канбан-доски с фильтром app.get('/api/kanban-tasks', requireAuth, (req, res) => { const userId = req.session.user.id; const days = parseInt(req.query.days) || 7; const filter = req.query.filter || 'all'; // 'all', 'created', 'assigned' const now = new Date(); const futureDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000); const futureISO = futureDate.toISOString(); let 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 <= ?) `; const params = [futureISO]; // Применяем фильтр if (filter === 'created') { // Задачи, которые я создал query += ` AND t.created_by = ?`; params.push(userId); } else if (filter === 'assigned') { // Задачи, которые мне назначены query += ` AND ta.user_id = ?`; params.push(userId); } else { // Все задачи (по умолчанию) query += ` AND (t.created_by = ? OR ta.user_id = ?)`; params.push(userId, userId); } query += " GROUP BY t.id ORDER BY t.due_date ASC, t.created_at DESC"; db.all(query, params, async (err, tasks) => { if (err) { res.status(500).json({ error: err.message }); return; } // Добавляем статус для Канбана и загружаем файлы 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'; } } // Загружаем файлы const files = await new Promise((resolve) => { db.all(` SELECT tf.*, u.name as user_name FROM task_files tf LEFT JOIN users u ON tf.user_id = u.id WHERE tf.task_id = ? ORDER BY tf.uploaded_at DESC `, [task.id], (err, rows) => { resolve(rows || []); }); }); // Определяем роль пользователя в задаче let userRole = 'observer'; if (parseInt(task.created_by) === userId) { userRole = 'creator'; } else if (assignments.some(a => parseInt(a.user_id) === userId)) { userRole = 'assignee'; } return { ...task, kanbanStatus, assignments, files, userRole, canEdit: canUserEditTask(task, req.session.user) }; })); res.json({ tasks: tasksWithKanban, filter: filter, userRole: req.session.user.role }); }); }); // API для статистики Канбана app.get('/api/kanban-stats', requireAuth, (req, res) => { const userId = req.session.user.id; const filter = req.query.filter || 'all'; const now = new Date(); const futureDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); const futureISO = futureDate.toISOString(); let query = ` SELECT COUNT(DISTINCT t.id) as total_tasks, COUNT(DISTINCT CASE WHEN ta.status = 'assigned' THEN t.id END) as assigned_count, COUNT(DISTINCT CASE WHEN ta.status = 'in_progress' THEN t.id END) as in_progress_count, COUNT(DISTINCT CASE WHEN ta.status = 'rework' THEN t.id END) as rework_count, COUNT(DISTINCT CASE WHEN ta.status = 'overdue' THEN t.id END) as overdue_count, COUNT(DISTINCT CASE WHEN ta.status = 'completed' THEN t.id END) as completed_count, COUNT(DISTINCT CASE WHEN ta.user_id IS NULL THEN t.id END) as unassigned_count, COUNT(DISTINCT CASE WHEN t.created_by = ? THEN t.id END) as created_by_me, COUNT(DISTINCT CASE WHEN ta.user_id = ? THEN t.id END) as assigned_to_me FROM tasks t LEFT JOIN task_assignments ta ON t.id = ta.task_id AND ta.user_id = ? WHERE t.status = 'active' AND t.closed_at IS NULL AND (t.due_date IS NULL OR t.due_date <= ?) `; const params = [userId, userId, userId, futureISO]; // Применяем фильтр if (filter === 'created') { query += ` AND t.created_by = ?`; params.push(userId); } else if (filter === 'assigned') { query += ` AND ta.user_id = ?`; params.push(userId); } else { query += ` AND (t.created_by = ? OR ta.user_id = ?)`; params.push(userId, userId); } db.get(query, params, (err, stats) => { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ stats: stats || { total_tasks: 0, assigned_count: 0, in_progress_count: 0, rework_count: 0, overdue_count: 0, completed_count: 0, unassigned_count: 0, created_by_me: 0, assigned_to_me: 0 }, filter: filter, timestamp: new Date().toISOString() }); }); }); // 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; const { status } = req.body; const currentUserId = req.session.user.id; if (!status) { return res.status(400).json({ error: 'Статус обязателен' }); } // Проверяем, что пользователь назначен на задачу db.get("SELECT 1 FROM task_assignments WHERE task_id = ? AND user_id = ?", [taskId, currentUserId], (err, assignment) => { if (err || !assignment) { return res.status(403).json({ error: 'Вы не назначены на эту задачу' }); } // Определяем реальный статус из канбан-статуса let actualStatus = status; if (status === 'unassigned') { actualStatus = 'assigned'; // Нельзя вернуть в не назначенные } db.run( "UPDATE task_assignments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?", [actualStatus, taskId, currentUserId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { return res.status(404).json({ error: 'Назначение не найдено' }); } logActivity(taskId, currentUserId, 'STATUS_CHANGED', `Статус изменен на: ${actualStatus} через Канбан`); // Отправляем уведомление db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => { if (!err && taskData) { sendTaskNotifications( 'status_changed', taskId, taskData.title, taskData.description, currentUserId, '', actualStatus, req.session.user.name ); } }); res.json({ success: true, message: 'Статус обновлен' }); } ); }); }); app.post('/api/tasks', requireAuth, upload.array('files', 15), (req, res) => { const { title, description, assignedUsers, originalTaskId, dueDate } = req.body; const createdBy = req.session.user.id; if (!title) { return res.status(400).json({ error: 'Название задачи обязательно' }); } if (!dueDate) { return res.status(400).json({ error: 'Дата и время выполнения обязательны' }); } db.serialize(() => { const startDate = new Date().toISOString(); db.run( "INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date) VALUES (?, ?, ?, ?, ?, ?)", [title, description, createdBy, originalTaskId || null, startDate, dueDate || null], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } const taskId = this.lastID; saveTaskMetadata(taskId, title, description, createdBy, originalTaskId, startDate, dueDate); const action = originalTaskId ? 'TASK_COPIED' : 'TASK_CREATED'; const details = originalTaskId ? `Создана копия задачи: ${title}` : `Создана задача: ${title}`; logActivity(taskId, createdBy, action, details); 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}`); }); const tempDir = path.join(__dirname, 'data', 'uploads', 'temp'); if (fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true, force: true }); } } if (assignedUsers) { const userIds = Array.isArray(assignedUsers) ? assignedUsers : [assignedUsers]; userIds.forEach(userId => { db.run( "INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)", [taskId, userId, startDate, dueDate || null] ); logActivity(taskId, createdBy, 'TASK_ASSIGNED', `Задача назначена пользователю ${userId}`); }); sendTaskNotifications('created', taskId, title, description, createdBy); } res.json({ success: true, taskId: taskId, message: originalTaskId ? 'Копия задачи создана' : 'Задача успешно создана' }); } ); }); }); app.post('/api/tasks/:taskId/copy', requireAuth, (req, res) => { const { taskId } = req.params; const { assignedUsers, dueDate } = req.body; const createdBy = req.session.user.id; if (!dueDate) { return res.status(400).json({ error: 'Дата и время выполнения обязательны для копии задачи' }); } checkTaskAccess(createdBy, taskId, (err, hasAccess) => { if (err || !hasAccess) { return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' }); } db.serialize(() => { db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, originalTask) => { if (err || !originalTask) { return res.status(404).json({ error: 'Оригинальная задача не найдена' }); } const newTitle = `Копия: ${originalTask.title}`; const startDate = new Date().toISOString(); db.run( "INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date) VALUES (?, ?, ?, ?, ?, ?)", [newTitle, originalTask.description, createdBy, taskId, startDate, dueDate || null], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } const newTaskId = this.lastID; saveTaskMetadata(newTaskId, newTitle, originalTask.description, createdBy, taskId, startDate, dueDate); logActivity(newTaskId, createdBy, 'TASK_COPIED', `Создана копия задачи: ${newTitle}`); db.all("SELECT * FROM task_files WHERE task_id = ?", [taskId], (err, originalFiles) => { if (!err && originalFiles && originalFiles.length > 0) { originalFiles.forEach(originalFile => { const originalFilePath = originalFile.file_path; const newFilename = Date.now() + '-' + Math.round(Math.random() * 1E9) + path.extname(originalFile.original_name); const userFolder = createUserTaskFolder(newTaskId, req.session.user.login); const newFilePath = path.join(userFolder, newFilename); if (fs.existsSync(originalFilePath)) { fs.copyFileSync(originalFilePath, newFilePath); db.run( "INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)", [newTaskId, createdBy, newFilename, originalFile.original_name, newFilePath, originalFile.file_size] ); logActivity(newTaskId, createdBy, 'FILE_COPIED', `Скопирован файл: ${originalFile.original_name}`); } }); } }); if (assignedUsers && assignedUsers.length > 0) { assignedUsers.forEach(userId => { db.run( "INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)", [newTaskId, userId, startDate, dueDate || null] ); }); logActivity(newTaskId, createdBy, 'TASK_ASSIGNED', `Задача назначена пользователям: ${assignedUsers.join(', ')}`); sendTaskNotifications('created', newTaskId, newTitle, originalTask.description, createdBy); } res.json({ success: true, taskId: newTaskId, message: 'Копия задачи успешно создана' }); } ); }); }); }); }); app.put('/api/tasks/:taskId', requireAuth, upload.array('files', 15), (req, res) => { const { taskId } = req.params; const { title, description, assignedUsers, dueDate } = req.body; const userId = req.session.user.id; if (!title) { return res.status(400).json({ error: 'Название задачи обязательно' }); } if (!dueDate) { return res.status(400).json({ error: 'Дата и время выполнения обязательны' }); } db.get("SELECT created_by, status, closed_at FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } // Проверяем права на редактирование if (!canUserEditTask(task, req.session.user)) { return res.status(403).json({ error: 'У вас нет прав для редактирования этой задачи' }); } db.serialize(() => { db.run( "UPDATE tasks SET title = ?, description = ?, due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [title, description, dueDate || null, taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } updateTaskMetadata(taskId, { title, description, due_date: dueDate }); logActivity(taskId, userId, 'TASK_UPDATED', `Задача обновлена: ${title}`); 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, userId, path.basename(file.filename), originalName, newPath, file.size] ); logActivity(taskId, userId, 'FILE_UPLOADED', `Загружен файл: ${originalName}`); }); const tempDir = path.join(__dirname, 'data', 'uploads', 'temp'); if (fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true, force: true }); } } if (assignedUsers) { db.run("DELETE FROM task_assignments WHERE task_id = ?", [taskId], (err) => { if (err) { console.error('❌ Ошибка удаления старых назначений:', err); } const userIds = Array.isArray(assignedUsers) ? assignedUsers : [assignedUsers]; userIds.forEach(userId => { const startDate = new Date().toISOString(); db.run( "INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)", [taskId, userId, startDate, dueDate || null] ); }); logActivity(taskId, userId, 'TASK_ASSIGNMENTS_UPDATED', `Назначения обновлены`); sendTaskNotifications('updated', taskId, title, description, userId); }); } else { sendTaskNotifications('updated', taskId, title, description, userId); } res.json({ success: true, message: 'Задача обновлена' }); } ); }); }); }); app.post('/api/tasks/:taskId/rework', requireAuth, (req, res) => { const { taskId } = req.params; const { comment } = req.body; const userId = req.session.user.id; db.get("SELECT created_by, status FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } // Проверяем права на возврат на доработку if (!canUserEditTask(task, req.session.user)) { return res.status(403).json({ error: 'У вас нет прав для возврата задачи на доработку' }); } db.serialize(() => { db.run( "UPDATE tasks SET rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [comment || 'Требуется доработка', taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } db.run( "UPDATE task_assignments SET status = 'rework', rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ?", [comment || 'Требуется доработка', taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } logActivity(taskId, userId, 'TASK_SENT_FOR_REWORK', `Задача возвращена на доработку: ${comment}`); db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => { if (!err && taskData) { sendTaskNotifications('rework', taskId, taskData.title, taskData.description, userId, comment); } }); res.json({ success: true, message: 'Задача возвращена на доработку' }); } ); } ); }); }); }); app.post('/api/tasks/:taskId/close', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } // Создатель и администратор могут закрывать задачу if (req.session.user.role !== 'admin' && task.created_by !== userId) { return res.status(403).json({ error: 'У вас нет прав для закрытия этой задачи' }); } db.run( "UPDATE tasks SET closed_at = CURRENT_TIMESTAMP, closed_by = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [userId, taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } logActivity(taskId, userId, 'TASK_CLOSED', `Задача закрыта`); db.get("SELECT title FROM tasks WHERE id = ?", [taskId], (err, taskData) => { if (!err && taskData) { sendTaskNotifications('closed', taskId, taskData.title, '', userId); } }); res.json({ success: true, message: 'Задача закрыта' }); } ); }); }); app.post('/api/tasks/:taskId/reopen', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } // Создатель и администратор могут открывать задачу if (req.session.user.role !== 'admin' && task.created_by !== userId) { return res.status(403).json({ error: 'У вас нет прав для открытия этой задачи' }); } db.run( "UPDATE tasks SET closed_at = NULL, closed_by = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } logActivity(taskId, userId, 'TASK_REOPENED', `Задача открыта`); res.json({ success: true, message: 'Задача открыта' }); } ); }); }); app.put('/api/tasks/:taskId/assignment/:userId', requireAuth, (req, res) => { const { taskId, userId } = req.params; const { dueDate } = req.body; const currentUserId = req.session.user.id; if (!dueDate) { return res.status(400).json({ error: 'Дата и время выполнения обязательны' }); } db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } // Проверяем права на изменение сроков if (!canUserEditTask(task, req.session.user)) { return res.status(403).json({ error: 'У вас нет прав для редактирования сроков' }); } db.run( "UPDATE task_assignments SET due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?", [dueDate || null, taskId, userId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { return res.status(404).json({ error: 'Назначение не найдено' }); } logActivity(taskId, currentUserId, 'ASSIGNMENT_UPDATED', `Обновлены сроки для пользователя ${userId}`); db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => { if (!err && taskData) { sendTaskNotifications('updated', taskId, taskData.title, taskData.description, currentUserId); } }); res.json({ success: true, message: 'Сроки обновлены' }); } ); }); }); app.delete('/api/tasks/:taskId', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; db.get("SELECT created_by, status FROM tasks WHERE id = ? AND status = 'active'", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } // Проверяем права на удаление if (!canUserEditTask(task, req.session.user)) { return res.status(403).json({ error: 'У вас нет прав для удаления этой задачи' }); } db.run( "UPDATE tasks SET status = 'deleted', deleted_at = CURRENT_TIMESTAMP, deleted_by = ? WHERE id = ?", [userId, taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } updateTaskMetadata(taskId, { status: 'deleted', deleted_at: new Date().toISOString(), deleted_by: userId }); logActivity(taskId, userId, 'TASK_DELETED', `Задача помечена как удаленная`); res.json({ success: true, message: 'Задача удалена' }); } ); }); }); app.post('/api/tasks/:taskId/restore', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; if (req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } db.run( "UPDATE tasks SET status = 'active', deleted_at = NULL, deleted_by = NULL WHERE id = ?", [taskId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { return res.status(404).json({ error: 'Задача не найдена' }); } updateTaskMetadata(taskId, { status: 'active', deleted_at: null, deleted_by: null }); logActivity(taskId, userId, 'TASK_RESTORED', `Задача восстановлена`); res.json({ success: true, message: 'Задача восстановлена' }); } ); }); app.put('/api/tasks/:taskId/status', requireAuth, (req, res) => { const { taskId } = req.params; const { userId: targetUserId, status } = req.body; const currentUserId = req.session.user.id; if (parseInt(targetUserId) !== currentUserId) { return res.status(403).json({ error: 'Недостаточно прав' }); } if (!targetUserId || !status) { return res.status(400).json({ error: 'userId и status обязательны' }); } db.get("SELECT 1 FROM task_assignments WHERE task_id = ? AND user_id = ?", [taskId, currentUserId], (err, assignment) => { if (err || !assignment) { return res.status(403).json({ error: 'Вы не назначены на эту задачу' }); } db.get(` SELECT t.title, t.description, u.name as user_name FROM tasks t LEFT JOIN users u ON u.id = ? WHERE t.id = ? `, [currentUserId, taskId], (err, taskData) => { if (err) { console.error('❌ Ошибка получения данных задачи:', err); } const finalStatus = status === 'completed' ? 'completed' : status; db.run( "UPDATE task_assignments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?", [finalStatus, taskId, targetUserId], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } if (this.changes === 0) { res.status(404).json({ error: 'Назначение не найдено' }); return; } logActivity(taskId, targetUserId, 'STATUS_CHANGED', `Статус изменен на: ${finalStatus}`); if (taskData) { sendTaskNotifications( 'status_changed', taskId, taskData.title, taskData.description, currentUserId, '', finalStatus, taskData.user_name || req.session.user.name ); } res.json({ success: true, message: 'Статус обновлен' }); } ); }); }); }); app.post('/api/tasks/:taskId/files', requireAuth, upload.array('files', 15), (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; if (!req.files || req.files.length === 0) { return res.status(400).json({ error: 'Нет файлов для загрузки' }); } db.get("SELECT created_by, status, closed_at FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } // Проверяем права на загрузку файлов if (!canUserEditTask(task, req.session.user)) { return res.status(403).json({ error: 'У вас нет прав для загрузки файлов в эту задачу' }); } 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, userId, path.basename(file.filename), originalName, newPath, file.size] ); logActivity(taskId, userId, 'FILE_UPLOADED', `Загружен файл: ${originalName}`); }); res.json({ success: true, message: 'Файлы успешно загружены' }); }); }); // API для получения одной задачи app.get('/api/tasks/:taskId', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; checkTaskAccess(userId, taskId, (err, hasAccess) => { if (err || !hasAccess) { return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' }); } const query = ` SELECT t.*, u.name as creator_name, u.login as creator_login, ot.title as original_task_title, ou.name as original_creator_name 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 WHERE t.id = ? `; db.get(query, [taskId], async (err, task) => { if (err || !task) { return res.status(404).json({ error: 'Задача не найдена' }); } // Получаем назначения 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 = ? `, [taskId], (err, rows) => { resolve(rows || []); }); }); // Получаем файлы const files = await new Promise((resolve) => { db.all(` SELECT tf.*, u.name as user_name FROM task_files tf LEFT JOIN users u ON tf.user_id = u.id WHERE tf.task_id = ? ORDER BY tf.uploaded_at DESC `, [taskId], (err, rows) => { resolve(rows || []); }); }); task.assignments = assignments; task.files = files; task.canEdit = canUserEditTask(task, req.session.user); res.json(task); }); }); }); } module.exports = { setupTaskEndpoints,getApproverUsers };