Files
TaskMate/backend/services/notificationService.js
hendrik_gebhardt@gmx.de 671aaadc26 Datei Upload und Download fix
2026-01-10 20:54:24 +00:00

331 Zeilen
9.5 KiB
JavaScript

/**
* 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;