Dieser Commit ist enthalten in:
Claude Project Manager
2025-12-28 21:36:45 +00:00
Commit ab1e5be9a9
146 geänderte Dateien mit 65525 neuen und 0 gelöschten Zeilen

Datei anzeigen

@ -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;