feat: Tab Einnahmen (bezahlt_am Zeitraum) + Bestellungen (created_at diesen Monat)
This commit is contained in:
@@ -190,6 +190,14 @@ router.get('/overview', authenticate, (req, res) => {
|
||||
base_cost: round2(getOrderBaseCost(o.id)),
|
||||
}));
|
||||
|
||||
const paidOrdersOut = paidOrders.slice(0,50).map(o => ({
|
||||
id: o.id, name: o.name, status: o.status,
|
||||
bezahlt: o.bezahlt, abgeholt: o.abgeholt,
|
||||
created_at: o.created_at, bezahlt_am: o.bezahlt_am,
|
||||
revenue: round2(getOrderRevenue(o)),
|
||||
base_cost: round2(getOrderBaseCost(o.id)),
|
||||
}));
|
||||
|
||||
res.json({
|
||||
totalRevenue: round2(totalRevenue),
|
||||
openRevenue: round2(openRevenue),
|
||||
@@ -202,9 +210,29 @@ router.get('/overview', authenticate, (req, res) => {
|
||||
byStatus, byCategory, monthlyData, cumulativeData,
|
||||
expenses: expenses.slice(0, 100),
|
||||
recentOrders,
|
||||
paidOrders: paidOrdersOut,
|
||||
});
|
||||
});
|
||||
|
||||
// Bestellungen nach created_at (für "Bestellungen diesen Monat" Tab)
|
||||
router.get('/month-orders', authenticate, (req, res) => {
|
||||
const { from, to } = req.query;
|
||||
const u = uid(req);
|
||||
let q = 'SELECT * FROM orders WHERE user_id=?';
|
||||
const p = [u];
|
||||
if (from) { q += ' AND DATE(created_at)>=?'; p.push(from); }
|
||||
if (to) { q += ' AND DATE(created_at)<=?'; p.push(to); }
|
||||
q += ' ORDER BY created_at DESC';
|
||||
const orders = db.prepare(q).all(...p);
|
||||
res.json(orders.map(o => ({
|
||||
id: o.id, name: o.name, status: o.status,
|
||||
bezahlt: o.bezahlt, abgeholt: o.abgeholt,
|
||||
created_at: o.created_at, bezahlt_am: o.bezahlt_am,
|
||||
revenue: round2(getOrderRevenue(o)),
|
||||
base_cost: round2(getOrderBaseCost(o.id)),
|
||||
})));
|
||||
});
|
||||
|
||||
// Debug: rohe Auftragsdaten mit Revenue
|
||||
router.get('/debug', authenticate, (req, res) => {
|
||||
const u = uid(req);
|
||||
|
||||
@@ -96,12 +96,64 @@ function KPI({ label, value, color, sub }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Bestellungen diesen Monat – eigene Komponente mit eigenem Fetch
|
||||
function MonthOrders({ currentMonth }) {
|
||||
const [orders, setOrders] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const from = currentMonth + '-01';
|
||||
const last = new Date(currentMonth.split('-')[0], currentMonth.split('-')[1], 0).getDate();
|
||||
const to = currentMonth + '-' + String(last).padStart(2,'0');
|
||||
api(`/tools/statistik/month-orders?from=${from}&to=${to}`).then(setOrders).catch(()=>setOrders([]));
|
||||
}, [currentMonth]);
|
||||
|
||||
const statusColor = { warteliste:'#ffe66d', in_arbeit:'#4ecdc4', fertig:'#6bcb77' };
|
||||
|
||||
return (
|
||||
<div style={S.card}>
|
||||
<div style={S.head}>
|
||||
BESTELLUNGEN DIESEN MONAT {orders ? `(${orders.length})` : ''}
|
||||
</div>
|
||||
{!orders
|
||||
? <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:12}}>Lädt…</div>
|
||||
: !orders.length
|
||||
? <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:12}}>Keine Bestellungen diesen Monat.</div>
|
||||
: orders.map(o => {
|
||||
const st = !!o.bezahlt && !!o.abgeholt ? 'abgeschlossen' : !!o.bezahlt ? 'bezahlt' : o.status;
|
||||
const col = st==='abgeschlossen'?'#4ade80': st==='bezahlt'?'#c084fc': statusColor[st]||'#888';
|
||||
return (
|
||||
<div key={o.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 0',
|
||||
borderBottom:'1px solid rgba(255,255,255,0.05)',flexWrap:'wrap'}}>
|
||||
<span style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,flexShrink:0}}>
|
||||
{new Date(o.created_at).toLocaleDateString('de-DE')}
|
||||
</span>
|
||||
<span style={{color:'rgba(255,255,255,0.8)',fontFamily:'monospace',fontSize:12,flex:1}}>
|
||||
{o.name}
|
||||
</span>
|
||||
<span style={{background:`${col}18`,border:`1px solid ${col}44`,borderRadius:6,
|
||||
padding:'2px 8px',color:col,fontFamily:'monospace',fontSize:9,flexShrink:0}}>
|
||||
{st}
|
||||
</span>
|
||||
<span style={{color:o.revenue>0?'#4ade80':'rgba(255,255,255,0.3)',
|
||||
fontFamily:"'Space Mono',monospace",fontSize:12,fontWeight:700,flexShrink:0}}>
|
||||
{fmt(o.revenue||0)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Statistik({ mobile }) {
|
||||
const [period, setPeriod] = useState('month');
|
||||
const [custom, setCustom] = useState({ type:'month', month: new Date().toISOString().slice(0,7), year: new Date().getFullYear() });
|
||||
const [data, setData] = useState(null);
|
||||
const [loading,setLoading] = useState(false);
|
||||
const [tab, setTab] = useState('overview');
|
||||
// Bestellungen-Tab: immer aktueller Monat
|
||||
const currentMonth = new Date().toISOString().slice(0,7);
|
||||
const [expForm,setExpForm] = useState({ date: new Date().toISOString().slice(0,10), category:'Filament', description:'', amount:'' });
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
@@ -188,7 +240,7 @@ export default function Statistik({ mobile }) {
|
||||
|
||||
{/* ── Tabs ── */}
|
||||
<div style={{display:'flex',gap:4,marginBottom:16}}>
|
||||
{[['overview','📈 Übersicht'],['expenses','💸 Ausgaben'],['orders','📦 Bestellungen']].map(([id,label])=>(
|
||||
{[['overview','📈 Übersicht'],['revenue','💰 Einnahmen'],['expenses','💸 Ausgaben'],['orders','📦 Bestellungen']].map(([id,label])=>(
|
||||
<button key={id} onClick={()=>setTab(id)} style={{
|
||||
background: tab===id ? 'rgba(255,255,255,0.08)' : 'transparent',
|
||||
border: `1px solid ${tab===id ? 'rgba(255,255,255,0.2)' : 'rgba(255,255,255,0.08)'}`,
|
||||
@@ -369,39 +421,57 @@ export default function Statistik({ mobile }) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ══ BESTELLUNGEN ══ */}
|
||||
{tab==='orders' && data && (
|
||||
{/* ══ EINNAHMEN ══ */}
|
||||
{tab==='revenue' && data && (
|
||||
<div style={S.card}>
|
||||
<div style={S.head}>BESTELLUNGEN IM ZEITRAUM ({data.recentOrders.length})</div>
|
||||
{!data.recentOrders.length
|
||||
? <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:12}}>Keine Bestellungen im gewählten Zeitraum.</div>
|
||||
: data.recentOrders.map(o => {
|
||||
const statusColor = { warteliste:'#ffe66d', in_arbeit:'#4ecdc4', fertig:'#6bcb77', bezahlt:'#c084fc' };
|
||||
const st = !!o.bezahlt && !!o.abgeholt ? 'abgeschlossen' : !!o.bezahlt ? 'bezahlt' : o.status;
|
||||
const col = st==='abgeschlossen' ? '#4ade80' : statusColor[st] || '#888';
|
||||
return (
|
||||
<div key={o.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 0',
|
||||
borderBottom:'1px solid rgba(255,255,255,0.05)',flexWrap:'wrap'}}>
|
||||
<span style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,flexShrink:0}}>
|
||||
{new Date(o.created_at).toLocaleDateString('de-DE')}
|
||||
<div style={S.head}>
|
||||
IM ZEITRAUM BEZAHLTE AUFTRÄGE ({data.paidOrders?.length||0}) · {fmt(data.totalRevenue)} EINGENOMMEN
|
||||
</div>
|
||||
{!data.paidOrders?.length
|
||||
? <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:12}}>
|
||||
Keine bezahlten Aufträge im gewählten Zeitraum.
|
||||
</div>
|
||||
: data.paidOrders.map(o => (
|
||||
<div key={o.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 0',
|
||||
borderBottom:'1px solid rgba(255,255,255,0.05)',flexWrap:'wrap'}}>
|
||||
<span style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,flexShrink:0}}>
|
||||
{o.bezahlt_am ? new Date(o.bezahlt_am).toLocaleDateString('de-DE') : '—'}
|
||||
</span>
|
||||
<span style={{color:'rgba(255,255,255,0.8)',fontFamily:'monospace',fontSize:12,flex:1}}>
|
||||
{o.name}
|
||||
</span>
|
||||
<span style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,flexShrink:0}}>
|
||||
Erstellt: {new Date(o.created_at).toLocaleDateString('de-DE')}
|
||||
</span>
|
||||
<div style={{display:'flex',gap:6,flexShrink:0}}>
|
||||
<span style={{color:'#f87171',fontFamily:"'Space Mono',monospace",fontSize:11}}>
|
||||
-{fmt(o.base_cost)}
|
||||
</span>
|
||||
<span style={{color:'rgba(255,255,255,0.8)',fontFamily:'monospace',fontSize:12,flex:1}}>
|
||||
{o.name}
|
||||
</span>
|
||||
<span style={{background:`${col}18`,border:`1px solid ${col}44`,borderRadius:6,
|
||||
padding:'2px 8px',color:col,fontFamily:'monospace',fontSize:9,flexShrink:0}}>
|
||||
{st}
|
||||
</span>
|
||||
<span style={{color:o.revenue>0?'#4ade80':'rgba(255,255,255,0.3)',
|
||||
fontFamily:"'Space Mono',monospace",fontSize:12,fontWeight:700,flexShrink:0}}>
|
||||
{fmt(o.revenue||0)}
|
||||
<span style={{color:'#4ade80',fontFamily:"'Space Mono',monospace",fontSize:12,fontWeight:700}}>
|
||||
{fmt(o.revenue)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{data.paidOrders?.length > 0 && (
|
||||
<div style={{display:'flex',justifyContent:'space-between',paddingTop:10,
|
||||
borderTop:'1px solid rgba(255,255,255,0.07)'}}>
|
||||
<span style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:11}}>
|
||||
Materialkosten: {fmt(data.totalBaseCost)}
|
||||
</span>
|
||||
<span style={{color:'#60a5fa',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700}}>
|
||||
Rohgewinn: {fmt(data.totalProfit)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ══ BESTELLUNGEN (dieser Monat) ══ */}
|
||||
{tab==='orders' && (
|
||||
<MonthOrders currentMonth={currentMonth}/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user