Files
minicrm/email-notifications.js
2026-01-26 17:44:28 +05:00

525 lines
26 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.
// email-notifications.js
const nodemailer = require('nodemailer');
const { getDb } = require('./database');
class EmailNotifications {
constructor() {
this.transporter = null;
this.initialized = false;
this.init();
}
async init() {
try {
console.log('🔧 Инициализация Email уведомлений...');
if (!process.env.YANDEX_EMAIL || !process.env.YANDEX_PASSWORD) {
console.warn('⚠️ Настройки Яндекс почты не указаны в .env');
console.warn(' Email уведомления будут отключены');
this.initialized = false;
return;
}
this.transporter = nodemailer.createTransport({
host: process.env.YANDEX_SMTP_HOST || 'smtp.yandex.ru',
port: parseInt(process.env.YANDEX_SMTP_PORT) || 587,
secure: process.env.YANDEX_SMTP_SECURE === 'true',
auth: {
user: process.env.YANDEX_EMAIL,
pass: process.env.YANDEX_PASSWORD
},
tls: {
rejectUnauthorized: false
}
});
// Тестируем подключение
await this.transporter.verify();
this.initialized = true;
console.log('✅ Email уведомления инициализированы');
console.log(`📧 Отправитель: ${process.env.YANDEX_EMAIL}`);
} catch (error) {
console.error('❌ Ошибка инициализации Email уведомлений:', error.message);
this.initialized = false;
}
}
async getUserNotificationSettings(userId) {
if (!getDb) return null;
return new Promise((resolve, reject) => {
const db = getDb();
db.get(`
SELECT us.*, u.email as user_email, u.name as user_name
FROM user_settings us
LEFT JOIN users u ON us.user_id = u.id
WHERE us.user_id = ?
`, [userId], (err, settings) => {
if (err) {
reject(err);
} else {
resolve(settings);
}
});
});
}
async saveUserNotificationSettings(userId, settings) {
if (!getDb) return false;
return new Promise((resolve, reject) => {
const db = getDb();
const {
email_notifications = true,
notification_email = '',
telegram_notifications = false,
telegram_chat_id = '',
vk_notifications = false,
vk_user_id = ''
} = settings;
// Проверяем существование записи
db.get("SELECT id FROM user_settings WHERE user_id = ?", [userId], (err, existing) => {
if (err) {
reject(err);
return;
}
if (existing) {
// Обновляем существующую запись
db.run(`
UPDATE user_settings
SET email_notifications = ?,
notification_email = ?,
telegram_notifications = ?,
telegram_chat_id = ?,
vk_notifications = ?,
vk_user_id = ?,
updated_at = CURRENT_TIMESTAMP
WHERE user_id = ?
`, [
email_notifications ? 1 : 0,
notification_email,
telegram_notifications ? 1 : 0,
telegram_chat_id,
vk_notifications ? 1 : 0,
vk_user_id,
userId
], function(err) {
if (err) reject(err);
else resolve(true);
});
} else {
// Создаем новую запись
db.run(`
INSERT INTO user_settings (
user_id, email_notifications, notification_email,
telegram_notifications, telegram_chat_id,
vk_notifications, vk_user_id
) VALUES (?, ?, ?, ?, ?, ?, ?)
`, [
userId,
email_notifications ? 1 : 0,
notification_email,
telegram_notifications ? 1 : 0,
telegram_chat_id,
vk_notifications ? 1 : 0,
vk_user_id
], function(err) {
if (err) reject(err);
else resolve(true);
});
}
});
});
}
async sendEmailNotification(to, subject, htmlContent) {
if (!this.initialized || !this.transporter) {
console.warn('⚠️ Email уведомления отключены');
return false;
}
try {
const info = await this.transporter.sendMail({
from: `"School CRM" <${process.env.YANDEX_EMAIL}>`,
to: to,
subject: subject,
html: htmlContent,
text: htmlContent.replace(/<[^>]*>/g, '') // Конвертируем HTML в текст
});
console.log(`📧 Email отправлен: ${to}, Message ID: ${info.messageId}`);
return true;
} catch (error) {
console.error('❌ Ошибка отправки email:', error.message);
return false;
}
}
async sendTaskNotification(userId, taskData, notificationType) {
try {
const settings = await this.getUserNotificationSettings(userId);
if (!settings || !settings.email_notifications) {
return false;
}
// Используем указанную email или email из профиля пользователя
const emailTo = settings.notification_email || settings.user_email;
if (!emailTo) {
console.log(`⚠️ У пользователя ${userId} не указан email для уведомлений`);
return false;
}
let subject = '';
let htmlContent = '';
switch (notificationType) {
case 'created':
subject = `Новая задача: ${taskData.title}`;
htmlContent = this.getTaskCreatedHtml(taskData);
break;
case 'updated':
subject = `Обновлена задача: ${taskData.title}`;
htmlContent = this.getTaskUpdatedHtml(taskData);
break;
case 'rework':
subject = `Задача возвращена на доработку: ${taskData.title}`;
htmlContent = this.getTaskReworkHtml(taskData);
break;
case 'closed':
subject = `Задача закрыта: ${taskData.title}`;
htmlContent = this.getTaskClosedHtml(taskData);
break;
case 'status_changed':
subject = `Изменен статус задачи: ${taskData.title}`;
htmlContent = this.getStatusChangedHtml(taskData);
break;
case 'deadline':
subject = `Скоро срок выполнения: ${taskData.title}`;
htmlContent = this.getDeadlineHtml(taskData);
break;
default:
subject = `Уведомление по задаче: ${taskData.title}`;
htmlContent = this.getDefaultHtml(taskData);
}
return await this.sendEmailNotification(emailTo, subject, htmlContent);
} catch (error) {
console.error('❌ Ошибка отправки уведомления о задаче:', error);
return false;
}
}
// HTML шаблоны для разных типов уведомлений
getTaskCreatedHtml(taskData) {
return `
<!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: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px 10px 0 0; }
.content { padding: 20px; border: 1px solid #ddd; border-top: none; }
.task-title { font-size: 18px; font-weight: bold; margin-bottom: 10px; }
.task-info { margin-bottom: 15px; }
.button { display: inline-block; background: #667eea; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; }
.footer { margin-top: 20px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>📋 Новая задача</h2>
</div>
<div class="content">
<div class="task-title">${taskData.title}</div>
<div class="task-info">
<p><strong>Описание:</strong> ${taskData.description || 'Без описания'}</p>
<p><strong>Срок выполнения:</strong> ${new Date(taskData.due_date).toLocaleString('ru-RU')}</p>
<p><strong>Создал:</strong> ${taskData.author_name || 'Неизвестно'}</p>
</div>
<p>Для просмотра подробной информации перейдите в систему управления задачами.</p>
<a href="${process.env.APP_URL || 'http://localhost:3000'}" class="button">Перейти в CRM</a>
</div>
<div class="footer">
<p>Это автоматическое уведомление от School CRM системы.</p>
<p>Вы можете изменить настройки уведомлений в личном кабинете.</p>
</div>
</div>
</body>
</html>
`;
}
getTaskUpdatedHtml(taskData) {
return `
<!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: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%); color: white; padding: 20px; border-radius: 10px 10px 0 0; }
.content { padding: 20px; border: 1px solid #ddd; border-top: none; }
.task-title { font-size: 18px; font-weight: bold; margin-bottom: 10px; }
.task-info { margin-bottom: 15px; }
.button { display: inline-block; background: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; }
.footer { margin-top: 20px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>🔄 Обновлена задача</h2>
</div>
<div class="content">
<div class="task-title">${taskData.title}</div>
<div class="task-info">
<p><strong>Изменения внес:</strong> ${taskData.author_name || 'Неизвестно'}</p>
<p><strong>Время:</strong> ${new Date().toLocaleString('ru-RU')}</p>
</div>
<p>Для просмотра изменений перейдите в систему управления задачами.</p>
<a href="${process.env.APP_URL || 'http://localhost:3000'}" class="button">Перейти в CRM</a>
</div>
<div class="footer">
<p>Это автоматическое уведомление от School CRM системы.</p>
<p>Вы можете изменить настройки уведомлений в личном кабинете.</p>
</div>
</div>
</body>
</html>
`;
}
getTaskReworkHtml(taskData) {
return `
<!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: linear-gradient(135deg, #FF9800 0%, #F57C00 100%); color: white; padding: 20px; border-radius: 10px 10px 0 0; }
.content { padding: 20px; border: 1px solid #ddd; border-top: none; }
.task-title { font-size: 18px; font-weight: bold; margin-bottom: 10px; }
.task-info { margin-bottom: 15px; }
.comment-box { background: #FFF3E0; padding: 15px; border-left: 4px solid #FF9800; margin: 15px 0; }
.button { display: inline-block; background: #FF9800; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; }
.footer { margin-top: 20px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>🔄 Задача возвращена на доработку</h2>
</div>
<div class="content">
<div class="task-title">${taskData.title}</div>
<div class="task-info">
<p><strong>Автор замечания:</strong> ${taskData.author_name || 'Неизвестно'}</p>
</div>
<div class="comment-box">
<p><strong>Комментарий:</strong></p>
<p>${taskData.comment || 'Требуется доработка'}</p>
</div>
<p>Пожалуйста, исправьте замечания и обновите статус задачи.</p>
<a href="${process.env.APP_URL || 'http://localhost:3000'}" class="button">Перейти к задаче</a>
</div>
<div class="footer">
<p>Это автоматическое уведомление от School CRM системы.</p>
<p>Вы можете изменить настройки уведомлений в личном кабинете.</p>
</div>
</div>
</body>
</html>
`;
}
getTaskClosedHtml(taskData) {
return `
<!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: linear-gradient(135deg, #9E9E9E 0%, #616161 100%); color: white; padding: 20px; border-radius: 10px 10px 0 0; }
.content { padding: 20px; border: 1px solid #ddd; border-top: none; }
.task-title { font-size: 18px; font-weight: bold; margin-bottom: 10px; }
.task-info { margin-bottom: 15px; }
.button { display: inline-block; background: #9E9E9E; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; }
.footer { margin-top: 20px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>✅ Задача закрыта</h2>
</div>
<div class="content">
<div class="task-title">${taskData.title}</div>
<div class="task-info">
<p><strong>Закрыта:</strong> ${taskData.author_name || 'Неизвестно'}</p>
<p><strong>Время закрытия:</strong> ${new Date().toLocaleString('ru-RU')}</p>
</div>
<p>Задача завершена и перемещена в архив.</p>
<a href="${process.env.APP_URL || 'http://localhost:3000'}" class="button">Перейти в CRM</a>
</div>
<div class="footer">
<p>Это автоматическое уведомление от School CRM системы.</p>
<p>Вы можете изменить настройки уведомлений в личном кабинете.</p>
</div>
</div>
</body>
</html>
`;
}
getStatusChangedHtml(taskData) {
return `
<!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: linear-gradient(135deg, #2196F3 0%, #1976D2 100%); color: white; padding: 20px; border-radius: 10px 10px 0 0; }
.content { padding: 20px; border: 1px solid #ddd; border-top: none; }
.task-title { font-size: 18px; font-weight: bold; margin-bottom: 10px; }
.task-info { margin-bottom: 15px; }
.status-badge { display: inline-block; padding: 5px 10px; border-radius: 3px; color: white; }
.status-assigned { background: #FF9800; }
.status-in-progress { background: #2196F3; }
.status-completed { background: #4CAF50; }
.button { display: inline-block; background: #2196F3; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; }
.footer { margin-top: 20px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>🔄 Изменен статус задачи</h2>
</div>
<div class="content">
<div class="task-title">${taskData.title}</div>
<div class="task-info">
<p><strong>Новый статус:</strong> <span class="status-badge status-${taskData.status}">${this.getStatusText(taskData.status)}</span></p>
<p><strong>Изменил:</strong> ${taskData.user_name || taskData.author_name || 'Неизвестно'}</p>
<p><strong>Время:</strong> ${new Date().toLocaleString('ru-RU')}</p>
</div>
<p>Для просмотра деталей перейдите в систему управления задачами.</p>
<a href="${process.env.APP_URL || 'http://localhost:3000'}" class="button">Перейти в CRM</a>
</div>
<div class="footer">
<p>Это автоматическое уведомление от School CRM системы.</p>
<p>Вы можете изменить настройки уведомлений в личном кабинете.</p>
</div>
</div>
</body>
</html>
`;
}
getDeadlineHtml(taskData) {
return `
<!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: linear-gradient(135deg, #F44336 0%, #D32F2F 100%); color: white; padding: 20px; border-radius: 10px 10px 0 0; }
.content { padding: 20px; border: 1px solid #ddd; border-top: none; }
.task-title { font-size: 18px; font-weight: bold; margin-bottom: 10px; }
.task-info { margin-bottom: 15px; }
.deadline-warning { background: #FFEBEE; padding: 15px; border-left: 4px solid #F44336; margin: 15px 0; }
.button { display: inline-block; background: #F44336; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; }
.footer { margin-top: 20px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>⚠️ Скоро срок выполнения</h2>
</div>
<div class="content">
<div class="task-title">${taskData.title}</div>
<div class="deadline-warning">
<p><strong>ВНИМАНИЕ!</strong> До окончания срока задачи осталось менее ${taskData.hours_left} часов!</p>
<p><strong>Срок выполнения:</strong> ${new Date(taskData.due_date).toLocaleString('ru-RU')}</p>
</div>
<p>Пожалуйста, завершите задачу в указанный срок.</p>
<a href="${process.env.APP_URL || 'http://localhost:3000'}" class="button">Перейти к задаче</a>
</div>
<div class="footer">
<p>Это автоматическое уведомление от School CRM системы.</p>
<p>Вы можете изменить настройки уведомлений в личном кабинете.</p>
</div>
</div>
</body>
</html>
`;
}
getStatusText(status) {
const statusMap = {
'assigned': 'Назначена',
'in_progress': 'В работе',
'completed': 'Завершена',
'overdue': 'Просрочена',
'rework': 'На доработке'
};
return statusMap[status] || status;
}
getDefaultHtml(taskData) {
return `
<!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: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px 10px 0 0; }
.content { padding: 20px; border: 1px solid #ddd; border-top: none; }
.task-title { font-size: 18px; font-weight: bold; margin-bottom: 10px; }
.task-info { margin-bottom: 15px; }
.button { display: inline-block; background: #667eea; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; }
.footer { margin-top: 20px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>📢 Уведомление от School CRM</h2>
</div>
<div class="content">
<div class="task-title">${taskData.title}</div>
<div class="task-info">
<p>${taskData.message || 'Новое уведомление по задаче'}</p>
</div>
<a href="${process.env.APP_URL || 'http://localhost:3000'}" class="button">Перейти в CRM</a>
</div>
<div class="footer">
<p>Это автоматическое уведомление от School CRM системы.</p>
<p>Вы можете изменить настройки уведомлений в личном кабинете.</p>
</div>
</div>
</body>
</html>
`;
}
isReady() {
return this.initialized;
}
}
// Singleton
const emailNotifications = new EmailNotifications();
module.exports = emailNotifications;