Files
minicrm/notifications.js
2026-03-16 22:38:16 +05:00

345 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// notifications.js
const postgresLogger = require('./postgres');
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} участников (старое поведение)`);
}
}
// Логируем в PostgreSQL (если настроено)
let postgresLogIds = [];
if (postgresLogger.initialized) {
postgresLogIds = await logNotificationToPostgres({
type,
taskId,
taskTitle,
taskDescription,
authorId,
authorName,
authorLogin,
recipients,
comment,
status,
userName
});
}
// 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);
}
}
});
}
/**
* Логирует уведомление в PostgreSQL
*/
async function logNotificationToPostgres(data) {
try {
const {
type,
taskId,
taskTitle,
taskDescription,
authorId,
authorName,
authorLogin,
recipients = [],
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 logIds = [];
for (const recipient of recipients) {
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 [];
}
}
/**
* Обновляет статус доставки уведомления в PostgreSQL
*/
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;
}
// Экспортируем функции
module.exports = {
sendTaskNotifications,
checkUpcomingDeadlines,
sendDeadlineNotification,
getStatusText,
emailNotifications // Экспортируем для доступа к методам
};