283 lines
9.8 KiB
JavaScript
283 lines
9.8 KiB
JavaScript
|
|
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);
|
||
|
|
});
|
||
|
|
|
||
|
|
// 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, ausschuesse, personen } = req.body;
|
||
|
|
|
||
|
|
const result = db.prepare(`
|
||
|
|
INSERT INTO antraege (titel, kurzbeschreibung, prioritaet_id, status_id, bereich_id)
|
||
|
|
VALUES (?, ?, ?, ?, ?)
|
||
|
|
`).run(titel, kurzbeschreibung, prioritaet_id || 2, status_id || 1, bereich_id || 1);
|
||
|
|
|
||
|
|
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}`);
|
||
|
|
});
|