diff --git a/database.js b/database.js index 36dfae6..82bb00d 100644 --- a/database.js +++ b/database.js @@ -93,6 +93,18 @@ function initializeSQLite() { } function createSQLiteTables() { + // notification_history + db.run(`CREATE TABLE IF NOT EXISTS notification_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + task_id INTEGER NOT NULL, + notification_type TEXT NOT NULL, + last_sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id), + FOREIGN KEY (task_id) REFERENCES tasks (id), + UNIQUE(user_id, task_id, notification_type) + )`); // SQLite таблицы db.run(`CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/email-notifications.js b/email-notifications.js index 374f8d6..2bb9260 100644 --- a/email-notifications.js +++ b/email-notifications.js @@ -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; } @@ -237,18 +429,22 @@ class EmailNotifications {

📋 Новая задача

-
${taskData.title}
+
${taskData.title || 'Без названия'}

Описание: ${taskData.description || 'Без описания'}

-

Срок выполнения: ${new Date(taskData.due_date).toLocaleString('ru-RU')}

-

Создал: ${taskData.author_name || 'Неизвестно'}

+ ${taskData.due_date ? `

Срок выполнения: ${new Date(taskData.due_date).toLocaleString('ru-RU')}

` : ''} + ${taskData.author_name ? `

Создал: ${taskData.author_name}

` : ''}

Для просмотра подробной информации перейдите в систему управления задачами.

Перейти в CRM +
+

⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.

+

Вы можете изменить настройки уведомлений в личном кабинете.

+
@@ -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; } @@ -278,17 +475,20 @@ class EmailNotifications {

🔄 Обновлена задача

-
${taskData.title}
+
${taskData.title || 'Без названия'}
-

Изменения внес: ${taskData.author_name || 'Неизвестно'}

+ ${taskData.author_name ? `

Изменения внес: ${taskData.author_name}

` : ''}

Время: ${new Date().toLocaleString('ru-RU')}

Для просмотра изменений перейдите в систему управления задачами.

Перейти в CRM +
+

⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.

+
@@ -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; } @@ -319,9 +520,9 @@ class EmailNotifications {

🔄 Задача возвращена на доработку

-
${taskData.title}
+
${taskData.title || 'Без названия'}
-

Автор замечания: ${taskData.author_name || 'Неизвестно'}

+ ${taskData.author_name ? `

Автор замечания: ${taskData.author_name}

` : ''}

Комментарий:

@@ -329,10 +530,13 @@ class EmailNotifications {

Пожалуйста, исправьте замечания и обновите статус задачи.

Перейти к задаче +
+

⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.

+
@@ -362,9 +566,9 @@ class EmailNotifications {

✅ Задача закрыта

-
${taskData.title}
+
${taskData.title || 'Без названия'}
-

Закрыта: ${taskData.author_name || 'Неизвестно'}

+ ${taskData.author_name ? `

Закрыта: ${taskData.author_name}

` : ''}

Время закрытия: ${new Date().toLocaleString('ru-RU')}

Задача завершена и перемещена в архив.

@@ -372,7 +576,7 @@ class EmailNotifications {
@@ -381,6 +585,7 @@ class EmailNotifications { } getStatusChangedHtml(taskData) { + const statusText = this.getStatusText(taskData.status); return ` @@ -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; } @@ -406,18 +614,21 @@ class EmailNotifications {

🔄 Изменен статус задачи

-
${taskData.title}
+
${taskData.title || 'Без названия'}
-

Новый статус: ${this.getStatusText(taskData.status)}

+

Новый статус: ${statusText}

Изменил: ${taskData.user_name || taskData.author_name || 'Неизвестно'}

Время: ${new Date().toLocaleString('ru-RU')}

Для просмотра деталей перейдите в систему управления задачами.

Перейти в CRM +
+

⚠️ Следующее уведомление по этой задаче будет отправлено не ранее чем через 12 часов.

+
@@ -426,6 +637,7 @@ class EmailNotifications { } getDeadlineHtml(taskData) { + const hoursLeft = taskData.hours_left || 24; return ` @@ -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; } @@ -448,17 +661,63 @@ class EmailNotifications {

⚠️ Скоро срок выполнения

-
${taskData.title}
+
${taskData.title || 'Без названия'}
-

ВНИМАНИЕ! До окончания срока задачи осталось менее ${taskData.hours_left} часов!

-

Срок выполнения: ${new Date(taskData.due_date).toLocaleString('ru-RU')}

+

ВНИМАНИЕ! До окончания срока задачи осталось менее ${hoursLeft} часов!

+ ${taskData.due_date ? `

Срок выполнения: ${new Date(taskData.due_date).toLocaleString('ru-RU')}

` : ''}

Пожалуйста, завершите задачу в указанный срок.

Перейти к задаче +
+

⚠️ Следующее уведомление о дедлайне будет отправлено не ранее чем через 12 часов.

+

Если дедлайн изменится, вы получите новое уведомление.

+
+
+ + + + + `; + } + + getOverdueHtml(taskData) { + return ` + + + + + + +
+
+

🚨 Задача просрочена

+
+
+
${taskData.title || 'Без названия'}
+
+

ВНИМАНИЕ! Срок выполнения задачи истек!

+ ${taskData.due_date ? `

Срок выполнения был: ${new Date(taskData.due_date).toLocaleString('ru-RU')}

` : ''} +

Текущее время: ${new Date().toLocaleString('ru-RU')}

+
+

Пожалуйста, завершите задачу как можно скорее и обновите ее статус.

+ Перейти к задаче
@@ -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 {

📢 Уведомление от School CRM

-
${taskData.title}
+
${taskData.title || 'Без названия'}

${taskData.message || 'Новое уведомление по задаче'}

@@ -507,7 +770,7 @@ class EmailNotifications {
@@ -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; } diff --git a/public/admin-doc.html b/public/admin-doc.html index 25d8174..ad9c369 100644 --- a/public/admin-doc.html +++ b/public/admin-doc.html @@ -842,7 +842,7 @@ // Вернуться в CRM function goBack() { - window.location.href = '/admin'; + window.location.href = '/'; } // Инициализация diff --git a/public/admin.html b/public/admin.html index f63dffa..11cf8f2 100644 --- a/public/admin.html +++ b/public/admin.html @@ -272,7 +272,8 @@
- + +
diff --git a/public/doc.html b/public/doc.html index f37b124..91de44d 100644 --- a/public/doc.html +++ b/public/doc.html @@ -3,431 +3,317 @@ - Согласование документов - School CRM + School CRM - Управление согласованиями DOC - -
-
+
+
-

Согласование документов

+

School CRM - Управление согласованиями DOC

- -
- -
-

Создание документа для согласования

- -
-
-
- - +
+

Все согласования

+
+
+
+ +
- -
- - + + + + + + + +
- -
- - -
- -
- - -
- -
- - -
- -
- - +
- -
- - +
+ + +
+
+ +
- -
- - + +
+
+
+ +
+

Создать новое согласование DOC

+ +
+ +
-
- - +
+ +
- -
- + +
+ + +
+ +
+ + +
+ + В качестве исполнителей можно выбрать только пользователей с ролью "Секретарь" + +
+ +
+
- -
-
+
- +
- -
-

Мои документы на согласование

-
- -
-
- - -
-

Документы для согласования

-
- -
+
+

Лог активности

+
- -