- Session-Timeout auf 60 Minuten erhöht (ACCESS_TOKEN_EXPIRY + SESSION_TIMEOUT) - AegisSight Light Theme: Gold-Akzent (#C8A851) statt Indigo - Navigation-Tabs in eigene Zeile unter Header verschoben (HTML-Struktur) - Filter-Bar durch kompaktes Popover mit Checkboxen ersetzt (Mehrfachauswahl) - Archiv-Funktion repariert (lädt jetzt per API statt leerem Store) - Filter-Bugs behoben: Reset-Button ID, Default-Werte, Ohne-Datum-Filter - Mehrspalten-Layout Feature entfernt - Online-Status vom Header an User-Avatar verschoben (grüner Punkt) - Lupen-Icon entfernt - CLAUDE.md: Docker-Deploy und CSS-Tricks Regeln aktualisiert Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
446 Zeilen
13 KiB
JavaScript
446 Zeilen
13 KiB
JavaScript
/**
|
|
* TASKMATE - Kontakte API
|
|
* ========================
|
|
* REST API für Kontaktmanagement
|
|
*/
|
|
|
|
const router = require('express').Router();
|
|
const { getDb } = require('../database');
|
|
const logger = require('../utils/logger');
|
|
const { authenticateToken } = require('../middleware/auth');
|
|
const { upload } = require('../middleware/upload');
|
|
|
|
// =====================
|
|
// KONTAKTE CRUD
|
|
// =====================
|
|
|
|
// GET /api/contacts - Alle Kontakte abrufen
|
|
router.get('/', authenticateToken, (req, res) => {
|
|
// Kein Caching für Kontakte
|
|
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
res.set('Pragma', 'no-cache');
|
|
|
|
try {
|
|
const db = getDb();
|
|
const currentUser = req.user;
|
|
const { project_id, type, status, search } = req.query;
|
|
|
|
logger.info('Kontakte abrufen', { user_id: currentUser.id, filters: req.query });
|
|
|
|
let query = `
|
|
SELECT
|
|
c.*,
|
|
u.display_name as created_by_name,
|
|
pc.display_name as parent_company_name
|
|
FROM contacts c
|
|
LEFT JOIN users u ON c.created_by = u.id
|
|
LEFT JOIN contacts pc ON c.parent_company_id = pc.id
|
|
WHERE 1=1
|
|
`;
|
|
const params = [];
|
|
|
|
// Filter nach Projekt
|
|
if (project_id) {
|
|
query += ' AND c.project_id = ?';
|
|
params.push(project_id);
|
|
}
|
|
|
|
// Filter nach Typ
|
|
if (type && ['person', 'company'].includes(type)) {
|
|
query += ' AND c.type = ?';
|
|
params.push(type);
|
|
}
|
|
|
|
// Filter nach Status
|
|
if (status && ['active', 'inactive', 'archived'].includes(status)) {
|
|
query += ' AND c.status = ?';
|
|
params.push(status);
|
|
}
|
|
|
|
// Suche
|
|
if (search) {
|
|
query += ` AND (
|
|
c.display_name LIKE ? OR
|
|
c.first_name LIKE ? OR
|
|
c.last_name LIKE ? OR
|
|
c.company_name LIKE ? OR
|
|
c.tags LIKE ? OR
|
|
c.notes LIKE ?
|
|
)`;
|
|
const searchPattern = `%${search}%`;
|
|
params.push(searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern);
|
|
}
|
|
|
|
query += ' ORDER BY c.display_name ASC';
|
|
|
|
const contacts = db.prepare(query).all(...params);
|
|
|
|
// Details für jeden Kontakt abrufen
|
|
const contactsWithDetails = contacts.map(contact => {
|
|
// Kontaktdetails abrufen
|
|
const details = db.prepare(`
|
|
SELECT * FROM contact_details
|
|
WHERE contact_id = ?
|
|
ORDER BY is_primary DESC, type, label
|
|
`).all(contact.id);
|
|
|
|
return {
|
|
...contact,
|
|
details
|
|
};
|
|
});
|
|
|
|
res.json({ success: true, data: contactsWithDetails });
|
|
} catch (error) {
|
|
logger.error('Fehler beim Abrufen der Kontakte:', error);
|
|
res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// GET /api/contacts/:id - Einzelnen Kontakt abrufen
|
|
router.get('/:id', authenticateToken, (req, res) => {
|
|
try {
|
|
const db = getDb();
|
|
const { id } = req.params;
|
|
|
|
const contact = db.prepare(`
|
|
SELECT
|
|
c.*,
|
|
u.display_name as created_by_name,
|
|
pc.display_name as parent_company_name
|
|
FROM contacts c
|
|
LEFT JOIN users u ON c.created_by = u.id
|
|
LEFT JOIN contacts pc ON c.parent_company_id = pc.id
|
|
WHERE c.id = ?
|
|
`).get(id);
|
|
|
|
if (!contact) {
|
|
return res.status(404).json({ success: false, error: 'Kontakt nicht gefunden' });
|
|
}
|
|
|
|
// Details abrufen
|
|
contact.details = db.prepare(`
|
|
SELECT * FROM contact_details
|
|
WHERE contact_id = ?
|
|
ORDER BY is_primary DESC, type, label
|
|
`).all(id);
|
|
|
|
|
|
// Interaktionen abrufen
|
|
contact.interactions = db.prepare(`
|
|
SELECT ci.*, u.display_name as created_by_name
|
|
FROM contact_interactions ci
|
|
LEFT JOIN users u ON ci.created_by = u.id
|
|
WHERE ci.contact_id = ?
|
|
ORDER BY ci.interaction_date DESC
|
|
LIMIT 10
|
|
`).all(id);
|
|
|
|
res.json({ success: true, data: contact });
|
|
} catch (error) {
|
|
logger.error('Fehler beim Abrufen des Kontakts:', error);
|
|
res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// POST /api/contacts - Neuen Kontakt erstellen
|
|
router.post('/', authenticateToken, (req, res) => {
|
|
try {
|
|
const db = getDb();
|
|
const currentUser = req.user;
|
|
const { type, project_id, details = [], ...contactData } = req.body;
|
|
|
|
// Validierung
|
|
if (!type || !['person', 'company'].includes(type)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Kontakttyp muss "person" oder "company" sein'
|
|
});
|
|
}
|
|
|
|
if (!project_id) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Projekt-ID ist erforderlich'
|
|
});
|
|
}
|
|
|
|
// Display Name generieren falls nicht vorhanden
|
|
if (!contactData.display_name) {
|
|
if (type === 'person') {
|
|
contactData.display_name = `${contactData.first_name || ''} ${contactData.last_name || ''}`.trim();
|
|
} else {
|
|
contactData.display_name = contactData.company_name || '';
|
|
}
|
|
}
|
|
|
|
// Transaktion starten
|
|
const insertContact = db.prepare(`
|
|
INSERT INTO contacts (
|
|
type, project_id, created_by,
|
|
display_name, status, tags, notes, avatar_url,
|
|
salutation, first_name, last_name, position, department,
|
|
parent_company_id,
|
|
company_name, company_type, industry, website
|
|
) VALUES (
|
|
?, ?, ?,
|
|
?, ?, ?, ?, ?,
|
|
?, ?, ?, ?, ?,
|
|
?,
|
|
?, ?, ?, ?
|
|
)
|
|
`);
|
|
|
|
const insertDetail = db.prepare(`
|
|
INSERT INTO contact_details (contact_id, type, label, value, is_primary)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
|
|
const result = db.transaction(() => {
|
|
// Kontakt einfügen
|
|
const info = insertContact.run(
|
|
type, project_id, currentUser.id,
|
|
contactData.display_name,
|
|
contactData.status || 'active',
|
|
contactData.tags || null,
|
|
contactData.notes || null,
|
|
contactData.avatar_url || null,
|
|
// Person-Felder
|
|
type === 'person' ? contactData.salutation : null,
|
|
type === 'person' ? contactData.first_name : null,
|
|
type === 'person' ? contactData.last_name : null,
|
|
type === 'person' ? contactData.position : null,
|
|
type === 'person' ? contactData.department : null,
|
|
type === 'person' ? contactData.parent_company_id : null,
|
|
// Company-Felder
|
|
type === 'company' ? contactData.company_name : null,
|
|
type === 'company' ? contactData.company_type : null,
|
|
type === 'company' ? contactData.industry : null,
|
|
type === 'company' ? contactData.website : null
|
|
);
|
|
|
|
const contactId = info.lastInsertRowid;
|
|
|
|
// Details einfügen
|
|
for (const detail of details) {
|
|
if (detail.value) {
|
|
insertDetail.run(
|
|
contactId,
|
|
detail.type,
|
|
detail.label || 'Arbeit',
|
|
detail.value,
|
|
detail.is_primary ? 1 : 0
|
|
);
|
|
}
|
|
}
|
|
|
|
return contactId;
|
|
})();
|
|
|
|
// Neu erstellten Kontakt mit Details abrufen
|
|
const newContact = db.prepare(`
|
|
SELECT * FROM contacts WHERE id = ?
|
|
`).get(result);
|
|
|
|
newContact.details = db.prepare(`
|
|
SELECT * FROM contact_details WHERE contact_id = ?
|
|
`).all(result);
|
|
|
|
|
|
logger.info('Kontakt erstellt', { contact_id: result, user_id: currentUser.id });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: newContact,
|
|
message: 'Kontakt erfolgreich erstellt'
|
|
});
|
|
} catch (error) {
|
|
logger.error('Fehler beim Erstellen des Kontakts:', error);
|
|
res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// PUT /api/contacts/:id - Kontakt aktualisieren
|
|
router.put('/:id', authenticateToken, (req, res) => {
|
|
try {
|
|
const db = getDb();
|
|
const { id } = req.params;
|
|
const { details = [], ...contactData } = req.body;
|
|
|
|
// Prüfen ob Kontakt existiert
|
|
const existing = db.prepare('SELECT * FROM contacts WHERE id = ?').get(id);
|
|
if (!existing) {
|
|
return res.status(404).json({ success: false, error: 'Kontakt nicht gefunden' });
|
|
}
|
|
|
|
// Update-Query dynamisch aufbauen
|
|
const fields = [];
|
|
const values = [];
|
|
|
|
// Erlaubte Felder für Update
|
|
const allowedFields = [
|
|
'display_name', 'status', 'tags', 'notes', 'avatar_url',
|
|
'salutation', 'first_name', 'last_name', 'position', 'department',
|
|
'parent_company_id',
|
|
'company_name', 'company_type', 'industry', 'website'
|
|
];
|
|
|
|
for (const field of allowedFields) {
|
|
if (contactData.hasOwnProperty(field)) {
|
|
fields.push(`${field} = ?`);
|
|
values.push(contactData[field]);
|
|
}
|
|
}
|
|
|
|
values.push(id);
|
|
|
|
// Transaktion für Updates
|
|
db.transaction(() => {
|
|
// Kontakt aktualisieren
|
|
if (fields.length > 0) {
|
|
db.prepare(`
|
|
UPDATE contacts
|
|
SET ${fields.join(', ')}
|
|
WHERE id = ?
|
|
`).run(...values);
|
|
}
|
|
|
|
// Details aktualisieren - alte löschen und neue einfügen
|
|
db.prepare('DELETE FROM contact_details WHERE contact_id = ?').run(id);
|
|
|
|
const insertDetail = db.prepare(`
|
|
INSERT INTO contact_details (contact_id, type, label, value, is_primary)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
for (const detail of details) {
|
|
if (detail.value) {
|
|
insertDetail.run(
|
|
id,
|
|
detail.type,
|
|
detail.label || 'Arbeit',
|
|
detail.value,
|
|
detail.is_primary ? 1 : 0
|
|
);
|
|
}
|
|
}
|
|
|
|
})();
|
|
|
|
// Aktualisierten Kontakt abrufen
|
|
const updatedContact = db.prepare(`
|
|
SELECT * FROM contacts WHERE id = ?
|
|
`).get(id);
|
|
|
|
updatedContact.details = db.prepare(`
|
|
SELECT * FROM contact_details WHERE contact_id = ?
|
|
`).all(id);
|
|
logger.info('Kontakt aktualisiert', { contact_id: id });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: updatedContact,
|
|
message: 'Kontakt erfolgreich aktualisiert'
|
|
});
|
|
} catch (error) {
|
|
logger.error('Fehler beim Aktualisieren des Kontakts:', error);
|
|
res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// DELETE /api/contacts/:id - Kontakt löschen
|
|
router.delete('/:id', authenticateToken, (req, res) => {
|
|
try {
|
|
const db = getDb();
|
|
const { id } = req.params;
|
|
|
|
const result = db.prepare('DELETE FROM contacts WHERE id = ?').run(id);
|
|
|
|
if (result.changes === 0) {
|
|
return res.status(404).json({ success: false, error: 'Kontakt nicht gefunden' });
|
|
}
|
|
|
|
logger.info('Kontakt gelöscht', { contact_id: id });
|
|
|
|
res.json({ success: true, message: 'Kontakt erfolgreich gelöscht' });
|
|
} catch (error) {
|
|
logger.error('Fehler beim Löschen des Kontakts:', error);
|
|
res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// =====================
|
|
// INTERAKTIONEN (nach /:id Routen)
|
|
// =====================
|
|
|
|
// POST /api/contacts/:id/interactions - Neue Interaktion hinzufügen
|
|
router.post('/:id/interactions', authenticateToken, (req, res) => {
|
|
try {
|
|
const db = getDb();
|
|
const currentUser = req.user;
|
|
const { id } = req.params;
|
|
const { type, subject, content } = req.body;
|
|
|
|
if (!type || !['call', 'email', 'meeting', 'note', 'task'].includes(type)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Ungültiger Interaktionstyp'
|
|
});
|
|
}
|
|
|
|
const result = db.prepare(`
|
|
INSERT INTO contact_interactions (
|
|
contact_id, type, subject, content, created_by
|
|
) VALUES (?, ?, ?, ?, ?)
|
|
`).run(id, type, subject, content, currentUser.id);
|
|
|
|
const newInteraction = db.prepare(`
|
|
SELECT ci.*, u.display_name as created_by_name
|
|
FROM contact_interactions ci
|
|
LEFT JOIN users u ON ci.created_by = u.id
|
|
WHERE ci.id = ?
|
|
`).get(result.lastInsertRowid);
|
|
|
|
res.json({ success: true, data: newInteraction });
|
|
} catch (error) {
|
|
logger.error('Fehler beim Hinzufügen der Interaktion:', error);
|
|
res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// =====================
|
|
// DATEI-UPLOAD (nach /:id Routen)
|
|
// =====================
|
|
|
|
// POST /api/contacts/:id/avatar - Avatar hochladen
|
|
router.post('/:id/avatar', authenticateToken, upload.single('files'), async (req, res) => {
|
|
try {
|
|
const db = getDb();
|
|
const { id } = req.params;
|
|
|
|
if (!req.file) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Keine Datei hochgeladen'
|
|
});
|
|
}
|
|
|
|
// Avatar-URL speichern
|
|
const avatarUrl = `/uploads/${req.file.filename}`;
|
|
|
|
db.prepare('UPDATE contacts SET avatar_url = ? WHERE id = ?')
|
|
.run(avatarUrl, id);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: { avatar_url: avatarUrl },
|
|
message: 'Avatar erfolgreich hochgeladen'
|
|
});
|
|
} catch (error) {
|
|
logger.error('Fehler beim Avatar-Upload:', error);
|
|
res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router; |