// 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
};