Initial commit
Dieser Commit ist enthalten in:
290
backend/services/notificationService.js
Normale Datei
290
backend/services/notificationService.js
Normale Datei
@ -0,0 +1,290 @@
|
||||
/**
|
||||
* 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}"`
|
||||
},
|
||||
'approval:granted': {
|
||||
title: (data) => 'Genehmigung erteilt',
|
||||
message: (data) => `"${data.proposalTitle}" wurde genehmigt`
|
||||
},
|
||||
'approval:rejected': {
|
||||
title: (data) => 'Genehmigung abgelehnt',
|
||||
message: (data) => `"${data.proposalTitle}" wurde abgelehnt`
|
||||
}
|
||||
};
|
||||
|
||||
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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
userId,
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
data.taskId || null,
|
||||
data.projectId || null,
|
||||
data.proposalId || null,
|
||||
data.actorId || null,
|
||||
persistent ? 1 : 0
|
||||
);
|
||||
|
||||
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) {
|
||||
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
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = notificationService;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren