Files
dickendock/frontend/src/tools/statistik.jsx

510 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell, PieChart, Pie, Legend } from 'recharts';
const api = (path, opts={}) => {
const token = localStorage.getItem('sk_token');
return fetch('/api' + path, {
headers: { 'Content-Type':'application/json', ...(token?{'Authorization':'Bearer '+token}:{}) },
...(opts.body ? { method: opts.method||'POST', body: JSON.stringify(opts.body) } : { method: opts.method||'GET' }),
}).then(r => r.json());
};
const S = {
card: { background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.07)', borderRadius:12, padding:'16px 18px', marginBottom:12 },
head: { color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:10, letterSpacing:1, marginBottom:10, fontWeight:700 },
val: (c='#fff') => ({ color:c, fontFamily:"'Space Mono',monospace", fontSize:22, fontWeight:700 }),
sub: { color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:10 },
inp: { background:'rgba(255,255,255,0.06)', border:'1px solid rgba(255,255,255,0.12)', borderRadius:8, padding:'8px 12px', color:'#fff', fontFamily:'monospace', fontSize:12, outline:'none', width:'100%', boxSizing:'border-box' },
btn: (c='#4ecdc4') => ({ background:`${c}18`, border:`1px solid ${c}44`, borderRadius:8, padding:'8px 14px', color:c, fontFamily:'monospace', fontSize:11, cursor:'pointer' }),
};
const CATS = ['Filament','Zubehör','Ersatzteile','Werkzeug','Sonstiges'];
const CAT_COLORS = { Filament:'#4ecdc4', Zubehör:'#ffe66d', Ersatzteile:'#ff6b9d', Werkzeug:'#60a5fa', Sonstiges:'#c084fc' };
const fmt = v => `${(v||0).toFixed(2)}`;
// Lokales Datum als YYYY-MM-DD (kein UTC-Bug wie bei toISOString)
function todayLocal() {
const d = new Date();
const y = d.getFullYear(), m = String(d.getMonth()+1).padStart(2,'0'), day = String(d.getDate()).padStart(2,'0');
return `${y}-${m}-${day}`;
}
const fmtMonth = m => {
if (!m) return '';
if (/^\d{4}-\d{2}$/.test(m)) {
const [y,mo] = m.split('-');
return ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'][parseInt(mo)-1]+' '+y.slice(2);
}
if (/^\d{4}-\d{2}-\d{2}$/.test(m)) {
const [,mo,d] = m.split('-');
return `${d}.${mo}.`;
}
return m;
};
// Bei Wochen-Granularität: zeigt "15.06. 21.06." statt nur den Montag
const fmtMonthRange = (m, granularity) => {
if (!m || granularity !== 'week' || !/^\d{4}-\d{2}-\d{2}$/.test(m)) return fmtMonth(m);
const [y,mo,d] = m.split('-').map(Number);
const start = new Date(y, mo-1, d);
const end = new Date(y, mo-1, d+6);
const f = dt => `${String(dt.getDate()).padStart(2,'0')}.${String(dt.getMonth()+1).padStart(2,'0')}.`;
return `${f(start)} ${f(end)}`;
};
const PERIODS = [
{ id:'week', label:'Diese Woche' },
{ id:'month', label:'Dieser Monat' },
{ id:'year', label:'Dieses Jahr' },
{ id:'custom', label:'Eigener Zeitraum' },
{ id:'all', label:'Gesamt'},
];
// Generiere Monat-Optionen (letzten 24 Monate)
function getMonthOptions() {
const opts = [];
const now = new Date();
for (let i = 0; i < 24; i++) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
const val = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
const label = d.toLocaleDateString('de-DE', { month:'long', year:'numeric' });
opts.push({ val, label });
}
return opts;
}
// Generiere Jahr-Optionen
function getYearOptions() {
const opts = [];
const now = new Date().getFullYear();
for (let y = now; y >= now - 5; y--) opts.push(y);
return opts;
}
function periodRange(id, custom) {
const now = new Date();
const pad = n => String(n).padStart(2,'0');
const iso = d => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;
if (id==='week') {
const d = new Date(now);
const day = d.getDay() || 7; // 1=Mo … 7=So
d.setDate(d.getDate() - (day - 1)); // zurück auf Montag
return { from:iso(d), to:iso(now) };
}
if (id==='month') { const d=new Date(now.getFullYear(),now.getMonth(),1); return { from:iso(d), to:iso(now) }; }
if (id==='year') { return { from:`${now.getFullYear()}-01-01`, to:iso(now) }; }
if (id==='custom' && custom) {
if (custom.type==='month' && custom.month) {
const [y,m] = custom.month.split('-').map(Number);
const last = new Date(y, m, 0);
return { from:`${y}-${pad(m)}-01`, to:iso(last) };
}
if (custom.type==='year' && custom.year) {
return { from:`${custom.year}-01-01`, to:`${custom.year}-12-31` };
}
}
return {};
}
function KPI({ label, value, color, sub }) {
return (
<div style={{flex:1,minWidth:130}}>
<div style={S.sub}>{label}</div>
<div style={S.val(color)}>{value}</div>
{sub && <div style={{...S.sub,marginTop:2}}>{sub}</div>}
</div>
);
}
// Bestellungen im gewählten Zeitraum
function OrdersInPeriod({ fromDate, toDate }) {
const [orders, setOrders] = useState(null);
useEffect(() => {
const q = (fromDate || toDate) ? `?from=${fromDate}&to=${toDate}` : '';
api(`/tools/statistik/month-orders${q}`).then(setOrders).catch(()=>setOrders([]));
}, [fromDate, toDate]);
const STATUS_ORDER = ['warteliste','in_arbeit','fertig','bezahlt','abgeschlossen'];
const STATUS_COLOR = { warteliste:'#ffe66d', in_arbeit:'#4ecdc4', fertig:'#6bcb77', bezahlt:'#c084fc', abgeschlossen:'#4ade80' };
const STATUS_LABEL = { warteliste:'Warteliste', in_arbeit:'In Arbeit', fertig:'Fertig', bezahlt:'Bezahlt', abgeschlossen:'Abgeschlossen' };
const periodLabel = (fromDate || toDate) ? 'IM ZEITRAUM' : 'GESAMT';
// Gruppieren nach Status
const grouped = {};
(orders||[]).forEach(o => {
const st = !!o.bezahlt && !!o.abgeholt ? 'abgeschlossen' : !!o.bezahlt ? 'bezahlt' : o.status;
if (!grouped[st]) grouped[st] = [];
grouped[st].push({ ...o, _st: st });
});
// Innerhalb jeder Gruppe nach Datum sortieren (neueste zuerst)
Object.values(grouped).forEach(g => g.sort((a,b) => new Date(b.created_at) - new Date(a.created_at)));
return (
<div style={S.card}>
<div style={S.head}>BESTELLUNGEN {periodLabel} {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 im Zeitraum.</div>
: STATUS_ORDER.filter(st => grouped[st]?.length).map(st => {
const col = STATUS_COLOR[st];
const group = grouped[st];
const total = group.reduce((s,o)=>s+(o.revenue||0),0);
return (
<div key={st} style={{marginBottom:16}}>
{/* Status-Header mit Summe */}
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',
padding:'6px 10px',background:`${col}10`,borderRadius:8,marginBottom:6}}>
<div style={{display:'flex',alignItems:'center',gap:8}}>
<span style={{background:`${col}20`,border:`1px solid ${col}44`,borderRadius:10,
padding:'2px 10px',color:col,fontFamily:'monospace',fontSize:10}}>
{STATUS_LABEL[st]}
</span>
<span style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10}}>
{group.length} Bestellung{group.length!==1?'en':''}
</span>
</div>
{total > 0 && (
<span style={{color:col,fontFamily:"'Space Mono',monospace",fontSize:12,fontWeight:700}}>
Σ {fmt(total)}
</span>
)}
</div>
{/* Einträge */}
{group.map(o => (
<div key={o.id} style={{display:'flex',alignItems:'center',gap:10,padding:'7px 10px',
borderBottom:'1px solid rgba(255,255,255,0.04)'}}>
<span style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,flexShrink:0,width:60}}>
{new Date(o.created_at).toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit'})}
</span>
<span style={{color:'rgba(255,255,255,0.8)',fontFamily:'monospace',fontSize:12,flex:1}}>
{o.name}
</span>
<span style={{color:o.revenue>0?'#4ade80':'rgba(255,255,255,0.25)',
fontFamily:"'Space Mono',monospace",fontSize:12,fontWeight:700,flexShrink:0}}>
{fmt(o.revenue||0)}
</span>
</div>
))}
</div>
);
})
}
</div>
);
}
export default function Statistik({ mobile }) {
const [fromDate, setFromDate] = useState('');
const [toDate, setToDate] = useState(todayLocal());
const [data, setData] = useState(null);
const [loading,setLoading] = useState(false);
const [tab, setTab] = useState('overview');
const [expForm,setExpForm] = useState({ date: todayLocal(), category:'Filament', description:'', amount:'' });
const [saving, setSaving] = useState(false);
function load() {
setLoading(true);
const q = (fromDate || toDate) ? `?from=${fromDate}&to=${toDate}` : '';
api('/tools/statistik/overview'+q)
.then(d => setData(d))
.catch(()=>{})
.finally(()=>setLoading(false));
}
useEffect(() => { load(); }, [fromDate, toDate]);
async function addExpense() {
if (!expForm.description || !expForm.amount) return;
setSaving(true);
try {
await api('/tools/statistik/expenses', { body: expForm });
setExpForm(p => ({ ...p, description:'', amount:'' }));
load();
} finally { setSaving(false); }
}
async function delExpense(id) {
await api(`/tools/statistik/expenses/${id}`, { method:'DELETE' });
load();
}
const netColor = (data?.netProfit||0) >= 0 ? '#4ade80' : '#f87171';
return (
<div style={{ padding: mobile?'14px 14px 90px':'28px 36px', maxWidth:900 }}>
{/* ── Header ── */}
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:20,flexWrap:'wrap',gap:10}}>
<h2 style={{color:'#fff',fontFamily:'monospace',fontSize:18,fontWeight:700,margin:0}}>
📊 3D-Druck Statistik
</h2>
{/* Zeitraum */}
<div style={{display:'flex',gap:8,alignItems:'center',flexWrap:'wrap'}}>
<div style={{display:'flex',alignItems:'center',gap:6}}>
<span style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11}}>Von</span>
<input type="date" value={fromDate} onChange={e=>setFromDate(e.target.value)}
style={{background:'rgba(255,255,255,0.06)',border:'1px solid rgba(255,255,255,0.12)',
borderRadius:8,padding:'5px 10px',color:'#fff',fontFamily:'monospace',fontSize:11,cursor:'pointer'}}/>
</div>
<div style={{display:'flex',alignItems:'center',gap:6}}>
<span style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11}}>Bis</span>
<input type="date" value={toDate} onChange={e=>setToDate(e.target.value)}
style={{background:'rgba(255,255,255,0.06)',border:'1px solid rgba(255,255,255,0.12)',
borderRadius:8,padding:'5px 10px',color:'#fff',fontFamily:'monospace',fontSize:11,cursor:'pointer'}}/>
</div>
{fromDate && (
<button onClick={()=>{setFromDate('');setToDate(todayLocal());}}
style={{background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.1)',
borderRadius:8,padding:'5px 10px',color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:11,cursor:'pointer'}}>
Zurücksetzen
</button>
)}
</div>
</div>
{/* ── Tabs ── */}
<div style={{display:'flex',gap:4,marginBottom:16}}>
{[['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)'}`,
borderRadius:8, padding:'6px 14px',
color: tab===id ? '#fff' : 'rgba(255,255,255,0.4)',
fontFamily:'monospace', fontSize:11, cursor:'pointer',
}}>{label}</button>
))}
</div>
{loading && <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:12}}>Lädt</div>}
{/* ══ ÜBERSICHT ══ */}
{tab==='overview' && data && (
<>
{/* KPI Kacheln */}
<div style={{...S.card}}>
<div style={S.head}>EINNAHMEN & GEWINN</div>
<div style={{display:'flex',flexWrap:'wrap',gap:20,marginBottom:16}}>
<KPI label="Eingenommen" value={fmt(data.totalRevenue)} color="#4ade80" sub="bezahlt"/>
<KPI label="Offen" value={fmt(data.openRevenue)} color="#ffe66d" sub="in Arbeit/Fertig"/>
<KPI label="Materialkosten" value={fmt(data.totalBaseCost)} color="#f87171" sub="Selbstkosten"/>
<KPI label="Rohgewinn" value={fmt(data.totalProfit)} color="#60a5fa" sub="nach Materialkosten"/>
</div>
<div style={{borderTop:'1px solid rgba(255,255,255,0.06)',paddingTop:16,display:'flex',flexWrap:'wrap',gap:20}}>
<KPI label="Ausgaben" value={fmt(data.totalExpenses)} color="#f87171" sub="Einkäufe"/>
<KPI label="NETTOGEWINN" value={fmt(data.netProfit)} color={netColor} sub="Rohgewinn Ausgaben"/>
<KPI label="Bestellungen" value={data.totalOrders} color="rgba(255,255,255,0.6)" sub="im Zeitraum"/>
</div>
</div>
{/* Status-Verteilung */}
<div style={{...S.card}}>
<div style={S.head}>BESTELLUNGEN NACH STATUS</div>
<div style={{display:'flex',flexWrap:'wrap',gap:8}}>
{[
['Warteliste', data.byStatus.warteliste, '#ffe66d'],
['In Arbeit', data.byStatus.in_arbeit, '#4ecdc4'],
['Fertig', data.byStatus.fertig, '#6bcb77'],
['Bezahlt', data.byStatus.bezahlt, '#c084fc'],
['Abgeschlossen', data.byStatus.abgeschlossen, '#4ade80'],
].map(([label, val, color]) => val > 0 && (
<div key={label} style={{background:`${color}12`,border:`1px solid ${color}33`,
borderRadius:8,padding:'8px 14px',textAlign:'center'}}>
<div style={{color,fontFamily:"'Space Mono',monospace",fontSize:18,fontWeight:700}}>{val}</div>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:9}}>{label}</div>
</div>
))}
</div>
</div>
{/* Balkendiagramm Einnahmen vs Ausgaben */}
{data.monthlyData.length > 0 && (
<div style={S.card}>
<div style={S.head}>EINNAHMEN VS. AUSGABEN {data.granularity==='day'?'(TÄGLICH)':data.granularity==='week'?'(WÖCHENTLICH)':'(MONATLICH)'}</div>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={data.monthlyData} margin={{top:0,right:0,bottom:0,left:0}}>
<XAxis dataKey="month" tickFormatter={fmtMonth} tick={{fill:'rgba(255,255,255,0.3)',fontSize:10}} axisLine={false} tickLine={false}/>
<YAxis tick={{fill:'rgba(255,255,255,0.3)',fontSize:10}} axisLine={false} tickLine={false} tickFormatter={v=>`${v}`} width={45}/>
<Tooltip contentStyle={{background:'#1a1d2e',border:'1px solid rgba(255,255,255,0.1)',borderRadius:8,fontFamily:'monospace',fontSize:11,color:'#fff'}}
labelStyle={{color:'rgba(255,255,255,0.6)'}} itemStyle={{color:'#fff'}}
formatter={(v,n)=>[`${v.toFixed(2)}`, n==='revenue'?'Einnahmen':'Ausgaben']}
labelFormatter={m => fmtMonthRange(m, data.granularity)}/>
<Bar dataKey="revenue" fill="#4ade80" radius={[4,4,0,0]} maxBarSize={28}/>
<Bar dataKey="expenses" fill="#f87171" radius={[4,4,0,0]} maxBarSize={28}/>
</BarChart>
</ResponsiveContainer>
</div>
)}
{/* Kumulierter Gewinn + Ausgaben nach Kategorie */}
<div style={{display:'flex',gap:12,flexWrap:'wrap'}}>
{data.cumulativeData.length > 1 && (
<div style={{...S.card,flex:1,minWidth:200}}>
<div style={S.head}>NETTOGEWINN IM ZEITVERLAUF</div>
<ResponsiveContainer width="100%" height={160}>
<LineChart data={data.cumulativeData} margin={{top:5,right:5,bottom:0,left:0}}>
<XAxis dataKey="month" tickFormatter={fmtMonth} tick={{fill:'rgba(255,255,255,0.3)',fontSize:9}} axisLine={false} tickLine={false}/>
<YAxis tick={{fill:'rgba(255,255,255,0.3)',fontSize:9}} axisLine={false} tickLine={false} tickFormatter={v=>`${v}`} width={42}/>
<Tooltip contentStyle={{background:'#1a1d2e',border:'1px solid rgba(255,255,255,0.1)',borderRadius:8,fontFamily:'monospace',fontSize:11,color:'#fff'}}
labelStyle={{color:'rgba(255,255,255,0.6)'}} itemStyle={{color:'#4ecdc4'}}
formatter={v=>[`${v.toFixed(2)}`, 'Gewinn kumuliert']} labelFormatter={m => fmtMonthRange(m, data.granularity)}/>
<Line type="monotone" dataKey="value" stroke="#4ecdc4" strokeWidth={2} dot={false}/>
</LineChart>
</ResponsiveContainer>
</div>
)}
{Object.keys(data.byCategory).length > 0 && (
<div style={{...S.card,flex:1,minWidth:200}}>
<div style={S.head}>AUSGABEN NACH KATEGORIE</div>
<ResponsiveContainer width="100%" height={160}>
<PieChart>
<Pie data={Object.entries(data.byCategory).map(([name,value])=>({name,value}))}
cx="50%" cy="50%" innerRadius={40} outerRadius={65}
dataKey="value" paddingAngle={3}>
{Object.entries(data.byCategory).map(([name],i)=>(
<Cell key={name} fill={CAT_COLORS[name]||'#888'}/>
))}
</Pie>
<Tooltip contentStyle={{background:'#1a1d2e',border:'1px solid rgba(255,255,255,0.1)',borderRadius:8,fontFamily:'monospace',fontSize:11,color:'#fff'}}
labelStyle={{color:'rgba(255,255,255,0.6)'}} itemStyle={{color:'#fff'}}
formatter={v=>[`${v.toFixed(2)}`]}/>
<Legend iconType="circle" iconSize={8}
formatter={v=><span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:9}}>{v}</span>}/>
</PieChart>
</ResponsiveContainer>
</div>
)}
</div>
</>
)}
{/* ══ AUSGABEN ══ */}
{tab==='expenses' && (
<>
{/* Formular */}
<div style={S.card}>
<div style={S.head}>NEUE AUSGABE</div>
<div style={{display:'flex',gap:8,flexWrap:'wrap'}}>
<input type="date" value={expForm.date}
onChange={e=>setExpForm(p=>({...p,date:e.target.value}))}
style={{...S.inp,width:140}}/>
<select value={expForm.category}
onChange={e=>setExpForm(p=>({...p,category:e.target.value}))}
style={{...S.inp,width:130}}>
{CATS.map(c=><option key={c}>{c}</option>)}
</select>
<input placeholder="Beschreibung" value={expForm.description}
onChange={e=>setExpForm(p=>({...p,description:e.target.value}))}
style={{...S.inp,flex:1,minWidth:140}}/>
<input type="number" placeholder="0.00" step="0.01" min="0" value={expForm.amount}
onChange={e=>setExpForm(p=>({...p,amount:e.target.value}))}
onKeyDown={e=>e.key==='Enter'&&addExpense()}
style={{...S.inp,width:90,textAlign:'right'}}/>
<button onClick={addExpense} disabled={saving}
style={{...S.btn('#4ade80'),opacity:saving?0.5:1,whiteSpace:'nowrap'}}>
{saving?'…':'+ Hinzufügen'}
</button>
</div>
</div>
{/* Liste */}
<div style={S.card}>
<div style={S.head}>AUSGABEN {(fromDate||toDate!==todayLocal())&&'(IM ZEITRAUM)'}</div>
{!data?.expenses?.length
? <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:12}}>Keine Ausgaben im gewählten Zeitraum.</div>
: data.expenses.map(e => (
<div key={e.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 0',
borderBottom:'1px solid rgba(255,255,255,0.05)'}}>
<span style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,flexShrink:0,width:80}}>
{e.date}
</span>
<span style={{background:`${CAT_COLORS[e.category]||'#888'}18`,
border:`1px solid ${CAT_COLORS[e.category]||'#888'}44`,
borderRadius:6,padding:'2px 8px',color:CAT_COLORS[e.category]||'#888',
fontFamily:'monospace',fontSize:9,flexShrink:0}}>
{e.category}
</span>
<span style={{color:'rgba(255,255,255,0.7)',fontFamily:'monospace',fontSize:12,flex:1}}>
{e.description}
</span>
<span style={{color:'#f87171',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700,flexShrink:0}}>
{fmt(e.amount)}
</span>
<button onClick={()=>delExpense(e.id)}
style={{background:'rgba(248,113,113,0.1)',border:'1px solid rgba(248,113,113,0.2)',
borderRadius:6,padding:'3px 8px',color:'#f87171',fontFamily:'monospace',fontSize:10,cursor:'pointer',flexShrink:0}}>
</button>
</div>
))
}
{data?.expenses?.length > 0 && (
<div style={{display:'flex',justifyContent:'flex-end',paddingTop:10}}>
<span style={{color:'#f87171',fontFamily:"'Space Mono',monospace",fontSize:14,fontWeight:700}}>
Σ {fmt(data.totalExpenses)}
</span>
</div>
)}
</div>
</>
)}
{/* ══ EINNAHMEN ══ */}
{tab==='revenue' && data && (
<div style={S.card}>
<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:'#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' && (
<OrdersInPeriod fromDate={fromDate} toDate={toDate}/>
)}
</div>
);
}