diff --git a/backend/routes/customers.js b/backend/routes/customers.js new file mode 100644 index 0000000..2ee1dcb --- /dev/null +++ b/backend/routes/customers.js @@ -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; \ No newline at end of file diff --git a/backend/routes/dashboard.js b/backend/routes/dashboard.js new file mode 100644 index 0000000..bf7a1fd --- /dev/null +++ b/backend/routes/dashboard.js @@ -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; \ No newline at end of file diff --git a/backend/routes/invoices.js b/backend/routes/invoices.js new file mode 100644 index 0000000..8c12f30 --- /dev/null +++ b/backend/routes/invoices.js @@ -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; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index cf53d25..9d19923 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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);