Files
buchhaltung/backend/server.js
T
2026-04-26 07:51:39 +02:00

1583 lines
51 KiB
JavaScript

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);
});