Kontakt-Modul
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
623bbdf5dd
Commit
7d67557be4
@ -485,6 +485,41 @@ function createTables() {
|
||||
)
|
||||
`);
|
||||
|
||||
// Erinnerungen
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS reminders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
reminder_date DATE NOT NULL,
|
||||
reminder_time TIME DEFAULT '09:00',
|
||||
color TEXT DEFAULT '#F59E0B',
|
||||
advance_days TEXT DEFAULT '1',
|
||||
repeat_type TEXT DEFAULT 'none',
|
||||
repeat_interval INTEGER DEFAULT 1,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_by INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Erinnerungs-Benachrichtigungen (für Tracking welche bereits gesendet wurden)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS reminder_notifications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
reminder_id INTEGER NOT NULL,
|
||||
notification_date DATE NOT NULL,
|
||||
sent INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE CASCADE,
|
||||
UNIQUE(reminder_id, notification_date)
|
||||
)
|
||||
`);
|
||||
|
||||
// Wissensmanagement - Kategorien
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS knowledge_categories (
|
||||
@ -561,6 +596,31 @@ function createTables() {
|
||||
logger.info('Migration: claude_instructions Spalte zu coding_directories hinzugefuegt');
|
||||
}
|
||||
|
||||
// Kontakte
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
company TEXT,
|
||||
position TEXT,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
mobile TEXT,
|
||||
address TEXT,
|
||||
postal_code TEXT,
|
||||
city TEXT,
|
||||
country TEXT,
|
||||
website TEXT,
|
||||
notes TEXT,
|
||||
tags TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INTEGER,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Indizes für Performance
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
||||
@ -581,6 +641,8 @@ function createTables() {
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_entries_category ON knowledge_entries(category_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_attachments_entry ON knowledge_attachments(entry_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_coding_directories_position ON coding_directories(position);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_company ON contacts(company);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_tags ON contacts(tags);
|
||||
`);
|
||||
|
||||
logger.info('Datenbank-Tabellen erstellt');
|
||||
|
||||
@ -305,6 +305,46 @@ function sanitizeMiddleware(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kontakt-Validierung Middleware
|
||||
*/
|
||||
validators.contact = function(req, res, next) {
|
||||
const errors = [];
|
||||
const { firstName, lastName, company, email, phone, mobile, website } = req.body;
|
||||
|
||||
// Mindestens ein Name oder Firma muss vorhanden sein
|
||||
if (!firstName && !lastName && !company) {
|
||||
errors.push('Mindestens Vorname, Nachname oder Firma muss angegeben werden');
|
||||
}
|
||||
|
||||
// Email validieren
|
||||
if (email) {
|
||||
const emailError = validators.email(email, 'E-Mail');
|
||||
if (emailError) errors.push(emailError);
|
||||
}
|
||||
|
||||
// Website URL validieren
|
||||
if (website) {
|
||||
const urlError = validators.url(website, 'Website');
|
||||
if (urlError) errors.push(urlError);
|
||||
}
|
||||
|
||||
// Telefonnummer Format (optional)
|
||||
if (phone && !/^[\d\s\-\+\(\)]+$/.test(phone)) {
|
||||
errors.push('Telefonnummer enthält ungültige Zeichen');
|
||||
}
|
||||
|
||||
if (mobile && !/^[\d\s\-\+\(\)]+$/.test(mobile)) {
|
||||
errors.push('Mobilnummer enthält ungültige Zeichen');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return res.status(400).json({ errors });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
stripHtml,
|
||||
sanitizeMarkdown,
|
||||
|
||||
439
backend/routes/contacts.js
Normale Datei
439
backend/routes/contacts.js
Normale Datei
@ -0,0 +1,439 @@
|
||||
/**
|
||||
* TASKMATE - Contact Routes
|
||||
* =========================
|
||||
* CRUD für Kontakte
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const { validators } = require('../middleware/validation');
|
||||
|
||||
/**
|
||||
* GET /api/contacts
|
||||
* Alle Kontakte abrufen mit optionalem Filter
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const { search, tag, sortBy = 'created_at', sortOrder = 'desc' } = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT c.*, u.display_name as creator_name
|
||||
FROM contacts c
|
||||
LEFT JOIN users u ON c.created_by = u.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
// Suchfilter
|
||||
if (search) {
|
||||
query += ` AND (
|
||||
c.first_name LIKE ? OR
|
||||
c.last_name LIKE ? OR
|
||||
c.company LIKE ? OR
|
||||
c.email LIKE ? OR
|
||||
c.phone LIKE ? OR
|
||||
c.mobile LIKE ?
|
||||
)`;
|
||||
const searchParam = `%${search}%`;
|
||||
params.push(searchParam, searchParam, searchParam, searchParam, searchParam, searchParam);
|
||||
}
|
||||
|
||||
// Tag-Filter
|
||||
if (tag) {
|
||||
query += ` AND c.tags LIKE ?`;
|
||||
params.push(`%${tag}%`);
|
||||
}
|
||||
|
||||
// Sortierung
|
||||
const validSortFields = ['first_name', 'last_name', 'company', 'created_at', 'updated_at'];
|
||||
const sortField = validSortFields.includes(sortBy) ? sortBy : 'created_at';
|
||||
const order = sortOrder.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
|
||||
query += ` ORDER BY c.${sortField} ${order}`;
|
||||
|
||||
const contacts = db.prepare(query).all(params);
|
||||
|
||||
res.json(contacts.map(c => ({
|
||||
id: c.id,
|
||||
firstName: c.first_name,
|
||||
lastName: c.last_name,
|
||||
company: c.company,
|
||||
position: c.position,
|
||||
email: c.email,
|
||||
phone: c.phone,
|
||||
mobile: c.mobile,
|
||||
address: c.address,
|
||||
postalCode: c.postal_code,
|
||||
city: c.city,
|
||||
country: c.country,
|
||||
website: c.website,
|
||||
notes: c.notes,
|
||||
tags: c.tags ? c.tags.split(',').map(t => t.trim()) : [],
|
||||
createdAt: c.created_at,
|
||||
updatedAt: c.updated_at,
|
||||
createdBy: c.created_by,
|
||||
creatorName: c.creator_name
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Kontakte:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/contacts/:id
|
||||
* Einzelnen Kontakt abrufen
|
||||
*/
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const contactId = req.params.id;
|
||||
|
||||
const contact = db.prepare(`
|
||||
SELECT c.*, u.display_name as creator_name
|
||||
FROM contacts c
|
||||
LEFT JOIN users u ON c.created_by = u.id
|
||||
WHERE c.id = ?
|
||||
`).get(contactId);
|
||||
|
||||
if (!contact) {
|
||||
return res.status(404).json({ error: 'Kontakt nicht gefunden' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: contact.id,
|
||||
firstName: contact.first_name,
|
||||
lastName: contact.last_name,
|
||||
company: contact.company,
|
||||
position: contact.position,
|
||||
email: contact.email,
|
||||
phone: contact.phone,
|
||||
mobile: contact.mobile,
|
||||
address: contact.address,
|
||||
postalCode: contact.postal_code,
|
||||
city: contact.city,
|
||||
country: contact.country,
|
||||
website: contact.website,
|
||||
notes: contact.notes,
|
||||
tags: contact.tags ? contact.tags.split(',').map(t => t.trim()) : [],
|
||||
createdAt: contact.created_at,
|
||||
updatedAt: contact.updated_at,
|
||||
createdBy: contact.created_by,
|
||||
creatorName: contact.creator_name
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen des Kontakts:', { error: error.message, contactId: req.params.id });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/contacts
|
||||
* Neuen Kontakt erstellen
|
||||
*/
|
||||
router.post('/', validators.contact, (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const userId = req.user.id;
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
company,
|
||||
position,
|
||||
email,
|
||||
phone,
|
||||
mobile,
|
||||
address,
|
||||
postalCode,
|
||||
city,
|
||||
country,
|
||||
website,
|
||||
notes,
|
||||
tags
|
||||
} = req.body;
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO contacts (
|
||||
first_name, last_name, company, position,
|
||||
email, phone, mobile, address, postal_code,
|
||||
city, country, website, notes, tags,
|
||||
created_by
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
firstName || null,
|
||||
lastName || null,
|
||||
company || null,
|
||||
position || null,
|
||||
email || null,
|
||||
phone || null,
|
||||
mobile || null,
|
||||
address || null,
|
||||
postalCode || null,
|
||||
city || null,
|
||||
country || null,
|
||||
website || null,
|
||||
notes || null,
|
||||
Array.isArray(tags) ? tags.join(', ') : null,
|
||||
userId
|
||||
);
|
||||
|
||||
const newContact = db.prepare(`
|
||||
SELECT c.*, u.display_name as creator_name
|
||||
FROM contacts c
|
||||
LEFT JOIN users u ON c.created_by = u.id
|
||||
WHERE c.id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
|
||||
// Socket.io Event
|
||||
const io = req.app.get('io');
|
||||
io.emit('contact:created', {
|
||||
contact: {
|
||||
id: newContact.id,
|
||||
firstName: newContact.first_name,
|
||||
lastName: newContact.last_name,
|
||||
company: newContact.company,
|
||||
position: newContact.position,
|
||||
email: newContact.email,
|
||||
phone: newContact.phone,
|
||||
mobile: newContact.mobile,
|
||||
address: newContact.address,
|
||||
postalCode: newContact.postal_code,
|
||||
city: newContact.city,
|
||||
country: newContact.country,
|
||||
website: newContact.website,
|
||||
notes: newContact.notes,
|
||||
tags: newContact.tags ? newContact.tags.split(',').map(t => t.trim()) : [],
|
||||
createdAt: newContact.created_at,
|
||||
updatedAt: newContact.updated_at,
|
||||
createdBy: newContact.created_by,
|
||||
creatorName: newContact.creator_name
|
||||
},
|
||||
userId
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: newContact.id,
|
||||
firstName: newContact.first_name,
|
||||
lastName: newContact.last_name,
|
||||
company: newContact.company,
|
||||
position: newContact.position,
|
||||
email: newContact.email,
|
||||
phone: newContact.phone,
|
||||
mobile: newContact.mobile,
|
||||
address: newContact.address,
|
||||
postalCode: newContact.postal_code,
|
||||
city: newContact.city,
|
||||
country: newContact.country,
|
||||
website: newContact.website,
|
||||
notes: newContact.notes,
|
||||
tags: newContact.tags ? newContact.tags.split(',').map(t => t.trim()) : [],
|
||||
createdAt: newContact.created_at,
|
||||
updatedAt: newContact.updated_at,
|
||||
createdBy: newContact.created_by,
|
||||
creatorName: newContact.creator_name
|
||||
});
|
||||
|
||||
logger.info('Kontakt erstellt', { contactId: newContact.id, userId });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen des Kontakts:', { error: error.message, body: req.body });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/contacts/:id
|
||||
* Kontakt aktualisieren
|
||||
*/
|
||||
router.put('/:id', validators.contact, (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const contactId = req.params.id;
|
||||
const userId = req.user.id;
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
company,
|
||||
position,
|
||||
email,
|
||||
phone,
|
||||
mobile,
|
||||
address,
|
||||
postalCode,
|
||||
city,
|
||||
country,
|
||||
website,
|
||||
notes,
|
||||
tags
|
||||
} = req.body;
|
||||
|
||||
// Prüfen ob Kontakt existiert
|
||||
const existing = db.prepare('SELECT id FROM contacts WHERE id = ?').get(contactId);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: 'Kontakt nicht gefunden' });
|
||||
}
|
||||
|
||||
// Update
|
||||
db.prepare(`
|
||||
UPDATE contacts SET
|
||||
first_name = ?,
|
||||
last_name = ?,
|
||||
company = ?,
|
||||
position = ?,
|
||||
email = ?,
|
||||
phone = ?,
|
||||
mobile = ?,
|
||||
address = ?,
|
||||
postal_code = ?,
|
||||
city = ?,
|
||||
country = ?,
|
||||
website = ?,
|
||||
notes = ?,
|
||||
tags = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
firstName || null,
|
||||
lastName || null,
|
||||
company || null,
|
||||
position || null,
|
||||
email || null,
|
||||
phone || null,
|
||||
mobile || null,
|
||||
address || null,
|
||||
postalCode || null,
|
||||
city || null,
|
||||
country || null,
|
||||
website || null,
|
||||
notes || null,
|
||||
Array.isArray(tags) ? tags.join(', ') : null,
|
||||
contactId
|
||||
);
|
||||
|
||||
const updatedContact = db.prepare(`
|
||||
SELECT c.*, u.display_name as creator_name
|
||||
FROM contacts c
|
||||
LEFT JOIN users u ON c.created_by = u.id
|
||||
WHERE c.id = ?
|
||||
`).get(contactId);
|
||||
|
||||
// Socket.io Event
|
||||
const io = req.app.get('io');
|
||||
io.emit('contact:updated', {
|
||||
contact: {
|
||||
id: updatedContact.id,
|
||||
firstName: updatedContact.first_name,
|
||||
lastName: updatedContact.last_name,
|
||||
company: updatedContact.company,
|
||||
position: updatedContact.position,
|
||||
email: updatedContact.email,
|
||||
phone: updatedContact.phone,
|
||||
mobile: updatedContact.mobile,
|
||||
address: updatedContact.address,
|
||||
postalCode: updatedContact.postal_code,
|
||||
city: updatedContact.city,
|
||||
country: updatedContact.country,
|
||||
website: updatedContact.website,
|
||||
notes: updatedContact.notes,
|
||||
tags: updatedContact.tags ? updatedContact.tags.split(',').map(t => t.trim()) : [],
|
||||
createdAt: updatedContact.created_at,
|
||||
updatedAt: updatedContact.updated_at,
|
||||
createdBy: updatedContact.created_by,
|
||||
creatorName: updatedContact.creator_name
|
||||
},
|
||||
userId
|
||||
});
|
||||
|
||||
res.json({
|
||||
id: updatedContact.id,
|
||||
firstName: updatedContact.first_name,
|
||||
lastName: updatedContact.last_name,
|
||||
company: updatedContact.company,
|
||||
position: updatedContact.position,
|
||||
email: updatedContact.email,
|
||||
phone: updatedContact.phone,
|
||||
mobile: updatedContact.mobile,
|
||||
address: updatedContact.address,
|
||||
postalCode: updatedContact.postal_code,
|
||||
city: updatedContact.city,
|
||||
country: updatedContact.country,
|
||||
website: updatedContact.website,
|
||||
notes: updatedContact.notes,
|
||||
tags: updatedContact.tags ? updatedContact.tags.split(',').map(t => t.trim()) : [],
|
||||
createdAt: updatedContact.created_at,
|
||||
updatedAt: updatedContact.updated_at,
|
||||
createdBy: updatedContact.created_by,
|
||||
creatorName: updatedContact.creator_name
|
||||
});
|
||||
|
||||
logger.info('Kontakt aktualisiert', { contactId, userId });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aktualisieren des Kontakts:', { error: error.message, contactId: req.params.id });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/contacts/:id
|
||||
* Kontakt löschen
|
||||
*/
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const contactId = req.params.id;
|
||||
const userId = req.user.id;
|
||||
|
||||
// Prüfen ob Kontakt existiert
|
||||
const existing = db.prepare('SELECT id FROM contacts WHERE id = ?').get(contactId);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: 'Kontakt nicht gefunden' });
|
||||
}
|
||||
|
||||
// Löschen
|
||||
db.prepare('DELETE FROM contacts WHERE id = ?').run(contactId);
|
||||
|
||||
// Socket.io Event
|
||||
const io = req.app.get('io');
|
||||
io.emit('contact:deleted', { contactId, userId });
|
||||
|
||||
res.json({ success: true });
|
||||
|
||||
logger.info('Kontakt gelöscht', { contactId, userId });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen des Kontakts:', { error: error.message, contactId: req.params.id });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/contacts/tags
|
||||
* Alle verwendeten Tags abrufen
|
||||
*/
|
||||
router.get('/tags/all', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
|
||||
const contacts = db.prepare('SELECT DISTINCT tags FROM contacts WHERE tags IS NOT NULL').all();
|
||||
|
||||
// Alle Tags sammeln und deduplizieren
|
||||
const allTags = new Set();
|
||||
contacts.forEach(contact => {
|
||||
if (contact.tags) {
|
||||
contact.tags.split(',').forEach(tag => {
|
||||
const trimmedTag = tag.trim();
|
||||
if (trimmedTag) {
|
||||
allTags.add(trimmedTag);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
res.json(Array.from(allTags).sort());
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Tags:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -110,13 +110,13 @@ router.post('/categories', (req, res) => {
|
||||
return res.status(400).json({ error: 'Eine Kategorie mit diesem Namen existiert bereits' });
|
||||
}
|
||||
|
||||
// Position ermitteln
|
||||
const lastPosition = db.prepare(
|
||||
'SELECT MAX(position) as max_pos FROM knowledge_categories'
|
||||
).get();
|
||||
const position = (lastPosition.max_pos ?? -1) + 1;
|
||||
// Alle bestehenden Kategorien um 1 nach unten verschieben
|
||||
db.prepare(`
|
||||
UPDATE knowledge_categories
|
||||
SET position = position + 1
|
||||
`).run();
|
||||
|
||||
// Einfügen
|
||||
// Neue Kategorie an Position 0 (ganz oben) einfügen
|
||||
const result = db.prepare(`
|
||||
INSERT INTO knowledge_categories (name, description, color, icon, position, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
@ -125,7 +125,7 @@ router.post('/categories', (req, res) => {
|
||||
description ? stripHtml(description) : null,
|
||||
color || '#3B82F6',
|
||||
icon || null,
|
||||
position,
|
||||
0, // Neue Kategorien immer an Position 0 (oben)
|
||||
req.user.id
|
||||
);
|
||||
|
||||
@ -473,13 +473,14 @@ router.post('/entries', (req, res) => {
|
||||
return res.status(404).json({ error: 'Kategorie nicht gefunden' });
|
||||
}
|
||||
|
||||
// Position ermitteln
|
||||
const lastPosition = db.prepare(
|
||||
'SELECT MAX(position) as max_pos FROM knowledge_entries WHERE category_id = ?'
|
||||
).get(categoryId);
|
||||
const position = (lastPosition.max_pos ?? -1) + 1;
|
||||
// Alle bestehenden Einträge um 1 nach unten verschieben
|
||||
db.prepare(`
|
||||
UPDATE knowledge_entries
|
||||
SET position = position + 1
|
||||
WHERE category_id = ?
|
||||
`).run(categoryId);
|
||||
|
||||
// Einfügen
|
||||
// Neuen Eintrag an Position 0 (ganz oben) einfügen
|
||||
const result = db.prepare(`
|
||||
INSERT INTO knowledge_entries (category_id, title, url, notes, position, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
@ -488,7 +489,7 @@ router.post('/entries', (req, res) => {
|
||||
stripHtml(title),
|
||||
url || null,
|
||||
notes || null,
|
||||
position,
|
||||
0, // Neue Einträge immer an Position 0 (oben)
|
||||
req.user.id
|
||||
);
|
||||
|
||||
|
||||
360
backend/routes/reminders.js
Normale Datei
360
backend/routes/reminders.js
Normale Datei
@ -0,0 +1,360 @@
|
||||
/**
|
||||
* TASKMATE - Reminders API
|
||||
* ========================
|
||||
* API endpoints für Erinnerungen
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const reminderService = require('../services/reminderService');
|
||||
|
||||
// GET /api/reminders - Alle Erinnerungen für ein Projekt abrufen
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const { project_id } = req.query;
|
||||
const db = getDb();
|
||||
|
||||
if (!project_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'project_id ist erforderlich'
|
||||
});
|
||||
}
|
||||
|
||||
const reminders = db.prepare(`
|
||||
SELECT r.*, u.display_name as creator_name
|
||||
FROM reminders r
|
||||
LEFT JOIN users u ON r.created_by = u.id
|
||||
WHERE r.project_id = ? AND r.is_active = 1
|
||||
ORDER BY r.reminder_date ASC, r.reminder_time ASC
|
||||
`).all(project_id);
|
||||
|
||||
// Advance days von String zu Array konvertieren
|
||||
reminders.forEach(reminder => {
|
||||
reminder.advance_days = reminder.advance_days ? reminder.advance_days.split(',') : ['1'];
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: reminders
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching reminders:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Interner Server-Fehler'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/reminders/:id - Einzelne Erinnerung abrufen
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const reminder = db.prepare(`
|
||||
SELECT r.*, u.display_name as creator_name
|
||||
FROM reminders r
|
||||
LEFT JOIN users u ON r.created_by = u.id
|
||||
WHERE r.id = ?
|
||||
`).get(req.params.id);
|
||||
|
||||
if (!reminder) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Erinnerung nicht gefunden'
|
||||
});
|
||||
}
|
||||
|
||||
// Advance days von String zu Array konvertieren
|
||||
reminder.advance_days = reminder.advance_days ? reminder.advance_days.split(',') : ['1'];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: reminder
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching reminder:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Interner Server-Fehler'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/reminders - Neue Erinnerung erstellen
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
project_id,
|
||||
title,
|
||||
description,
|
||||
reminder_date,
|
||||
reminder_time,
|
||||
color,
|
||||
advance_days,
|
||||
repeat_type,
|
||||
repeat_interval
|
||||
} = req.body;
|
||||
|
||||
if (!project_id || !title || !reminder_date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'project_id, title und reminder_date sind erforderlich'
|
||||
});
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Advance days Array zu String konvertieren
|
||||
const advanceDaysStr = Array.isArray(advance_days) ? advance_days.join(',') : '1';
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO reminders (
|
||||
project_id, title, description, reminder_date, reminder_time,
|
||||
color, advance_days, repeat_type, repeat_interval, created_by
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
project_id,
|
||||
title,
|
||||
description || null,
|
||||
reminder_date,
|
||||
reminder_time || '09:00',
|
||||
color || '#F59E0B',
|
||||
advanceDaysStr,
|
||||
repeat_type || 'none',
|
||||
repeat_interval || 1,
|
||||
req.user.id
|
||||
);
|
||||
|
||||
// Benachrichtigungs-Termine mit ReminderService erstellen
|
||||
if (advance_days && Array.isArray(advance_days)) {
|
||||
const serviceInstance = reminderService.getInstance();
|
||||
serviceInstance.createNotificationSchedule(result.lastInsertRowid, reminder_date, advance_days);
|
||||
}
|
||||
|
||||
// Neue Erinnerung abrufen
|
||||
const newReminder = db.prepare(`
|
||||
SELECT r.*, u.display_name as creator_name
|
||||
FROM reminders r
|
||||
LEFT JOIN users u ON r.created_by = u.id
|
||||
WHERE r.id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
|
||||
newReminder.advance_days = newReminder.advance_days ? newReminder.advance_days.split(',') : ['1'];
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: newReminder
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating reminder:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Interner Server-Fehler'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/reminders/:id - Erinnerung bearbeiten
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
reminder_date,
|
||||
reminder_time,
|
||||
color,
|
||||
advance_days,
|
||||
repeat_type,
|
||||
repeat_interval,
|
||||
is_active
|
||||
} = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Prüfen ob Erinnerung existiert
|
||||
const existing = db.prepare('SELECT * FROM reminders WHERE id = ?').get(req.params.id);
|
||||
if (!existing) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Erinnerung nicht gefunden'
|
||||
});
|
||||
}
|
||||
|
||||
// Advance days Array zu String konvertieren
|
||||
const advanceDaysStr = Array.isArray(advance_days) ? advance_days.join(',') : existing.advance_days;
|
||||
|
||||
db.prepare(`
|
||||
UPDATE reminders SET
|
||||
title = ?,
|
||||
description = ?,
|
||||
reminder_date = ?,
|
||||
reminder_time = ?,
|
||||
color = ?,
|
||||
advance_days = ?,
|
||||
repeat_type = ?,
|
||||
repeat_interval = ?,
|
||||
is_active = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
title || existing.title,
|
||||
description !== undefined ? description : existing.description,
|
||||
reminder_date || existing.reminder_date,
|
||||
reminder_time || existing.reminder_time,
|
||||
color || existing.color,
|
||||
advanceDaysStr,
|
||||
repeat_type || existing.repeat_type,
|
||||
repeat_interval || existing.repeat_interval,
|
||||
is_active !== undefined ? is_active : existing.is_active,
|
||||
req.params.id
|
||||
);
|
||||
|
||||
// Benachrichtigungs-Termine neu berechnen wenn sich das Datum geändert hat
|
||||
if (reminder_date || advance_days) {
|
||||
const finalAdvanceDays = advance_days || existing.advance_days.split(',');
|
||||
const finalReminderDate = reminder_date || existing.reminder_date;
|
||||
|
||||
const serviceInstance = reminderService.getInstance();
|
||||
serviceInstance.createNotificationSchedule(req.params.id, finalReminderDate, finalAdvanceDays);
|
||||
}
|
||||
|
||||
// Aktualisierte Erinnerung abrufen
|
||||
const updatedReminder = db.prepare(`
|
||||
SELECT r.*, u.display_name as creator_name
|
||||
FROM reminders r
|
||||
LEFT JOIN users u ON r.created_by = u.id
|
||||
WHERE r.id = ?
|
||||
`).get(req.params.id);
|
||||
|
||||
updatedReminder.advance_days = updatedReminder.advance_days ? updatedReminder.advance_days.split(',') : ['1'];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: updatedReminder
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating reminder:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Interner Server-Fehler'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/reminders/:id - Erinnerung löschen
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
|
||||
const result = db.prepare('DELETE FROM reminders WHERE id = ?').run(req.params.id);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Erinnerung nicht gefunden'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Erinnerung erfolgreich gelöscht'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting reminder:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Interner Server-Fehler'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/reminders/due/check - Fällige Erinnerungen prüfen (für Cron-Job)
|
||||
router.get('/due/check', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Fällige Benachrichtigungen finden
|
||||
const dueNotifications = db.prepare(`
|
||||
SELECT rn.*, r.title, r.description, r.project_id, r.created_by, r.reminder_date, r.color
|
||||
FROM reminder_notifications rn
|
||||
JOIN reminders r ON rn.reminder_id = r.id
|
||||
WHERE rn.notification_date <= ? AND rn.sent = 0 AND r.is_active = 1
|
||||
ORDER BY rn.notification_date ASC
|
||||
`).all(today);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: dueNotifications
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error checking due reminders:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Interner Server-Fehler'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/reminders/due/mark-sent - Benachrichtigung als gesendet markieren
|
||||
router.post('/due/mark-sent', (req, res) => {
|
||||
try {
|
||||
const { notification_id } = req.body;
|
||||
const db = getDb();
|
||||
|
||||
db.prepare('UPDATE reminder_notifications SET sent = 1 WHERE id = ?').run(notification_id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Benachrichtigung als gesendet markiert'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error marking notification as sent:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Interner Server-Fehler'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/reminders/stats - Debug-Statistiken für Reminder Service
|
||||
router.get('/stats', (req, res) => {
|
||||
try {
|
||||
const serviceInstance = reminderService.getInstance();
|
||||
const stats = serviceInstance.getStats();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting reminder stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Interner Server-Fehler'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/reminders/check-now - Manuelle Prüfung fälliger Erinnerungen
|
||||
router.post('/check-now', async (req, res) => {
|
||||
try {
|
||||
const serviceInstance = reminderService.getInstance();
|
||||
await serviceInstance.manualCheck();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Manuelle Reminder-Prüfung durchgeführt'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during manual reminder check:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Interner Server-Fehler'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -41,11 +41,13 @@ const adminRoutes = require('./routes/admin');
|
||||
const proposalRoutes = require('./routes/proposals');
|
||||
const notificationRoutes = require('./routes/notifications');
|
||||
const notificationService = require('./services/notificationService');
|
||||
const reminderService = require('./services/reminderService');
|
||||
const gitRoutes = require('./routes/git');
|
||||
const applicationsRoutes = require('./routes/applications');
|
||||
const giteaRoutes = require('./routes/gitea');
|
||||
const knowledgeRoutes = require('./routes/knowledge');
|
||||
const codingRoutes = require('./routes/coding');
|
||||
const reminderRoutes = require('./routes/reminders');
|
||||
|
||||
// Express App erstellen
|
||||
const app = express();
|
||||
@ -106,8 +108,17 @@ app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
// Statische Dateien (Frontend)
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
// Statische Dateien (Frontend) - ohne Caching für Development
|
||||
app.use(express.static(path.join(__dirname, 'public'), {
|
||||
etag: false,
|
||||
lastModified: false,
|
||||
cacheControl: false,
|
||||
setHeaders: (res, path) => {
|
||||
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
res.setHeader('Expires', '0');
|
||||
}
|
||||
}));
|
||||
|
||||
// Uploads-Ordner
|
||||
app.use('/uploads', authenticateToken, express.static(process.env.UPLOAD_DIR || path.join(__dirname, 'uploads')));
|
||||
@ -160,6 +171,12 @@ app.use('/api/knowledge', authenticateToken, csrfProtection, knowledgeRoutes);
|
||||
// Coding-Routes (Entwicklungsverzeichnisse mit Claude/Codex)
|
||||
app.use('/api/coding', authenticateToken, csrfProtection, codingRoutes);
|
||||
|
||||
// Reminder-Routes (Erinnerungen)
|
||||
app.use('/api/reminders', authenticateToken, csrfProtection, reminderRoutes);
|
||||
|
||||
// Contacts-Routes (Kontakte)
|
||||
app.use('/api/contacts', authenticateToken, csrfProtection, require('./routes/contacts'));
|
||||
|
||||
// =============================================================================
|
||||
// SOCKET.IO
|
||||
// =============================================================================
|
||||
@ -296,6 +313,10 @@ database.initialize()
|
||||
notificationService.checkDueTasks(io);
|
||||
logger.info('Fälligkeits-Check für Benachrichtigungen gestartet');
|
||||
}, 60 * 1000);
|
||||
|
||||
// Reminder Service starten
|
||||
const reminderServiceInstance = reminderService.getInstance(io);
|
||||
reminderServiceInstance.start();
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -306,6 +327,11 @@ database.initialize()
|
||||
// Graceful Shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
logger.info('SIGTERM empfangen, fahre herunter...');
|
||||
|
||||
// Reminder Service stoppen
|
||||
const reminderServiceInstance = reminderService.getInstance();
|
||||
reminderServiceInstance.stop();
|
||||
|
||||
server.close(() => {
|
||||
database.close();
|
||||
logger.info('Server beendet');
|
||||
@ -315,6 +341,11 @@ process.on('SIGTERM', () => {
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('SIGINT empfangen, fahre herunter...');
|
||||
|
||||
// Reminder Service stoppen
|
||||
const reminderServiceInstance = reminderService.getInstance();
|
||||
reminderServiceInstance.stop();
|
||||
|
||||
server.close(() => {
|
||||
database.close();
|
||||
logger.info('Server beendet');
|
||||
|
||||
@ -47,6 +47,10 @@ const NOTIFICATION_TYPES = {
|
||||
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`
|
||||
@ -284,6 +288,24 @@ const notificationService = {
|
||||
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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
242
backend/services/reminderService.js
Normale Datei
242
backend/services/reminderService.js
Normale Datei
@ -0,0 +1,242 @@
|
||||
/**
|
||||
* TASKMATE - Reminder Service
|
||||
* ===========================
|
||||
* Service für Erinnerungsbenachrichtigungen und Scheduling
|
||||
*/
|
||||
|
||||
const { getDb } = require('../database');
|
||||
const notificationService = require('./notificationService');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class ReminderService {
|
||||
constructor(io = null) {
|
||||
this.io = io;
|
||||
this.intervalId = null;
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet den Reminder-Check-Service
|
||||
* Läuft alle 5 Minuten (kann für Produktion auf 1 Stunde erhöht werden)
|
||||
*/
|
||||
start() {
|
||||
if (this.isRunning) {
|
||||
logger.warn('Reminder Service ist bereits gestartet');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
|
||||
// Sofort prüfen
|
||||
this.checkDueReminders();
|
||||
|
||||
// Dann alle 5 Minuten (300000 ms)
|
||||
// In Produktion könnte das auf 1 Stunde (3600000 ms) erhöht werden
|
||||
this.intervalId = setInterval(() => {
|
||||
this.checkDueReminders();
|
||||
}, 300000); // 5 Minuten
|
||||
|
||||
logger.info('Reminder Service gestartet - prüft alle 5 Minuten');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stoppt den Reminder-Check-Service
|
||||
*/
|
||||
stop() {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
logger.info('Reminder Service gestoppt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft fällige Erinnerungen und sendet Benachrichtigungen
|
||||
*/
|
||||
async checkDueReminders() {
|
||||
try {
|
||||
const db = getDb();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Finde alle fälligen Benachrichtigungen die noch nicht gesendet wurden
|
||||
const dueNotifications = db.prepare(`
|
||||
SELECT
|
||||
rn.*,
|
||||
r.id as reminder_id,
|
||||
r.title as reminder_title,
|
||||
r.description as reminder_description,
|
||||
r.reminder_date,
|
||||
r.reminder_time,
|
||||
r.color,
|
||||
r.project_id,
|
||||
r.created_by,
|
||||
r.advance_days
|
||||
FROM reminder_notifications rn
|
||||
JOIN reminders r ON rn.reminder_id = r.id
|
||||
WHERE rn.notification_date <= ?
|
||||
AND rn.sent = 0
|
||||
AND r.is_active = 1
|
||||
ORDER BY rn.notification_date ASC, r.reminder_time ASC
|
||||
`).all(today);
|
||||
|
||||
if (dueNotifications.length === 0) {
|
||||
logger.debug('Keine fälligen Erinnerungen gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`${dueNotifications.length} fällige Erinnerung(en) gefunden`);
|
||||
|
||||
// Verarbeite jede fällige Benachrichtigung
|
||||
for (const notification of dueNotifications) {
|
||||
await this.processReminderNotification(notification);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Prüfen fälliger Erinnerungen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet eine einzelne fällige Erinnerungs-Benachrichtigung
|
||||
*/
|
||||
async processReminderNotification(notification) {
|
||||
try {
|
||||
const db = getDb();
|
||||
|
||||
// Berechne wie viele Tage im Voraus diese Benachrichtigung ist
|
||||
const reminderDate = new Date(notification.reminder_date);
|
||||
const notificationDate = new Date(notification.notification_date);
|
||||
const daysDiff = Math.ceil((reminderDate - notificationDate) / (1000 * 60 * 60 * 24));
|
||||
|
||||
const reminder = {
|
||||
id: notification.reminder_id,
|
||||
title: notification.reminder_title,
|
||||
description: notification.reminder_description,
|
||||
project_id: notification.project_id,
|
||||
created_by: notification.created_by,
|
||||
color: notification.color
|
||||
};
|
||||
|
||||
// Erstelle Benachrichtigung
|
||||
const createdNotification = notificationService.createReminderNotification(
|
||||
reminder,
|
||||
daysDiff,
|
||||
this.io
|
||||
);
|
||||
|
||||
if (createdNotification) {
|
||||
// Markiere als gesendet
|
||||
db.prepare(`
|
||||
UPDATE reminder_notifications
|
||||
SET sent = 1
|
||||
WHERE id = ?
|
||||
`).run(notification.id);
|
||||
|
||||
logger.info(`Reminder-Benachrichtigung gesendet: "${notification.reminder_title}" (${daysDiff} Tage vorher)`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Fehler beim Verarbeiten der Reminder-Benachrichtigung ${notification.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt Benachrichtigungstermine für eine neue Erinnerung
|
||||
*/
|
||||
createNotificationSchedule(reminderId, reminderDate, advanceDays) {
|
||||
try {
|
||||
const db = getDb();
|
||||
const baseDate = new Date(reminderDate);
|
||||
|
||||
// Lösche alte Termine falls vorhanden
|
||||
db.prepare('DELETE FROM reminder_notifications WHERE reminder_id = ?').run(reminderId);
|
||||
|
||||
// Erstelle neue Termine für jeden advance day
|
||||
advanceDays.forEach(days => {
|
||||
const notificationDate = new Date(baseDate);
|
||||
notificationDate.setDate(notificationDate.getDate() - parseInt(days));
|
||||
|
||||
const notificationDateStr = notificationDate.toISOString().split('T')[0];
|
||||
|
||||
// Nur zukünftige Termine erstellen
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
if (notificationDateStr >= today) {
|
||||
db.prepare(`
|
||||
INSERT OR IGNORE INTO reminder_notifications (reminder_id, notification_date)
|
||||
VALUES (?, ?)
|
||||
`).run(reminderId, notificationDateStr);
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug(`Benachrichtigungstermine erstellt für Reminder ${reminderId}`);
|
||||
} catch (error) {
|
||||
logger.error(`Fehler beim Erstellen der Benachrichtigungstermine für Reminder ${reminderId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manuelle Prüfung für API-Endpoint
|
||||
*/
|
||||
async manualCheck() {
|
||||
logger.info('Manuelle Reminder-Prüfung ausgelöst');
|
||||
return await this.checkDueReminders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiken für Debugging
|
||||
*/
|
||||
getStats() {
|
||||
try {
|
||||
const db = getDb();
|
||||
|
||||
const stats = {
|
||||
isRunning: this.isRunning,
|
||||
activeReminders: db.prepare('SELECT COUNT(*) as count FROM reminders WHERE is_active = 1').get().count,
|
||||
pendingNotifications: db.prepare('SELECT COUNT(*) as count FROM reminder_notifications WHERE sent = 0').get().count,
|
||||
nextDueDate: db.prepare(`
|
||||
SELECT MIN(notification_date) as next_date
|
||||
FROM reminder_notifications
|
||||
WHERE sent = 0 AND notification_date >= date('now')
|
||||
`).get().next_date
|
||||
};
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Reminder-Statistiken:', error);
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Socket.io Instanz setzen/aktualisieren
|
||||
*/
|
||||
setSocketIO(io) {
|
||||
this.io = io;
|
||||
logger.debug('Socket.IO Instanz für Reminder Service aktualisiert');
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton Export
|
||||
let instance = null;
|
||||
|
||||
module.exports = {
|
||||
getInstance(io = null) {
|
||||
if (!instance) {
|
||||
instance = new ReminderService(io);
|
||||
} else if (io) {
|
||||
instance.setSocketIO(io);
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
|
||||
// Für Tests und Debugging
|
||||
createInstance(io = null) {
|
||||
return new ReminderService(io);
|
||||
}
|
||||
};
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren