const express = require('express'); const cors = require('cors'); const multer = require('multer'); const sharp = require('sharp'); const Tesseract = require('tesseract.js'); const { Pool } = require('pg'); const { v4: uuidv4 } = require('uuid'); const fs = require('fs'); const path = require('path'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); // Auth imports const authRoutes = require('./routes/auth'); const { authRequired, adminRequired } = require('./middleware/auth'); const app = express(); const PORT = process.env.PORT || 3001; // Database connection - Docker-compatible defaults const pool = new Pool({ host: process.env.DB_HOST || 'buchhaltung-db', port: process.env.DB_PORT || 5432, database: process.env.DB_NAME || 'buchhaltung', user: process.env.DB_USER || 'postgres', password: process.env.DB_PASSWORD || 'postgres', }); // Middleware app.use(cors()); app.use(express.json()); app.use('/uploads', express.static('uploads')); // Auth Routes app.use('/api/auth', authRoutes); // Nebenkosten Routes laden const nebenkostenRoutes = require('./routes/nebenkosten'); nebenkostenRoutes(app); // Ensure uploads directory exists const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // Multer configuration const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, 'uploads/'); }, filename: (req, file, cb) => { const uniqueName = `${Date.now()}-${uuidv4()}${path.extname(file.originalname)}`; cb(null, uniqueName); } }); const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit fileFilter: (req, file, cb) => { const allowedTypes = /jpeg|jpg|png|pdf|webp/; const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); const mimetype = allowedTypes.test(file.mimetype); if (extname && mimetype) { return cb(null, true); } cb(new Error('Nur Bilder und PDFs erlaubt')); } }); // Initialize database tables async function initDatabase() { const client = await pool.connect(); try { await client.query(` CREATE TABLE IF NOT EXISTS belege ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), date DATE NOT NULL, haendler VARCHAR(255), betrag DECIMAL(10,2) NOT NULL, mwst_satz DECIMAL(4,2) DEFAULT 19.00, kategorie VARCHAR(100), status VARCHAR(50) DEFAULT 'neu', file_path VARCHAR(500), ocr_text TEXT, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ) `); await client.query(` CREATE TABLE IF NOT EXISTS rechnungen ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), rechnung_nr VARCHAR(50) UNIQUE NOT NULL, kunde VARCHAR(255) NOT NULL, kunde_email VARCHAR(255), kunde_adresse TEXT, leistung TEXT NOT NULL, betrag DECIMAL(10,2) NOT NULL, ust_satz DECIMAL(4,2) DEFAULT 19.00, datum DATE NOT NULL, faelligkeit DATE, status VARCHAR(50) DEFAULT 'offen', pdf_path VARCHAR(500), created_at TIMESTAMP DEFAULT NOW() ) `); await client.query(` CREATE TABLE IF NOT EXISTS nebenkosten ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), jahr INTEGER NOT NULL, wohnung VARCHAR(100), mieter VARCHAR(255), kaltmiete DECIMAL(10,2), nebenkosten DECIMAL(10,2), heizkosten DECIMAL(10,2), wasser DECIMAL(10,2), muell DECIMAL(10,2), versicherung DECIMAL(10,2), sonstiges DECIMAL(10,2), created_at TIMESTAMP DEFAULT NOW() ) `); // Kredite Tabelle await client.query(` CREATE TABLE IF NOT EXISTS kredite ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, kreditgeber VARCHAR(255), person VARCHAR(100), -- 'Kerstin', 'Niki', etc. richtung VARCHAR(50) DEFAULT 'ausgehend', -- 'eingehend' (Forderung) oder 'ausgehend' (Schulden) ursprungsschuld DECIMAL(12,2) NOT NULL, restschuld DECIMAL(12,2) NOT NULL, monatsrate DECIMAL(10,2) NOT NULL, zinssatz DECIMAL(5,2) DEFAULT 4.00, start_datum DATE NOT NULL, end_datum DATE, laufzeit_monate INTEGER, faelligkeit_tag INTEGER DEFAULT 1, status VARCHAR(50) DEFAULT 'aktiv', notizen TEXT, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ) `); // Kredit-Buchungen (Tilgungsverlauf) await client.query(` CREATE TABLE IF NOT EXISTS kredit_buchungen ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), kredit_id UUID REFERENCES kredite(id) ON DELETE CASCADE, datum DATE NOT NULL, rate_betrag DECIMAL(10,2) NOT NULL, zinsen DECIMAL(10,2) DEFAULT 0, tilgung DECIMAL(10,2) NOT NULL, restschuld_nach DECIMAL(12,2) NOT NULL, created_at TIMESTAMP DEFAULT NOW() ) `); // Fixe Ausgaben (für Cashflow) await client.query(` CREATE TABLE IF NOT EXISTS fixe_ausgaben ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, kategorie VARCHAR(100), betrag DECIMAL(10,2) NOT NULL, intervall VARCHAR(50) DEFAULT 'monatlich', faelligkeit_tag INTEGER, is_active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT NOW() ) `); // Stunden Tabelle await client.query(` CREATE TABLE IF NOT EXISTS stunden ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), datum DATE NOT NULL, kunde VARCHAR(255) NOT NULL, beschreibung TEXT, stunden DECIMAL(10,2) NOT NULL, stundensatz DECIMAL(10,2) NOT NULL, betrag DECIMAL(10,2) NOT NULL, status VARCHAR(50) DEFAULT 'offen', created_at TIMESTAMP DEFAULT NOW() ) `); // Kostenplanung Tabelle await client.query(` CREATE TABLE IF NOT EXISTS kostenplanung ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), objekt VARCHAR(100) NOT NULL, name VARCHAR(255) NOT NULL, kategorie VARCHAR(100) NOT NULL, monate DECIMAL(10,2)[] DEFAULT ARRAY[0,0,0,0,0,0,0,0,0,0,0,0], jahr INTEGER DEFAULT EXTRACT(YEAR FROM CURRENT_DATE), created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ) `); // Benutzer (für Auth) await client.query(` CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(100) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, pin VARCHAR(10), role VARCHAR(50) DEFAULT 'user', is_active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT NOW(), last_login TIMESTAMP ) `); console.log('✅ Datenbank-Tabellen erstellt'); // Migration: Füge richtung-Spalte zu bestehenden kredite-Tabellen hinzu await client.query(` ALTER TABLE kredite ADD COLUMN IF NOT EXISTS richtung VARCHAR(50) DEFAULT 'ausgehend' `); console.log('✅ Migration: richtung-Spalte geprüft/hinzugefügt'); } finally { client.release(); } } // OCR Endpoint app.post('/api/ocr', upload.single('image'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'Kein Bild hochgeladen' }); } const filePath = req.file.path; // Preprocess image with sharp const processedPath = filePath + '-processed.png'; await sharp(filePath) .resize(2000, null, { withoutEnlargement: true }) .greyscale() .normalize() .toFile(processedPath); // Run OCR with German language const result = await Tesseract.recognize( processedPath, 'deu', { logger: m => console.log(m) } ); const text = result.data.text; // Extract data with regex patterns const extractedData = extractReceiptData(text); // Save to database const query = ` INSERT INTO belege (date, haendler, betrag, mwst_satz, kategorie, status, file_path, ocr_text) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING * `; const values = [ extractedData.date || new Date(), extractedData.haendler || 'Unbekannt', extractedData.betrag || 0, extractedData.mwst || 19.00, 'unbekannt', 'neu', req.file.filename, text ]; const dbResult = await pool.query(query, values); // Cleanup processed file fs.unlinkSync(processedPath); res.json({ success: true, beleg: dbResult.rows[0], extracted: extractedData, rawText: text.substring(0, 500) // First 500 chars }); } catch (error) { console.error('OCR Error:', error); res.status(500).json({ error: error.message }); } }); // Extract receipt data using regex patterns function extractReceiptData(text) { const data = { haendler: null, betrag: null, date: null, mwst: 19.00 }; // Try to find total amount const totalPatterns = [ /(?:gesamt|summe|total|betrag|zu zahlen)[\s:]*([\d.,]+)/i, /(?:eur|€)[\s:]*([\d.,]+)/i, /([\d.,]+)\s*(?:eur|€)/i ]; for (const pattern of totalPatterns) { const match = text.match(pattern); if (match) { let amount = match[1].replace(/\./g, '').replace(',', '.'); data.betrag = parseFloat(amount); break; } } // Try to find date const datePatterns = [ /(\d{2})[\/.-](\d{2})[\/.-](\d{4})/, // DD.MM.YYYY /(\d{2})[\/.-](\d{2})[\/.-](\d{2})/, // DD.MM.YY ]; for (const pattern of datePatterns) { const match = text.match(pattern); if (match) { const day = match[1]; const month = match[2]; const year = match[3].length === 2 ? '20' + match[3] : match[3]; data.date = `${year}-${month}-${day}`; break; } } // Try to find merchant name (first line often contains it) const lines = text.split('\n').filter(l => l.trim()); if (lines.length > 0) { // Skip common header words const skipWords = ['bon', 'beleg', 'quittung', 'kasse', 'rechnung']; for (const line of lines.slice(0, 5)) { const cleanLine = line.trim(); if (cleanLine.length > 2 && !skipWords.some(w => cleanLine.toLowerCase().includes(w))) { data.haendler = cleanLine; break; } } } return data; } // Belege API app.get('/api/belege', async (req, res) => { try { const { status, limit = 50 } = req.query; let query = 'SELECT * FROM belege ORDER BY created_at DESC LIMIT $1'; const values = [limit]; if (status) { query = 'SELECT * FROM belege WHERE status = $2 ORDER BY created_at DESC LIMIT $1'; values.push(status); } const result = await pool.query(query, values); res.json(result.rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/belege/:id', async (req, res) => { try { const result = await pool.query('SELECT * FROM belege WHERE id = $1', [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Beleg nicht gefunden' }); } res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/belege/:id', async (req, res) => { try { const { haendler, betrag, kategorie, status } = req.body; const query = ` UPDATE belege SET haendler = $1, betrag = $2, kategorie = $3, status = $4, updated_at = NOW() WHERE id = $5 RETURNING * `; const result = await pool.query(query, [haendler, betrag, kategorie, status, req.params.id]); res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/belege/:id', async (req, res) => { try { // Get file path first const beleg = await pool.query('SELECT file_path FROM belege WHERE id = $1', [req.params.id]); if (beleg.rows.length > 0 && beleg.rows[0].file_path) { const filePath = path.join(uploadsDir, beleg.rows[0].file_path); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } await pool.query('DELETE FROM belege WHERE id = $1', [req.params.id]); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Rechnungen API app.get('/api/rechnungen', async (req, res) => { try { const result = await pool.query('SELECT * FROM rechnungen ORDER BY datum DESC'); res.json(result.rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/rechnungen', async (req, res) => { try { const { kunde, kunde_email, kunde_adresse, leistung, betrag, ust_satz, datum, faelligkeit } = req.body; const rechnungNr = `RE-${new Date().getFullYear()}-${String(await getNextRechnungNr()).padStart(3, '0')}`; const query = ` INSERT INTO rechnungen (rechnung_nr, kunde, kunde_email, kunde_adresse, leistung, betrag, ust_satz, datum, faelligkeit) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING * `; const result = await pool.query(query, [rechnungNr, kunde, kunde_email, kunde_adresse, leistung, betrag, ust_satz, datum, faelligkeit]); res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); async function getNextRechnungNr() { const result = await pool.query("SELECT COUNT(*) FROM rechnungen WHERE rechnung_nr LIKE 'RE-' || EXTRACT(YEAR FROM CURRENT_DATE) || '-%'"); return parseInt(result.rows[0].count) + 1; } // EÜR API app.get('/api/euer', async (req, res) => { try { const { year = new Date().getFullYear() } = req.query; // Einnahmen aus Rechnungen const einnahmen = await pool.query(` SELECT COALESCE(SUM(betrag), 0) as total FROM rechnungen WHERE status = 'bezahlt' AND EXTRACT(YEAR FROM datum) = $1 `, [year]); // Ausgaben aus Belegen const ausgaben = await pool.query(` SELECT COALESCE(SUM(betrag), 0) as total FROM belege WHERE status = 'fertig' AND EXTRACT(YEAR FROM date) = $1 `, [year]); const buchungen = await pool.query(` SELECT * FROM euer_buchungen WHERE EXTRACT(YEAR FROM date) = $1 ORDER BY date DESC `, [year]); res.json({ jahr: year, einnahmen: einnahmen.rows[0].total, ausgaben: ausgaben.rows[0].total, ergebnis: einnahmen.rows[0].total - ausgaben.rows[0].total, buchungen: buchungen.rows }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Kunden API app.get('/api/kunden', async (req, res) => { try { const result = await pool.query('SELECT * FROM kunden ORDER BY name'); res.json(result.rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/kunden/:id', async (req, res) => { try { const result = await pool.query('SELECT * FROM kunden WHERE id = $1', [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Kunde nicht gefunden' }); } res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/kunden', async (req, res) => { try { const { name, adresse, plz, ort, email, telefon, notizen } = req.body; const query = ` INSERT INTO kunden (name, adresse, plz, ort, email, telefon, notizen) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * `; const result = await pool.query(query, [name, adresse, plz, ort, email, telefon, notizen]); res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/kunden/:id', async (req, res) => { try { const { name, adresse, plz, ort, email, telefon, notizen } = req.body; const query = ` UPDATE kunden SET name = COALESCE($1, name), adresse = COALESCE($2, adresse), plz = COALESCE($3, plz), ort = COALESCE($4, ort), email = COALESCE($5, email), telefon = COALESCE($6, telefon), notizen = COALESCE($7, notizen) WHERE id = $8 RETURNING * `; const result = await pool.query(query, [name, adresse, plz, ort, email, telefon, notizen, req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Kunde nicht gefunden' }); } res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/kunden/:id', async (req, res) => { try { const result = await pool.query('DELETE FROM kunden WHERE id = $1 RETURNING *', [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Kunde nicht gefunden' }); } res.json({ success: true, deleted: result.rows[0] }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Auftragsnachweise API app.get('/api/auftragsnachweise', async (req, res) => { try { const result = await pool.query(` SELECT an.*, k.name as kunden_name FROM auftragsnachweise an JOIN kunden k ON an.kunde_id = k.id ORDER BY an.datum DESC `); res.json(result.rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/auftragsnachweise/:id', async (req, res) => { try { const result = await pool.query(` SELECT an.*, k.name as kunden_name FROM auftragsnachweise an JOIN kunden k ON an.kunde_id = k.id WHERE an.id = $1 `, [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Auftragsnachweis nicht gefunden' }); } res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/auftragsnachweise', async (req, res) => { try { const { kunde_id, datum, beschreibung, stunden_ids, art_der_arbeit, anfahrt_km, pauschale } = req.body; const query = ` INSERT INTO auftragsnachweise (kunde_id, datum, beschreibung, stunden_ids, art_der_arbeit, anfahrt_km, pauschale) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * `; const result = await pool.query(query, [ kunde_id, datum, beschreibung, JSON.stringify(stunden_ids || []), JSON.stringify(art_der_arbeit || []), anfahrt_km || 0, pauschale || 0 ]); res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/auftragsnachweise/:id', async (req, res) => { try { const result = await pool.query('DELETE FROM auftragsnachweise WHERE id = $1 RETURNING *', [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Auftragsnachweis nicht gefunden' }); } res.json({ success: true, deleted: result.rows[0] }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Nebenkosten API app.get('/api/nebenkosten', async (req, res) => { try { const { jahr } = req.query; let query = 'SELECT * FROM nebenkosten'; const values = []; if (jahr) { query += ' WHERE jahr = $1'; values.push(jahr); } query += ' ORDER BY jahr DESC'; const result = await pool.query(query, values); res.json(result.rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/nebenkosten', async (req, res) => { try { const { jahr, wohnung, mieter, kaltmiete, nebenkosten, heizkosten, wasser, muell, versicherung, sonstiges } = req.body; const query = ` INSERT INTO nebenkosten (jahr, wohnung, mieter, kaltmiete, nebenkosten, heizkosten, wasser, muell, versicherung, sonstiges) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING * `; const result = await pool.query(query, [jahr, wohnung, mieter, kaltmiete, nebenkosten, heizkosten, wasser, muell, versicherung, sonstiges]); res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); // Nebenkosten PUT (Update) app.put('/api/nebenkosten/:id', async (req, res) => { try { const { jahr, wohnung, mieter, kaltmiete, nebenkosten, heizkosten, wasser, muell, versicherung, sonstiges } = req.body; const query = ` UPDATE nebenkosten SET jahr = $1, wohnung = $2, mieter = $3, kaltmiete = $4, nebenkosten = $5, heizkosten = $6, wasser = $7, muell = $8, versicherung = $9, sonstiges = $10 WHERE id = $11 RETURNING * `; const result = await pool.query(query, [jahr, wohnung, mieter, kaltmiete, nebenkosten, heizkosten, wasser, muell, versicherung, sonstiges, req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Nebenkosten nicht gefunden' }); } res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); // Nebenkosten DELETE app.delete('/api/nebenkosten/:id', async (req, res) => { try { const result = await pool.query('DELETE FROM nebenkosten WHERE id = $1 RETURNING *', [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Nebenkosten nicht gefunden' }); } res.json({ success: true, deleted: result.rows[0] }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Kredite API app.get('/api/kredite', async (req, res) => { try { const { person, status } = req.query; let query = 'SELECT * FROM kredite'; const values = []; const conditions = []; if (person) { conditions.push('person = $' + (values.length + 1)); values.push(person); } if (status) { conditions.push('status = $' + (values.length + 1)); values.push(status); } if (conditions.length > 0) { query += ' WHERE ' + conditions.join(' AND '); } query += ' ORDER BY created_at DESC'; const result = await pool.query(query, values); res.json(result.rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/kredite', async (req, res) => { const client = await pool.connect(); try { await client.query('BEGIN'); const { name, kreditgeber, person, ursprungsschuld, monatsrate, zinssatz, start_datum, laufzeit_monate, faelligkeit_tag, notizen, richtung } = req.body; // Ensure richtung column exists await client.query(` ALTER TABLE kredite ADD COLUMN IF NOT EXISTS richtung VARCHAR(50) DEFAULT 'ausgehend' `); // 1. Kredit erstellen const kreditQuery = ` INSERT INTO kredite (name, kreditgeber, person, ursprungsschuld, restschuld, monatsrate, zinssatz, start_datum, laufzeit_monate, faelligkeit_tag, notizen, richtung) VALUES ($1, $2, $3, $4, $4, $5, $6, $7, $8, $9, $10, COALESCE($11, 'ausgehend')) RETURNING * `; const kreditResult = await client.query(kreditQuery, [name, kreditgeber, person, ursprungsschuld, monatsrate, zinssatz, start_datum, laufzeit_monate, faelligkeit_tag, notizen, richtung]); const kredit = kreditResult.rows[0]; // 2. Startbuchung erstellen await client.query( 'INSERT INTO kredit_buchungen (kredit_id, datum, rate_betrag, zinsen, tilgung, restschuld_nach) VALUES ($1, $2, $3, $4, $5, $6)', [kredit.id, start_datum || new Date().toISOString().split('T')[0], 0, 0, 0, ursprungsschuld] ); // 3. Monatsbuchungen erstellen (Zinsen immer, Tilgung optional) // Auch ohne Rate werden Zinsbuchungen erstellt wenn Zinssatz > 0 const hatZinsen = zinssatz && parseFloat(zinssatz) > 0; const hatRate = monatsrate && parseFloat(monatsrate) > 0; if (hatZinsen || hatRate) { const startDate = new Date(start_datum || new Date()); const laufzeit = laufzeit_monate || (hatZinsen && !hatRate ? 12 : 60); // Bei reinen Zinskrediten: 1 Jahr Default let restschuld = parseFloat(ursprungsschuld); const rate = parseFloat(monatsrate || 0); const zins = parseFloat(zinssatz || 0) / 100 / 12; for (let i = 0; i < laufzeit; i++) { const buchungDatum = new Date(startDate); buchungDatum.setMonth(buchungDatum.getMonth() + i + 1); // +1 weil Startbuchung bereits erstellt const zinsen = restschuld * zins; let tilgung = 0; if (hatRate) { // Normale Annuitätentilgung tilgung = rate - zinsen; restschuld -= tilgung; } else if (hatZinsen) { // Rate = 0 aber Zinsen > 0: Zinsen werden zum Kapital addiert (Zinseszins) restschuld += zinsen; } // Bei Rate = 0 und Zinsen = 0: Restschuld bleibt gleich if (restschuld < 0) restschuld = 0; await client.query( 'INSERT INTO kredit_buchungen (kredit_id, datum, rate_betrag, zinsen, tilgung, restschuld_nach) VALUES ($1, $2, $3, $4, $5, $6)', [kredit.id, buchungDatum.toISOString().split('T')[0], rate, zinsen, tilgung, restschuld] ); if (restschuld <= 0) break; // Kredit abbezahlt } // Restschuld aktualisieren await client.query('UPDATE kredite SET restschuld = $1 WHERE id = $2', [restschuld, kredit.id]); } await client.query('COMMIT'); res.json(kredit); } catch (error) { await client.query('ROLLBACK'); res.status(500).json({ error: error.message }); } finally { client.release(); } }); app.get('/api/kredite/:id/buchungen', async (req, res) => { try { const result = await pool.query( 'SELECT * FROM kredit_buchungen WHERE kredit_id = $1 ORDER BY datum DESC', [req.params.id] ); res.json(result.rows); } catch (error) { res.status(500).json({ error: error.message }); } }); // Kredit PUT (Update) - Verwendet COALESCE für partielle Updates app.put('/api/kredite/:id', async (req, res) => { try { const { name, kreditgeber, person, richtung, ursprungsschuld, restschuld, monatsrate, zinssatz, start_datum, end_datum, laufzeit_monate, faelligkeit_tag, status, notizen } = req.body; const query = ` UPDATE kredite SET name = COALESCE($1, name), kreditgeber = COALESCE($2, kreditgeber), person = COALESCE($3, person), richtung = COALESCE($4, richtung), ursprungsschuld = COALESCE($5, ursprungsschuld), restschuld = COALESCE($6, restschuld), monatsrate = COALESCE($7, monatsrate), zinssatz = COALESCE($8, zinssatz), start_datum = COALESCE($9, start_datum), end_datum = COALESCE($10, end_datum), laufzeit_monate = COALESCE($11, laufzeit_monate), faelligkeit_tag = COALESCE($12, faelligkeit_tag), status = COALESCE($13, status), notizen = COALESCE($14, notizen), updated_at = NOW() WHERE id = $15 RETURNING * `; const result = await pool.query(query, [name, kreditgeber, person, richtung, ursprungsschuld, restschuld, monatsrate, zinssatz, start_datum, end_datum, laufzeit_monate, faelligkeit_tag, status, notizen, req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Kredit nicht gefunden' }); } res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); // Kredit PATCH (partielles Update) - für z.B. Restschuld-Updates app.patch('/api/kredite/:id', async (req, res) => { try { const { restschuld, status, notizen } = req.body; const updates = []; const values = []; if (restschuld !== undefined) { updates.push(`restschuld = $${values.length + 1}`); values.push(restschuld); } if (status !== undefined) { updates.push(`status = $${values.length + 1}`); values.push(status); } if (notizen !== undefined) { updates.push(`notizen = $${values.length + 1}`); values.push(notizen); } if (updates.length === 0) { return res.status(400).json({ error: 'Keine Felder zum Aktualisieren angegeben' }); } updates.push(`updated_at = NOW()`); values.push(req.params.id); const query = ` UPDATE kredite SET ${updates.join(', ')} WHERE id = $${values.length} RETURNING * `; const result = await pool.query(query, values); if (result.rows.length === 0) { return res.status(404).json({ error: 'Kredit nicht gefunden' }); } res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); // Kredit DELETE app.delete('/api/kredite/:id', async (req, res) => { try { // Lösche zuerst alle Buchungen await pool.query('DELETE FROM kredit_buchungen WHERE kredit_id = $1', [req.params.id]); // Dann den Kredit const result = await pool.query('DELETE FROM kredite WHERE id = $1 RETURNING *', [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Kredit nicht gefunden' }); } res.json({ success: true, deleted: result.rows[0] }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Kredit Buchung hinzufügen app.post('/api/kredite/:id/buchungen', async (req, res) => { try { const { datum, rate_betrag, zinsen, tilgung, restschuld_nach } = req.body; const query = ` INSERT INTO kredit_buchungen (kredit_id, datum, rate_betrag, zinsen, tilgung, restschuld_nach) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `; const result = await pool.query(query, [req.params.id, datum, rate_betrag, zinsen, tilgung, restschuld_nach]); // Update Restschuld im Kredit await pool.query('UPDATE kredite SET restschuld = $1, updated_at = NOW() WHERE id = $2', [restschuld_nach, req.params.id]); res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); // Kredit Buchung löschen app.delete('/api/kredite/:id/buchungen/:buchungId', async (req, res) => { try { await pool.query('DELETE FROM kredit_buchungen WHERE id = $1 AND kredit_id = $2', [req.params.buchungId, req.params.id]); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Kredit Buchungen NEU BERECHNEN (für bestehende Kredite ohne oder mit fehlerhaften Buchungen) app.post('/api/kredite/:id/buchungen/neuberechnen', async (req, res) => { const client = await pool.connect(); try { await client.query('BEGIN'); // 1. Kredit-Daten holen const kreditResult = await client.query( 'SELECT * FROM kredite WHERE id = $1', [req.params.id] ); if (kreditResult.rows.length === 0) { await client.query('ROLLBACK'); return res.status(404).json({ error: 'Kredit nicht gefunden' }); } const kredit = kreditResult.rows[0]; const { start_datum, ursprungsschuld, zinssatz, monatsrate, richtung } = kredit; // 2. Alte Buchungen löschen await client.query('DELETE FROM kredit_buchungen WHERE kredit_id = $1', [req.params.id]); // 3. Startbuchung erstellen await client.query( 'INSERT INTO kredit_buchungen (kredit_id, datum, rate_betrag, zinsen, tilgung, restschuld_nach) VALUES ($1, $2, $3, $4, $5, $6)', [req.params.id, start_datum, 0, 0, 0, ursprungsschuld] ); // 4. ALLE Zahlungen holen und chronologisch verarbeiten const zahlungenResult = await client.query( 'SELECT * FROM kredit_zahlungen WHERE kredit_id = $1 ORDER BY datum', [req.params.id] ); const zahlungen = zahlungenResult.rows; const zinsProMonat = parseFloat(zinssatz || 0) / 100 / 12; let restschuld = parseFloat(ursprungsschuld); const istForderung = richtung === 'eingehend'; let letzteBuchungDatum = new Date(start_datum); // Für jeden Monat von Start bis heute: Buchung erstellen const startDate = new Date(start_datum); const endDate = new Date(); let aktuellesDatum = new Date(startDate); aktuellesDatum.setMonth(aktuellesDatum.getMonth() + 1); // Erster Monat nach Start while (aktuellesDatum <= endDate) { const jahr = aktuellesDatum.getFullYear(); const monat = aktuellesDatum.getMonth() + 1; const monatStart = new Date(jahr, aktuellesDatum.getMonth(), 1); const monatEnde = new Date(jahr, aktuellesDatum.getMonth() + 1, 0); // Zahlungen in diesem Monat finden const zahlungenImMonat = zahlungen.filter(z => { const zDatum = new Date(z.datum); return zDatum >= monatStart && zDatum <= monatEnde; }); // Zinsen für den Monat berechnen const zinsen = restschuld * zinsProMonat; if (zahlungenImMonat.length > 0) { // Es gab Zahlungen in diesem Monat let monatsTilgung = 0; let monatsRate = 0; for (const z of zahlungenImMonat) { const betrag = parseFloat(z.betrag); if (z.typ === 'auslage') { // Auslage erhöht die Schuld restschuld += betrag; monatsTilgung -= betrag; // Negative Tilgung = Erhöhung } else { // Ratenzahlung const rateZinsen = restschuld * zinsProMonat; const rateTilgung = betrag - rateZinsen; restschuld -= rateTilgung; monatsTilgung += rateTilgung; monatsRate += betrag; } } await client.query( 'INSERT INTO kredit_buchungen (kredit_id, datum, rate_betrag, zinsen, tilgung, restschuld_nach) VALUES ($1, $2, $3, $4, $5, $6)', [req.params.id, monatEnde.toISOString().split('T')[0], monatsRate, zinsen.toFixed(2), monatsTilgung.toFixed(2), restschuld] ); } else { // Keine Zahlung in diesem Monat if (istForderung) { // Bei Forderung: Zinsen erhöhen die Restschuld restschuld += zinsen; } // Bei Schuld: Zinsen werden nicht zum Kapitalisiert (nur Zinszahlung fällig) await client.query( 'INSERT INTO kredit_buchungen (kredit_id, datum, rate_betrag, zinsen, tilgung, restschuld_nach) VALUES ($1, $2, $3, $4, $5, $6)', [req.params.id, monatEnde.toISOString().split('T')[0], 0, zinsen.toFixed(2), 0, restschuld] ); } // Nächster Monat aktuellesDatum.setMonth(aktuellesDatum.getMonth() + 1); } // 5. Restschuld aktualisieren await client.query('UPDATE kredite SET restschuld = $1 WHERE id = $2', [restschuld, req.params.id]); await client.query('COMMIT'); res.json({ success: true, message: 'Buchungen neu berechnet', kreditId: req.params.id, neueRestschuld: restschuld }); } catch (error) { await client.query('ROLLBACK'); res.status(500).json({ error: error.message }); } finally { client.release(); } }); // Kredit Zahlungen Tabelle erstellen async function initZahlungenTable() { const client = await pool.connect(); try { await client.query(` CREATE TABLE IF NOT EXISTS kredit_zahlungen ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), kredit_id UUID REFERENCES kredite(id) ON DELETE CASCADE, betrag DECIMAL(10,2) NOT NULL, datum DATE NOT NULL, typ VARCHAR(50) NOT NULL DEFAULT 'monatsrate', notiz TEXT, created_at TIMESTAMP DEFAULT NOW() ) `); console.log('✅ Kredit Zahlungen Tabelle erstellt'); } finally { client.release(); } } // Zahlungen Endpunkte (Alias für Frontend) app.get('/api/kredite/:id/zahlungen', async (req, res) => { try { // Erstelle Tabelle falls nicht vorhanden await initZahlungenTable(); const result = await pool.query( 'SELECT * FROM kredit_zahlungen WHERE kredit_id = $1 ORDER BY datum DESC', [req.params.id] ); res.json(result.rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/kredite/:id/zahlungen', async (req, res) => { const client = await pool.connect(); try { await client.query('BEGIN'); await initZahlungenTable(); const { betrag, datum, typ, notiz } = req.body; // 1. Zahlung speichern const zahlungResult = await client.query( 'INSERT INTO kredit_zahlungen (kredit_id, betrag, datum, typ, notiz) VALUES ($1, $2, $3, $4, $5) RETURNING *', [req.params.id, betrag, datum, typ || 'monatsrate', notiz] ); // 2. Kredit-Daten holen (inkl. Zinssatz) const kreditResult = await client.query('SELECT restschuld, zinssatz FROM kredite WHERE id = $1', [req.params.id]); let restschuld = parseFloat(kreditResult.rows[0].restschuld); const zinssatz = parseFloat(kreditResult.rows[0].zinssatz) || 0; let zinsen = 0; let tilgung = 0; // 3. Zinsen berechnen für Ratenzahlungen (nicht für Auslagen) if (typ !== 'auslage') { // Monatlicher Zinssatz (jährlich / 12 / 100) const monatlicherZinssatz = zinssatz / 12 / 100; zinsen = restschuld * monatlicherZinssatz; tilgung = parseFloat(betrag) - zinsen; restschuld -= tilgung; } else { // Auslage: erhöht die Restschuld tilgung = parseFloat(betrag); restschuld += tilgung; } // 4. Kredit aktualisieren await client.query('UPDATE kredite SET restschuld = $1 WHERE id = $2', [restschuld, req.params.id]); // 5. Buchung erstellen await client.query( 'INSERT INTO kredit_buchungen (kredit_id, datum, rate_betrag, zinsen, tilgung, restschuld_nach) VALUES ($1, $2, $3, $4, $5, $6)', [req.params.id, datum, typ === 'auslage' ? 0 : betrag, zinsen.toFixed(2), tilgung.toFixed(2), restschuld] ); await client.query('COMMIT'); res.json(zahlungResult.rows[0]); } catch (error) { await client.query('ROLLBACK'); res.status(500).json({ error: error.message }); } finally { client.release(); } }); app.delete('/api/kredite/:id/zahlungen/:zahlungId', async (req, res) => { const client = await pool.connect(); try { await client.query('BEGIN'); // 1. Zahlung holen const zahlungResult = await client.query('SELECT * FROM kredit_zahlungen WHERE id = $1 AND kredit_id = $2', [req.params.zahlungId, req.params.id]); if (zahlungResult.rows.length === 0) { await client.query('ROLLBACK'); return res.status(404).json({ error: 'Zahlung nicht gefunden' }); } const zahlung = zahlungResult.rows[0]; // 2. Zahlung löschen await client.query('DELETE FROM kredit_zahlungen WHERE id = $1 AND kredit_id = $2', [req.params.zahlungId, req.params.id]); // 3. Aktuelle Restschuld holen const kreditResult = await client.query('SELECT restschuld FROM kredite WHERE id = $1', [req.params.id]); let restschuld = parseFloat(kreditResult.rows[0].restschuld); // 4. Restschuld zurücksetzen if (zahlung.typ === 'auslage') { restschuld -= parseFloat(zahlung.betrag); } else { restschuld += parseFloat(zahlung.betrag); } // 5. Kredit aktualisieren await client.query('UPDATE kredite SET restschuld = $1 WHERE id = $2', [restschuld, req.params.id]); // 6. Buchungen neu berechnen (einfacher: alles löschen und neu erstellen) await client.query('DELETE FROM kredit_buchungen WHERE kredit_id = $1', [req.params.id]); // Startbuchung const kreditInfo = await client.query('SELECT ursprungsschuld, start_datum FROM kredite WHERE id = $1', [req.params.id]); let r = parseFloat(kreditInfo.rows[0].ursprungsschuld); await client.query( 'INSERT INTO kredit_buchungen (kredit_id, datum, rate_betrag, zinsen, tilgung, restschuld_nach) VALUES ($1, $2, $3, $4, $5, $6)', [req.params.id, kreditInfo.rows[0].start_datum, 0, 0, 0, r] ); // Alle verbleibenden Zahlungen als Buchungen erstellen const remainingZahlungen = await client.query('SELECT * FROM kredit_zahlungen WHERE kredit_id = $1 ORDER BY datum', [req.params.id]); for (const z of remainingZahlungen.rows) { if (z.typ === 'auslage') { r += parseFloat(z.betrag); await client.query( 'INSERT INTO kredit_buchungen (kredit_id, datum, rate_betrag, zinsen, tilgung, restschuld_nach) VALUES ($1, $2, $3, $4, $5, $6)', [req.params.id, z.datum, 0, 0, z.betrag, r] ); } else { r -= parseFloat(z.betrag); await client.query( 'INSERT INTO kredit_buchungen (kredit_id, datum, rate_betrag, zinsen, tilgung, restschuld_nach) VALUES ($1, $2, $3, $4, $5, $6)', [req.params.id, z.datum, z.betrag, 0, z.betrag, r] ); } } await client.query('COMMIT'); res.json({ success: true, restschuld: r }); } catch (error) { await client.query('ROLLBACK'); res.status(500).json({ error: error.message }); } finally { client.release(); } }); // Kredit-Tilgung simulieren/berechnen app.post('/api/kredite/:id/simulieren', async (req, res) => { try { const kredit = await pool.query('SELECT * FROM kredite WHERE id = $1', [req.params.id]); if (kredit.rows.length === 0) { return res.status(404).json({ error: 'Kredit nicht gefunden' }); } const k = kredit.rows[0]; let restschuld = parseFloat(k.restschuld); const monatsrate = parseFloat(k.monatsrate); const zinssatz = parseFloat(k.zinssatz) / 100 / 12; // Monatlicher Zinssatz const plan = []; let monat = 0; while (restschuld > 0 && monat < 600) { // Max 50 Jahre monat++; const zinsen = restschuld * zinssatz; let tilgung = monatsrate - zinsen; if (tilgung > restschuld) { tilgung = restschuld; } restschuld -= tilgung; const datum = new Date(k.start_datum); datum.setMonth(datum.getMonth() + monat); plan.push({ monat, datum: datum.toISOString().split('T')[0], rate: monatsrate, zinsen: zinsen.toFixed(2), tilgung: tilgung.toFixed(2), restschuld: restschuld.toFixed(2) }); if (restschuld <= 0) break; } res.json({ kredit: k, tilgungsplan: plan, gesamt_monate: monat, end_datum: plan.length > 0 ? plan[plan.length - 1].datum : null }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Fixe Ausgaben API app.get('/api/fixe-ausgaben', async (req, res) => { try { const result = await pool.query('SELECT * FROM fixe_ausgaben WHERE is_active = true ORDER BY kategorie, name'); res.json(result.rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/fixe-ausgaben', async (req, res) => { try { const { name, kategorie, betrag, intervall, faelligkeit_tag } = req.body; const query = ` INSERT INTO fixe_ausgaben (name, kategorie, betrag, intervall, faelligkeit_tag) VALUES ($1, $2, $3, $4, $5) RETURNING * `; const result = await pool.query(query, [name, kategorie, betrag, intervall, faelligkeit_tag]); res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); // Cashflow Übersicht app.get('/api/cashflow', async (req, res) => { try { const { monat, jahr = new Date().getFullYear() } = req.query; // Einnahmen aus Rechnungen const einnahmenResult = await pool.query(` SELECT COALESCE(SUM(betrag), 0) as total FROM rechnungen WHERE status = 'bezahlt' AND EXTRACT(MONTH FROM datum) = $1 AND EXTRACT(YEAR FROM datum) = $2 `, [monat, jahr]); // Variable Ausgaben aus Belegen const ausgabenResult = await pool.query(` SELECT COALESCE(SUM(betrag), 0) as total FROM belege WHERE status = 'fertig' AND EXTRACT(MONTH FROM date) = $1 AND EXTRACT(YEAR FROM date) = $2 `, [monat, jahr]); // Fixe Ausgaben (monatlich) const fixeResult = await pool.query(` SELECT COALESCE(SUM(betrag), 0) as total FROM fixe_ausgaben WHERE is_active = true AND intervall = 'monatlich' `); // Kreditraten (monatlich) const krediteResult = await pool.query(` SELECT COALESCE(SUM(monatsrate), 0) as total FROM kredite WHERE status = 'aktiv' `); const einnahmen = parseFloat(einnahmenResult.rows[0].total); const ausgaben = parseFloat(ausgabenResult.rows[0].total); const fixe = parseFloat(fixeResult.rows[0].total); const raten = parseFloat(krediteResult.rows[0].total); res.json({ monat, jahr, einnahmen, variable_ausgaben: ausgaben, fixe_ausgaben: fixe, kreditraten: raten, gesamt_ausgaben: ausgaben + fixe + raten, verfuegbar: einnahmen - ausgaben - fixe - raten }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Stunden API app.get('/api/stunden', async (req, res) => { try { const { status, jahr } = req.query; let query = 'SELECT * FROM stunden ORDER BY datum DESC'; const values = []; if (status) { query = 'SELECT * FROM stunden WHERE status = $1 ORDER BY datum DESC'; values.push(status); } else if (jahr) { query = 'SELECT * FROM stunden WHERE EXTRACT(YEAR FROM datum) = $1 ORDER BY datum DESC'; values.push(jahr); } const result = await pool.query(query, values); res.json(result.rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/stunden', async (req, res) => { try { const { datum, kunde, beschreibung, stunden, stundensatz } = req.body; const betrag = stunden * stundensatz; const query = ` INSERT INTO stunden (datum, kunde, beschreibung, stunden, stundensatz, betrag) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `; const result = await pool.query(query, [datum, kunde, beschreibung, stunden, stundensatz, betrag]); res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/stunden/:id', async (req, res) => { try { const { datum, kunde, beschreibung, stunden, stundensatz, status } = req.body; const betrag = stunden * stundensatz; const query = ` UPDATE stunden SET datum = $1, kunde = $2, beschreibung = $3, stunden = $4, stundensatz = $5, betrag = $6, status = $7 WHERE id = $8 RETURNING * `; const result = await pool.query(query, [datum, kunde, beschreibung, stunden, stundensatz, betrag, status, req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Stunden nicht gefunden' }); } res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/stunden/:id', async (req, res) => { try { const result = await pool.query('DELETE FROM stunden WHERE id = $1 RETURNING *', [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Stunden nicht gefunden' }); } res.json({ success: true, deleted: result.rows[0] }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Kostenplanung API app.get('/api/kostenplanung', async (req, res) => { try { const { objekt, jahr } = req.query; let query = 'SELECT * FROM kostenplanung'; const values = []; const conditions = []; if (objekt) { conditions.push('objekt = $' + (values.length + 1)); values.push(objekt); } if (jahr) { conditions.push('jahr = $' + (values.length + 1)); values.push(jahr); } if (conditions.length > 0) { query += ' WHERE ' + conditions.join(' AND '); } query += ' ORDER BY kategorie, name'; const result = await pool.query(query, values); res.json(result.rows); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/kostenplanung', async (req, res) => { try { const { objekt, name, kategorie, monate, jahr } = req.body; const query = ` INSERT INTO kostenplanung (objekt, name, kategorie, monate, jahr) VALUES ($1, $2, $3, $4, $5) RETURNING * `; const result = await pool.query(query, [objekt, name, kategorie, monate, jahr || new Date().getFullYear()]); res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/kostenplanung/:id', async (req, res) => { try { const { objekt, name, kategorie, monate, jahr, ist_einnahme } = req.body; const query = ` UPDATE kostenplanung SET objekt = $1, name = $2, kategorie = $3, monate = $4, jahr = $5, ist_einnahme = $6, updated_at = NOW() WHERE id = $7 RETURNING * `; const result = await pool.query(query, [objekt, name, kategorie, monate, jahr, ist_einnahme, req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Kostenplanung nicht gefunden' }); } res.json(result.rows[0]); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/kostenplanung/:id', async (req, res) => { try { const result = await pool.query('DELETE FROM kostenplanung WHERE id = $1 RETURNING *', [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Kostenplanung nicht gefunden' }); } res.json({ success: true, deleted: result.rows[0] }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Geschäftsplanung monatliche Summen API (für Dashboard) app.get('/api/geschaeftsplanung/monatlich', async (req, res) => { try { const { jahr } = req.query; const year = jahr || new Date().getFullYear(); // Hole alle Kostenplanung-Einträge für das Jahr const result = await pool.query( 'SELECT * FROM kostenplanung WHERE jahr = $1', [year] ); // Berechne Summen pro Kategorie const summen = { einnahmen: 0, betriebskosten: 0, betriebsergebnis: 0 }; const monatlich = Array(12).fill(0).map(() => ({ einnahmen: 0, betriebskosten: 0, betriebsergebnis: 0 })); result.rows.forEach(row => { const monate = row.monate || Array(12).fill(0); const kategorie = row.kategorie?.toLowerCase() || ''; if (kategorie === 'einnahmen') { monate.forEach((betrag, idx) => { const val = parseFloat(betrag) || 0; monatlich[idx].einnahmen += val; summen.einnahmen += val; }); } else if (kategorie === 'betriebskosten') { monate.forEach((betrag, idx) => { const val = parseFloat(betrag) || 0; monatlich[idx].betriebskosten += val; summen.betriebskosten += val; }); } }); summen.betriebsergebnis = summen.einnahmen - summen.betriebskosten; monatlich.forEach(m => { m.betriebsergebnis = m.einnahmen - m.betriebskosten; }); res.json({ jahr: year, summen, monatlich }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Error handler app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: err.message }); }); // Health check endpoint app.get('/api/health', (req, res) => { res.json({ status: 'ok', service: 'buchhaltung-backend', timestamp: new Date().toISOString() }); }); // Start server initDatabase().then(() => { app.listen(PORT, '0.0.0.0', () => { console.log(`🚀 Backend läuft auf Port ${PORT}`); console.log(`📁 Uploads: ${uploadsDir}`); }); }).catch(err => { console.error('Datenbank-Fehler:', err); process.exit(1); }); // Graceful shutdown process.on('SIGTERM', async () => { console.log('SIGTERM empfangen, schließe Verbindungen...'); await pool.end(); process.exit(0); });