12h
This commit is contained in:
@@ -6,6 +6,7 @@ class EmailNotifications {
|
||||
constructor() {
|
||||
this.transporter = null;
|
||||
this.initialized = false;
|
||||
this.notificationCooldown = 12 * 60 * 60 * 1000; // 12 часов в миллисекундах
|
||||
this.init();
|
||||
}
|
||||
|
||||
@@ -45,17 +46,104 @@ class EmailNotifications {
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
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 {
|
||||
@@ -88,45 +176,43 @@ class EmailNotifications {
|
||||
|
||||
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) {
|
||||
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 (
|
||||
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) {
|
||||
) 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);
|
||||
});
|
||||
@@ -159,10 +245,100 @@ class EmailNotifications {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -201,15 +377,30 @@ class EmailNotifications {
|
||||
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);
|
||||
}
|
||||
|
||||
return await this.sendEmailNotification(emailTo, subject, htmlContent);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -229,6 +420,7 @@ class EmailNotifications {
|
||||
.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>
|
||||
@@ -237,18 +429,22 @@ class EmailNotifications {
|
||||
<h2>📋 Новая задача</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="task-title">${taskData.title}</div>
|
||||
<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>
|
||||
${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>
|
||||
<p>Вы получили это письмо, потому что подписаны на уведомления о задачах.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -270,6 +466,7 @@ class EmailNotifications {
|
||||
.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>
|
||||
@@ -278,17 +475,20 @@ class EmailNotifications {
|
||||
<h2>🔄 Обновлена задача</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="task-title">${taskData.title}</div>
|
||||
<div class="task-title">${taskData.title || 'Без названия'}</div>
|
||||
<div class="task-info">
|
||||
<p><strong>Изменения внес:</strong> ${taskData.author_name || 'Неизвестно'}</p>
|
||||
${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>
|
||||
<p>Вы получили это письмо, потому что подписаны на уведомления о задачах.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -311,6 +511,7 @@ class EmailNotifications {
|
||||
.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>
|
||||
@@ -319,9 +520,9 @@ class EmailNotifications {
|
||||
<h2>🔄 Задача возвращена на доработку</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="task-title">${taskData.title}</div>
|
||||
<div class="task-title">${taskData.title || 'Без названия'}</div>
|
||||
<div class="task-info">
|
||||
<p><strong>Автор замечания:</strong> ${taskData.author_name || 'Неизвестно'}</p>
|
||||
${taskData.author_name ? `<p><strong>Автор замечания:</strong> ${taskData.author_name}</p>` : ''}
|
||||
</div>
|
||||
<div class="comment-box">
|
||||
<p><strong>Комментарий:</strong></p>
|
||||
@@ -329,10 +530,13 @@ class EmailNotifications {
|
||||
</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>
|
||||
<p>Вы получили это письмо, потому что подписаны на уведомления о задачах.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -362,9 +566,9 @@ class EmailNotifications {
|
||||
<h2>✅ Задача закрыта</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="task-title">${taskData.title}</div>
|
||||
<div class="task-title">${taskData.title || 'Без названия'}</div>
|
||||
<div class="task-info">
|
||||
<p><strong>Закрыта:</strong> ${taskData.author_name || 'Неизвестно'}</p>
|
||||
${taskData.author_name ? `<p><strong>Закрыта:</strong> ${taskData.author_name}</p>` : ''}
|
||||
<p><strong>Время закрытия:</strong> ${new Date().toLocaleString('ru-RU')}</p>
|
||||
</div>
|
||||
<p>Задача завершена и перемещена в архив.</p>
|
||||
@@ -372,7 +576,7 @@ class EmailNotifications {
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Это автоматическое уведомление от School CRM системы.</p>
|
||||
<p>Вы можете изменить настройки уведомлений в личном кабинете.</p>
|
||||
<p>Вы получили это письмо, потому что подписаны на уведомления о задачах.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -381,6 +585,7 @@ class EmailNotifications {
|
||||
}
|
||||
|
||||
getStatusChangedHtml(taskData) {
|
||||
const statusText = this.getStatusText(taskData.status);
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -396,8 +601,11 @@ class EmailNotifications {
|
||||
.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>
|
||||
@@ -406,18 +614,21 @@ class EmailNotifications {
|
||||
<h2>🔄 Изменен статус задачи</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="task-title">${taskData.title}</div>
|
||||
<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> <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>
|
||||
<p>Вы получили это письмо, потому что подписаны на уведомления о задачах.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -426,6 +637,7 @@ class EmailNotifications {
|
||||
}
|
||||
|
||||
getDeadlineHtml(taskData) {
|
||||
const hoursLeft = taskData.hours_left || 24;
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -440,6 +652,7 @@ class EmailNotifications {
|
||||
.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>
|
||||
@@ -448,17 +661,63 @@ class EmailNotifications {
|
||||
<h2>⚠️ Скоро срок выполнения</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="task-title">${taskData.title}</div>
|
||||
<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>
|
||||
<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>
|
||||
<p>Вы получили это письмо, потому что подписаны на уведомления о задачах.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -472,7 +731,11 @@ class EmailNotifications {
|
||||
'in_progress': 'В работе',
|
||||
'completed': 'Завершена',
|
||||
'overdue': 'Просрочена',
|
||||
'rework': 'На доработке'
|
||||
'rework': 'На доработке',
|
||||
'pending': 'В ожидании',
|
||||
'in_review': 'На проверке',
|
||||
'approved': 'Согласовано',
|
||||
'rejected': 'Отклонено'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
@@ -499,7 +762,7 @@ class EmailNotifications {
|
||||
<h2>📢 Уведомление от School CRM</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="task-title">${taskData.title}</div>
|
||||
<div class="task-title">${taskData.title || 'Без названия'}</div>
|
||||
<div class="task-info">
|
||||
<p>${taskData.message || 'Новое уведомление по задаче'}</p>
|
||||
</div>
|
||||
@@ -507,7 +770,7 @@ class EmailNotifications {
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Это автоматическое уведомление от School CRM системы.</p>
|
||||
<p>Вы можете изменить настройки уведомлений в личном кабинете.</p>
|
||||
<p>Вы получили это письмо, потому что подписаны на уведомления о задачах.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -515,6 +778,73 @@ class EmailNotifications {
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user