Backend: Neue API-Endpunkte hinzugefügt

- /api/dashboard/* - Dashboard Übersicht, Cashflow, Steuer-Preview
- /api/customers/* - Kunden CRUD API
- /api/invoices/* - Rechnungen CRUD API + Zahlungen

Server.js angepasst um neue Routen einzubinden.
This commit is contained in:
root
2026-05-07 13:11:10 +00:00
parent b29c467187
commit 0f8475ce87
4 changed files with 750 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
const express = require('express');
const { Pool } = require('pg');
const { v4: uuidv4 } = require('uuid');
const router = express.Router();
// Database pool
const pool = new Pool({
host: process.env.DB_HOST || 'db',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'steuer',
user: process.env.DB_USER || 'app',
password: process.env.DB_PASSWORD || 'app123',
});
// GET /api/customers - Alle Kunden
router.get('/', async (req, res) => {
try {
const { search, limit = 100 } = req.query;
let query = `
SELECT k.*,
(SELECT COUNT(*) FROM rechnungen WHERE kunde = k.name) as rechnungen_count,
(SELECT COALESCE(SUM(betrag), 0) FROM rechnungen WHERE kunde = k.name AND status = 'bezahlt') as total_umsatz
FROM kunden k
`;
const values = [];
if (search) {
query += ` WHERE k.name ILIKE $1 OR k.email ILIKE $1 OR k.adresse ILIKE $1`;
values.push(`%${search}%`);
}
query += ` ORDER BY k.name ASC LIMIT $${values.length + 1}`;
values.push(limit);
const result = await pool.query(query, values);
res.json({
success: true,
customers: result.rows.map(row => ({
...row,
rechnungen_count: parseInt(row.rechnungen_count) || 0,
total_umsatz: parseFloat(row.total_umsatz) || 0
}))
});
} catch (error) {
console.error('Customers List Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// GET /api/customers/:id - Einzelner Kunde
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
// Kunde + Rechnungen
const [kundeResult, rechnungenResult] = await Promise.all([
pool.query('SELECT * FROM kunden WHERE id = $1', [id]),
pool.query(`
SELECT * FROM rechnungen
WHERE kunde = (SELECT name FROM kunden WHERE id = $1)
ORDER BY datum DESC
`, [id])
]);
if (kundeResult.rows.length === 0) {
return res.status(404).json({ success: false, error: 'Kunde nicht gefunden' });
}
const kunde = kundeResult.rows[0];
const rechnungen = rechnungenResult.rows;
// Zusammenfassung
const totalUmsatz = rechnungen.reduce((sum, r) => sum + parseFloat(r.betrag || 0), 0);
const paidUmsatz = rechnungen
.filter(r => r.status === 'bezahlt')
.reduce((sum, r) => sum + parseFloat(r.betrag || 0), 0);
const openUmsatz = rechnungen
.filter(r => r.status === 'offen')
.reduce((sum, r) => sum + parseFloat(r.betrag || 0), 0);
res.json({
success: true,
customer: {
...kunde,
rechnungen,
summary: {
total_rechnungen: rechnungen.length,
total_umsatz: totalUmsatz,
paid_umsatz: paidUmsatz,
open_umsatz: openUmsatz
}
}
});
} catch (error) {
console.error('Customer Detail Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// POST /api/customers - Kunde erstellen
router.post('/', async (req, res) => {
try {
const { name, adresse, plz, ort, email, telefon, notizen } = req.body;
if (!name) {
return res.status(400).json({ success: false, error: 'Name ist erforderlich' });
}
const query = `
INSERT INTO kunden (id, name, adresse, plz, ort, email, telefon, notizen, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
RETURNING *
`;
const values = [uuidv4(), name, adresse || null, plz || null, ort || null, email || null, telefon || null, notizen || null];
const result = await pool.query(query, values);
res.status(201).json({
success: true,
customer: result.rows[0]
});
} catch (error) {
console.error('Customer Create Error:', error);
if (error.code === '23505') {
return res.status(409).json({ success: false, error: 'Kunde mit diesem Namen existiert bereits' });
}
res.status(500).json({ success: false, error: error.message });
}
});
// PUT /api/customers/:id - Kunde aktualisieren
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
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),
updated_at = NOW()
WHERE id = $8
RETURNING *
`;
const values = [name, adresse, plz, ort, email, telefon, notizen, id];
const result = await pool.query(query, values);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, error: 'Kunde nicht gefunden' });
}
res.json({
success: true,
customer: result.rows[0]
});
} catch (error) {
console.error('Customer Update Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// DELETE /api/customers/:id - Kunde löschen
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
// Prüfe ob Kunde Rechnungen hat
const checkResult = await pool.query(`
SELECT COUNT(*) as count FROM rechnungen
WHERE kunde = (SELECT name FROM kunden WHERE id = $1)
`, [id]);
if (parseInt(checkResult.rows[0].count) > 0) {
return res.status(400).json({
success: false,
error: 'Kunde kann nicht gelöscht werden - es existieren Rechnungen'
});
}
const result = await pool.query('DELETE FROM kunden WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, error: 'Kunde nicht gefunden' });
}
res.json({
success: true,
message: 'Kunde erfolgreich gelöscht',
deleted: result.rows[0]
});
} catch (error) {
console.error('Customer Delete Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
module.exports = router;
+220
View File
@@ -0,0 +1,220 @@
const express = require('express');
const { Pool } = require('pg');
const router = express.Router();
// Database pool
const pool = new Pool({
host: process.env.DB_HOST || 'db',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'steuer',
user: process.env.DB_USER || 'app',
password: process.env.DB_PASSWORD || 'app123',
});
// GET /api/dashboard/summary
router.get('/summary', async (req, res) => {
try {
const currentYear = new Date().getFullYear();
// Gesamtübersichten parallel abfragen
const [
rechnungenResult,
belegeResult,
krediteResult,
stundenResult,
kundenResult
] = await Promise.all([
// Rechnungen dieses Jahr
pool.query(`
SELECT
COUNT(*) as total_count,
COALESCE(SUM(CASE WHEN status = 'bezahlt' THEN betrag ELSE 0 END), 0) as paid_total,
COALESCE(SUM(CASE WHEN status = 'offen' THEN betrag ELSE 0 END), 0) as open_total,
COALESCE(SUM(betrag), 0) as total
FROM rechnungen
WHERE EXTRACT(YEAR FROM datum) = $1
`, [currentYear]),
// Belege/Ausgaben dieses Jahr
pool.query(`
SELECT
COUNT(*) as total_count,
COALESCE(SUM(betrag), 0) as total
FROM belege
WHERE EXTRACT(YEAR FROM date) = $1
`, [currentYear]),
// Kredite
pool.query(`
SELECT
COUNT(*) as total_count,
COUNT(CASE WHEN status = 'aktiv' THEN 1 END) as active_count,
COALESCE(SUM(CASE WHEN status = 'aktiv' THEN restschuld ELSE 0 END), 0) as total_restschuld,
COALESCE(SUM(CASE WHEN status = 'aktiv' THEN monatsrate ELSE 0 END), 0) as total_rate
FROM kredite
`),
// Stunden dieses Jahr
pool.query(`
SELECT
COUNT(*) as total_count,
COALESCE(SUM(stunden), 0) as total_stunden,
COALESCE(SUM(betrag), 0) as total_umsatz
FROM stunden
WHERE EXTRACT(YEAR FROM datum) = $1
`, [currentYear]),
// Kunden
pool.query(`
SELECT COUNT(*) as total_count FROM kunden
`)
]);
const rechnungen = rechnungenResult.rows[0];
const belege = belegeResult.rows[0];
const kredite = krediteResult.rows[0];
const stunden = stundenResult.rows[0];
const kunden = kundenResult.rows[0];
res.json({
success: true,
year: currentYear,
summary: {
einnahmen: {
total: parseFloat(rechnungen.total) || 0,
paid: parseFloat(rechnungen.paid_total) || 0,
open: parseFloat(rechnungen.open_total) || 0,
count: parseInt(rechnungen.total_count) || 0
},
ausgaben: {
total: parseFloat(belege.total) || 0,
count: parseInt(belege.total_count) || 0
},
kredite: {
total: parseInt(kredite.total_count) || 0,
active: parseInt(kredite.active_count) || 0,
restschuld: parseFloat(kredite.total_restschuld) || 0,
monatsrate: parseFloat(kredite.total_rate) || 0
},
stunden: {
total: parseFloat(stunden.total_stunden) || 0,
umsatz: parseFloat(stunden.total_umsatz) || 0,
count: parseInt(stunden.total_count) || 0
},
kunden: {
total: parseInt(kunden.total_count) || 0
},
gewinn: (parseFloat(rechnungen.total) || 0) - (parseFloat(belege.total) || 0)
}
});
} catch (error) {
console.error('Dashboard Summary Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// GET /api/dashboard/cashflow
router.get('/cashflow', async (req, res) => {
try {
const { months = 6 } = req.query;
// Monatliche Cashflow-Daten
const cashflowData = [];
for (let i = months - 1; i >= 0; i--) {
const date = new Date();
date.setMonth(date.getMonth() - i);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const [einnahmenResult, ausgabenResult] = await Promise.all([
pool.query(`
SELECT COALESCE(SUM(betrag), 0) as total
FROM rechnungen
WHERE EXTRACT(YEAR FROM datum) = $1 AND EXTRACT(MONTH FROM datum) = $2
AND status = 'bezahlt'
`, [year, month]),
pool.query(`
SELECT COALESCE(SUM(betrag), 0) as total
FROM belege
WHERE EXTRACT(YEAR FROM date) = $1 AND EXTRACT(MONTH FROM date) = $2
`, [year, month])
]);
const einnahmen = parseFloat(einnahmenResult.rows[0].total) || 0;
const ausgaben = parseFloat(ausgabenResult.rows[0].total) || 0;
cashflowData.push({
year,
month,
monthName: date.toLocaleString('de-DE', { month: 'short' }),
einnahmen,
ausgaben,
saldo: einnahmen - ausgaben
});
}
res.json({
success: true,
cashflow: cashflowData
});
} catch (error) {
console.error('Dashboard Cashflow Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// GET /api/dashboard/tax-preview
router.get('/tax-preview', async (req, res) => {
try {
const { year = new Date().getFullYear() } = req.query;
const [einnahmenResult, ausgabenResult, stundenResult] = await Promise.all([
pool.query(`
SELECT COALESCE(SUM(betrag), 0) as total
FROM rechnungen
WHERE EXTRACT(YEAR FROM datum) = $1 AND status = 'bezahlt'
`, [year]),
pool.query(`
SELECT COALESCE(SUM(betrag), 0) as total
FROM belege
WHERE EXTRACT(YEAR FROM date) = $1
`, [year]),
pool.query(`
SELECT COALESCE(SUM(betrag), 0) as total
FROM stunden
WHERE EXTRACT(YEAR FROM datum) = $1 AND status = 'bezahlt'
`, [year])
]);
const einnahmen = parseFloat(einnahmenResult.rows[0].total) || 0;
const ausgaben = parseFloat(ausgabenResult.rows[0].total) || 0;
const stundenEinnahmen = parseFloat(stundenResult.rows[0].total) || 0;
const gesamtEinnahmen = einnahmen + stundenEinnahmen;
const gewinn = gesamtEinnahmen - ausgaben;
// Vereinfachte Steuerberechnung (nür Hinweis)
const estVorschuss = gewinn > 0 ? gewinn * 0.25 : 0; // ca. 25% ESt
res.json({
success: true,
year: parseInt(year),
preview: {
einnahmen: {
rechnungen: einnahmen,
stunden: stundenEinnahmen,
gesamt: gesamtEinnahmen
},
ausgaben,
gewinn,
estVorschuss: Math.max(0, estVorschuss)
}
});
} catch (error) {
console.error('Dashboard Tax Preview Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
module.exports = router;
+314
View File
@@ -0,0 +1,314 @@
const express = require('express');
const { Pool } = require('pg');
const { v4: uuidv4 } = require('uuid');
const router = express.Router();
// Database pool
const pool = new Pool({
host: process.env.DB_HOST || 'db',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'steuer',
user: process.env.DB_USER || 'app',
password: process.env.DB_PASSWORD || 'app123',
});
// GET /api/invoices - Alle Rechnungen
router.get('/', async (req, res) => {
try {
const { status, customer, year, limit = 100, offset = 0 } = req.query;
let query = `
SELECT r.*,
COALESCE(SUM(b.betrag), 0) as bezahlt
FROM rechnungen r
LEFT JOIN rechnung_zahlungen b ON r.id = b.rechnung_id
WHERE 1=1
`;
const values = [];
let paramCount = 0;
if (status) {
paramCount++;
query += ` AND r.status = $${paramCount}`;
values.push(status);
}
if (customer) {
paramCount++;
query += ` AND r.kunde ILIKE $${paramCount}`;
values.push(`%${customer}%`);
}
if (year) {
paramCount++;
query += ` AND EXTRACT(YEAR FROM r.datum) = $${paramCount}`;
values.push(year);
}
query += ` GROUP BY r.id ORDER BY r.datum DESC LIMIT $${++paramCount} OFFSET $${++paramCount}`;
values.push(limit, offset);
const result = await pool.query(query, values);
// Summen berechnen
const summaryQuery = `
SELECT
COUNT(*) as total_count,
COALESCE(SUM(betrag), 0) as total_betrag,
COALESCE(SUM(CASE WHEN status = 'offen' THEN betrag ELSE 0 END), 0) as open_total,
COALESCE(SUM(CASE WHEN status = 'bezahlt' THEN betrag ELSE 0 END), 0) as paid_total
FROM rechnungen
WHERE 1=1
${status ? ' AND status = $1' : ''}
${year ? (status ? ' AND' : '') + ` EXTRACT(YEAR FROM datum) = $${status ? 2 : 1}` : ''}
`;
const summaryValues = [];
if (status) summaryValues.push(status);
if (year) summaryValues.push(year);
const summaryResult = await pool.query(summaryQuery, summaryValues);
res.json({
success: true,
invoices: result.rows.map(row => ({
...row,
betrag: parseFloat(row.betrag) || 0,
ust_satz: parseFloat(row.ust_satz) || 19,
bezahlt: parseFloat(row.bezahlt) || 0,
offen: (parseFloat(row.betrag) || 0) - (parseFloat(row.bezahlt) || 0)
})),
summary: {
total_count: parseInt(summaryResult.rows[0].total_count) || 0,
total_betrag: parseFloat(summaryResult.rows[0].total_betrag) || 0,
open_total: parseFloat(summaryResult.rows[0].open_total) || 0,
paid_total: parseFloat(summaryResult.rows[0].paid_total) || 0
}
});
} catch (error) {
console.error('Invoices List Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// GET /api/invoices/:id - Einzelne Rechnung
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const [rechnungResult, zahlungenResult] = await Promise.all([
pool.query('SELECT * FROM rechnungen WHERE id = $1', [id]),
pool.query(`
SELECT * FROM rechnung_zahlungen
WHERE rechnung_id = $1
ORDER BY datum DESC
`, [id])
]);
if (rechnungResult.rows.length === 0) {
return res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
}
const rechnung = rechnungResult.rows[0];
const zahlungen = zahlungenResult.rows;
const bezahlt = zahlungen.reduce((sum, z) => sum + parseFloat(z.betrag || 0), 0);
res.json({
success: true,
invoice: {
...rechnung,
betrag: parseFloat(rechnung.betrag) || 0,
ust_satz: parseFloat(rechnung.ust_satz) || 19,
zahlungen,
bezahlt,
offen: (parseFloat(rechnung.betrag) || 0) - bezahlt
}
});
} catch (error) {
console.error('Invoice Detail Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// POST /api/invoices - Rechnung erstellen
router.post('/', async (req, res) => {
try {
const {
kunde,
kunde_email,
kunde_adresse,
leistung,
betrag,
ust_satz = 19,
datum,
faelligkeit,
rechnung_nr
} = req.body;
if (!kunde || !betrag) {
return res.status(400).json({
success: false,
error: 'Kunde und Betrag sind erforderlich'
});
}
// Rechnungsnummer generieren falls nicht angegeben
let finalRechnungNr = rechnung_nr;
if (!finalRechnungNr) {
const countResult = await pool.query(`
SELECT COUNT(*) FROM rechnungen
WHERE EXTRACT(YEAR FROM datum) = EXTRACT(YEAR FROM CURRENT_DATE)
`);
const count = parseInt(countResult.rows[0].count) + 1;
finalRechnungNr = `RE-${new Date().getFullYear()}-${String(count).padStart(3, '0')}`;
}
const query = `
INSERT INTO rechnungen (
id, rechnung_nr, kunde, kunde_email, kunde_adresse,
leistung, betrag, ust_satz, datum, faelligkeit, status, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'offen', NOW())
RETURNING *
`;
const values = [
uuidv4(),
finalRechnungNr,
kunde,
kunde_email || null,
kunde_adresse || null,
leistung || null,
betrag,
ust_satz,
datum || new Date(),
faelligkeit || null
];
const result = await pool.query(query, values);
res.status(201).json({
success: true,
invoice: result.rows[0]
});
} catch (error) {
console.error('Invoice Create Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// PUT /api/invoices/:id - Rechnung aktualisieren
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const {
kunde, kunde_email, kunde_adresse, leistung,
betrag, ust_satz, datum, faelligkeit, status
} = req.body;
const query = `
UPDATE rechnungen
SET
kunde = COALESCE($1, kunde),
kunde_email = COALESCE($2, kunde_email),
kunde_adresse = COALESCE($3, kunde_adresse),
leistung = COALESCE($4, leistung),
betrag = COALESCE($5, betrag),
ust_satz = COALESCE($6, ust_satz),
datum = COALESCE($7, datum),
faelligkeit = COALESCE($8, faelligkeit),
status = COALESCE($9, status)
WHERE id = $10
RETURNING *
`;
const values = [
kunde, kunde_email, kunde_adresse, leistung,
betrag, ust_satz, datum, faelligkeit, status, id
];
const result = await pool.query(query, values);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
}
res.json({
success: true,
invoice: result.rows[0]
});
} catch (error) {
console.error('Invoice Update Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// DELETE /api/invoices/:id - Rechnung löschen
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
// Lösche zuerst Zahlungen
await pool.query('DELETE FROM rechnung_zahlungen WHERE rechnung_id = $1', [id]);
const result = await pool.query('DELETE FROM rechnungen WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
}
res.json({
success: true,
message: 'Rechnung erfolgreich gelöscht',
deleted: result.rows[0]
});
} catch (error) {
console.error('Invoice Delete Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// POST /api/invoices/:id/payment - Zahlung hinzufügen
router.post('/:id/payment', async (req, res) => {
try {
const { id } = req.params;
const { betrag, datum, methode, notizen } = req.body;
if (!betrag) {
return res.status(400).json({ success: false, error: 'Betrag ist erforderlich' });
}
const query = `
INSERT INTO rechnung_zahlungen (id, rechnung_id, betrag, datum, methode, notizen, created_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW())
RETURNING *
`;
const values = [uuidv4(), id, betrag, datum || new Date(), methode || 'Überweisung', notizen || null];
const result = await pool.query(query, values);
// Prüfe ob Rechnung nun vollständig bezahlt
const [rechnungResult, zahlungenSumResult] = await Promise.all([
pool.query('SELECT betrag FROM rechnungen WHERE id = $1', [id]),
pool.query('SELECT COALESCE(SUM(betrag), 0) as total FROM rechnung_zahlungen WHERE rechnung_id = $1', [id])
]);
const rechnungBetrag = parseFloat(rechnungResult.rows[0].betrag);
const bezahlt = parseFloat(zahlungenSumResult.rows[0].total);
if (bezahlt >= rechnungBetrag) {
await pool.query(`UPDATE rechnungen SET status = 'bezahlt' WHERE id = $1`, [id]);
} else if (bezahlt > 0) {
await pool.query(`UPDATE rechnungen SET status = 'teilweise' WHERE id = $1`, [id]);
}
res.json({
success: true,
payment: result.rows[0],
bezahlt,
offen: rechnungBetrag - bezahlt
});
} catch (error) {
console.error('Invoice Payment Error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
module.exports = router;
+9
View File
@@ -34,6 +34,15 @@ app.use('/uploads', express.static('uploads'));
// Auth Routes
app.use('/api/auth', authRoutes);
// Neue API Routen (für Frontend)
const dashboardRoutes = require('./routes/dashboard');
const customersRoutes = require('./routes/customers');
const invoicesRoutes = require('./routes/invoices');
app.use('/api/dashboard', dashboardRoutes);
app.use('/api/customers', customersRoutes);
app.use('/api/invoices', invoicesRoutes);
// Nebenkosten Routes laden
const nebenkostenRoutes = require('./routes/nebenkosten');
nebenkostenRoutes(app);