313 Zeilen
8.9 KiB
JavaScript
313 Zeilen
8.9 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`
|
|
}
|
|
};
|
|
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* 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;
|