const fetch = require('node-fetch'); const postgresLogger = require('./postgres'); const { getDb } = require('./database'); async function sendDeadlineNotification(assignment, hoursLeft) { try { if (!process.env.NOTIFICATION_SERVICE_URL || !process.env.NOTIFICATION_SERVICE_LOGIN || !process.env.NOTIFICATION_SERVICE_PASSWORD) { return; } const notificationKey = `deadline_${hoursLeft}h_task_${assignment.task_id}_user_${assignment.user_id}`; const lastSent = await getLastNotificationSent(notificationKey); const now = new Date(); if (lastSent) { const timeSinceLast = now.getTime() - new Date(lastSent).getTime(); if (timeSinceLast < 12 * 60 * 60 * 1000) { return; } } const subject = `⚠️ До окончания срока задачи осталось менее ${hoursLeft} часов`; const content = `Задача: ${assignment.title}\n\n` + `Описание: ${assignment.description || 'Без описания'}\n` + `Срок выполнения: ${new Date(assignment.due_date).toLocaleString('ru-RU')}\n` + `Осталось времени: ${hoursLeft} часов\n\n` + `Пожалуйста, завершите задачу в срок.`; const recipients = [ { id: assignment.user_id, name: assignment.user_name, email: assignment.user_email }, { id: assignment.created_by, name: assignment.creator_name, email: assignment.creator_email } ].filter((value, index, self) => self.findIndex(r => r.id === value.id) === index ); const recipientIds = recipients.map(r => r.id); const authHeader = encodeBasicAuth( process.env.NOTIFICATION_SERVICE_LOGIN, process.env.NOTIFICATION_SERVICE_PASSWORD ); const FormData = require('form-data'); const formData = new FormData(); formData.append('subject', subject); formData.append('content', content); formData.append('recipients', JSON.stringify(recipientIds)); formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk'])); const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, { method: 'POST', headers: { 'Authorization': `Basic ${authHeader}` }, body: formData }); if (response.ok) { await saveNotificationSent(notificationKey); console.log(`✅ Уведомление о сроке (${hoursLeft}ч) отправлено для задачи ${assignment.task_id}`); } } catch (error) { console.error('❌ Ошибка отправки уведомления о сроке:', error); } } function getLastNotificationSent(key) { return new Promise((resolve) => { const db = getDb(); if (!db) { resolve(null); return; } db.get("SELECT created_at FROM notification_logs WHERE notification_key = ? ORDER BY created_at DESC LIMIT 1", [key], (err, row) => { resolve(row ? row.created_at : null); } ); }); } function saveNotificationSent(key) { const db = getDb(); if (!db) return; db.run("INSERT INTO notification_logs (notification_key) VALUES (?)", [key]); } function encodeBasicAuth(login, password) { return Buffer.from(`${login}:${password}`).toString('base64'); } async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, authorId, comment = '', status = '', userName = '') { try { const db = getDb(); if (!db) { console.log('❌ База данных не доступна для отправки уведомлений'); return; } if (!process.env.NOTIFICATION_SERVICE_URL || !process.env.NOTIFICATION_SERVICE_LOGIN || !process.env.NOTIFICATION_SERVICE_PASSWORD) { console.log('⚠️ Настройки сервиса уведомлений не заданы'); // Логируем в PostgreSQL даже если уведомления не отправляются await logNotificationToPostgres({ type, taskId, taskTitle, taskDescription, authorId, comment, status, userName, error: 'Сервис уведомлений не настроен' }); return; } console.log(`📢 Начинаем отправку уведомлений для задачи ${taskId}:`); console.log(` Тип: ${type}, Автор действия: ${authorId}, Название: ${taskTitle}`); // Получаем заказчика (создателя задачи) ОТДЕЛЬНО const creator = await new Promise((resolve, reject) => { db.get(` SELECT t.created_by as user_id, u.name as user_name, u.login as user_login, u.email FROM tasks t LEFT JOIN users u ON t.created_by = u.id WHERE t.id = ? `, [taskId], (err, row) => { if (err) reject(err); else resolve(row); }); }); // Получаем исполнителей ОТДЕЛЬНО const assignees = await new Promise((resolve, reject) => { db.all(` SELECT ta.user_id, u.name as user_name, u.login as user_login, u.email FROM task_assignments ta LEFT JOIN users u ON ta.user_id = u.id WHERE ta.task_id = ? `, [taskId], (err, rows) => { if (err) reject(err); else resolve(rows || []); }); }); // Собираем всех участников const participants = []; if (creator) { participants.push({ ...creator, role: 'creator', is_creator: true }); } if (assignees && assignees.length > 0) { assignees.forEach(assignee => { participants.push({ ...assignee, role: 'assignee', is_creator: false }); }); } // Получаем информацию об авторе действия const author = await new Promise((resolve, reject) => { db.get("SELECT name, login FROM users WHERE id = ?", [authorId], (err, row) => { if (err) reject(err); else resolve(row); }); }); const authorName = author ? author.name : 'Система'; const authorLogin = author ? author.login : 'system'; // Логируем в PostgreSQL const postgresLogIds = await logNotificationToPostgres({ type, taskId, taskTitle, taskDescription, authorId, authorName, authorLogin, participants, comment, status, userName }); let subject, content; switch (type) { case 'created': subject = `Новая задача: ${taskTitle}`; content = `Создана новая задача:\n\n` + `📋 ${taskTitle}\n` + `📝 ${taskDescription || 'Без описания'}\n` + `👤 Автор: ${authorName}\n\n` + `Для просмотра перейдите в систему управления задачами.`; break; case 'updated': subject = `Обновлена задача: ${taskTitle}`; content = `Задача была обновлена:\n\n` + `📋 ${taskTitle}\n` + `📝 ${taskDescription || 'Без описания'}\n` + `👤 Изменено: ${authorName}\n\n` + `Для просмотра изменений перейдите в систему управления задачами.`; break; case 'rework': subject = `Задача возвращена на доработку: ${taskTitle}`; content = `Задача возвращена на доработку:\n\n` + `📋 ${taskTitle}\n` + `📝 Комментарий: ${comment}\n` + `👤 Автор замечания: ${authorName}\n\n` + `Пожалуйста, исправьте замечания и обновите статус задачи.`; break; case 'closed': subject = `Задача закрыта: ${taskTitle}`; content = `Задача была закрыта:\n\n` + `📋 ${taskTitle}\n` + `👤 Закрыта: ${authorName}\n\n` + `Задача завершена и перемещена в архив.`; break; case 'status_changed': const statusText = getStatusText(status); subject = `Изменен статус задачи: ${taskTitle}`; content = `Статус задачи изменен:\n\n` + `📋 ${taskTitle}\n` + `🔄 Новый статус: ${statusText}\n` + `👤 Изменил: ${userName || authorName}\n\n` + `Для просмотра перейдите в систему управления задачами.`; break; default: console.log(`⚠️ Неизвестный тип уведомления: ${type}`); return; } // Фильтруем получателей: исключаем автора действия const recipientIds = participants .filter(p => { const shouldExclude = p.user_id === authorId; if (shouldExclude) { console.log(` ✋ Исключаем автора действия: ${p.user_name} (ID: ${p.user_id})`); } return !shouldExclude; }) .map(p => p.user_id); if (recipientIds.length === 0) { console.log('❌ Нет получателей для уведомления (все участники - автор изменения)'); // Обновляем статус в PostgreSQL await updatePostgresLogStatus(postgresLogIds, 'no_recipients', 'Нет получателей после фильтрации'); return; } const authHeader = encodeBasicAuth( process.env.NOTIFICATION_SERVICE_LOGIN, process.env.NOTIFICATION_SERVICE_PASSWORD ); const FormData = require('form-data'); const formData = new FormData(); formData.append('subject', subject); formData.append('content', content); formData.append('recipients', JSON.stringify(recipientIds)); formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk'])); console.log(`🚀 Отправляем запрос на сервис уведомлений...`); try { const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, { method: 'POST', headers: { 'Authorization': `Basic ${authHeader}` }, body: formData }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); console.log(`✅ Уведомления успешно отправлены для задачи ${taskId}`); // Обновляем статус в PostgreSQL await updatePostgresLogStatus(postgresLogIds, 'sent', null, new Date().toISOString()); console.log(` Результат от сервиса:`, result); } catch (error) { console.error('❌ Ошибка отправки уведомлений:', error); // Обновляем статус в PostgreSQL await updatePostgresLogStatus(postgresLogIds, 'failed', error.message); console.error(' Детали ошибки:', { taskId, type, authorId, errorMessage: error.message, stack: error.stack }); } } catch (error) { console.error('❌ Общая ошибка при обработке уведомлений:', error); } } // Вспомогательные функции для работы с PostgreSQL async function logNotificationToPostgres(data) { try { const { type, taskId, taskTitle, taskDescription, authorId, authorName, authorLogin, participants = [], comment = '', status = '', userName = '', error = '' } = data; // Создаем сообщение let messageContent = ''; switch (type) { case 'created': messageContent = `Создана новая задача: ${taskTitle}`; break; case 'updated': messageContent = `Обновлена задача: ${taskTitle}`; break; case 'rework': messageContent = `Задача возвращена на доработку: ${taskTitle}. Комментарий: ${comment}`; break; case 'closed': messageContent = `Задача закрыта: ${taskTitle}`; break; case 'status_changed': messageContent = `Изменен статус задачи: ${taskTitle}. Новый статус: ${getStatusText(status)}`; break; } // Логируем для каждого получателя отдельно const recipientsToNotify = participants.filter(p => p.user_id !== authorId); const logIds = []; for (const recipient of recipientsToNotify) { const logId = await postgresLogger.logNotification({ taskId, taskTitle, taskDescription, notificationType: type, authorId, authorName, authorLogin, recipientId: recipient.user_id, recipientName: recipient.user_name, recipientLogin: recipient.user_login, messageContent: `${messageContent}\n\nЗадача: ${taskTitle}\nОписание: ${taskDescription || 'Без описания'}\nАвтор: ${authorName}`, messageSubject: getNotificationSubject(type, taskTitle), deliveryMethods: ['email', 'telegram', 'vk'], comments: error ? `Ошибка: ${error}` : comment }); if (logId) { logIds.push(logId); } } return logIds; } catch (error) { console.error('❌ Ошибка логирования в PostgreSQL:', error); return []; } } async function updatePostgresLogStatus(logIds, status, errorMessage = null, sentAt = null) { if (!logIds || logIds.length === 0) return; for (const logId of logIds) { await postgresLogger.updateNotificationStatus(logId, status, errorMessage, sentAt); } } function getNotificationSubject(type, taskTitle) { switch (type) { case 'created': return `Новая задача: ${taskTitle}`; case 'updated': return `Обновлена задача: ${taskTitle}`; case 'rework': return `Задача возвращена на доработку: ${taskTitle}`; case 'closed': return `Задача закрыта: ${taskTitle}`; case 'status_changed': return `Изменен статус задачи: ${taskTitle}`; default: return `Уведомление по задаче: ${taskTitle}`; } } function getStatusText(status) { const statusMap = { 'assigned': 'Назначена', 'in_progress': 'В работе', 'completed': 'Завершена', 'overdue': 'Просрочена', 'rework': 'На доработке' }; return statusMap[status] || status; } function checkUpcomingDeadlines() { const now = new Date(); const in48Hours = new Date(now.getTime() + 48 * 60 * 60 * 1000).toISOString(); const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(); const nowISO = now.toISOString(); const query = ` SELECT DISTINCT ta.*, t.title, t.description, t.created_by, u.name as user_name, u.email as user_email, creator.name as creator_name, creator.email as creator_email FROM task_assignments ta JOIN tasks t ON ta.task_id = t.id JOIN users u ON ta.user_id = u.id JOIN users creator ON t.created_by = creator.id WHERE ta.due_date IS NOT NULL AND ta.due_date > ? AND ta.due_date <= ? AND ta.status NOT IN ('completed', 'overdue') AND t.status = 'active' AND t.closed_at IS NULL `; const db = getDb(); if (!db) { console.error('❌ База данных не доступна для проверки сроков'); return; } db.all(query, [nowISO, in48Hours], async (err, assignments) => { if (err) { console.error('❌ Ошибка при проверке сроков задач:', err); return; } for (const assignment of assignments) { const dueDate = new Date(assignment.due_date); const timeLeft = dueDate.getTime() - now.getTime(); const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000)); if (hoursLeft <= 48 && hoursLeft > 24) { await sendDeadlineNotification(assignment, 48); } else if (hoursLeft <= 24) { await sendDeadlineNotification(assignment, 24); } } }); } // Экспортируем функции module.exports = { sendTaskNotifications, checkUpcomingDeadlines, sendDeadlineNotification, getStatusText };