import express from 'express'; import { pool } from '../db.js'; import { requireAuth } from '../middleware/auth.js'; const router = express.Router(); router.use(requireAuth); const STATUSES = new Set([ 'Neu', 'Kontaktiert', 'Qualifiziert', 'Angebot', 'Gewonnen', 'Verloren', ]); // Felder, die per API gesetzt werden duerfen. owner_id/created_by werden // separat behandelt, alles andere ist nicht beschreibbar. const TEXT_FIELDS = [ 'salutation', 'first_name', 'last_name', 'company', 'position', 'email', 'phone', 'mobile', 'linkedin', 'website', 'street', 'zip', 'city', 'country', 'source', 'notes', 'avatar', ]; function emptyToNull(value) { if (value === null || value === undefined) return null; const text = String(value).trim(); return text === '' ? null : text; } function parseTags(raw) { if (!raw) return []; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.filter((t) => typeof t === 'string') : []; } catch { return []; } } function serializeTags(value) { if (!Array.isArray(value)) return null; const tags = [...new Set(value.map((t) => String(t).trim()).filter(Boolean))]; return JSON.stringify(tags); } function mapLead(row) { return { ...row, tags: parseTags(row.tags), owner_id: row.owner_id, created_by: row.created_by }; } const SELECT_LEADS = ` SELECT l.*, creator.name AS created_by_name, owner.name AS owner_name FROM crm_leads l LEFT JOIN crm_users creator ON creator.id = l.created_by LEFT JOIN crm_users owner ON owner.id = l.owner_id `; async function fetchLead(id) { const [rows] = await pool.query(`${SELECT_LEADS} WHERE l.id = ? LIMIT 1`, [id]); return rows[0] ? mapLead(rows[0]) : null; } /** Prueft, ob eine uebergebene Benutzer-ID existiert. */ async function userExists(id) { if (id === null) return true; const [rows] = await pool.query('SELECT id FROM crm_users WHERE id = ? LIMIT 1', [id]); return Boolean(rows[0]); } // GET /api/leads router.get('/', async (req, res, next) => { try { const [rows] = await pool.query(`${SELECT_LEADS} ORDER BY l.updated_at DESC, l.id DESC`); res.json(rows.map(mapLead)); } catch (err) { next(err); } }); // POST /api/leads router.post('/', async (req, res, next) => { const firstName = emptyToNull(req.body?.first_name); const lastName = emptyToNull(req.body?.last_name); if (!firstName || !lastName) { return res.status(400).json({ error: 'Vorname und Nachname sind erforderlich.' }); } const status = STATUSES.has(req.body?.status) ? req.body.status : 'Neu'; const ownerId = req.body?.owner_id === undefined || req.body.owner_id === null || req.body.owner_id === '' ? req.user.id : Number(req.body.owner_id); if (Number.isNaN(ownerId) || !(await userExists(ownerId))) { return res.status(400).json({ error: 'Zustaendiger Benutzer existiert nicht.' }); } const columns = [...TEXT_FIELDS, 'status', 'tags', 'owner_id', 'created_by']; const values = [ ...TEXT_FIELDS.map((field) => field === 'first_name' ? firstName : field === 'last_name' ? lastName : emptyToNull(req.body?.[field]) ), status, serializeTags(req.body?.tags) ?? '[]', ownerId, req.user.id, ]; try { const [result] = await pool.query( `INSERT INTO crm_leads (${columns.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`, values ); res.status(201).json(await fetchLead(result.insertId)); } catch (err) { next(err); } }); // PATCH /api/leads/:id router.patch('/:id', async (req, res, next) => { const id = Number(req.params.id); if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Lead-ID.' }); const fields = []; const values = []; for (const field of TEXT_FIELDS) { if (req.body?.[field] === undefined) continue; const value = emptyToNull(req.body[field]); if ((field === 'first_name' || field === 'last_name') && !value) { return res.status(400).json({ error: 'Vorname und Nachname duerfen nicht leer sein.' }); } fields.push(`${field} = ?`); values.push(value); } if (req.body?.status !== undefined) { if (!STATUSES.has(req.body.status)) return res.status(400).json({ error: 'Ungueltiger Status.' }); fields.push('status = ?'); values.push(req.body.status); } if (req.body?.tags !== undefined) { fields.push('tags = ?'); values.push(serializeTags(req.body.tags) ?? '[]'); } if (req.body?.owner_id !== undefined) { const ownerId = req.body.owner_id === null || req.body.owner_id === '' ? null : Number(req.body.owner_id); if (ownerId !== null && (Number.isNaN(ownerId) || !(await userExists(ownerId)))) { return res.status(400).json({ error: 'Zustaendiger Benutzer existiert nicht.' }); } fields.push('owner_id = ?'); values.push(ownerId); } if (fields.length === 0) return res.status(400).json({ error: 'Keine Aenderungen uebergeben.' }); try { values.push(id); const [result] = await pool.query( `UPDATE crm_leads SET ${fields.join(', ')} WHERE id = ?`, values ); if (result.affectedRows === 0) return res.status(404).json({ error: 'Lead nicht gefunden.' }); res.json(await fetchLead(id)); } catch (err) { next(err); } }); // DELETE /api/leads/:id - Verbindungen entfernt die DB per ON DELETE CASCADE router.delete('/:id', async (req, res, next) => { const id = Number(req.params.id); if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungueltige Lead-ID.' }); try { const [result] = await pool.query('DELETE FROM crm_leads WHERE id = ?', [id]); if (result.affectedRows === 0) return res.status(404).json({ error: 'Lead nicht gefunden.' }); res.json({ ok: true }); } catch (err) { next(err); } }); export default router;