email and fix

This commit is contained in:
2026-01-26 17:44:28 +05:00
parent 4985b4727b
commit 77122aa9ee
16 changed files with 3531 additions and 2330 deletions

525
email-notifications.js Normal file
View File

@@ -0,0 +1,525 @@
// 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;