402 lines
17 KiB
JavaScript
402 lines
17 KiB
JavaScript
// 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;
|
||
}
|
||
|
||
/**
|
||
* Отправляет сводку о непрочитанных сообщениях в чатах всем пользователям,
|
||
* у которых есть такие сообщения (раз в час).
|
||
*/
|
||
async function sendChatSummaryNotifications() {
|
||
const db = getDb();
|
||
if (!db) {
|
||
console.error('❌ База данных не доступна для отправки сводки чата');
|
||
return;
|
||
}
|
||
|
||
// Получаем всех пользователей, у которых включены 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 += `
|
||
<li style="margin-bottom: 15px;">
|
||
<strong><a href="${taskUrl}" style="color: #3498db;">${escapeHtml(task.title)}</a></strong><br>
|
||
Новых сообщений: ${task.new_messages_count}
|
||
</li>
|
||
`;
|
||
}
|
||
|
||
const subject = `📬 Новые сообщения в чатах задач (${tasks.length} задач)`;
|
||
const htmlContent = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; line-height: 1.6; }
|
||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||
.header { background: #3498db; color: white; padding: 15px; border-radius: 10px 10px 0 0; }
|
||
.content { padding: 20px; border: 1px solid #ddd; border-top: none; }
|
||
.footer { margin-top: 20px; font-size: 12px; color: #666; text-align: center; }
|
||
ul { padding-left: 20px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h2>💬 Новые сообщения в чатах</h2>
|
||
</div>
|
||
<div class="content">
|
||
<p>Здравствуйте, ${escapeHtml(user.name)}!</p>
|
||
<p>У вас есть непрочитанные сообщения в следующих задачах:</p>
|
||
<ul>${tasksHtml}</ul>
|
||
<p>Вы получили это письмо, потому что подписаны на уведомления о чатах.<br>
|
||
Уведомления приходят раз в час. Чтобы отключить их, измените настройки в личном кабинете.</p>
|
||
</div>
|
||
<div class="footer">
|
||
<p>© School CRM, ${new Date().getFullYear()}</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
// Отправляем 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
|
||
}; |