/** * TASKMATE - Notification Service * ================================ * Zentrale Logik für das Benachrichtigungssystem */ const { getDb } = require('../database'); const logger = require('../utils/logger'); /** * Benachrichtigungstypen mit Titeln und Icons */ const NOTIFICATION_TYPES = { 'task:assigned': { title: (data) => 'Neue Aufgabe zugewiesen', message: (data) => `Du wurdest der Aufgabe "${data.taskTitle}" zugewiesen` }, 'task:unassigned': { title: (data) => 'Zuweisung entfernt', message: (data) => `Du wurdest von der Aufgabe "${data.taskTitle}" entfernt` }, 'task:due_soon': { title: (data) => 'Aufgabe bald fällig', message: (data) => `Die Aufgabe "${data.taskTitle}" ist morgen fällig` }, 'task:completed': { title: (data) => 'Aufgabe erledigt', message: (data) => `Die Aufgabe "${data.taskTitle}" wurde erledigt` }, 'task:due_changed': { title: (data) => 'Fälligkeitsdatum geändert', message: (data) => `Das Fälligkeitsdatum von "${data.taskTitle}" wurde geändert` }, 'task:priority_up': { title: (data) => 'Priorität erhöht', message: (data) => `Die Priorität von "${data.taskTitle}" wurde auf "Hoch" gesetzt` }, 'comment:created': { title: (data) => 'Neuer Kommentar', message: (data) => `${data.actorName} hat "${data.taskTitle}" kommentiert` }, 'comment:mention': { title: (data) => 'Du wurdest erwähnt', message: (data) => `${data.actorName} hat dich in "${data.taskTitle}" erwähnt` }, 'approval:pending': { title: (data) => 'Genehmigung erforderlich', message: (data) => `Neue Genehmigung: "${data.proposalTitle}"` }, 'reminder:due': { title: (data) => 'Erinnerung', message: (data) => `${data.reminderTitle} - ${data.daysAdvance === '0' ? 'Heute' : `in ${data.daysAdvance} Tag${data.daysAdvance > 1 ? 'en' : ''}`}` }, 'approval:granted': { title: (data) => 'Genehmigung erteilt', message: (data) => `"${data.proposalTitle}" wurde genehmigt` }, 'approval:rejected': { title: (data) => 'Genehmigung abgelehnt', message: (data) => `"${data.proposalTitle}" wurde abgelehnt` }, 'knowledge:new_entry': { title: (data) => 'Neuer Wissenseintrag', message: (data) => `Neuer Eintrag: "${data.entryTitle}" in ${data.categoryName}` } }; const notificationService = { /** * Benachrichtigung erstellen und per WebSocket senden * @param {number} userId - Empfänger * @param {string} type - Benachrichtigungstyp * @param {object} data - Zusätzliche Daten * @param {object} io - Socket.io Instanz * @param {boolean} persistent - Ob die Benachrichtigung persistent ist */ create(userId, type, data, io, persistent = false) { try { const db = getDb(); const typeConfig = NOTIFICATION_TYPES[type]; if (!typeConfig) { logger.warn(`Unbekannter Benachrichtigungstyp: ${type}`); return null; } const title = typeConfig.title(data); const message = typeConfig.message(data); const result = db.prepare(` INSERT INTO notifications (user_id, type, title, message, task_id, project_id, proposal_id, actor_id, is_persistent, data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( userId, type, title, message, data.taskId || null, data.projectId || null, data.proposalId || null, data.actorId || null, persistent ? 1 : 0, JSON.stringify(data) // Zusätzliche Daten als JSON speichern ); const notification = db.prepare(` SELECT n.*, u.display_name as actor_name, u.color as actor_color FROM notifications n LEFT JOIN users u ON n.actor_id = u.id WHERE n.id = ? `).get(result.lastInsertRowid); // WebSocket-Event senden if (io) { io.to(`user:${userId}`).emit('notification:new', { notification: this.formatNotification(notification) }); // Auch aktualisierte Zählung senden const count = this.getUnreadCount(userId); io.to(`user:${userId}`).emit('notification:count', { count }); } logger.info(`Benachrichtigung erstellt: ${type} für User ${userId}`); return notification; } catch (error) { logger.error('Fehler beim Erstellen der Benachrichtigung:', error); return null; } }, /** * Alle Benachrichtigungen für einen User abrufen */ getForUser(userId, limit = 50) { const db = getDb(); const notifications = db.prepare(` SELECT n.*, u.display_name as actor_name, u.color as actor_color FROM notifications n LEFT JOIN users u ON n.actor_id = u.id WHERE n.user_id = ? ORDER BY n.is_persistent DESC, n.created_at DESC LIMIT ? `).all(userId, limit); return notifications.map(n => this.formatNotification(n)); }, /** * Ungelesene Anzahl ermitteln */ getUnreadCount(userId) { const db = getDb(); const result = db.prepare(` SELECT COUNT(*) as count FROM notifications WHERE user_id = ? AND is_read = 0 `).get(userId); return result.count; }, /** * Als gelesen markieren */ markAsRead(notificationId, userId) { const db = getDb(); const result = db.prepare(` UPDATE notifications SET is_read = 1 WHERE id = ? AND user_id = ? `).run(notificationId, userId); return result.changes > 0; }, /** * Alle als gelesen markieren */ markAllAsRead(userId) { const db = getDb(); const result = db.prepare(` UPDATE notifications SET is_read = 1 WHERE user_id = ? AND is_read = 0 `).run(userId); return result.changes; }, /** * Benachrichtigung löschen (nur nicht-persistente) */ delete(notificationId, userId) { const db = getDb(); const result = db.prepare(` DELETE FROM notifications WHERE id = ? AND user_id = ? AND is_persistent = 0 `).run(notificationId, userId); return result.changes > 0; }, /** * Persistente Benachrichtigungen auflösen (z.B. bei Genehmigung) */ resolvePersistent(proposalId) { const db = getDb(); const result = db.prepare(` DELETE FROM notifications WHERE proposal_id = ? AND is_persistent = 1 `).run(proposalId); logger.info(`${result.changes} persistente Benachrichtigungen für Proposal ${proposalId} aufgelöst`); return result.changes; }, /** * Fälligkeits-Check für Aufgaben (1 Tag vorher) */ checkDueTasks(io) { try { const db = getDb(); // Morgen berechnen const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const tomorrowStr = tomorrow.toISOString().split('T')[0]; // Aufgaben die morgen fällig sind const tasks = db.prepare(` SELECT t.id, t.title, t.project_id, ta.user_id as assignee_id FROM tasks t JOIN task_assignees ta ON t.id = ta.task_id LEFT JOIN columns c ON t.column_id = c.id WHERE t.due_date = ? AND t.archived = 0 AND c.filter_category != 'completed' AND NOT EXISTS ( SELECT 1 FROM notifications n WHERE n.task_id = t.id AND n.user_id = ta.user_id AND n.type = 'task:due_soon' AND DATE(n.created_at) = DATE('now') ) `).all(tomorrowStr); let count = 0; tasks.forEach(task => { this.create(task.assignee_id, 'task:due_soon', { taskId: task.id, taskTitle: task.title, projectId: task.project_id }, io); count++; }); if (count > 0) { logger.info(`${count} Fälligkeits-Benachrichtigungen erstellt`); } return count; } catch (error) { logger.error('Fehler beim Fälligkeits-Check:', error); return 0; } }, /** * Benachrichtigung formatieren für Frontend */ formatNotification(notification) { // Parse zusätzliche Daten wenn vorhanden let additionalData = {}; if (notification.data) { try { additionalData = JSON.parse(notification.data); } catch (e) { // Ignore parse errors } } return { id: notification.id, userId: notification.user_id, type: notification.type, title: notification.title, message: notification.message, taskId: notification.task_id, projectId: notification.project_id, proposalId: notification.proposal_id, actorId: notification.actor_id, actorName: notification.actor_name, actorColor: notification.actor_color, isRead: notification.is_read === 1, isPersistent: notification.is_persistent === 1, createdAt: notification.created_at, // Zusätzliche Daten für Knowledge-Einträge entryId: additionalData.entryId || null, categoryId: additionalData.categoryId || null }; }, /** * Benachrichtigung an mehrere User senden */ createForMultiple(userIds, type, data, io, persistent = false) { const results = []; userIds.forEach(userId => { const result = this.create(userId, type, data, io, persistent); if (result) results.push(result); }); return results; }, /** * Reminder-Benachrichtigung erstellen */ createReminderNotification(reminder, daysAdvance, io) { return this.create( reminder.created_by, 'reminder:due', { reminderTitle: reminder.title, daysAdvance: daysAdvance.toString(), projectId: reminder.project_id, reminderId: reminder.id }, io, false ); } }; module.exports = notificationService;