fix: revenue aus order_items korrekt berechnet, custom Zeitraum (Monat/Jahr)

This commit is contained in:
2026-06-17 15:02:07 +02:00
parent 23f798e1d1
commit c1db0c1410
2 changed files with 164 additions and 60 deletions

View File

@@ -12,8 +12,7 @@ router.get('/expenses', authenticate, (req, res) => {
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));
res.json(db.prepare(q + ' ORDER BY date DESC, id DESC').all(...p));
});
router.post('/expenses', authenticate, (req, res) => {
@@ -31,41 +30,60 @@ router.delete('/expenses/:id', authenticate, (req, res) => {
res.json({ ok: true });
});
// ── Statistik-Daten ────────────────────────────────────────────────────────
// ── Hilfsfunktion: revenue eines Auftrags ─────────────────────────────────
// Preis: custom_price direkt auf order ODER Summe aus order_items
function getOrderRevenue(order) {
if (order.custom_price != null && order.custom_price > 0) return parseFloat(order.custom_price);
const r = db.prepare(
'SELECT COALESCE(SUM(custom_price * stueckzahl), 0) as s FROM order_items WHERE order_id=? AND custom_price IS NOT NULL'
).get(order.id);
return r?.s || 0;
}
function getOrderBaseCost(orderId) {
const r = db.prepare(`
SELECT COALESCE(SUM(COALESCE(c.preis_freundschaft, oi.preis_freundschaft, 0) * oi.stueckzahl), 0) as s
FROM order_items oi
LEFT JOIN calculations c ON c.id = oi.calculation_id
WHERE oi.order_id = ?
`).get(orderId);
return r?.s || 0;
}
// ── Statistik-Übersicht ────────────────────────────────────────────────────
router.get('/overview', authenticate, (req, res) => {
const { from, to } = req.query;
const u = uid(req);
// Bezahlte Aufträge: nach bezahlt_am filtern; offene: nach created_at
// Zeitraum-Filter: zeige Auftrag wenn er im Zeitraum bezahlt wurde (bezahlt)
// ODER noch offen ist und im Zeitraum erstellt wurde
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 && to) {
oq += ` AND (
(o.bezahlt=1 AND DATE(o.bezahlt_am)>=? AND DATE(o.bezahlt_am)<=?)
OR
(o.bezahlt=0 AND DATE(o.created_at)>=? AND DATE(o.created_at)<=?)
)`;
op.push(from, to, from, to);
}
oq += ' ORDER BY COALESCE(o.bezahlt_am, o.created_at) DESC';
const orders = db.prepare(oq).all(...op);
// Alle Aufträge des Users
const allOrders = db.prepare('SELECT * FROM orders WHERE user_id=? ORDER BY created_at DESC').all(u);
// Zeitraum-Filter anwenden:
// bezahlte Aufträge → bezahlt_am im Zeitraum
// offene Aufträge → created_at im Zeitraum
const inRange = (dateStr) => {
if (!dateStr) return false;
if (!from && !to) return true;
const d = dateStr.slice(0,10);
if (from && d < from) return false;
if (to && d > to) return false;
return true;
};
const orders = allOrders.filter(o => {
if (o.bezahlt) return inRange(o.bezahlt_am || o.created_at);
return inRange(o.created_at);
});
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 totalRevenue = paidOrders.reduce((s,o) => s + getOrderRevenue(o), 0);
const openRevenue = openOrders.reduce((s,o) => s + getOrderRevenue(o), 0);
const totalBaseCost = paidOrders.reduce((s,o) => s + getOrderBaseCost(o.id), 0);
const totalProfit = totalRevenue - totalBaseCost;
// Ausgaben im Zeitraum
let eq = 'SELECT * FROM expenses WHERE user_id=?';
const ep = [u];
if (from) { eq += ' AND date>=?'; ep.push(from); }
@@ -74,6 +92,7 @@ router.get('/overview', authenticate, (req, res) => {
const totalExpenses = expenses.reduce((s,e) => s + e.amount, 0);
const netProfit = totalProfit - totalExpenses;
// Status-Verteilung (gefilterte Aufträge)
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++;
@@ -81,44 +100,66 @@ router.get('/overview', authenticate, (req, res) => {
else if (o.status in byStatus) byStatus[o.status]++;
}
// Ausgaben nach Kategorie (gefiltert)
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', CASE WHEN bezahlt=1 THEN bezahlt_am ELSE created_at END) 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 };
// Monatliche Daten: IMMER alle Daten (letzte 12 Monate) für das Balkendiagramm
// Revenue korrekt: für jeden bezahlten Auftrag getOrderRevenue aufrufen
const monthBuckets = {};
for (const o of allOrders) {
// Monat: für bezahlte = bezahlt_am, für offene = created_at
const dateStr = o.bezahlt ? (o.bezahlt_am || o.created_at) : o.created_at;
const month = dateStr?.slice(0,7);
if (!month) continue;
if (!monthBuckets[month]) monthBuckets[month] = { month, revenue:0, orders:0, expenses:0 };
if (o.bezahlt) {
monthBuckets[month].revenue += getOrderRevenue(o);
}
monthBuckets[month].orders++;
}
const monthlyData = Object.values(monthlyMap).sort((a,b)=>a.month.localeCompare(b.month)).slice(-12);
// Ausgaben in Monats-Buckets
const allExpenses = db.prepare('SELECT * FROM expenses WHERE user_id=?').all(u);
for (const e of allExpenses) {
const month = e.date?.slice(0,7);
if (!month) continue;
if (!monthBuckets[month]) monthBuckets[month] = { month, revenue:0, orders:0, expenses:0 };
monthBuckets[month].expenses += e.amount;
}
const monthlyData = Object.values(monthBuckets)
.sort((a,b) => a.month.localeCompare(b.month))
.slice(-12)
.map(m => ({ ...m, revenue: Math.round(m.revenue*100)/100, expenses: Math.round(m.expenses*100)/100 }));
// Kumulierter Nettogewinn
let cum = 0;
const cumulativeData = monthlyData.map(m => {
cum += (m.revenue||0) - (m.expenses||0);
cum += m.revenue - m.expenses;
return { month: m.month, value: Math.round(cum*100)/100 };
});
// Aufträge für Tabelle: mit korrektem revenue
const recentOrders = orders.slice(0,30).map(o => ({
...o,
revenue: getOrderRevenue(o),
base_cost: getOrderBaseCost(o.id),
}));
res.json({
totalRevenue, openRevenue, totalBaseCost, totalProfit,
totalExpenses, netProfit, totalOrders: orders.length,
totalRevenue: Math.round(totalRevenue*100)/100,
openRevenue: Math.round(openRevenue*100)/100,
totalBaseCost: Math.round(totalBaseCost*100)/100,
totalProfit: Math.round(totalProfit*100)/100,
totalExpenses: Math.round(totalExpenses*100)/100,
netProfit: Math.round(netProfit*100)/100,
totalOrders: orders.length,
byStatus, byCategory, monthlyData, cumulativeData,
expenses: expenses.slice(0,100),
recentOrders: orders.slice(0,30),
recentOrders,
});
});