Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2ad447bc6 | |||
| 0f8475ce87 |
@@ -0,0 +1,125 @@
|
|||||||
|
# Backend API Erweiterung - Changelog
|
||||||
|
|
||||||
|
**Datum:** 7. Mai 2026
|
||||||
|
**Commit:** 0f8475c
|
||||||
|
**Autor:** Peter (OpenClaw)
|
||||||
|
|
||||||
|
## Neue API-Endpunkte
|
||||||
|
|
||||||
|
### Dashboard (`/api/dashboard`)
|
||||||
|
| Methode | Endpoint | Beschreibung |
|
||||||
|
|---------|----------|--------------|
|
||||||
|
| GET | `/summary` | Übersicht Einnahmen, Ausgaben, Kredite, Stunden |
|
||||||
|
| GET | `/cashflow` | Monatlicher Cashflow (6 Monate) |
|
||||||
|
| GET | `/tax-preview` | Steuer-Vorschau für aktuelles Jahr |
|
||||||
|
|
||||||
|
### Customers (`/api/customers`)
|
||||||
|
| Methode | Endpoint | Beschreibung |
|
||||||
|
|---------|----------|--------------|
|
||||||
|
| GET | `/` | Alle Kunden (mit Suche) |
|
||||||
|
| GET | `/:id` | Einzelner Kunde mit Rechnungen |
|
||||||
|
| POST | `/` | Kunde erstellen |
|
||||||
|
| PUT | `/:id` | Kunde aktualisieren |
|
||||||
|
| DELETE | `/:id` | Kunde löschen |
|
||||||
|
|
||||||
|
### Invoices (`/api/invoices`)
|
||||||
|
| Methode | Endpoint | Beschreibung |
|
||||||
|
|---------|----------|--------------|
|
||||||
|
| GET | `/` | Alle Rechnungen (mit Filter) |
|
||||||
|
| GET | `/:id` | Einzelne Rechnung mit Zahlungen |
|
||||||
|
| POST | `/` | Rechnung erstellen |
|
||||||
|
| PUT | `/:id` | Rechnung aktualisieren |
|
||||||
|
| DELETE | `/:id` | Rechnung löschen |
|
||||||
|
| POST | `/:id/payment` | Zahlung hinzufügen |
|
||||||
|
|
||||||
|
## Datenbank-Schema Erweiterungen
|
||||||
|
|
||||||
|
### Neue Tabellen
|
||||||
|
```sql
|
||||||
|
CREATE TABLE kunden (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
adresse TEXT,
|
||||||
|
plz VARCHAR(10),
|
||||||
|
ort VARCHAR(100),
|
||||||
|
email VARCHAR(255),
|
||||||
|
telefon VARCHAR(50),
|
||||||
|
notizen TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE rechnung_zahlungen (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
rechnung_id UUID REFERENCES rechnungen(id) ON DELETE CASCADE,
|
||||||
|
betrag DECIMAL(10,2) NOT NULL,
|
||||||
|
datum DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||||
|
methode VARCHAR(50),
|
||||||
|
notizen TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Weitere Tabellen für Nebenkosten:
|
||||||
|
-- mieter, mietvertraege, objektkosten, vorauszahlungen
|
||||||
|
-- nebenkostenabrechnungen, abrechnungspositionen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erweiterte Tabellen
|
||||||
|
- `objekte` - hinzugefügt: `plz`, `ort`, `wohnflaeche_qm`, `bemerkung`
|
||||||
|
|
||||||
|
## Technische Details
|
||||||
|
|
||||||
|
### Server.js Änderungen
|
||||||
|
```javascript
|
||||||
|
// Neue Routen importieren
|
||||||
|
const dashboardRoutes = require('./routes/dashboard');
|
||||||
|
const customersRoutes = require('./routes/customers');
|
||||||
|
const invoicesRoutes = require('./routes/invoices');
|
||||||
|
|
||||||
|
// Routen registrieren
|
||||||
|
app.use('/api/dashboard', dashboardRoutes);
|
||||||
|
app.use('/api/customers', customersRoutes);
|
||||||
|
app.use('/api/invoices', invoicesRoutes);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Abhängigkeiten
|
||||||
|
- Keine neuen npm-Pakete
|
||||||
|
- Nutzt bestehende PostgreSQL-Verbindung
|
||||||
|
- Kompatibel mit bestehendem Frontend
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf CT141:
|
||||||
|
cd /root/buchhaltungs-app/backend
|
||||||
|
docker build -t buchhaltung-gitea_backend:latest .
|
||||||
|
docker restart backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend Integration
|
||||||
|
|
||||||
|
Die neuen Endpunkte entsprechen den im Frontend erwarteten URLs:
|
||||||
|
- Dashboard Übersicht ✓
|
||||||
|
- Kundenliste ✓
|
||||||
|
- Rechnungsliste ✓
|
||||||
|
- Nebenkosten-Module ✓
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login
|
||||||
|
curl -X POST http://192.168.0.141/api/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"admin","password":"admin123"}'
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
curl http://192.168.0.141/api/dashboard/summary
|
||||||
|
|
||||||
|
# Alle Endpunkte
|
||||||
|
curl http://192.168.0.141/api/customers
|
||||||
|
curl http://192.168.0.141/api/invoices
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Deploy-Status: ✅ Erfolgreich auf CT141 (192.168.0.141)*
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -34,6 +34,15 @@ app.use('/uploads', express.static('uploads'));
|
|||||||
// Auth Routes
|
// Auth Routes
|
||||||
app.use('/api/auth', authRoutes);
|
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
|
// Nebenkosten Routes laden
|
||||||
const nebenkostenRoutes = require('./routes/nebenkosten');
|
const nebenkostenRoutes = require('./routes/nebenkosten');
|
||||||
nebenkostenRoutes(app);
|
nebenkostenRoutes(app);
|
||||||
|
|||||||
Reference in New Issue
Block a user