feat: 3D-Statistik Tool mit Haushaltsbuch, Diagrammen und Ausgaben-Tracker
This commit is contained in:
@@ -590,4 +590,17 @@ db.exec(`
|
||||
)
|
||||
`);
|
||||
|
||||
// 3D-Druck Ausgaben (Haushaltsbuch)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS expenses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
date TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'Sonstiges',
|
||||
description TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
module.exports = db;
|
||||
|
||||
116
backend/src/tools/statistik/routes.js
Normal file
116
backend/src/tools/statistik/routes.js
Normal 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;
|
||||
Reference in New Issue
Block a user