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:
@@ -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;
|
||||
Reference in New Issue
Block a user