feat: 3D-Statistik Gewinn-Sektion + Status-Badges im zugeklappten Zustand

This commit is contained in:
2026-06-16 14:48:09 +02:00
parent 6f51302700
commit 30ecc89ba6
2 changed files with 98 additions and 16 deletions

View File

@@ -133,7 +133,38 @@ router.get('/stats', authenticate, (req, res) => {
byStatus[r.status] = r.c;
}
res.json({ totalOrders, bezahltOrders, totalRevenue, offeneRevenue, ordersThisMonth, totalCalcs, byStatus });
// Grundkosten berechnen (preis_freundschaft = Selbstkosten pro Stück aus Kalkulation)
function getBaseCost(orderId) {
const items = db.prepare(`
SELECT oi.stueckzahl, oi.custom_price,
COALESCE(c.preis_freundschaft, oi.preis_freundschaft) as basis
FROM order_items oi
LEFT JOIN calculations c ON c.id = oi.calculation_id
WHERE oi.order_id = ?
`).all(orderId);
return items.reduce((sum, i) => sum + (i.basis * i.stueckzahl), 0);
}
function getRevenue(order) {
if (order.custom_price != null) return parseFloat(order.custom_price);
return db.prepare('SELECT COALESCE(SUM(custom_price*stueckzahl),0) s FROM order_items WHERE order_id=? AND custom_price IS NOT NULL').get(order.id).s;
}
let totalProfit = 0;
for (const o of paidOrders) {
const rev = getRevenue(o);
const cost = getBaseCost(o.id);
totalProfit += rev - cost;
}
let offeneProfit = 0;
for (const o of openOrders) {
const rev = getRevenue(o);
const cost = getBaseCost(o.id);
offeneProfit += rev - cost;
}
res.json({ totalOrders, bezahltOrders, totalRevenue, offeneRevenue, ordersThisMonth, totalCalcs, byStatus, totalProfit, offeneProfit });
});
// ── Ideen-Board ───────────────────────────────────────────────────────────────

View File

@@ -726,44 +726,95 @@ function BestellStats() {
if (loaded) return;
api('/dashboard/stats').then(s => { setStats(s); setLoaded(true); }).catch(() => {});
};
const toggle = () => { setOpen(v => !v); if (!open) load(); };
const Row = ({label, val, color='#fff'}) => (
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',padding:'7px 0',borderBottom:'1px solid rgba(255,255,255,0.05)'}}>
<span style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:12}}>{label}</span>
const Row = ({ label, val, color='rgba(255,255,255,0.85)', sub }) => (
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',
padding:'7px 0',borderBottom:'1px solid rgba(255,255,255,0.05)'}}>
<div>
<span style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:12}}>{label}</span>
{sub && <span style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:10,marginLeft:6}}>{sub}</span>}
</div>
<span style={{color,fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700}}>{val}</span>
</div>
);
// Status-Badge für zugeklappte Ansicht
const Badge = ({ val, color, label }) => val > 0 ? (
<span style={{display:'inline-flex',alignItems:'center',gap:3,
background:`${color}18`,border:`1px solid ${color}44`,
borderRadius:10,padding:'1px 7px',fontSize:10,color,fontFamily:'monospace',whiteSpace:'nowrap'}}>
{val} {label}
</span>
) : null;
return (
<div style={{...S.card,marginBottom:10}}>
<button onClick={toggle} style={{width:'100%',background:'transparent',border:'none',
display:'flex',justifyContent:'space-between',alignItems:'center',cursor:'pointer',padding:0}}>
<div style={{...S.head, marginBottom:0}}>3D-DRUCK STATISTIK</div>
<span style={{color:'rgba(255,255,255,0.3)',fontSize:11,display:'inline-block',
display:'flex',alignItems:'center',cursor:'pointer',padding:0,gap:8,flexWrap:'wrap'}}>
<div style={{...S.head,marginBottom:0,flexShrink:0}}>3D-DRUCK STATISTIK</div>
{/* Status-Badges im zugeklappten Zustand */}
{!open && stats && (
<div style={{display:'flex',gap:4,flexWrap:'wrap',flex:1}}>
<Badge val={stats.byStatus.warteliste||0} color="#ffe66d" label="Warteliste"/>
<Badge val={stats.byStatus.in_arbeit||0} color="#4ecdc4" label="In Arbeit"/>
<Badge val={stats.byStatus.fertig||0} color="#6bcb77" label="Fertig"/>
<Badge val={stats.bezahltOrders} color="#c084fc" label="Bezahlt"/>
</div>
)}
<span style={{color:'rgba(255,255,255,0.3)',fontSize:11,marginLeft:'auto',flexShrink:0,
transform:open?'rotate(90deg)':'rotate(0)',transition:'transform 0.2s'}}></span>
</button>
{open && (
<div style={{marginTop:12}}>
{!stats ? (
<div style={{color:'rgba(255,255,255,0.55)',fontFamily:'monospace',fontSize:12}}>Lädt</div>
) : (
<>
{/* Bestellungen */}
<div style={{marginBottom:12}}>
<div style={{...S.head,marginBottom:6}}>BESTELLUNGEN</div>
<Row label="Gesamt" val={stats.totalOrders}/>
<Row label="Diesen Monat" val={stats.ordersThisMonth} color="#4ecdc4"/>
<Row label="Warteliste" val={stats.byStatus.warteliste||0} color="#ffe66d"/>
<Row label="In Arbeit" val={stats.byStatus.in_arbeit||0} color="#4ecdc4"/>
<Row label="Fertig (unbezahlt)" val={stats.byStatus.fertig||0} color="#6bcb77"/>
<Row label="Bezahlt" val={stats.bezahltOrders} color="#c084fc"/>
<Row label="Warteliste" val={stats.byStatus.warteliste||0} color="#ffe66d"/>
<Row label="In Arbeit" val={stats.byStatus.in_arbeit||0} color="#4ecdc4"/>
<Row label="Fertig (unbezahlt)" val={stats.byStatus.fertig||0} color="#6bcb77"/>
<Row label="Bezahlt" val={stats.bezahltOrders} color="#c084fc"/>
</div>
<div>
{/* Einnahmen + Gewinn */}
<div style={{marginBottom:12}}>
<div style={{...S.head,marginBottom:6}}>EINNAHMEN</div>
<Row label="Eingenommen (bezahlt)" val={`${stats.totalRevenue.toFixed(2)}`} color="#6bcb77"/>
<Row label="Offen (in Arbeit/Fertig)" val={`${stats.offeneRevenue.toFixed(2)}`} color="#ffe66d"/>
<Row label="Archiv-Einträge" val={stats.totalCalcs} color="rgba(255,255,255,0.5)"/>
<Row label="Eingenommen" val={`${stats.totalRevenue.toFixed(2)}`} color="#6bcb77" sub="bezahlt"/>
<Row label="Offen" val={`${stats.offeneRevenue.toFixed(2)}`} color="#ffe66d" sub="in Arbeit/Fertig"/>
</div>
{/* Gewinn */}
<div style={{marginBottom:12}}>
<div style={{...S.head,marginBottom:6}}>GEWINN <span style={{color:'rgba(255,255,255,0.25)',fontWeight:400,fontSize:9}}>nach Materialkosten</span></div>
<Row
label="Verdient"
val={`${(stats.totalProfit??0).toFixed(2)}`}
color={(stats.totalProfit??0) >= 0 ? '#6bcb77' : '#f87171'}
sub="bezahlt"
/>
<Row
label="Ausstehend"
val={`${(stats.offeneProfit??0).toFixed(2)}`}
color={(stats.offeneProfit??0) >= 0 ? '#ffe66d' : '#f87171'}
sub="in Arbeit/Fertig"
/>
<Row
label="Gesamt-Potenzial"
val={`${((stats.totalProfit??0)+(stats.offeneProfit??0)).toFixed(2)}`}
color="rgba(255,255,255,0.5)"
sub="bezahlt + offen"
/>
</div>
<div style={{borderTop:'1px solid rgba(255,255,255,0.05)',paddingTop:8}}>
<Row label="Archiv-Einträge" val={stats.totalCalcs} color="rgba(255,255,255,0.4)"/>
</div>
</>
)}