UI-Redesign: AegisSight Design, Filter-Popover, Header-Umbau
- 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>
Dieser Commit ist enthalten in:
@@ -113,7 +113,7 @@ function createTables() {
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL,
|
||||
column_id INTEGER NOT NULL,
|
||||
column_id INTEGER,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
priority TEXT DEFAULT 'medium',
|
||||
@@ -128,7 +128,7 @@ function createTables() {
|
||||
created_by INTEGER,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (assigned_to) REFERENCES users(id),
|
||||
FOREIGN KEY (depends_on) REFERENCES tasks(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
@@ -613,31 +613,109 @@ function createTables() {
|
||||
)
|
||||
`);
|
||||
|
||||
// Kontakte
|
||||
// Neues optimiertes Kontakt-Schema
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL CHECK(type IN ('person', 'company')),
|
||||
|
||||
-- Gemeinsame Felder
|
||||
display_name TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'inactive', 'archived')),
|
||||
tags TEXT,
|
||||
notes TEXT,
|
||||
avatar_url TEXT,
|
||||
|
||||
-- Person-spezifische Felder
|
||||
salutation TEXT,
|
||||
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,
|
||||
department TEXT,
|
||||
parent_company_id INTEGER,
|
||||
|
||||
-- Firma-spezifische Felder
|
||||
company_name TEXT,
|
||||
company_type TEXT,
|
||||
industry TEXT,
|
||||
website TEXT,
|
||||
notes TEXT,
|
||||
tags TEXT,
|
||||
|
||||
-- Meta
|
||||
project_id INTEGER NOT NULL,
|
||||
created_by INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INTEGER,
|
||||
|
||||
FOREIGN KEY (parent_company_id) REFERENCES contacts(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Kontaktdetails (E-Mails, Telefone, Adressen)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS contact_details (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
contact_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('email', 'phone', 'mobile', 'fax', 'address', 'social')),
|
||||
label TEXT DEFAULT 'Arbeit',
|
||||
value TEXT NOT NULL,
|
||||
is_primary BOOLEAN DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Kontakt-Kategorien
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS contact_categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT DEFAULT '#6B7280',
|
||||
icon TEXT,
|
||||
project_id INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Kontakt-Kategorie Zuordnungen
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS contact_to_categories (
|
||||
contact_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (contact_id, category_id),
|
||||
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (category_id) REFERENCES contact_categories(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Interaktionen/Historie
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS contact_interactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
contact_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('call', 'email', 'meeting', 'note', 'task')),
|
||||
subject TEXT,
|
||||
content TEXT,
|
||||
interaction_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Trigger für updated_at auf Kontakte-Tabelle
|
||||
db.exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS update_contacts_timestamp
|
||||
AFTER UPDATE ON contacts
|
||||
BEGIN
|
||||
UPDATE contacts SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END
|
||||
`);
|
||||
|
||||
// Indizes für Performance
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
||||
@@ -660,8 +738,16 @@ function createTables() {
|
||||
CREATE INDEX IF NOT EXISTS idx_coding_directories_position ON coding_directories(position);
|
||||
CREATE INDEX IF NOT EXISTS idx_coding_usage_directory ON coding_usage(directory_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_coding_usage_timestamp ON coding_usage(timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_company ON contacts(company);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_tags ON contacts(tags);
|
||||
-- Kontakt-Indizes für Performance
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_type ON contacts(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_status ON contacts(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_project ON contacts(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_parent ON contacts(parent_company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_display_name ON contacts(display_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_details_contact ON contact_details(contact_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_details_type ON contact_details(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_interactions_contact ON contact_interactions(contact_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_interactions_date ON contact_interactions(interaction_date);
|
||||
`);
|
||||
|
||||
logger.info('Datenbank-Tabellen erstellt');
|
||||
|
||||
@@ -13,7 +13,7 @@ const JWT_SECRET = process.env.JWT_SECRET;
|
||||
if (!JWT_SECRET || JWT_SECRET.length < 32) {
|
||||
throw new Error('JWT_SECRET muss in .env gesetzt und mindestens 32 Zeichen lang sein!');
|
||||
}
|
||||
const ACCESS_TOKEN_EXPIRY = 15; // Minuten (kürzer für mehr Sicherheit)
|
||||
const ACCESS_TOKEN_EXPIRY = 60; // Minuten (kürzer für mehr Sicherheit)
|
||||
const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60; // 7 Tage in Minuten
|
||||
const SESSION_TIMEOUT = parseInt(process.env.SESSION_TIMEOUT) || 30; // Minuten
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const backup = require('../utils/backup');
|
||||
*/
|
||||
const DEFAULT_UPLOAD_SETTINGS = {
|
||||
maxFileSizeMB: 15,
|
||||
allowedExtensions: ['pdf', 'docx', 'txt']
|
||||
allowedExtensions: ['pdf', 'docx', 'doc', 'txt', 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'xls', 'xlsx', 'ppt', 'pptx', 'rtf', 'csv', 'json', 'html']
|
||||
};
|
||||
|
||||
// Alle Admin-Routes erfordern Authentifizierung und Admin-Rolle
|
||||
|
||||
@@ -252,17 +252,20 @@ router.delete('/:id', (req, res) => {
|
||||
return res.status(404).json({ error: 'Spalte nicht gefunden' });
|
||||
}
|
||||
|
||||
// Prüfen ob Aufgaben in der Spalte sind
|
||||
const taskCount = db.prepare(
|
||||
'SELECT COUNT(*) as count FROM tasks WHERE column_id = ?'
|
||||
// Prüfen ob AKTIVE (nicht-archivierte) Aufgaben in der Spalte sind
|
||||
const activeTaskCount = db.prepare(
|
||||
'SELECT COUNT(*) as count FROM tasks WHERE column_id = ? AND (archived IS NULL OR archived = 0)'
|
||||
).get(columnId).count;
|
||||
|
||||
if (taskCount > 0) {
|
||||
if (activeTaskCount > 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Spalte enthält noch Aufgaben. Verschiebe oder lösche diese zuerst.'
|
||||
});
|
||||
}
|
||||
|
||||
// Archivierte Aufgaben: column_id auf NULL setzen (bleiben im Archiv erhalten)
|
||||
db.prepare('UPDATE tasks SET column_id = NULL WHERE column_id = ? AND archived = 1').run(columnId);
|
||||
|
||||
// Mindestens eine Spalte muss bleiben
|
||||
const columnCount = db.prepare(
|
||||
'SELECT COUNT(*) as count FROM columns WHERE project_id = ?'
|
||||
|
||||
@@ -1,438 +1,445 @@
|
||||
/**
|
||||
* TASKMATE - Contact Routes
|
||||
* =========================
|
||||
* CRUD für Kontakte
|
||||
* TASKMATE - Kontakte API
|
||||
* ========================
|
||||
* REST API für Kontaktmanagement
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const router = require('express').Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const { validators } = require('../middleware/validation');
|
||||
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');
|
||||
|
||||
/**
|
||||
* 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;
|
||||
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 creator_name
|
||||
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 = [];
|
||||
|
||||
// Suchfilter
|
||||
// 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 LIKE ? OR
|
||||
c.email LIKE ? OR
|
||||
c.phone LIKE ? OR
|
||||
c.mobile LIKE ?
|
||||
c.company_name LIKE ? OR
|
||||
c.tags LIKE ? OR
|
||||
c.notes LIKE ?
|
||||
)`;
|
||||
const searchParam = `%${search}%`;
|
||||
params.push(searchParam, searchParam, searchParam, searchParam, searchParam, searchParam);
|
||||
const searchPattern = `%${search}%`;
|
||||
params.push(searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern);
|
||||
}
|
||||
|
||||
// Tag-Filter
|
||||
if (tag) {
|
||||
query += ` AND c.tags LIKE ?`;
|
||||
params.push(`%${tag}%`);
|
||||
}
|
||||
query += ' ORDER BY c.display_name ASC';
|
||||
|
||||
// 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);
|
||||
|
||||
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);
|
||||
|
||||
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
|
||||
})));
|
||||
return {
|
||||
...contact,
|
||||
details
|
||||
};
|
||||
});
|
||||
|
||||
res.json({ success: true, data: contactsWithDetails });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Kontakte:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
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', (req, res) => {
|
||||
// GET /api/contacts/:id - Einzelnen Kontakt abrufen
|
||||
router.get('/:id', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const contactId = req.params.id;
|
||||
const { id } = req.params;
|
||||
|
||||
const contact = db.prepare(`
|
||||
SELECT c.*, u.display_name as creator_name
|
||||
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(contactId);
|
||||
`).get(id);
|
||||
|
||||
if (!contact) {
|
||||
return res.status(404).json({ error: 'Kontakt nicht gefunden' });
|
||||
return res.status(404).json({ success: false, 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
|
||||
});
|
||||
// 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: error.message, contactId: req.params.id });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
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('/', validators.contact, (req, res) => {
|
||||
// POST /api/contacts - Neuen Kontakt erstellen
|
||||
router.post('/', authenticateToken, (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 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 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
|
||||
);
|
||||
INSERT INTO contact_interactions (
|
||||
contact_id, type, subject, content, created_by
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
`).run(id, type, subject, content, currentUser.id);
|
||||
|
||||
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 = ?
|
||||
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);
|
||||
|
||||
// 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 });
|
||||
res.json({ success: true, data: newInteraction });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen des Kontakts:', { error: error.message, body: req.body });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
logger.error('Fehler beim Hinzufügen der Interaktion:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/contacts/:id
|
||||
* Kontakt aktualisieren
|
||||
*/
|
||||
router.put('/:id', validators.contact, (req, res) => {
|
||||
// =====================
|
||||
// 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 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;
|
||||
const { id } = req.params;
|
||||
|
||||
// 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' });
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Keine Datei hochgeladen'
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
// Avatar-URL speichern
|
||||
const avatarUrl = `/uploads/${req.file.filename}`;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
db.prepare('UPDATE contacts SET avatar_url = ? WHERE id = ?')
|
||||
.run(avatarUrl, id);
|
||||
|
||||
res.json(Array.from(allTags).sort());
|
||||
res.json({
|
||||
success: true,
|
||||
data: { avatar_url: avatarUrl },
|
||||
message: 'Avatar erfolgreich hochgeladen'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Tags:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
logger.error('Fehler beim Avatar-Upload:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -522,7 +522,7 @@ router.post('/entries', (req, res) => {
|
||||
actorId: req.user.id
|
||||
},
|
||||
io,
|
||||
false // nicht persistent
|
||||
true // persistent, damit die Benachrichtigung in der Inbox bleibt
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -828,7 +828,7 @@ router.post('/:id/duplicate', (req, res) => {
|
||||
router.put('/:id/archive', (req, res) => {
|
||||
try {
|
||||
const taskId = req.params.id;
|
||||
const { archived } = req.body;
|
||||
const { archived, columnId } = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
@@ -837,16 +837,42 @@ router.put('/:id/archive', (req, res) => {
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
db.prepare('UPDATE tasks SET archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||||
.run(archived ? 1 : 0, taskId);
|
||||
// Bei Wiederherstellung: Prüfen ob Spalte vorhanden
|
||||
if (!archived && !task.column_id) {
|
||||
// Task hat keine Spalte (wurde mit gelöschter Spalte archiviert)
|
||||
if (!columnId) {
|
||||
return res.status(400).json({
|
||||
error: 'Spalte erforderlich',
|
||||
requiresColumn: true
|
||||
});
|
||||
}
|
||||
// Prüfen ob Spalte existiert und zum Projekt gehört
|
||||
const column = db.prepare('SELECT * FROM columns WHERE id = ? AND project_id = ?')
|
||||
.get(columnId, task.project_id);
|
||||
if (!column) {
|
||||
return res.status(400).json({ error: 'Ungültige Spalte' });
|
||||
}
|
||||
// Wiederherstellen mit neuer Spalte
|
||||
db.prepare('UPDATE tasks SET archived = 0, column_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||||
.run(columnId, taskId);
|
||||
} else {
|
||||
// Normales Archivieren/Wiederherstellen
|
||||
db.prepare('UPDATE tasks SET archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||||
.run(archived ? 1 : 0, taskId);
|
||||
}
|
||||
|
||||
addHistory(db, taskId, req.user.id, archived ? 'archived' : 'restored');
|
||||
|
||||
logger.info(`Aufgabe ${archived ? 'archiviert' : 'wiederhergestellt'}: ${task.title}`);
|
||||
|
||||
// WebSocket
|
||||
// WebSocket - vollständige Task-Daten senden
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('task:archived', { id: taskId, archived: !!archived });
|
||||
const updatedTask = getFullTask(db, taskId);
|
||||
io.to(`project:${task.project_id}`).emit('task:archived', {
|
||||
id: taskId,
|
||||
archived: !!archived,
|
||||
columnId: updatedTask?.columnId
|
||||
});
|
||||
|
||||
res.json({ message: archived ? 'Aufgabe archiviert' : 'Aufgabe wiederhergestellt' });
|
||||
} catch (error) {
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren