// notifications.js const { getDb } = require('./database'); const emailNotifications = require('./email-notifications'); /** * Отправляет уведомления о событиях в задаче согласно новой логике: * - Если инициатор = автор → уведомления всем исполнителям * - Если инициатор = исполнитель → уведомление только автору * - Иначе (например, администратор вне задачи) → уведомления всем, кроме инициатора (старое поведение) */ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, authorId, comment = '', status = '', userName = '') { try { const db = getDb(); if (!db) { console.log('❌ База данных не доступна для отправки уведомлений'); return; } console.log(`📢 Начинаем отправку уведомлений для задачи ${taskId}:`); console.log(` Тип: ${type}, Автор действия: ${authorId}, Название: ${taskTitle}`); // 1. Получаем автора задачи (создателя) 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); }); }); if (!creator) { console.error(`❌ Задача ${taskId} не найдена или у неё нет автора`); return; } // 2. Получаем всех исполнителей задачи 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 || []); }); }); // 3. Получаем информацию об авторе действия (инициаторе) const author = await new Promise((resolve, reject) => { db.get("SELECT name, login, email 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'; // 4. Определяем получателей уведомления согласно новой логике let recipients = []; // Проверяем, является ли инициатор автором задачи if (parseInt(authorId) === parseInt(creator.user_id)) { // Инициатор = автор → уведомления всем исполнителям recipients = assignees; console.log(` Инициатор является автором. Получатели: ${assignees.length} исполнителей`); } // Проверяем, является ли инициатор одним из исполнителей else { const isInitiatorAssignee = assignees.some(a => parseInt(a.user_id) === parseInt(authorId)); if (isInitiatorAssignee) { // Инициатор = исполнитель → уведомление только автору recipients = [creator]; console.log(` Инициатор является исполнителем. Получатель: автор`); } else { // Инициатор не является ни автором, ни исполнителем (например, администратор) // Отправляем уведомления всем участникам, кроме инициатора (старое поведение) recipients = [creator, ...assignees].filter(p => parseInt(p.user_id) !== parseInt(authorId)); console.log(` Инициатор вне задачи. Получатели: ${recipients.length} участников (старое поведение)`); } } // 5. Отправляем email уведомления выбранным получателям for (const recipient of recipients) { const taskData = { taskId, title: taskTitle, description: taskDescription, due_date: null, // при необходимости можно добавить получение срока из БД author_name: authorName, comment: comment, status: status, user_name: userName || recipient.user_name, hours_left: type === 'deadline' ? 24 : null }; await emailNotifications.sendTaskNotification( recipient.user_id, taskData, type ); } console.log(`✅ Уведомления отправлены для задачи ${taskId}`); } catch (error) { console.error('❌ Общая ошибка при обработке уведомлений:', error); } } /** * Отправляет уведомление о приближающемся дедлайне */ async function sendDeadlineNotification(assignment, hoursLeft) { try { const taskData = { taskId: assignment.task_id, title: assignment.title, description: assignment.description || '', due_date: assignment.due_date, author_name: assignment.creator_name, hours_left: hoursLeft }; // Отправляем уведомление исполнителю await emailNotifications.sendTaskNotification( assignment.user_id, taskData, 'deadline' ); // Отправляем уведомление заказчику (автору) await emailNotifications.sendTaskNotification( assignment.created_by, taskData, 'deadline' ); console.log(`✅ Email уведомление о сроке (${hoursLeft}ч) отправлено для задачи ${assignment.task_id}`); } catch (error) { console.error('❌ Ошибка отправки email уведомления о сроке:', error); } } /** * Проверяет приближающиеся дедлайны и отправляет уведомления */ async 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); } } }); } /** * Возвращает тему уведомления в зависимости от типа */ 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; } /** * Отправляет сводку о непрочитанных сообщениях в чатах всем пользователям, * у которых есть такие сообщения (раз в час, только с 8 до 21 по часовому поясу сервера). */ async function sendChatSummaryNotifications() { const db = getDb(); if (!db) { console.error('❌ База данных не доступна для отправки сводки чата'); return; } // Определяем часовой пояс const timezone = process.env.TIMEZONE || 'Asia/Yekaterinburg'; const now = new Date(); // Форматтер для полного времени (логирование) const fullFormatter = new Intl.DateTimeFormat('ru-RU', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); // Форматтер только для часа const hourFormatter = new Intl.DateTimeFormat('ru-RU', { hour: 'numeric', timeZone: timezone }); const currentHour = parseInt(hourFormatter.format(now)); // Логируем текущее время и используемый час console.log(`📅 Текущее время (${timezone}): ${fullFormatter.format(now)} (час: ${currentHour})`); // Проверка рабочего времени (8:00 – 21:00) if (currentHour < 8 || currentHour >= 21) { console.log(`🕒 [Чат-сводка] Пропущена: сейчас ${currentHour}:00 (рабочие часы 8-21), время отправки не наступило`); return; } console.log(`✅ [Чат-сводка] Рабочие часы, отправляем уведомления...`); // Получаем всех пользователей, у которых включены email уведомления const users = await new Promise((resolve, reject) => { db.all(` SELECT u.id, u.name, u.email, u.login, COALESCE(us.email_notifications, 1) as email_notifications, us.notification_email, us.last_chat_notification_sent_at FROM users u LEFT JOIN user_settings us ON u.id = us.user_id WHERE u.email IS NOT NULL AND u.email != '' `, (err, rows) => { if (err) reject(err); else resolve(rows || []); }); }); for (const user of users) { // Проверяем настройки уведомлений if (!user.email_notifications) continue; const emailTo = user.notification_email || user.email; if (!emailTo) continue; // Время последней отправки уведомления (или NULL – тогда берём текущее, но при логине уже должно быть установлено) let lastSent = user.last_chat_notification_sent_at; if (!lastSent) { // Если нет метки – устанавливаем сейчас и пропускаем (при первом входе всё равно сбросится) await new Promise((resolve) => { db.run( `UPDATE user_settings SET last_chat_notification_sent_at = CURRENT_TIMESTAMP WHERE user_id = ?`, [user.id], resolve ); }); continue; } // Получаем задачи с количеством новых сообщений после lastSent const tasks = await new Promise((resolve, reject) => { db.all(` SELECT t.id, t.title, COUNT(m.id) as new_messages_count FROM tasks t LEFT JOIN task_chat_messages m ON t.id = m.task_id AND m.created_at > ? AND m.is_deleted = 0 WHERE (t.created_by = ? OR EXISTS ( SELECT 1 FROM task_assignments ta WHERE ta.task_id = t.id AND ta.user_id = ? )) GROUP BY t.id HAVING new_messages_count > 0 `, [lastSent, user.id, user.id], (err, rows) => { if (err) reject(err); else resolve(rows || []); }); }); if (tasks.length === 0) continue; // Формируем HTML‑письмо со списком задач const appUrl = process.env.APP_URL || 'http://localhost:3000'; let tasksHtml = ''; for (const task of tasks) { const taskUrl = `${appUrl}/task?id=${task.id}`; tasksHtml += `
  • ${escapeHtml(task.title)}
    Новых сообщений: ${task.new_messages_count}
  • `; } const subject = `📬 Новые сообщения в чатах задач (${tasks.length} задач)`; const htmlContent = `

    💬 Новые сообщения в чатах

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

    У вас есть непрочитанные сообщения в следующих задачах:

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

    `; // Отправляем email const result = await emailNotifications.sendEmailNotification( emailTo, subject, htmlContent, user.id, null, 'chat_summary' ); if (result.sent) { // Обновляем время последней отправки await new Promise((resolve) => { db.run( `UPDATE user_settings SET last_chat_notification_sent_at = CURRENT_TIMESTAMP WHERE user_id = ?`, [user.id], resolve ); }); console.log(`✅ Отправлена сводка чата для ${user.login} (${tasks.length} задач)`); } else { console.error(`❌ Не удалось отправить сводку чата для ${user.login}: ${result.error}`); // Не обновляем last_sent – в следующий раз попробуем снова } } } /** * Вспомогательная функция для экранирования HTML */ function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>]/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }); } // Экспортируем функции module.exports = { sendTaskNotifications, checkUpcomingDeadlines, sendDeadlineNotification, getStatusText, emailNotifications, sendChatSummaryNotifications };