feat: 3D-Statistik Gewinn-Sektion + Status-Badges im zugeklappten Zustand
This commit is contained in:
@@ -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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -726,30 +726,53 @@ 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)'}}>
|
||||
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}/>
|
||||
@@ -759,11 +782,39 @@ function BestellStats() {
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user