Files
minicrm/email-notifications.js
2026-01-27 21:47:05 +05:00

855 lines
43 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.notificationCooldown = 12 * 60 * 60 * 1000; // 12 часов в миллисекундах
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 canSendNotification(userId, taskId, notificationType) {
if (!getDb) return true; // Если БД не готова, разрешаем отправку
return new Promise((resolve, reject) => {
const db = getDb();
db.get(
`SELECT last_sent_at FROM notification_history
WHERE user_id = ? AND task_id = ? AND notification_type = ?`,
[userId, taskId, notificationType],
(err, record) => {
if (err) {
console.error('❌ Ошибка проверки истории уведомлений:', err);
resolve(true); // В случае ошибки разрешаем отправку
return;
}
if (!record) {
// Записи нет, можно отправлять
resolve(true);
return;
}
// Проверяем прошло ли 12 часов
const lastSent = new Date(record.last_sent_at);
const now = new Date();
const timeDiff = now.getTime() - lastSent.getTime();
if (timeDiff >= this.notificationCooldown) {
resolve(true);
} else {
const hoursLeft = Math.ceil((this.notificationCooldown - timeDiff) / (60 * 60 * 1000));
console.log(`⏰ Уведомление пропущено: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`);
console.log(` Последнее уведомление было отправлено: ${lastSent.toLocaleString('ru-RU')}`);
console.log(` Следующее уведомление можно отправить через: ${hoursLeft} часов`);
console.log(` Время следующей отправки: ${new Date(lastSent.getTime() + this.notificationCooldown).toLocaleString('ru-RU')}`);
resolve(false);
}
}
);
});
}
async recordNotificationSent(userId, taskId, notificationType) {
if (!getDb) return;
return new Promise((resolve, reject) => {
const db = getDb();
db.run(
`INSERT OR REPLACE INTO notification_history
(user_id, task_id, notification_type, last_sent_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)`,
[userId, taskId, notificationType],
(err) => {
if (err) {
console.error('❌ Ошибка записи истории уведомлений:', err);
reject(err);
} else {
console.log(`📝 Записана история уведомления: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`);
resolve();
}
}
);
});
}
async forceSendNotification(userId, taskId, notificationType) {
// Принудительно удаляем запись из истории, чтобы можно было отправить уведомление сразу
if (!getDb) return;
return new Promise((resolve, reject) => {
const db = getDb();
db.run(
`DELETE FROM notification_history
WHERE user_id = ? AND task_id = ? AND notification_type = ?`,
[userId, taskId, notificationType],
(err) => {
if (err) {
console.error('❌ Ошибка принудительного удаления истории:', err);
reject(err);
} else {
console.log(`♻️ История уведомления принудительно очищена: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`);
resolve();
}
}
);
});
}
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;
}
}
getTaskIdFromData(taskData) {
// Ищем ID задачи в различных возможных полях объекта
if (!taskData) {
console.error('❌ taskData is null or undefined');
return null;
}
// Проверяем различные возможные поля
if (taskData.id) {
return taskData.id;
}
if (taskData.task_id) {
return taskData.task_id;
}
if (taskData.taskId) {
return taskData.taskId;
}
// Если есть assignment_id, пытаемся найти задачу через БД
if (taskData.assignment_id && getDb) {
console.log(`🔍 Ищу ID задачи через assignment_id: ${taskData.assignment_id}`);
try {
const db = getDb();
// Используем синхронный запрос через промис
return new Promise((resolve) => {
db.get("SELECT task_id FROM task_assignments WHERE id = ?",
[taskData.assignment_id],
(err, row) => {
if (err || !row) {
console.error(`Не удалось найти задачу по assignment_id ${taskData.assignment_id}:`, err?.message);
resolve(null);
} else {
console.log(`✅ Найдена задача: ${row.task_id} для assignment_id ${taskData.assignment_id}`);
resolve(row.task_id);
}
}
);
});
} catch (error) {
console.error('❌ Ошибка поиска задачи по assignment_id:', error);
return null;
}
}
console.error('❌ Не удалось определить ID задачи из данных:',
Object.keys(taskData).length > 0 ?
JSON.stringify(taskData, null, 2).substring(0, 500) : 'empty object');
return null;
}
async sendTaskNotification(userId, taskData, notificationType) {
try {
// Получаем ID задачи (обрабатываем и синхронные и асинхронные случаи)
let taskId;
if (taskData.assignment_id && getDb) {
// Асинхронный поиск через assignment_id
taskId = await this.getTaskIdFromData(taskData);
} else {
// Синхронный поиск в полях объекта
taskId = this.getTaskIdFromData(taskData);
}
if (!taskId) {
console.error('❌ Не удалось определить ID задачи для уведомления. Данные:', {
userId,
notificationType,
taskDataKeys: Object.keys(taskData || {}),
taskDataSample: taskData ? {
id: taskData.id,
task_id: taskData.task_id,
taskId: taskData.taskId,
assignment_id: taskData.assignment_id,
title: taskData.title
} : 'null'
});
return false;
}
console.log(`🔍 Отправка уведомления: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`);
// Проверяем, можно ли отправлять уведомление
const canSend = await this.canSendNotification(userId, taskId, notificationType);
if (!canSend) {
console.log(`⏰ Пропущено уведомление для пользователя ${userId}, задача ${taskId}, тип ${notificationType} (12-часовой кд)`);
return false;
}
const settings = await this.getUserNotificationSettings(userId);
if (!settings || !settings.email_notifications) {
console.log(`⚠️ Пользователь ${userId} отключил email уведомления`);
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;
case 'overdue':
subject = `Задача просрочена: ${taskData.title}`;
htmlContent = this.getOverdueHtml(taskData);
break;
default:
subject = `Уведомление по задаче: ${taskData.title}`;
htmlContent = this.getDefaultHtml(taskData);
}
const result = await this.sendEmailNotification(emailTo, subject, htmlContent);
// Если уведомление успешно отправлено, записываем в историю
if (result) {
await this.recordNotificationSent(userId, taskId, notificationType);
console.log(`✅ Уведомление отправлено: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`);
} else {
console.log(`Не удалось отправить уведомление: пользователь ${userId}, задача ${taskId}, тип ${notificationType}`);
}
return result;
} catch (error) {
console.error('❌ Ошибка отправки уведомления о задаче:', error);
console.error('Stack trace:', error.stack);
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; }
.cooldown-notice { background: #f0f0f0; padding: 10px; border-radius: 5px; margin-top: 15px; font-size: 12px; }
</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>
${taskData.due_date ? `<p><strong>Срок выполнения:</strong> ${new Date(taskData.due_date).toLocaleString('ru-RU')}</p>` : ''}
${taskData.author_name ? `<p><strong>Создал:</strong> ${taskData.author_name}</p>` : ''}
</div>
<p>Для просмотра подробной информации перейдите в систему управления задачами.</p>
<a href="${process.env.APP_URL || 'http://localhost:3000'}" class="button">Перейти в CRM</a>
<div class="cooldown-notice">
<p>⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.</p>
<p>Вы можете изменить настройки уведомлений в личном кабинете.</p>
</div>
</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; }
.cooldown-notice { background: #f0f0f0; padding: 10px; border-radius: 5px; margin-top: 15px; font-size: 12px; }
</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">
${taskData.author_name ? `<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 class="cooldown-notice">
<p>⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.</p>
</div>
</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; }
.cooldown-notice { background: #f0f0f0; padding: 10px; border-radius: 5px; margin-top: 15px; font-size: 12px; }
</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">
${taskData.author_name ? `<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 class="cooldown-notice">
<p>⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.</p>
</div>
</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">
${taskData.author_name ? `<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) {
const statusText = this.getStatusText(taskData.status);
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; }
.status-overdue { background: #F44336; }
.status-rework { background: #FF9800; }
.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; }
.cooldown-notice { background: #f0f0f0; padding: 10px; border-radius: 5px; margin-top: 15px; font-size: 12px; }
</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}">${statusText}</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 class="cooldown-notice">
<p>⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.</p>
</div>
</div>
<div class="footer">
<p>Это автоматическое уведомление от School CRM системы.</p>
<p>Вы получили это письмо, потому что подписаны на уведомления о задачах.</p>
</div>
</div>
</body>
</html>
`;
}
getDeadlineHtml(taskData) {
const hoursLeft = taskData.hours_left || 24;
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; }
.cooldown-notice { background: #f0f0f0; padding: 10px; border-radius: 5px; margin-top: 15px; font-size: 12px; }
</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> До окончания срока задачи осталось менее ${hoursLeft} часов!</p>
${taskData.due_date ? `<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 class="cooldown-notice">
<p>⚠️ Следующее уведомление о дедлайне будет отправлено не ранее чем через 12 часов.</p>
<p>Если дедлайн изменится, вы получите новое уведомление.</p>
</div>
</div>
<div class="footer">
<p>Это автоматическое уведомление от School CRM системы.</p>
<p>Вы получили это письмо, потому что подписаны на уведомления о задачах.</p>
</div>
</div>
</body>
</html>
`;
}
getOverdueHtml(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, #D32F2F 0%, #B71C1C 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; }
.overdue-alert { background: #FFCDD2; padding: 15px; border-left: 4px solid #D32F2F; margin: 15px 0; }
.button { display: inline-block; background: #D32F2F; 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="overdue-alert">
<p><strong>ВНИМАНИЕ!</strong> Срок выполнения задачи истек!</p>
${taskData.due_date ? `<p><strong>Срок выполнения был:</strong> ${new Date(taskData.due_date).toLocaleString('ru-RU')}</p>` : ''}
<p><strong>Текущее время:</strong> ${new 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': 'На доработке',
'pending': 'В ожидании',
'in_review': 'На проверке',
'approved': 'Согласовано',
'rejected': 'Отклонено'
};
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>
`;
}
async getNotificationHistory(userId, taskId = null) {
if (!getDb) return [];
return new Promise((resolve, reject) => {
const db = getDb();
let query = `
SELECT nh.*, t.title as task_title
FROM notification_history nh
LEFT JOIN tasks t ON nh.task_id = t.id
WHERE nh.user_id = ?
`;
const params = [userId];
if (taskId) {
query += " AND nh.task_id = ?";
params.push(taskId);
}
query += " ORDER BY nh.last_sent_at DESC LIMIT 100";
db.all(query, params, (err, history) => {
if (err) {
reject(err);
} else {
resolve(history || []);
}
});
});
}
async getNotificationCooldownInfo(userId, taskId, notificationType) {
if (!getDb) return { canSend: true, timeUntilNext: 0 };
return new Promise((resolve, reject) => {
const db = getDb();
db.get(
`SELECT last_sent_at FROM notification_history
WHERE user_id = ? AND task_id = ? AND notification_type = ?`,
[userId, taskId, notificationType],
(err, record) => {
if (err) {
reject(err);
return;
}
if (!record) {
resolve({ canSend: true, timeUntilNext: 0, lastSent: null });
return;
}
const lastSent = new Date(record.last_sent_at);
const now = new Date();
const timeDiff = now.getTime() - lastSent.getTime();
const timeUntilNext = Math.max(0, this.notificationCooldown - timeDiff);
resolve({
canSend: timeDiff >= this.notificationCooldown,
timeUntilNext: timeUntilNext,
lastSent: lastSent,
nextAvailable: new Date(lastSent.getTime() + this.notificationCooldown)
});
}
);
});
}
isReady() {
return this.initialized;
}
}
// Singleton
const emailNotifications = new EmailNotifications();
module.exports = emailNotifications;