import express from 'express'; import cors from 'cors'; import Database from 'better-sqlite3'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __dirname = dirname(fileURLToPath(import.meta.url)); const app = express(); const PORT = process.env.PORT || 3000; const DB_PATH = process.env.DB_PATH || join(__dirname, '../data/antragsideen.db'); app.use(cors()); app.use(express.json({ limit: '10mb' })); // DB Connection const db = new Database(DB_PATH); db.pragma('foreign_keys = ON'); // Health check app.get('/health', (req, res) => res.json({ status: 'ok' })); // ============================================ // ANTRÄGE // ============================================ // Liste aller Anträge (mit Joins) app.get('/api/antraege', (req, res) => { const antraege = db.prepare(` SELECT a.*, p.name as prioritaet, p.farbe as prioritaet_farbe, s.name as status, s.farbe as status_farbe, b.name as bereich, b.farbe as bereich_farbe, b.icon as bereich_icon FROM antraege a LEFT JOIN prioritaeten p ON a.prioritaet_id = p.id LEFT JOIN status_typen s ON a.status_id = s.id LEFT JOIN bereiche b ON a.bereich_id = b.id ORDER BY p.reihenfolge, a.id `).all(); // Ausschüsse und Personen hinzufügen const ausschuesse = db.prepare(` SELECT aa.antrag_id, aus.id, aus.name, aus.kuerzel FROM antrag_ausschuesse aa JOIN ausschuesse aus ON aa.ausschuss_id = aus.id `).all(); const personen = db.prepare(` SELECT ap.antrag_id, p.id, p.name, p.rolle FROM antrag_personen ap JOIN personen p ON ap.person_id = p.id `).all(); const ausschuesseMap = {}; ausschuesse.forEach(a => { if (!ausschuesseMap[a.antrag_id]) ausschuesseMap[a.antrag_id] = []; ausschuesseMap[a.antrag_id].push({ id: a.id, name: a.name, kuerzel: a.kuerzel }); }); const personenMap = {}; personen.forEach(p => { if (!personenMap[p.antrag_id]) personenMap[p.antrag_id] = []; personenMap[p.antrag_id].push({ id: p.id, name: p.name, rolle: p.rolle }); }); antraege.forEach(a => { a.ausschuesse = ausschuesseMap[a.id] || []; a.personen = personenMap[a.id] || []; }); res.json(antraege); }); // Fuzzy-Suche für Duplikat-Prüfung (muss vor :id stehen) app.get('/api/antraege/suche', (req, res) => { const q = (req.query.q || '').trim().toLowerCase(); if (q.length < 2) return res.json([]); const words = q.split(/\s+/).filter(w => w.length >= 2); if (!words.length) return res.json([]); const antraege = db.prepare(` SELECT a.id, a.titel, a.kurzbeschreibung, p.name as prioritaet, s.name as status, b.name as bereich FROM antraege a LEFT JOIN prioritaeten p ON a.prioritaet_id = p.id LEFT JOIN status_typen s ON a.status_id = s.id LEFT JOIN bereiche b ON a.bereich_id = b.id `).all(); const results = antraege .map(a => { const text = (a.titel + ' ' + (a.kurzbeschreibung || '')).toLowerCase(); const score = words.reduce((s, w) => s + (text.includes(w) ? 1 : 0), 0); return { ...a, score }; }) .filter(a => a.score > 0) .sort((a, b) => b.score - a.score) .slice(0, 5); res.json(results); }); // Einzelner Antrag app.get('/api/antraege/:id', (req, res) => { const antrag = db.prepare(` SELECT a.*, p.name as prioritaet, s.name as status, b.name as bereich FROM antraege a LEFT JOIN prioritaeten p ON a.prioritaet_id = p.id LEFT JOIN status_typen s ON a.status_id = s.id LEFT JOIN bereiche b ON a.bereich_id = b.id WHERE a.id = ? `).get(req.params.id); if (!antrag) return res.status(404).json({ error: 'Nicht gefunden' }); antrag.ausschuesse = db.prepare(` SELECT aus.id, aus.name FROM antrag_ausschuesse aa JOIN ausschuesse aus ON aa.ausschuss_id = aus.id WHERE aa.antrag_id = ? `).all(req.params.id); antrag.personen = db.prepare(` SELECT p.id, p.name FROM antrag_personen ap JOIN personen p ON ap.person_id = p.id WHERE ap.antrag_id = ? `).all(req.params.id); res.json(antrag); }); // Neuer Antrag app.post('/api/antraege', (req, res) => { const { titel, kurzbeschreibung, prioritaet_id, status_id, bereich_id, dossier, antragstext, notizen, allris_referenzen, referenzen, ausschuesse, personen } = req.body; const result = db.prepare(` INSERT INTO antraege (titel, kurzbeschreibung, prioritaet_id, status_id, bereich_id, dossier, antragstext, notizen, allris_referenzen, referenzen) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run(titel, kurzbeschreibung, prioritaet_id || 2, status_id || 1, bereich_id || 1, dossier || null, antragstext || null, notizen || null, allris_referenzen || null, referenzen || null); const antragId = result.lastInsertRowid; if (ausschuesse?.length) { const insertAus = db.prepare('INSERT OR IGNORE INTO antrag_ausschuesse (antrag_id, ausschuss_id) VALUES (?, ?)'); ausschuesse.forEach(id => insertAus.run(antragId, id)); } if (personen?.length) { const insertPers = db.prepare('INSERT OR IGNORE INTO antrag_personen (antrag_id, person_id) VALUES (?, ?)'); personen.forEach(id => insertPers.run(antragId, id)); } res.status(201).json({ id: antragId }); }); // Antrag aktualisieren app.put('/api/antraege/:id', (req, res) => { const { titel, kurzbeschreibung, prioritaet_id, status_id, bereich_id, dossier, antragstext, notizen, ausschuesse, personen, position_x, position_y } = req.body; db.prepare(` UPDATE antraege SET titel = COALESCE(?, titel), kurzbeschreibung = COALESCE(?, kurzbeschreibung), prioritaet_id = COALESCE(?, prioritaet_id), status_id = COALESCE(?, status_id), bereich_id = COALESCE(?, bereich_id), dossier = COALESCE(?, dossier), antragstext = COALESCE(?, antragstext), notizen = COALESCE(?, notizen), position_x = COALESCE(?, position_x), position_y = COALESCE(?, position_y), aktualisiert_am = date('now') WHERE id = ? `).run(titel, kurzbeschreibung, prioritaet_id, status_id, bereich_id, dossier, antragstext, notizen, position_x, position_y, req.params.id); // Ausschüsse aktualisieren if (ausschuesse !== undefined) { db.prepare('DELETE FROM antrag_ausschuesse WHERE antrag_id = ?').run(req.params.id); const insertAus = db.prepare('INSERT INTO antrag_ausschuesse (antrag_id, ausschuss_id) VALUES (?, ?)'); ausschuesse.forEach(id => insertAus.run(req.params.id, id)); } // Personen aktualisieren if (personen !== undefined) { db.prepare('DELETE FROM antrag_personen WHERE antrag_id = ?').run(req.params.id); const insertPers = db.prepare('INSERT INTO antrag_personen (antrag_id, person_id) VALUES (?, ?)'); personen.forEach(id => insertPers.run(req.params.id, id)); } res.json({ success: true }); }); // Antrag löschen app.delete('/api/antraege/:id', (req, res) => { db.prepare('DELETE FROM antraege WHERE id = ?').run(req.params.id); res.json({ success: true }); }); // ============================================ // GRAPH-DATEN // ============================================ app.get('/api/graph', (req, res) => { const nodes = db.prepare(` SELECT a.id, a.titel, a.kurzbeschreibung, a.position_x, a.position_y, p.name as prioritaet, p.farbe as prioritaet_farbe, p.reihenfolge as prio_order, b.name as bereich, b.farbe as bereich_farbe, b.icon as bereich_icon, CASE WHEN length(a.dossier) > 100 THEN 1 ELSE 0 END as hat_dossier FROM antraege a LEFT JOIN prioritaeten p ON a.prioritaet_id = p.id LEFT JOIN bereiche b ON a.bereich_id = b.id `).all(); // Kanten basierend auf gemeinsamen Ausschüssen/Personen const edgesByAusschuss = db.prepare(` SELECT DISTINCT aa1.antrag_id as source, aa2.antrag_id as target, 'ausschuss' as typ FROM antrag_ausschuesse aa1 JOIN antrag_ausschuesse aa2 ON aa1.ausschuss_id = aa2.ausschuss_id WHERE aa1.antrag_id < aa2.antrag_id `).all(); const edgesByPerson = db.prepare(` SELECT DISTINCT ap1.antrag_id as source, ap2.antrag_id as target, 'person' as typ FROM antrag_personen ap1 JOIN antrag_personen ap2 ON ap1.person_id = ap2.person_id WHERE ap1.antrag_id < ap2.antrag_id `).all(); const manualEdges = db.prepare(` SELECT von_antrag_id as source, nach_antrag_id as target, typ, gewicht FROM verbindungen `).all(); res.json({ nodes, edges: { ausschuss: edgesByAusschuss, person: edgesByPerson, manuell: manualEdges } }); }); // Positionen speichern app.put('/api/graph/positions', (req, res) => { const { positions } = req.body; // { id: { x, y }, ... } const update = db.prepare('UPDATE antraege SET position_x = ?, position_y = ? WHERE id = ?'); db.exec('BEGIN'); for (const [id, pos] of Object.entries(positions)) { update.run(pos.x, pos.y, id); } db.exec('COMMIT'); res.json({ success: true }); }); // ============================================ // VERBINDUNGEN // ============================================ app.get('/api/verbindungen', (req, res) => { const verbindungen = db.prepare('SELECT * FROM verbindungen').all(); res.json(verbindungen); }); app.post('/api/verbindungen', (req, res) => { const { von_antrag_id, nach_antrag_id, typ, gewicht, notiz } = req.body; const result = db.prepare(` INSERT INTO verbindungen (von_antrag_id, nach_antrag_id, typ, gewicht, notiz) VALUES (?, ?, ?, ?, ?) `).run(von_antrag_id, nach_antrag_id, typ || 'manuell', gewicht || 1, notiz); res.status(201).json({ id: result.lastInsertRowid }); }); app.delete('/api/verbindungen/:id', (req, res) => { db.prepare('DELETE FROM verbindungen WHERE id = ?').run(req.params.id); res.json({ success: true }); }); // ============================================ // STAMMDATEN // ============================================ app.get('/api/stammdaten', (req, res) => { res.json({ prioritaeten: db.prepare('SELECT * FROM prioritaeten ORDER BY reihenfolge').all(), status_typen: db.prepare('SELECT * FROM status_typen ORDER BY reihenfolge').all(), bereiche: db.prepare('SELECT * FROM bereiche').all(), ausschuesse: db.prepare('SELECT * FROM ausschuesse ORDER BY name').all(), personen: db.prepare('SELECT * FROM personen ORDER BY name').all() }); }); // ============================================ // START // ============================================ app.listen(PORT, () => { console.log(`🚀 API läuft auf http://localhost:${PORT}`); });