diff --git a/api-doc.js b/api-doc.js new file mode 100644 index 0000000..e1324f9 --- /dev/null +++ b/api-doc.js @@ -0,0 +1,921 @@ +// api-doc.js - API endpoints для согласования документов +const path = require('path'); +const fs = require('fs'); +const archiver = require('archiver'); + +module.exports = function(app, db, upload) { + // API для типов документов + app.get('/api/document-types', requireAuth, (req, res) => { + db.all("SELECT * FROM document_types ORDER BY name", [], (err, rows) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + res.json(rows); + }); + }); + + // API для создания документа + app.post('/api/documents', requireAuth, upload.array('files', 15), async (req, res) => { + try { + const userId = req.session.user.id; + const { + title, + description, + dueDate, + documentTypeId, + documentNumber, + documentDate, + pagesCount, + urgencyLevel, + comment + } = req.body; + + // Валидация + if (!title || title.trim() === '') { + return res.status(400).json({ error: 'Название документа обязательно' }); + } + + // Получаем пользователей для согласования из .env + const doc1User = process.env.DOC1_USER; + const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; + + if (!doc1User) { + return res.status(400).json({ error: 'Не настроен DOC1_USER в системе' }); + } + + // Находим пользователя DOC1 + db.get("SELECT id FROM users WHERE login = ?", [doc1User], async (err, doc1) => { + if (err || !doc1) { + return res.status(400).json({ error: `Пользователь DOC1 (${doc1User}) не найден` }); + } + + // Создаем задачу для документа + db.run(` + INSERT INTO tasks (title, description, due_date, created_by, status, created_at) + VALUES (?, ?, ?, ?, 'active', datetime('now')) + `, [ + `Документ: ${title}`, + description || '', + dueDate || null, + userId + ], function(err) { + if (err) { + return res.status(500).json({ error: err.message }); + } + + const taskId = this.lastID; + + // Создаем запись документа + db.run(` + INSERT INTO documents ( + task_id, document_type_id, document_number, + document_date, pages_count, urgency_level, comment, + created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now')) + `, [ + taskId, + documentTypeId || null, + documentNumber || null, + documentDate || null, + pagesCount || null, + urgencyLevel || 'normal', + comment || null + ], function(err) { + if (err) { + console.error('❌ Ошибка создания документа:', err); + db.run("DELETE FROM tasks WHERE id = ?", [taskId]); + return res.status(500).json({ error: 'Ошибка создания записи документа' }); + } + + const documentId = this.lastID; + + // Назначаем DOC1 для предварительного согласования + db.run(` + INSERT INTO task_assignments (task_id, user_id, status, created_at) + VALUES (?, ?, 'assigned', datetime('now')) + `, [taskId, doc1.id], function(err) { + if (err) { + console.error('❌ Ошибка назначения DOC1:', err); + db.run("DELETE FROM documents WHERE id = ?", [documentId]); + db.run("DELETE FROM tasks WHERE id = ?", [taskId]); + return res.status(500).json({ error: 'Ошибка назначения документа на согласование' }); + } + + const doc1AssignmentId = this.lastID; + + // Загружаем файлы если есть + if (req.files && req.files.length > 0) { + const uploadPromises = req.files.map(file => { + return new Promise((resolve, reject) => { + const filePath = file.path; + const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8'); + + db.run(` + INSERT INTO task_files (task_id, user_id, file_path, original_name, file_size, uploaded_at) + VALUES (?, ?, ?, ?, ?, datetime('now')) + `, [taskId, userId, filePath, originalName, file.size], function(err) { + if (err) { + console.error('❌ Ошибка сохранения файла:', err); + reject(err); + } else { + resolve(); + } + }); + }); + }); + + Promise.all(uploadPromises) + .then(() => { + // Логируем создание документа + const { logActivity } = require('./database'); + logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ: ${title}`); + + // Отправляем уведомление DOC1 + sendDocumentNotification(doc1.id, taskId, 'new_document', title); + + res.json({ + success: true, + message: 'Документ успешно создан и отправлен на предварительное согласование' + }); + }) + .catch(error => { + // Все равно возвращаем успех, так как документ создан + const { logActivity } = require('./database'); + logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ: ${title}`); + + // Отправляем уведомление DOC1 + sendDocumentNotification(doc1.id, taskId, 'new_document', title); + + res.json({ + success: true, + message: 'Документ создан, но были проблемы с загрузкой файлов' + }); + }); + } else { + // Логируем создание документа + const { logActivity } = require('./database'); + logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ: ${title}`); + + // Отправляем уведомление DOC1 + sendDocumentNotification(doc1.id, taskId, 'new_document', title); + + res.json({ + success: true, + message: 'Документ успешно создан и отправлен на предварительное согласование' + }); + } + }); + }); + }); + }); + } catch (error) { + console.error('❌ Общая ошибка создания документа:', error); + res.status(500).json({ + error: 'Ошибка создания документа', + details: error.message + }); + } + }); + + // Получение моих документов + app.get('/api/documents/my', requireAuth, (req, res) => { + const userId = req.session.user.id; + + db.all(` + SELECT + t.id, + t.title, + t.description, + t.due_date, + t.created_at, + t.status, + t.closed_at, + d.id as document_id, + d.document_type_id, + dt.name as document_type_name, + d.document_number, + d.document_date, + d.pages_count, + d.urgency_level, + d.comment, + d.refusal_reason, + ta.status as assignment_status + FROM tasks t + LEFT JOIN documents d ON t.id = d.task_id + LEFT JOIN document_types dt ON d.document_type_id = dt.id + LEFT JOIN task_assignments ta ON t.id = ta.task_id + WHERE t.created_by = ? + AND t.title LIKE 'Документ:%' + ORDER BY t.created_at DESC + `, [userId], async (err, tasks) => { + if (err) { + return res.status(500).json({ error: err.message }); + } + + // Загружаем файлы для каждой задачи + const tasksWithFiles = await Promise.all(tasks.map(async (task) => { + try { + const files = await new Promise((resolve, reject) => { + 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) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + task.files = files || []; + } catch (error) { + task.files = []; + } + return task; + })); + + res.json(tasksWithFiles); + }); + }); + + // Получение документов для согласования (DOC1 и DOC2) + app.get('/api/documents/approval', requireAuth, (req, res) => { + const userId = req.session.user.id; + + // Проверяем, является ли пользователь DOC1 или DOC2 + db.get("SELECT login FROM users WHERE id = ?", [userId], (err, user) => { + if (err || !user) { + return res.status(403).json({ error: 'Пользователь не найден' }); + } + + const userLogin = user.login; + const doc1User = process.env.DOC1_USER; + const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; + + const isDoc1 = userLogin === doc1User; + const isDoc2 = doc2Users.includes(userLogin); + + if (!isDoc1 && !isDoc2) { + return res.status(403).json({ error: 'Недостаточно прав для согласования документов' }); + } + + // Определяем статус для фильтрации + let statusFilter = ''; + if (isDoc1) { + // DOC1 видит документы на предварительном согласовании + statusFilter = "AND ta.status IN ('assigned', 'pre_approved')"; + } else if (isDoc2) { + // DOC2 видит предварительно согласованные документы + statusFilter = "AND ta.status = 'pre_approved'"; + } + + db.all(` + SELECT + t.id, + t.title, + t.description, + t.due_date, + t.created_at, + d.id as document_id, + d.document_type_id, + dt.name as document_type_name, + d.document_number, + d.document_date, + d.pages_count, + d.urgency_level, + d.comment, + d.refusal_reason, + ta.status as assignment_status, + u.name as creator_name, + u.login as creator_login + FROM tasks t + JOIN documents d ON t.id = d.task_id + LEFT JOIN document_types dt ON d.document_type_id = dt.id + JOIN task_assignments ta ON t.id = ta.task_id + JOIN users u ON t.created_by = u.id + WHERE ta.user_id = ? + AND t.title LIKE 'Документ:%' + AND t.status = 'active' + AND t.closed_at IS NULL + ${statusFilter} + ORDER BY + CASE d.urgency_level + WHEN 'very_urgent' THEN 1 + WHEN 'urgent' THEN 2 + ELSE 3 + END, + t.due_date ASC, + t.created_at DESC + `, [userId], async (err, tasks) => { + if (err) { + return res.status(500).json({ error: err.message }); + } + + // Загружаем файлы для каждой задачи + const tasksWithFiles = await Promise.all(tasks.map(async (task) => { + try { + const files = await new Promise((resolve, reject) => { + 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) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + task.files = files || []; + } catch (error) { + task.files = []; + } + return task; + })); + + res.json(tasksWithFiles); + }); + }); + }); + + // Обновление статуса документа + app.put('/api/documents/:id/status', requireAuth, (req, res) => { + const documentId = req.params.id; + const { status, comment, refusalReason } = req.body; + const userId = req.session.user.id; + + // Проверяем права пользователя + db.get("SELECT login FROM users WHERE id = ?", [userId], (err, user) => { + if (err || !user) { + return res.status(403).json({ error: 'Пользователь не найден' }); + } + + const userLogin = user.login; + const doc1User = process.env.DOC1_USER; + const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; + + const isDoc1 = userLogin === doc1User; + const isDoc2 = doc2Users.includes(userLogin); + + if (!isDoc1 && !isDoc2) { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + + // Проверяем текущий статус документа + db.get(` + SELECT d.*, ta.status as assignment_status, ta.user_id as assignee_id, + t.title, t.created_by + FROM documents d + JOIN tasks t ON d.task_id = t.id + JOIN task_assignments ta ON t.id = ta.task_id + WHERE d.id = ? AND ta.user_id = ? + `, [documentId, userId], (err, document) => { + if (err || !document) { + return res.status(404).json({ error: 'Документ не найден или у вас нет прав' }); + } + + // Валидация переходов статусов + if (isDoc1) { + // DOC1 может только предварительно согласовать или отказать + if (status !== 'pre_approved' && status !== 'refused') { + return res.status(400).json({ error: 'DOC1 может только предварительно согласовать или отказать' }); + } + + if (status === 'pre_approved') { + // Если DOC1 предварительно согласовал, назначаем DOC2 + updateDoc1Status(); + } else { + // Если отказал, просто обновляем статус + updateStatus(); + } + } else if (isDoc2) { + // DOC2 может согласовать или отказать только предварительно согласованные документы + if (document.assignment_status !== 'pre_approved') { + return res.status(400).json({ error: 'DOC2 может работать только с предварительно согласованными документами' }); + } + + if (status === 'approved' || status === 'refused') { + updateStatus(); + } else { + return res.status(400).json({ error: 'DOC2 может только согласовать или отказать' }); + } + } + + function updateDoc1Status() { + // Обновляем статус DOC1 + db.run("UPDATE task_assignments SET status = ? WHERE task_id = ? AND user_id = ?", + [status, document.task_id, userId], function(err) { + if (err) { + return res.status(500).json({ error: err.message }); + } + + // Назначаем DOC2 пользователям + const doc2Logins = doc2Users; + + // Находим ID пользователей DOC2 + const placeholders = doc2Logins.map(() => '?').join(','); + db.all(`SELECT id FROM users WHERE login IN (${placeholders})`, doc2Logins, (err, doc2UsersList) => { + if (err || doc2UsersList.length === 0) { + console.error('DOC2 пользователи не найдены'); + // Все равно считаем успехом + return afterAssignments(); + } + + // Создаем задания для каждого DOC2 + const assignmentPromises = doc2UsersList.map(doc2User => { + return new Promise((resolve, reject) => { + db.run(` + INSERT INTO task_assignments (task_id, user_id, status, created_at) + VALUES (?, ?, 'assigned', datetime('now')) + `, [document.task_id, doc2User.id], function(err) { + if (err) reject(err); + else resolve(); + }); + }); + }); + + Promise.all(assignmentPromises) + .then(() => { + afterAssignments(); + }) + .catch(error => { + console.error('Ошибка назначения DOC2:', error); + afterAssignments(); // Все равно продолжаем + }); + }); + + function afterAssignments() { + // Логируем действие + const { logActivity } = require('./database'); + logActivity(document.task_id, userId, 'STATUS_CHANGED', + `Документ предварительно согласован. Назначен DOC2: ${doc2Logins.join(', ')}`); + + // Сохраняем комментарий + if (comment) { + db.run("UPDATE documents SET comment = COALESCE(comment, '') || '\n' || ? WHERE id = ?", + [`DOC1 (${new Date().toLocaleString('ru-RU')}): ${comment}`, documentId]); + } + + // Отправляем уведомление создателю + sendDocumentNotification(document.created_by, document.task_id, 'pre_approved', document.title); + + // Отправляем уведомления всем DOC2 + doc2Users.forEach(login => { + db.get("SELECT id FROM users WHERE login = ?", [login], (err, doc2User) => { + if (!err && doc2User) { + sendDocumentNotification(doc2User.id, document.task_id, 'new_document_for_doc2', document.title); + } + }); + }); + + res.json({ success: true }); + } + } + ); + } + + function updateStatus() { + // Обновляем статус + db.run("UPDATE task_assignments SET status = ? WHERE task_id = ? AND user_id = ?", + [status, document.task_id, userId], function(err) { + if (err) { + return res.status(500).json({ error: err.message }); + } + + // Сохраняем комментарий и причину отказа + if (comment) { + const role = isDoc1 ? 'DOC1' : 'DOC2'; + const timestamp = new Date().toLocaleString('ru-RU'); + db.run("UPDATE documents SET comment = COALESCE(comment, '') || '\n' || ? WHERE id = ?", + [`${role} (${timestamp}): ${comment}`, documentId]); + } + + if (status === 'refused' && refusalReason) { + db.run("UPDATE documents SET refusal_reason = ? WHERE id = ?", + [refusalReason, documentId]); + } + + // Логируем действие + const { logActivity } = require('./database'); + const actionText = isDoc1 ? + `DOC1: ${status === 'refused' ? 'Отказано' : 'Предварительно согласовано'}` : + `DOC2: ${status === 'refused' ? 'Отказано' : 'Согласовано'}`; + logActivity(document.task_id, userId, 'STATUS_CHANGED', actionText); + + // Отправляем уведомление создателю + if (status === 'approved') { + sendDocumentNotification(document.created_by, document.task_id, 'approved', document.title); + } else if (status === 'refused') { + sendDocumentNotification(document.created_by, document.task_id, 'refused', document.title); + } + + res.json({ success: true }); + } + ); + } + }); + }); + }); + + // Отзыв документа + app.post('/api/documents/:id/cancel', requireAuth, (req, res) => { + const documentId = req.params.id; + const userId = req.session.user.id; + + db.get("SELECT task_id FROM documents WHERE id = ?", [documentId], (err, document) => { + if (err || !document) { + return res.status(404).json({ error: 'Документ не найден' }); + } + + const taskId = document.task_id; + + // Проверяем, что пользователь создатель задачи + db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => { + if (err || !task) { + return res.status(404).json({ error: 'Задача не найдена' }); + } + + if (parseInt(task.created_by) !== parseInt(userId)) { + return res.status(403).json({ error: 'Вы не являетесь создателем этого документа' }); + } + + // Получаем всех согласующих для уведомлений + db.all("SELECT user_id FROM task_assignments WHERE task_id = ?", [taskId], (err, assignees) => { + if (err) { + console.error('Ошибка получения согласующих:', err); + } + + // Обновляем статус задачи + db.run("UPDATE tasks SET status = 'cancelled', closed_at = datetime('now') WHERE id = ?", [taskId], function(err) { + if (err) { + return res.status(500).json({ error: err.message }); + } + + // Логируем действие + const { logActivity } = require('./database'); + logActivity(taskId, userId, 'STATUS_CHANGED', 'Документ отозван создателем'); + + // Отправляем уведомления согласующим + if (assignees) { + assignees.forEach(assignee => { + if (assignee.user_id !== userId) { + sendDocumentNotification(assignee.user_id, taskId, 'cancelled', 'Документ отозван'); + } + }); + } + + res.json({ success: true }); + }); + }); + }); + }); + }); + + // Получение пакета документов (ZIP архив) + app.get('/api/documents/:id/package', requireAuth, async (req, res) => { + const documentId = req.params.id; + const userId = req.session.user.id; + + // Проверяем доступ к документу + db.get(` + SELECT t.id, t.created_by, t.title, d.document_number + FROM documents d + JOIN tasks t ON d.task_id = t.id + WHERE d.id = ? + `, [documentId], async (err, result) => { + if (err || !result) { + return res.status(404).json({ error: 'Документ не найден' }); + } + + // Проверяем, что пользователь имеет доступ + const isCreator = parseInt(result.created_by) === parseInt(userId); + const isDoc1orDoc2 = await checkIfDoc1orDoc2(userId); + + if (!isCreator && !isDoc1orDoc2) { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + + try { + // Получаем все файлы документа + db.all(` + SELECT tf.* + FROM task_files tf + JOIN tasks t ON tf.task_id = t.id + JOIN documents d ON t.id = d.task_id + WHERE d.id = ? + `, [documentId], async (err, files) => { + if (err) { + return res.status(500).json({ error: err.message }); + } + + if (files.length === 0) { + return res.json({ + success: true, + message: 'Нет файлов для скачивания' + }); + } + + // Создаем временный файл для архива + const tempDir = path.join(__dirname, 'temp'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + const zipFileName = `document_${documentId}_${Date.now()}.zip`; + const zipFilePath = path.join(tempDir, zipFileName); + + const output = fs.createWriteStream(zipFilePath); + const archive = archiver('zip', { + zlib: { level: 9 } + }); + + output.on('close', () => { + // Отправляем файл + res.download(zipFilePath, `Документ_${result.document_number || result.id}.zip`, (err) => { + // Удаляем временный файл после отправки + if (fs.existsSync(zipFilePath)) { + fs.unlinkSync(zipFilePath); + } + }); + }); + + archive.on('error', (err) => { + console.error('Ошибка создания архива:', err); + res.status(500).json({ error: 'Ошибка создания архива' }); + }); + + archive.pipe(output); + + // Добавляем файлы в архив + for (const file of files) { + if (fs.existsSync(file.file_path)) { + const fileName = path.basename(file.original_name); + archive.file(file.file_path, { name: fileName }); + } + } + + // Добавляем информацию о документе как текстовый файл + const docInfo = ` +Документ: ${result.title} +Номер документа: ${result.document_number || 'Не указан'} +Дата создания: ${new Date().toLocaleString('ru-RU')} + +Файлы в архиве: +${files.map((f, i) => `${i + 1}. ${f.original_name} (${formatFileSize(f.file_size)})`).join('\n')} + `; + + archive.append(docInfo, { name: 'Информация_о_документе.txt' }); + + await archive.finalize(); + }); + } catch (error) { + console.error('Ошибка создания пакета:', error); + res.status(500).json({ + success: false, + message: 'Ошибка создания пакета документов' + }); + } + }); + }); + + // Статистика по документам + app.get('/api/documents/stats', requireAuth, (req, res) => { + const userId = req.session.user.id; + + // Проверяем права + db.get("SELECT login FROM users WHERE id = ?", [userId], (err, user) => { + if (err || !user) { + return res.status(403).json({ error: 'Пользователь не найден' }); + } + + const userLogin = user.login; + const doc1User = process.env.DOC1_USER; + const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; + + const isDoc1 = userLogin === doc1User; + const isDoc2 = doc2Users.includes(userLogin); + const isAdmin = req.session.user.role === 'admin'; + + if (!isDoc1 && !isDoc2 && !isAdmin) { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + + let statsQuery = ''; + let params = []; + + if (isAdmin) { + // Админ видит все документы + statsQuery = ` + SELECT + COUNT(*) as total, + COUNT(CASE WHEN t.status = 'active' AND t.closed_at IS NULL THEN 1 END) as active, + COUNT(CASE WHEN ta.status = 'pre_approved' THEN 1 END) as pre_approved, + COUNT(CASE WHEN ta.status = 'approved' THEN 1 END) as approved, + COUNT(CASE WHEN ta.status = 'refused' THEN 1 END) as refused, + COUNT(CASE WHEN t.status = 'cancelled' THEN 1 END) as cancelled + FROM tasks t + LEFT JOIN documents d ON t.id = d.task_id + LEFT JOIN task_assignments ta ON t.id = ta.task_id + WHERE t.title LIKE 'Документ:%' + `; + } else { + // DOC1 и DOC2 видят только свои документы + statsQuery = ` + SELECT + COUNT(*) as total, + COUNT(CASE WHEN ta.status = 'assigned' THEN 1 END) as assigned, + COUNT(CASE WHEN ta.status = 'pre_approved' THEN 1 END) as pre_approved, + COUNT(CASE WHEN ta.status = 'approved' THEN 1 END) as approved, + COUNT(CASE WHEN ta.status = 'refused' THEN 1 END) as refused + FROM tasks t + JOIN documents d ON t.id = d.task_id + JOIN task_assignments ta ON t.id = ta.task_id + WHERE t.title LIKE 'Документ:%' + AND t.status = 'active' + AND t.closed_at IS NULL + AND ta.user_id = ? + `; + params = [userId]; + } + + db.get(statsQuery, params, (err, stats) => { + if (err) { + return res.status(500).json({ error: err.message }); + } + + res.json(stats || { + total: 0, + active: 0, + pre_approved: 0, + approved: 0, + refused: 0, + cancelled: 0 + }); + }); + }); + }); + + // Получение истории документа + app.get('/api/documents/:id/history', requireAuth, (req, res) => { + const documentId = req.params.id; + const userId = req.session.user.id; + + // Проверяем доступ к документу + db.get(` + SELECT t.created_by + FROM documents d + JOIN tasks t ON d.task_id = t.id + WHERE d.id = ? + `, [documentId], (err, document) => { + if (err || !document) { + return res.status(404).json({ error: 'Документ не найден' }); + } + + // Проверяем права + const isCreator = parseInt(document.created_by) === parseInt(userId); + const isDoc1orDoc2 = checkIfDoc1orDoc2Sync(userId); + + if (!isCreator && !isDoc1orDoc2) { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + + const taskIdQuery = "SELECT task_id FROM documents WHERE id = ?"; + db.get(taskIdQuery, [documentId], (err, result) => { + if (err || !result) { + return res.status(500).json({ error: 'Ошибка получения истории' }); + } + + const taskId = result.task_id; + + // Получаем историю активности + db.all(` + SELECT al.*, u.name as user_name + FROM activity_logs al + LEFT JOIN users u ON al.user_id = u.id + WHERE al.task_id = ? + ORDER BY al.created_at DESC + `, [taskId], (err, history) => { + if (err) { + return res.status(500).json({ error: err.message }); + } + + // Получаем комментарии из документа + db.get("SELECT comment FROM documents WHERE id = ?", [documentId], (err, doc) => { + if (err) { + doc = { comment: '' }; + } + + const comments = doc.comment ? doc.comment.split('\n').filter(c => c.trim()).map(c => { + return { + text: c, + type: 'comment' + }; + }) : []; + + res.json({ + activity: history || [], + comments: comments + }); + }); + }); + }); + }); + }); + + // Вспомогательные функции + + // Функция для проверки DOC1/DOC2 (асинхронная) + function checkIfDoc1orDoc2(userId) { + return new Promise((resolve, reject) => { + db.get("SELECT login FROM users WHERE id = ?", [userId], (err, user) => { + if (err || !user) { + resolve(false); + return; + } + + const userLogin = user.login; + const doc1User = process.env.DOC1_USER; + const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; + + const isDoc1 = userLogin === doc1User; + const isDoc2 = doc2Users.includes(userLogin); + + resolve(isDoc1 || isDoc2); + }); + }); + } + + // Функция для проверки DOC1/DOC2 (синхронная версия) + function checkIfDoc1orDoc2Sync(userId) { + // Эта функция используется в синхронных контекстах + // В реальном приложении нужно быть осторожным с синхронными вызовами + try { + const user = db.getSync("SELECT login FROM users WHERE id = ?", [userId]); + if (!user) return false; + + const userLogin = user.login; + const doc1User = process.env.DOC1_USER; + const doc2Users = process.env.DOC2_USERS ? process.env.DOC2_USERS.split(',') : []; + + const isDoc1 = userLogin === doc1User; + const isDoc2 = doc2Users.includes(userLogin); + + return isDoc1 || isDoc2; + } catch (error) { + return false; + } + } + + // Функция для отправки уведомлений о документах + function sendDocumentNotification(userId, taskId, type, documentTitle) { + try { + const { sendTaskNotifications } = require('./notifications'); + + let message = ''; + switch(type) { + case 'new_document': + message = `Новый документ на согласование: ${documentTitle}`; + break; + case 'new_document_for_doc2': + message = `Новый документ для согласования (DOC2): ${documentTitle}`; + break; + case 'pre_approved': + message = `Документ предварительно согласован: ${documentTitle}`; + break; + case 'approved': + message = `Документ согласован: ${documentTitle}`; + break; + case 'refused': + message = `В согласовании документа отказано: ${documentTitle}`; + break; + case 'cancelled': + message = `Документ отозван создателем: ${documentTitle}`; + break; + default: + message = `Обновление документа: ${documentTitle}`; + } + + sendTaskNotifications(taskId, userId, message); + } catch (error) { + console.error('Ошибка отправки уведомления:', error); + } + } + + // Вспомогательная функция для форматирования размера файла + function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + // Middleware для аутентификации (можно импортировать из server.js) + function requireAuth(req, res, next) { + if (!req.session.user) { + return res.status(401).json({ error: 'Требуется аутентификация' }); + } + next(); + } +}; \ No newline at end of file diff --git a/database.js b/database.js index 9de8bdc..428fe0a 100644 --- a/database.js +++ b/database.js @@ -2,6 +2,7 @@ const sqlite3 = require('sqlite3').verbose(); const { Pool } = require('pg'); const path = require('path'); const fs = require('fs'); +const initDocTables = require('./init-doc-tables'); require('dotenv').config(); // Определяем, какую базу использовать @@ -71,12 +72,49 @@ async function initializeDatabase() { await initializeSQLite(); } + // Инициализируем таблицы для документов (после создания основных таблиц) + try { + await initDocTables(db); + } catch (error) { + console.error('⚠️ Ошибка инициализации таблиц документов:', error.message); + } + // Синхронизируем группы пользователей await syncUserGroups(); return db; } +function initializeSQLite() { + return new Promise((resolve, reject) => { + db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error('❌ Ошибка подключения к SQLite:', err.message); + reject(err); + return; + } else { + console.log('✅ Подключение к SQLite установлено'); + console.log('📁 База данных расположена:', dbPath); + + // Используем serialize для последовательного выполнения + db.serialize(() => { + // Создаем основные таблицы + createSQLiteTables(); + + // Инициализируем таблицы для документов + initDocTables(db); + + // Добавляем группы по умолчанию + addDefaultGroups(); + + isInitialized = true; + resolve(db); + }); + } + }); + }); +} + function initializeSQLite() { return new Promise((resolve, reject) => { db = new sqlite3.Database(dbPath, (err) => { diff --git a/init-doc-tables.js b/init-doc-tables.js new file mode 100644 index 0000000..24cf7bc --- /dev/null +++ b/init-doc-tables.js @@ -0,0 +1,52 @@ +// init-doc-tables.js - Инициализация таблиц для документов +module.exports = function initDocTables(db) { + console.log('🔧 Инициализация таблиц для документов...'); + + // Создание таблицы типов документов + db.run(` + CREATE TABLE IF NOT EXISTS document_types ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + // Создание таблицы документов + db.run(` + CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id INTEGER NOT NULL, + document_type_id INTEGER, + document_number TEXT, + document_date DATE, + pages_count INTEGER, + urgency_level TEXT DEFAULT 'normal', + comment TEXT, + refusal_reason TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY (document_type_id) REFERENCES document_types(id) + ) + `); + + // Добавляем тестовые типы документов + const docTypes = [ + ['Приказ', 'Распорядительный документ'], + ['Распоряжение', 'Распорядительный документ'], + ['Письмо', 'Деловое письмо'], + ['Служебная записка', 'Внутренний документ'], + ['Договор', 'Юридический документ'], + ['Акт', 'Документ о выполнении работ'], + ['Протокол', 'Документ о проведении собрания'] + ]; + + docTypes.forEach(([name, description]) => { + db.run( + "INSERT OR IGNORE INTO document_types (name, description) VALUES (?, ?)", + [name, description] + ); + }); + + console.log('✅ Таблицы для документов инициализированы'); +}; \ No newline at end of file diff --git a/package.json b/package.json index 4d74cec..88648e7 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "nodemon server.js" }, "dependencies": { + "archiver": "^7.0.1", "bcryptjs": "~2.4.3", "dotenv": "~16.3.1", "express": "^4.21.2", diff --git a/public/doc.html b/public/doc.html index 7fdecb4..62a832e 100644 --- a/public/doc.html +++ b/public/doc.html @@ -3,281 +3,225 @@ - School CRM - Управление согласованиями DOC + Согласование документов + - - -
+
-

School CRM - Управление согласованиями DOC

+

Согласование документов

-
-
-

Все согласования

-
-
- -
- -
-
-
- -
-

Создать новое согласование DOC

-
-
- - + +
+

Создать новый документ

+ +
+
+ + +
+ +
+ + +
- +
- -
- - + +
+
+ + +
- +
- - - Автоматически будет назначено всем пользователям с ролью "Секретарь" - + +
- +
- +
- + +
+

Документ будет отправлен на согласование секретарю

+
+
- -
-

Лог активности

-
+ + +
+
+

Мои документы

+
+
+ + +
+
+ + +
+
+
+
+
Загрузка документов...
+
+
+ + +
+
+

Документы на согласование

+
+
+ + +
+
+
+
+
Загрузка документов...
+
- - - - - - - - - - -
-
-

Канбан-доска согласований

-

Перетаскивайте согласования между колонками для изменения статуса

-
-
- - -
-
-
- -
-
Загрузка Канбан-доски...
-
-
- +
+ + + Комментарий будет виден всем участникам согласования +
+ + + + + +
+ + - - - - - + + \ No newline at end of file diff --git a/public/doc.js b/public/doc.js new file mode 100644 index 0000000..af0c23a --- /dev/null +++ b/public/doc.js @@ -0,0 +1,568 @@ +// doc.js - Согласование документов +document.addEventListener('DOMContentLoaded', function() { + if (window.location.pathname === '/doc') { + loadDocumentTypes(); + setupDocumentForm(); + loadMyDocuments(); + setupDocumentFilters(); + } +}); + +let documentTypes = []; +let allDocuments = []; +let filteredDocuments = []; + +async function loadDocumentTypes() { + try { + const response = await fetch('/api/document-types'); + documentTypes = await response.json(); + populateDocumentTypeSelect(); + } catch (error) { + console.error('Ошибка загрузки типов документов:', error); + } +} + +function populateDocumentTypeSelect() { + const select = document.getElementById('document-type'); + if (!select) return; + + select.innerHTML = ''; + documentTypes.forEach(type => { + const option = document.createElement('option'); + option.value = type.id; + option.textContent = type.name; + select.appendChild(option); + }); +} + +function setupDocumentForm() { + const form = document.getElementById('create-document-form'); + if (!form) return; + + form.addEventListener('submit', createDocument); + + // Устанавливаем текущую дату для даты документа + const today = new Date().toISOString().split('T')[0]; + const documentDateInput = document.getElementById('document-date'); + if (documentDateInput) { + documentDateInput.value = today; + } + + // Устанавливаем дату выполнения (по умолчанию через 7 дней) + const dueDate = new Date(); + dueDate.setDate(dueDate.getDate() + 7); + const dueDateInput = document.getElementById('due-date'); + if (dueDateInput) { + dueDateInput.value = dueDate.toISOString().split('T')[0]; + } +} + +async function createDocument(event) { + event.preventDefault(); + + if (!currentUser) { + alert('Требуется аутентификация'); + return; + } + + const formData = new FormData(); + + // Собираем данные формы + const title = document.getElementById('title').value; + const description = document.getElementById('description').value; + const documentTypeId = document.getElementById('document-type').value; + const documentNumber = document.getElementById('document-number').value; + const documentDate = document.getElementById('document-date').value; + const pagesCount = document.getElementById('pages-count').value; + const urgencyLevel = document.getElementById('urgency-level').value; + const dueDate = document.getElementById('due-date').value; + const comment = document.getElementById('comment').value; + + if (!title || title.trim() === '') { + alert('Название документа обязательно'); + return; + } + + formData.append('title', title); + formData.append('description', description || ''); + formData.append('dueDate', dueDate || ''); + formData.append('documentTypeId', documentTypeId || ''); + formData.append('documentNumber', documentNumber || ''); + formData.append('documentDate', documentDate || ''); + formData.append('pagesCount', pagesCount || ''); + formData.append('urgencyLevel', urgencyLevel || 'normal'); + formData.append('comment', comment || ''); + + // Добавляем файлы + const files = document.getElementById('files').files; + for (let i = 0; i < files.length; i++) { + formData.append('files', files[i]); + } + + try { + const response = await fetch('/api/documents', { + method: 'POST', + body: formData + }); + + if (response.ok) { + const result = await response.json(); + alert(result.message || 'Документ успешно создан и отправлен на согласование!'); + + // Сбрасываем форму + document.getElementById('create-document-form').reset(); + document.getElementById('file-list').innerHTML = ''; + + // Загружаем мои документы + loadMyDocuments(); + + // Возвращаемся к списку документов + showDocumentSection('my-documents'); + } else { + const error = await response.json(); + alert(error.error || 'Ошибка создания документа'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка создания документа'); + } +} + +async function loadMyDocuments() { + try { + const response = await fetch('/api/documents/my'); + allDocuments = await response.json(); + filteredDocuments = [...allDocuments]; + renderMyDocuments(); + } catch (error) { + console.error('Ошибка загрузки документов:', error); + document.getElementById('my-documents-list').innerHTML = + '
Ошибка загрузки документов
'; + } +} + +async function loadSecretaryDocuments() { + try { + const response = await fetch('/api/documents/secretary'); + allDocuments = await response.json(); + filteredDocuments = [...allDocuments]; + renderSecretaryDocuments(); + } catch (error) { + console.error('Ошибка загрузки документов секретаря:', error); + document.getElementById('secretary-documents-list').innerHTML = + '
Ошибка загрузки документов
'; + } +} + +function renderMyDocuments() { + const container = document.getElementById('my-documents-list'); + + if (filteredDocuments.length === 0) { + container.innerHTML = '
Нет документов
'; + return; + } + + container.innerHTML = filteredDocuments.map(doc => { + const status = getDocumentStatus(doc); + const statusClass = getDocumentStatusClass(status); + const isCancelled = doc.status === 'cancelled'; + const isClosed = doc.closed_at !== null; + + const timeLeftInfo = getDocumentTimeLeftInfo(doc); + + return ` +
+
+
+ Док. №${doc.document_number || doc.id} + ${doc.title} + ${isCancelled ? 'Отозван' : ''} + ${isClosed ? 'Завершен' : ''} + ${timeLeftInfo ? `${timeLeftInfo.text}` : ''} + ${status} +
+
+ +
+
+ ${!isCancelled && !isClosed ? ` + + ` : ''} + ${doc.files && doc.files.length > 0 ? ` + + ` : ''} +
+ +
+
Тип документа: ${doc.document_type_name || 'Не указан'}
+ ${doc.description ? `
Описание: ${doc.description}
` : ''} +
Статус согласования: ${doc.assignment_status || 'Не назначен'}
+ ${doc.refusal_reason ? `
Причина отказа: ${doc.refusal_reason}
` : ''} + ${doc.comment ? `
Комментарий создателя: ${doc.comment}
` : ''} +
+ +
+
Дата документа: ${formatDate(doc.document_date)}
+
Количество страниц: ${doc.pages_count || 'Не указано'}
+
Срочность: ${getUrgencyText(doc.urgency_level)}
+
Срок согласования: ${formatDate(doc.due_date)}
+
+ +
+ Файлы: + ${doc.files && doc.files.length > 0 ? + renderDocumentFiles(doc.files) : + 'нет файлов' + } +
+ +
+ Создан: ${formatDateTime(doc.created_at)} + ${doc.closed_at ? `
Завершен: ${formatDateTime(doc.closed_at)}` : ''} +
+
+
+ `; + }).join(''); +} + +function renderSecretaryDocuments() { + const container = document.getElementById('secretary-documents-list'); + + if (filteredDocuments.length === 0) { + container.innerHTML = '
Нет документов для согласования
'; + return; + } + + container.innerHTML = filteredDocuments.map(doc => { + const status = getDocumentStatus(doc); + const statusClass = getDocumentStatusClass(status); + + const timeLeftInfo = getDocumentTimeLeftInfo(doc); + + return ` +
+
+
+ Док. №${doc.document_number || doc.id} + ${doc.title} + ${timeLeftInfo ? `${timeLeftInfo.text}` : ''} + ${status} + От: ${doc.creator_name} +
+
+ +
+
+ + + + ${doc.files && doc.files.length > 0 ? ` + + ` : ''} +
+ +
+
Тип документа: ${doc.document_type_name || 'Не указан'}
+ ${doc.description ? `
Описание: ${doc.description}
` : ''} + ${doc.comment ? `
Комментарий создателя: ${doc.comment}
` : ''} + ${doc.refusal_reason ? `
Ранее отказано: ${doc.refusal_reason}
` : ''} +
+ +
+
Дата документа: ${formatDate(doc.document_date)}
+
Номер документа: ${doc.document_number || 'Не указан'}
+
Количество страниц: ${doc.pages_count || 'Не указано'}
+
Срочность: ${getUrgencyText(doc.urgency_level)}
+
Срок согласования: ${formatDate(doc.due_date)}
+
+ +
+ Файлы: + ${doc.files && doc.files.length > 0 ? + renderDocumentFiles(doc.files) : + 'нет файлов' + } +
+ +
+ Создан: ${formatDateTime(doc.created_at)} +
+
+
+ `; + }).join(''); +} + +function renderDocumentFiles(files) { + return ` +
+ ${files.map(file => ` +
+ + ${file.original_name} + ${formatFileSize(file.file_size)} +
+ `).join('')} +
+ `; +} + +function getDocumentStatus(doc) { + if (doc.status === 'cancelled') return 'Отозван'; + if (doc.closed_at) return 'Завершен'; + + switch(doc.assignment_status) { + case 'pre_approved': return 'Предварительно согласован'; + case 'approved': return 'Согласован'; + case 'refused': return 'Отказано'; + case 'received': return 'Получен оригинал'; + case 'signed': return 'Подписан'; + case 'assigned': return 'На согласовании'; + default: return 'Создан'; + } +} + +function getDocumentStatusClass(status) { + switch(status) { + case 'Согласован': + case 'Подписан': + case 'Получен оригинал': return 'status-approved'; + case 'Предварительно согласован': return 'status-pre-approved'; + case 'Отказано': return 'status-refused'; + case 'Отозван': return 'status-cancelled'; + case 'Завершен': return 'status-closed'; + default: return 'status-pending'; + } +} + +function getUrgencyText(urgency) { + switch(urgency) { + case 'very_urgent': return 'Очень срочно'; + case 'urgent': return 'Срочно'; + default: return 'Обычная'; + } +} + +function getDocumentTimeLeftInfo(doc) { + if (!doc.due_date || doc.closed_at) return null; + + const dueDate = new Date(doc.due_date); + const now = new Date(); + const timeLeft = dueDate.getTime() - now.getTime(); + const daysLeft = Math.floor(timeLeft / (24 * 60 * 60 * 1000)); + + if (daysLeft <= 0) return null; + + if (daysLeft <= 1) { + return { + text: `Менее 1 дня`, + class: 'deadline-urgent' + }; + } else if (daysLeft <= 3) { + return { + text: `${daysLeft} дня`, + class: 'deadline-warning' + }; + } + + return null; +} + +function formatDate(dateString) { + if (!dateString) return 'Не указана'; + const date = new Date(dateString); + return date.toLocaleDateString('ru-RU'); +} + +function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +function downloadFile(fileId) { + window.open(`/api/files/${fileId}/download`, '_blank'); +} + +async function downloadDocumentPackage(documentId) { + try { + const response = await fetch(`/api/documents/${documentId}/package`); + const result = await response.json(); + + if (result.success && result.downloadUrl) { + window.open(result.downloadUrl, '_blank'); + } else { + alert(result.message || 'Функция создания пакета документов будет реализована в следующей версии'); + } + } catch (error) { + console.error('Ошибка скачивания пакета:', error); + alert('Ошибка скачивания пакета документов'); + } +} + +async function cancelDocument(documentId) { + if (!confirm('Вы уверены, что хотите отозвать этот документ?')) { + return; + } + + try { + const response = await fetch(`/api/documents/${documentId}/cancel`, { + method: 'POST' + }); + + if (response.ok) { + alert('Документ отозван!'); + loadMyDocuments(); + } else { + const error = await response.json(); + alert(error.error || 'Ошибка отзыва документа'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка отзыва документа'); + } +} + +function openPreApproveModal(documentId) { + document.getElementById('approve-modal-type').value = 'pre_approve'; + document.getElementById('approve-modal-document-id').value = documentId; + document.getElementById('approve-comment').value = ''; + document.getElementById('refusal-reason').style.display = 'none'; + document.getElementById('approve-modal').style.display = 'block'; +} + +function openApproveModal(documentId) { + document.getElementById('approve-modal-type').value = 'approve'; + document.getElementById('approve-modal-document-id').value = documentId; + document.getElementById('approve-comment').value = ''; + document.getElementById('refusal-reason').style.display = 'none'; + document.getElementById('approve-modal').style.display = 'block'; +} + +function openRefuseModal(documentId) { + document.getElementById('approve-modal-type').value = 'refuse'; + document.getElementById('approve-modal-document-id').value = documentId; + document.getElementById('approve-comment').value = ''; + document.getElementById('refusal-reason').style.display = 'block'; + document.getElementById('approve-modal').style.display = 'block'; +} + +function closeApproveModal() { + document.getElementById('approve-modal').style.display = 'none'; + document.getElementById('approve-comment').value = ''; + document.getElementById('refusal-reason-text').value = ''; +} + +async function submitDocumentStatus(event) { + event.preventDefault(); + + const documentId = document.getElementById('approve-modal-document-id').value; + const actionType = document.getElementById('approve-modal-type').value; + const comment = document.getElementById('approve-comment').value; + const refusalReason = document.getElementById('refusal-reason-text').value; + + let status = ''; + switch(actionType) { + case 'pre_approve': status = 'pre_approved'; break; + case 'approve': status = 'approved'; break; + case 'refuse': status = 'refused'; break; + default: return; + } + + try { + const response = await fetch(`/api/documents/${documentId}/status`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + status, + comment, + refusalReason: actionType === 'refuse' ? refusalReason : null + }) + }); + + if (response.ok) { + alert('Статус документа обновлен!'); + closeApproveModal(); + loadSecretaryDocuments(); + } else { + const error = await response.json(); + alert(error.error || 'Ошибка обновления статуса'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка обновления статуса документа'); + } +} + +function setupDocumentFilters() { + const searchInput = document.getElementById('search-documents'); + const statusFilter = document.getElementById('document-status-filter'); + + if (searchInput) { + searchInput.addEventListener('input', filterDocuments); + } + + if (statusFilter) { + statusFilter.addEventListener('change', filterDocuments); + } +} + +function filterDocuments() { + const search = document.getElementById('search-documents')?.value.toLowerCase() || ''; + const statusFilter = document.getElementById('document-status-filter')?.value || 'all'; + + filteredDocuments = allDocuments.filter(doc => { + // Поиск по названию и номеру + const matchesSearch = + doc.title.toLowerCase().includes(search) || + (doc.document_number && doc.document_number.toLowerCase().includes(search)) || + (doc.description && doc.description.toLowerCase().includes(search)); + + if (!matchesSearch) return false; + + // Фильтрация по статусу + if (statusFilter === 'all') return true; + + const docStatus = getDocumentStatus(doc); + return docStatus === statusFilter; + }); + + // Определяем, какую секцию рендерить + const activeSection = document.querySelector('.document-section.active'); + if (activeSection && activeSection.id === 'my-documents-section') { + renderMyDocuments(); + } else if (activeSection && activeSection.id === 'secretary-documents-section') { + renderSecretaryDocuments(); + } +} + +function showDocumentSection(sectionName) { + // Скрыть все секции + document.querySelectorAll('.document-section').forEach(section => { + section.classList.remove('active'); + }); + + // Скрыть все кнопки + document.querySelectorAll('.nav-btn').forEach(btn => { + btn.classList.remove('active'); + }); + + // Показать выбранную секцию + document.getElementById(sectionName + '-section').classList.add('active'); + + // Активировать соответствующую кнопку + const btn = document.querySelector(`.nav-btn[onclick*="${sectionName}"]`); + if (btn) btn.classList.add('active'); + + // Загрузить данные для секции + if (sectionName === 'my-documents') { + loadMyDocuments(); + } else if (sectionName === 'secretary-documents') { + loadSecretaryDocuments(); + } +} \ No newline at end of file diff --git a/public/style.css b/public/style.css index cb562d2..24185c8 100644 --- a/public/style.css +++ b/public/style.css @@ -2350,4 +2350,739 @@ small { .nav-btn.kanban:hover { box-shadow: 0 6px 20px rgba(243, 156, 18, 0.4); } .nav-btn.help:hover { box-shadow: 0 6px 20px rgba(23, 162, 184, 0.4); } .nav-btn.profile:hover { box-shadow: 0 6px 20px rgba(46, 204, 113, 0.4); } -.nav-btn.admin:hover { box-shadow: 0 6px 20px rgba(231, 76, 60, 0.4); } \ No newline at end of file +.nav-btn.admin:hover { box-shadow: 0 6px 20px rgba(231, 76, 60, 0.4); } +/* doc */ +/* doc.css */ +:root { + --primary-color: #3498db; + --secondary-color: #2c3e50; + --success-color: #27ae60; + --warning-color: #f39c12; + --danger-color: #e74c3c; + --light-color: #ecf0f1; + --dark-color: #34495e; + --text-color: #333; + --border-color: #ddd; + --sidebar-width: 250px; + --header-height: 70px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + color: var(--text-color); +} + +.doc-container { + max-width: 1400px; + margin: 20px auto; + background: white; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + overflow: hidden; +} + +/* Header */ +header { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + color: white; + padding: 15px 25px; + border-bottom: 3px solid rgba(255, 255, 255, 0.1); +} + +.header-top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + flex-wrap: wrap; + gap: 15px; +} + +.header-top h1 { + font-size: 24px; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; +} + +.user-info { + display: flex; + align-items: center; + gap: 15px; +} + +.user-info span { + background: rgba(255, 255, 255, 0.1); + padding: 8px 15px; + border-radius: 20px; + font-size: 14px; + backdrop-filter: blur(10px); +} + +/* Navigation */ +.doc-nav { + display: flex; + gap: 10px; + flex-wrap: wrap; + padding-top: 10px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.nav-btn { + background: rgba(255, 255, 255, 0.1); + color: white; + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 8px; + backdrop-filter: blur(10px); +} + +.nav-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); +} + +.nav-btn.active { + background: white; + color: var(--primary-color); + font-weight: 600; +} + +.btn-logout { + background: rgba(231, 76, 60, 0.2); + color: white; + border: 1px solid rgba(231, 76, 60, 0.3); + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 8px; + backdrop-filter: blur(10px); + margin-left: auto; +} + +.btn-logout:hover { + background: rgba(231, 76, 60, 0.3); + transform: translateY(-2px); +} + +.btn-back { + background: rgba(255, 255, 255, 0.1); + color: white; + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 8px 15px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 5px; +} + +.btn-back:hover { + background: rgba(255, 255, 255, 0.2); + transform: translateX(-3px); +} + +/* Main content */ +main { + padding: 25px; + min-height: 500px; +} + +.document-section { + display: none; + animation: fadeIn 0.5s ease; +} + +.document-section.active { + display: block; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +h2 { + color: var(--secondary-color); + margin-bottom: 20px; + font-size: 22px; + display: flex; + align-items: center; + gap: 10px; + padding-bottom: 10px; + border-bottom: 2px solid var(--light-color); +} + +/* Forms */ +form { + background: var(--light-color); + padding: 25px; + border-radius: 12px; + margin-top: 20px; +} + +.form-row { + display: flex; + gap: 20px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.form-group { + flex: 1; + min-width: 200px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: var(--dark-color); + font-size: 14px; + display: flex; + align-items: center; + gap: 8px; +} + +.form-group input[type="text"], +.form-group input[type="number"], +.form-group input[type="date"], +.form-group input[type="datetime-local"], +.form-group select, +.form-group textarea { + width: 100%; + padding: 12px 15px; + border: 2px solid #ddd; + border-radius: 8px; + font-size: 14px; + transition: all 0.3s ease; + background: white; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); +} + +.form-group textarea { + resize: vertical; + min-height: 100px; +} + +/* File upload */ +.file-upload { + position: relative; + margin-top: 10px; +} + +.file-upload input[type="file"] { + display: none; +} + +.file-upload-label { + display: inline-block; + background: var(--primary-color); + color: white; + padding: 12px 25px; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + font-weight: 500; + display: flex; + align-items: center; + gap: 10px; + width: fit-content; +} + +.file-upload-label:hover { + background: #2980b9; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(52, 152, 219, 0.3); +} + +#file-list { + margin-top: 15px; +} + +.file-item { + background: white; + padding: 12px 15px; + border-radius: 8px; + margin-bottom: 10px; + border-left: 4px solid var(--primary-color); + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +.file-item-info { + display: flex; + align-items: center; + gap: 10px; +} + +.file-item-actions button { + background: none; + border: none; + color: var(--danger-color); + cursor: pointer; + font-size: 16px; + transition: color 0.3s ease; +} + +.file-item-actions button:hover { + color: #c0392b; +} + +.form-info { + background: rgba(52, 152, 219, 0.1); + padding: 15px; + border-radius: 8px; + margin: 20px 0; + border-left: 4px solid var(--primary-color); +} + +.form-info p { + margin: 5px 0; + color: var(--dark-color); + font-size: 14px; + display: flex; + align-items: flex-start; + gap: 8px; +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary-color), #2980b9); + color: white; + border: none; + padding: 14px 30px; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + font-weight: 600; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 10px; + margin-top: 20px; +} + +.btn-primary:hover { + transform: translateY(-3px); + box-shadow: 0 7px 20px rgba(52, 152, 219, 0.3); +} + +.btn-secondary { + background: var(--light-color); + color: var(--dark-color); + border: 1px solid var(--border-color); + padding: 14px 30px; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + font-weight: 600; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 10px; +} + +.btn-secondary:hover { + background: #e0e0e0; +} + +/* Section header */ +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 15px; +} + +.document-filters { + display: flex; + gap: 15px; + flex-wrap: wrap; +} + +.filter-group { + display: flex; + align-items: center; + gap: 10px; +} + +.filter-group label { + font-weight: 600; + color: var(--dark-color); + font-size: 14px; + display: flex; + align-items: center; + gap: 5px; +} + +.filter-group input[type="text"], +.filter-group select { + padding: 8px 12px; + border: 2px solid #ddd; + border-radius: 6px; + font-size: 14px; + min-width: 200px; +} + +/* Documents list */ +.documents-list { + display: grid; + gap: 15px; + margin-top: 20px; +} + +.document-card { + background: white; + border-radius: 12px; + padding: 20px; + border-left: 4px solid var(--primary-color); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.document-card:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); +} + +.document-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 15px; +} + +.document-title { + font-size: 18px; + font-weight: 600; + color: var(--secondary-color); + margin-bottom: 5px; + display: flex; + align-items: center; + gap: 10px; +} + +.document-meta { + display: flex; + flex-wrap: wrap; + gap: 15px; + margin-bottom: 15px; + font-size: 13px; + color: #666; +} + +.document-meta span { + display: flex; + align-items: center; + gap: 5px; +} + +.document-description { + color: var(--text-color); + margin-bottom: 15px; + line-height: 1.5; +} + +.document-files { + margin: 15px 0; +} + +.document-files h4 { + margin-bottom: 10px; + color: var(--dark-color); + font-size: 14px; + display: flex; + align-items: center; + gap: 8px; +} + +.file-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.file-tag { + background: var(--light-color); + padding: 5px 10px; + border-radius: 15px; + font-size: 12px; + color: var(--dark-color); + display: flex; + align-items: center; + gap: 5px; + cursor: pointer; + transition: all 0.3s ease; +} + +.file-tag:hover { + background: var(--primary-color); + color: white; +} + +.document-actions { + display: flex; + gap: 10px; + margin-top: 15px; +} + +.btn-small { + padding: 8px 15px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + border: none; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 5px; +} + +.btn-approve { + background: linear-gradient(135deg, var(--success-color), #219653); + color: white; +} + +.btn-refuse { + background: linear-gradient(135deg, var(--danger-color), #c0392b); + color: white; +} + +.btn-cancel { + background: linear-gradient(135deg, #95a5a6, #7f8c8d); + color: white; +} + +.btn-download { + background: linear-gradient(135deg, var(--primary-color), #2980b9); + color: white; +} + +.btn-small:hover { + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(0,0,0,0.1); +} + +.document-status { + position: absolute; + top: 20px; + right: 20px; + padding: 5px 12px; + border-radius: 15px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; +} + +.status-created { background: #3498db; color: white; } +.status-assigned { background: #f39c12; color: white; } +.status-in_progress { background: #3498db; color: white; } +.status-pre_approved { background: #9b59b6; color: white; } +.status-approved { background: #27ae60; color: white; } +.status-refused { background: #e74c3c; color: white; } +.status-cancelled { background: #95a5a6; color: white; } +.status-overdue { background: #c0392b; color: white; } + +.loading { + text-align: center; + padding: 40px; + color: #666; + font-size: 16px; +} + +.loading i { + margin-right: 10px; + color: var(--primary-color); +} + +/* Modal */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(5px); + animation: fadeIn 0.3s ease; +} + +.modal-content { + background-color: white; + margin: 5% auto; + padding: 30px; + border-radius: 15px; + width: 90%; + max-width: 500px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { transform: translateY(-50px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +.modal-content h3 { + color: var(--secondary-color); + margin-bottom: 20px; + font-size: 20px; + display: flex; + align-items: center; + gap: 10px; +} + +.modal-content .close { + float: right; + font-size: 28px; + font-weight: bold; + color: #aaa; + cursor: pointer; + transition: color 0.3s ease; +} + +.modal-content .close:hover { + color: var(--text-color); +} + +.modal-buttons { + display: flex; + gap: 15px; + margin-top: 25px; +} + +.modal-buttons button { + flex: 1; +} + +/* Responsive */ +@media (max-width: 768px) { + .doc-container { + margin: 10px; + border-radius: 10px; + } + + main { + padding: 15px; + } + + .header-top { + flex-direction: column; + align-items: flex-start; + } + + .user-info { + width: 100%; + justify-content: space-between; + } + + .form-row { + flex-direction: column; + } + + .form-group { + min-width: 100%; + } + + .doc-nav { + justify-content: center; + } + + .section-header { + flex-direction: column; + align-items: flex-start; + } + + .document-filters { + width: 100%; + } + + .filter-group { + flex-direction: column; + align-items: flex-start; + width: 100%; + } + + .filter-group input[type="text"], + .filter-group select { + min-width: 100%; + } + + .document-card-header { + flex-direction: column; + gap: 10px; + } + + .document-status { + position: static; + margin-bottom: 10px; + width: fit-content; + } + + .modal-content { + margin: 20% auto; + width: 95%; + padding: 20px; + } +} + +@media (max-width: 480px) { + .nav-btn, .btn-logout, .btn-back { + padding: 8px 12px; + font-size: 13px; + } + + .header-top h1 { + font-size: 20px; + } + + .document-actions { + flex-wrap: wrap; + } + + .btn-small { + flex: 1; + justify-content: center; + min-width: 120px; + } +} \ No newline at end of file diff --git a/server.js b/server.js index 5aa2da7..9fb212d 100644 --- a/server.js +++ b/server.js @@ -13,6 +13,8 @@ const postgresLogger = require('./postgres'); const { sendTaskNotifications, checkUpcomingDeadlines, getStatusText } = require('./notifications'); const { setupUploadMiddleware } = require('./upload-middleware'); const { setupTaskEndpoints } = require('./task-endpoints'); +// doc +const apiDoc = require('./api-doc'); const app = express(); const PORT = process.env.PORT || 3000; @@ -732,520 +734,6 @@ app.get('/help', (req, res) => { } res.sendFile(path.join(__dirname, 'public/help.html')); }); -// API для типов документов -app.get('/api/document-types', requireAuth, (req, res) => { - db.all("SELECT * FROM simple_document_types ORDER BY name", [], (err, rows) => { - if (err) { - res.status(500).json({ error: err.message }); - return; - } - res.json(rows); - }); -}); - -// API для документов (ИСПРАВЛЕНО: upload определен в начале файла) -app.post('/api/documents', requireAuth, upload.array('files', 15), async (req, res) => { - try { - console.log('📝 Начало создания документа...'); - - const userId = req.session.user.id; - const { - title, - description, - dueDate, - documentTypeId, - documentNumber, - documentDate, - pagesCount, - urgencyLevel, - comment - } = req.body; - - console.log('📋 Данные документа:', { - title, userId, documentTypeId, documentNumber - }); - - // Валидация обязательных полей - только название - if (!title || title.trim() === '') { - return res.status(400).json({ error: 'Название документа обязательно' }); - } - - // Находим группу "Секретарь" - db.get(` - SELECT u.id - 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.name = 'Секретарь' - LIMIT 1 - `, async (err, secretary) => { - if (err) { - console.error('❌ Ошибка поиска секретаря:', err); - return res.status(500).json({ error: 'Ошибка поиска секретаря' }); - } - - if (!secretary) { - console.warn('⚠️ Секретарь не найден в группе "Секретарь"'); - return res.status(400).json({ - error: 'Не найден секретарь для согласования документов. Пожалуйста, добавьте пользователя в группу "Секретарь".' - }); - } - - console.log('✅ Найден секретарь из группы:', secretary.id); - - // Создаем задачу - db.run(` - INSERT INTO tasks (title, description, due_date, created_by, status, created_at) - VALUES (?, ?, ?, ?, 'active', datetime('now')) - `, [ - `Документ: ${title}`, - description || '', - dueDate || null, - userId - ], function(err) { - if (err) { - console.error('❌ Ошибка создания задачи:', err); - return res.status(500).json({ error: 'Ошибка создания задачи' }); - } - - const taskId = this.lastID; - console.log('✅ Задача создана, ID:', taskId); - - // Создаем запись документа в таблице simple_documents - // Тип документа не обязателен - может быть NULL - db.run(` - INSERT INTO simple_documents ( - task_id, document_type_id, document_number, - document_date, pages_count, urgency_level, comment - ) VALUES (?, ?, ?, ?, ?, ?, ?) - `, [ - taskId, - documentTypeId || null, // Может быть NULL - documentNumber || null, - documentDate || null, - pagesCount || null, - urgencyLevel || 'normal', - comment || null - ], function(err) { - if (err) { - console.error('❌ Ошибка создания записи документа:', err); - // Удаляем задачу если не удалось создать документ - db.run("DELETE FROM tasks WHERE id = ?", [taskId]); - return res.status(500).json({ error: 'Ошибка создания записи документа' }); - } - - const documentId = this.lastID; - console.log('✅ Запись документа создана, ID:', documentId); - - // Назначаем задачу секретарю - db.run(` - INSERT INTO task_assignments (task_id, user_id, status, created_at) - VALUES (?, ?, 'assigned', datetime('now')) - `, [taskId, secretary.id], function(err) { - if (err) { - console.error('❌ Ошибка назначения задачи секретарю:', err); - // Удаляем задачу и документ - db.run("DELETE FROM simple_documents WHERE task_id = ?", [taskId]); - db.run("DELETE FROM tasks WHERE id = ?", [taskId]); - return res.status(500).json({ error: 'Ошибка назначения задачи секретарю' }); - } - - console.log('✅ Задача назначена секретарю'); - - // Загружаем файлы если есть - if (req.files && req.files.length > 0) { - console.log('📁 Файлов для загрузки:', req.files.length); - - const uploadPromises = req.files.map(file => { - return new Promise((resolve, reject) => { - const filePath = file.path; - const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8'); - - db.run(` - INSERT INTO task_files (task_id, user_id, file_path, original_name, file_size, uploaded_at) - VALUES (?, ?, ?, ?, ?, datetime('now')) - `, [taskId, userId, filePath, originalName, file.size], function(err) { - if (err) { - console.error('❌ Ошибка сохранения файла в БД:', err); - reject(err); - } else { - console.log('✅ Файл сохранен:', originalName); - resolve(); - } - }); - }); - }); - - Promise.all(uploadPromises) - .then(() => { - console.log('✅ Все файлы загружены'); - - // Логируем действие - const { logActivity } = require('./database'); - logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ для согласования: ${title}`); - - res.json({ - success: true, - taskId: taskId, - documentId: documentId, - message: 'Документ успешно создан и отправлен на согласование' - }); - }) - .catch(error => { - console.error('❌ Ошибка загрузки файлов:', error); - // Все равно возвращаем успех, так как задача и документ созданы - const { logActivity } = require('./database'); - logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ для согласования: ${title}`); - - res.json({ - success: true, - taskId: taskId, - documentId: documentId, - message: 'Документ создан, но были проблемы с загрузкой файлов' - }); - }); - } else { - console.log('📁 Файлы не прикреплены'); - - // Логируем действие - const { logActivity } = require('./database'); - logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ для согласования: ${title}`); - - res.json({ - success: true, - taskId: taskId, - documentId: documentId, - message: 'Документ успешно создан и отправлен на согласование' - }); - } - }); - }); - }); - }); - } catch (error) { - console.error('❌ Общая ошибка создания документа:', error); - res.status(500).json({ - error: 'Ошибка создания документа', - details: error.message - }); - } -}); - -// Получение моих документов -app.get('/api/documents/my', requireAuth, (req, res) => { - const userId = req.session.user.id; - - console.log('📄 Запрос документов пользователя ID:', userId); - - db.all(` - SELECT - t.id, - t.title, - t.description, - t.due_date, - t.created_at, - t.status, - t.closed_at, - sd.id as document_id, - sd.document_type_id, - sdt.name as document_type_name, - sd.document_number, - sd.document_date, - sd.pages_count, - sd.urgency_level, - sd.comment, - sd.refusal_reason, - u.name as creator_name, - ta.status as assignment_status, - ta.user_id as assignee_id, - au.name as assignee_name - FROM tasks t - LEFT JOIN simple_documents sd ON t.id = sd.task_id - LEFT JOIN simple_document_types sdt ON sd.document_type_id = sdt.id - LEFT JOIN users u ON t.created_by = u.id - LEFT JOIN task_assignments ta ON t.id = ta.task_id - LEFT JOIN users au ON ta.user_id = au.id - WHERE t.created_by = ? - AND t.title LIKE 'Документ:%' - ORDER BY t.created_at DESC - `, [userId], async (err, tasks) => { - if (err) { - console.error('❌ Ошибка получения документов:', err); - return res.status(500).json({ - error: 'Ошибка получения документов', - details: err.message - }); - } - - console.log('✅ Найдено задач:', tasks.length); - - // Загружаем файлы для каждой задачи - const tasksWithFiles = await Promise.all(tasks.map(async (task) => { - try { - const files = await new Promise((resolve, reject) => { - 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) => { - if (err) reject(err); - else resolve(rows || []); - }); - }); - task.files = files || []; - } catch (error) { - console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, error); - task.files = []; - } - return task; - })); - - res.json(tasksWithFiles); - }); -}); - -// Получение документов для секретаря -app.get('/api/documents/secretary', requireAuth, (req, res) => { - const userId = req.session.user.id; - - console.log('📄 Запрос документов для секретаря ID:', userId); - - // Проверяем, что пользователь секретарь - db.get(` - SELECT 1 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 u.id = ? AND g.name = 'Секретарь' - `, [userId], (err, isSecretary) => { - if (err || !isSecretary) { - // Пробуем альтернативный способ проверки - db.get("SELECT groups FROM users WHERE id = ?", [userId], (err, user) => { - if (err || !user || !user.groups || !user.groups.includes('Секретарь')) { - console.log('⚠️ Пользователь не является секретарем:', userId); - return res.status(403).json({ error: 'Недостаточно прав. Требуется роль секретаря.' }); - } - fetchDocuments(); - }); - } else { - fetchDocuments(); - } - }); - - function fetchDocuments() { - db.all(` - SELECT - t.id, - t.title, - t.description, - t.due_date, - t.created_at, - ta.status as assignment_status, - sd.id as document_id, - sd.document_type_id, - sdt.name as document_type_name, - sd.document_number, - sd.document_date, - sd.pages_count, - sd.urgency_level, - sd.comment, - sd.refusal_reason, - u.name as creator_name - FROM tasks t - JOIN task_assignments ta ON t.id = ta.task_id - LEFT JOIN simple_documents sd ON t.id = sd.task_id - LEFT JOIN simple_document_types sdt ON sd.document_type_id = sdt.id - LEFT JOIN users u ON t.created_by = u.id - WHERE ta.user_id = ? - AND t.title LIKE 'Документ:%' - AND t.status = 'active' - AND t.closed_at IS NULL - ORDER BY - CASE sd.urgency_level - WHEN 'very_urgent' THEN 1 - WHEN 'urgent' THEN 2 - ELSE 3 - END, - t.due_date ASC, - t.created_at DESC - `, [userId], async (err, tasks) => { - if (err) { - console.error('❌ Ошибка получения документов для секретаря:', err); - return res.status(500).json({ - error: 'Ошибка получения документов', - details: err.message - }); - } - - console.log('✅ Найдено задач для секретаря:', tasks.length); - - // Загружаем файлы для каждой задачи - const tasksWithFiles = await Promise.all(tasks.map(async (task) => { - try { - const files = await new Promise((resolve, reject) => { - 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) => { - if (err) reject(err); - else resolve(rows || []); - }); - }); - task.files = files || []; - } catch (error) { - console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, error); - task.files = []; - } - return task; - })); - - res.json(tasksWithFiles); - }); - } -}); - -// Обновление статуса документа -app.put('/api/documents/:id/status', requireAuth, (req, res) => { - const documentId = req.params.id; - const { status, comment, refusalReason } = req.body; - const userId = req.session.user.id; - - // Проверяем права (только секретарь или администратор) - db.get(` - SELECT 1 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 u.id = ? AND g.name = 'Секретарь' - `, [userId], (err, isSecretary) => { - if (err || !isSecretary) { - db.get("SELECT groups FROM users WHERE id = ?", [userId], (err, user) => { - if (err || !user || !user.groups || !user.groups.includes('Секретарь')) { - if (req.session.user.role !== 'admin') { - return res.status(403).json({ error: 'Недостаточно прав' }); - } - } - updateDocumentStatus(); - }); - } else { - updateDocumentStatus(); - } - }); - - function updateDocumentStatus() { - db.get("SELECT task_id FROM simple_documents WHERE id = ?", [documentId], (err, document) => { - if (err || !document) { - return res.status(404).json({ error: 'Документ не найден' }); - } - - const taskId = document.task_id; - - // Обновляем статус в задании - db.run("UPDATE task_assignments SET status = ? WHERE task_id = ? AND user_id = ?", - [status, taskId, userId], function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - - // Обновляем причину отказа если есть - if (refusalReason) { - db.run("UPDATE simple_documents SET refusal_reason = ? WHERE id = ?", - [refusalReason, documentId]); - } - - // Логируем действие - const { logActivity } = require('./database'); - const actionMap = { - 'approved': 'Документ согласован', - 'completed': 'Документ согласован', - 'received': 'Оригинал документа получен', - 'signed': 'Документ подписан', - 'refused': 'В согласовании отказано' - }; - - const actionText = actionMap[status] || `Статус изменен на: ${status}`; - logActivity(taskId, userId, 'STATUS_CHANGED', actionText); - - res.json({ success: true }); - } - ); - }); - } -}); - -// Отзыв документа -app.post('/api/documents/:id/cancel', requireAuth, (req, res) => { - const documentId = req.params.id; - const userId = req.session.user.id; - - db.get("SELECT task_id FROM simple_documents WHERE id = ?", [documentId], (err, document) => { - if (err || !document) { - return res.status(404).json({ error: 'Документ не найден' }); - } - - const taskId = document.task_id; - - // Проверяем, что пользователь создатель задачи - db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => { - if (err || !task) { - return res.status(404).json({ error: 'Задача не найдена' }); - } - - if (parseInt(task.created_by) !== parseInt(userId)) { - return res.status(403).json({ error: 'Вы не являетесь создателем этого документа' }); - } - - // Обновляем статус задачи - db.run("UPDATE tasks SET status = 'cancelled', closed_at = datetime('now') WHERE id = ?", [taskId], function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - - // Логируем действие - const { logActivity } = require('./database'); - logActivity(taskId, userId, 'STATUS_CHANGED', 'Документ отозван создателем'); - - res.json({ success: true }); - }); - }); - }); -}); - -// Получение пакета документов -app.get('/api/documents/:id/package', requireAuth, async (req, res) => { - const documentId = req.params.id; - const userId = req.session.user.id; - - // Проверяем доступ к документу - db.get(` - SELECT t.id, t.created_by - FROM documents d - JOIN tasks t ON d.task_id = t.id - WHERE d.id = ? - `, [documentId], async (err, result) => { - if (err || !result) { - return res.status(404).json({ error: 'Документ не найден' }); - } - - // Проверяем, что пользователь имеет доступ (создатель или секретарь) - const isCreator = parseInt(result.created_by) === parseInt(userId); - const isSecretary = req.session.user.groups && req.session.user.groups.includes('Секретарь'); - - if (!isCreator && !isSecretary) { - return res.status(403).json({ error: 'Недостаточно прав' }); - } - - // Здесь будет логика создания ZIP архива с документами - // Пока возвращаем заглушку - res.json({ - success: true, - message: 'Функция создания пакета документов будет реализована в следующей версии' - }); - }); -}); // API для получения настроек уведомлений пользователя app.get('/api/user/settings', requireAuth, async (req, res) => { @@ -1703,6 +1191,9 @@ async function initializeServer() { // 4. Настраиваем endpoint'ы для задач (upload уже настроен в начале файла) setupTaskEndpoints(app, db, upload); console.log('✅ Endpoint\'ы задач настроены'); + + apiDoc(app, db, upload); + console.log('✅ Endpoint\'ы документов настроены'); // 5. Загружаем админ роутер динамически try {