feat: 3D-Statistik Tool mit Haushaltsbuch, Diagrammen und Ausgaben-Tracker

This commit is contained in:
2026-06-17 12:03:19 +02:00
parent cf03c49d03
commit ee12b7d57e
5 changed files with 462 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
const express = require('express');
const router = express.Router();
const db = require('../../db');
const { authenticate } = require('../../middleware/auth');
const uid = req => req.user.id;
// ── Ausgaben CRUD ──────────────────────────────────────────────────────────
router.get('/expenses', authenticate, (req, res) => {
const { from, to } = req.query;
let q = 'SELECT * FROM expenses WHERE user_id=?';
const p = [uid(req)];
if (from) { q += ' AND date>=?'; p.push(from); }
if (to) { q += ' AND date<=?'; p.push(to); }
q += ' ORDER BY date DESC, id DESC';
res.json(db.prepare(q).all(...p));
});
router.post('/expenses', authenticate, (req, res) => {
const { date, category, description, amount } = req.body;
if (!date || !description || !amount) return res.status(400).json({ error: 'Fehlende Felder' });
const r = db.prepare('INSERT INTO expenses (user_id,date,category,description,amount) VALUES (?,?,?,?,?)')
.run(uid(req), date, category||'Sonstiges', description, parseFloat(amount));
res.json(db.prepare('SELECT * FROM expenses WHERE id=?').get(r.lastInsertRowid));
});
router.delete('/expenses/:id', authenticate, (req, res) => {
const e = db.prepare('SELECT id FROM expenses WHERE id=? AND user_id=?').get(req.params.id, uid(req));
if (!e) return res.status(404).json({ error: 'Nicht gefunden' });
db.prepare('DELETE FROM expenses WHERE id=?').run(e.id);
res.json({ ok: true });
});
// ── Statistik-Daten ────────────────────────────────────────────────────────
router.get('/overview', authenticate, (req, res) => {
const { from, to } = req.query;
const u = uid(req);
let oq = `SELECT o.*,
COALESCE(o.custom_price,
(SELECT SUM(oi.custom_price * oi.stueckzahl) FROM order_items oi WHERE oi.order_id=o.id AND oi.custom_price IS NOT NULL)
) as revenue,
(SELECT COALESCE(SUM(COALESCE(c.preis_freundschaft, oi.preis_freundschaft, 0) * oi.stueckzahl),0)
FROM order_items oi LEFT JOIN calculations c ON c.id=oi.calculation_id WHERE oi.order_id=o.id) as base_cost
FROM orders o WHERE o.user_id=?`;
const op = [u];
if (from) { oq += ' AND DATE(o.created_at)>=?'; op.push(from); }
if (to) { oq += ' AND DATE(o.created_at)<=?'; op.push(to); }
oq += ' ORDER BY o.created_at DESC';
const orders = db.prepare(oq).all(...op);
const paidOrders = orders.filter(o => !!o.bezahlt);
const openOrders = orders.filter(o => !o.bezahlt && o.status !== 'warteliste');
const totalRevenue = paidOrders.reduce((s,o) => s + (o.revenue || 0), 0);
const openRevenue = openOrders.reduce((s,o) => s + (o.revenue || 0), 0);
const totalBaseCost = paidOrders.reduce((s,o) => s + (o.base_cost || 0), 0);
const totalProfit = totalRevenue - totalBaseCost;
let eq = 'SELECT * FROM expenses WHERE user_id=?';
const ep = [u];
if (from) { eq += ' AND date>=?'; ep.push(from); }
if (to) { eq += ' AND date<=?'; ep.push(to); }
const expenses = db.prepare(eq + ' ORDER BY date DESC').all(...ep);
const totalExpenses = expenses.reduce((s,e) => s + e.amount, 0);
const netProfit = totalProfit - totalExpenses;
const byStatus = { warteliste:0, in_arbeit:0, fertig:0, bezahlt:0, abgeschlossen:0 };
for (const o of orders) {
if (!!o.bezahlt && !!o.abgeholt) byStatus.abgeschlossen++;
else if (!!o.bezahlt) byStatus.bezahlt++;
else if (o.status in byStatus) byStatus[o.status]++;
}
const byCategory = {};
for (const e of expenses) {
byCategory[e.category] = (byCategory[e.category] || 0) + e.amount;
}
// Monatliche Daten
const monthlyRev = db.prepare(`
SELECT strftime('%Y-%m', created_at) as month,
SUM(CASE WHEN bezahlt=1 THEN COALESCE(custom_price,0) ELSE 0 END) as revenue,
COUNT(*) as orders
FROM orders WHERE user_id=? GROUP BY month ORDER BY month
`).all(u);
const monthlyExp = db.prepare(`
SELECT strftime('%Y-%m', date) as month, SUM(amount) as expenses
FROM expenses WHERE user_id=? GROUP BY month ORDER BY month
`).all(u);
const monthlyMap = {};
for (const m of monthlyRev) monthlyMap[m.month] = { month:m.month, revenue:m.revenue||0, orders:m.orders||0, expenses:0 };
for (const m of monthlyExp) {
if (monthlyMap[m.month]) monthlyMap[m.month].expenses = m.expenses||0;
else monthlyMap[m.month] = { month:m.month, revenue:0, orders:0, expenses:m.expenses||0 };
}
const monthlyData = Object.values(monthlyMap).sort((a,b)=>a.month.localeCompare(b.month)).slice(-12);
let cum = 0;
const cumulativeData = monthlyData.map(m => {
cum += (m.revenue||0) - (m.expenses||0);
return { month: m.month, value: Math.round(cum*100)/100 };
});
res.json({
totalRevenue, openRevenue, totalBaseCost, totalProfit,
totalExpenses, netProfit, totalOrders: orders.length,
byStatus, byCategory, monthlyData, cumulativeData,
expenses: expenses.slice(0,100),
recentOrders: orders.slice(0,30),
});
});
module.exports = router;