Dateien nach "frontend/src/tools" hochladen

This commit is contained in:
2026-05-26 00:15:04 +02:00
parent bee374c458
commit eb7a03328e

View File

@@ -1,12 +1,13 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { api, S } from '../lib.js'; import { api, S } from '../lib.js';
import { SearchIcon, PlusIcon, XIcon, EditIcon, TrashIcon, CheckIcon, ChevronIcon } from '../icons.jsx'; import { SearchIcon, PlusIcon, XIcon, EditIcon, TrashIcon, ChevronIcon } from '../icons.jsx';
const STATUS = { const STATUS = {
warteliste: { label:'Warteliste', color:'#ffe66d', bg:'rgba(255,230,109,0.12)' }, warteliste: { label:'Warteliste', color:'#ffe66d', bg:'rgba(255,230,109,0.12)' },
in_arbeit: { label:'In Arbeit', color:'#4ecdc4', bg:'rgba(78,205,196,0.12)' }, in_arbeit: { label:'In Arbeit', color:'#4ecdc4', bg:'rgba(78,205,196,0.12)' },
fertig: { label:'Fertig', color:'#6bcb77', bg:'rgba(107,203,119,0.12)' }, fertig: { label:'Fertig', color:'#6bcb77', bg:'rgba(107,203,119,0.12)' },
}; };
const STATUS_KEYS = ['warteliste','in_arbeit','fertig'];
const PREISE = [ const PREISE = [
{ key:'freundschaft', emoji:'💖', label:'Freundschaft', c:'#ff6b9d' }, { key:'freundschaft', emoji:'💖', label:'Freundschaft', c:'#ff6b9d' },
@@ -18,104 +19,105 @@ function StatusBadge({ status, onClick, small }) {
const s = STATUS[status] || STATUS.warteliste; const s = STATUS[status] || STATUS.warteliste;
return ( return (
<button onClick={onClick} style={{ <button onClick={onClick} style={{
background: s.bg, border:`1px solid ${s.color}44`, background:s.bg, border:`1px solid ${s.color}44`,
borderRadius:20, padding: small ? '3px 10px' : '5px 14px', borderRadius:20, padding:small?'3px 10px':'5px 14px',
color: s.color, fontFamily:'monospace', fontSize: small ? 10 : 11, color:s.color, fontFamily:'monospace', fontSize:small?10:11,
cursor: onClick ? 'pointer' : 'default', whiteSpace:'nowrap', cursor:onClick?'pointer':'default', whiteSpace:'nowrap',
}}>{s.label}</button> }}>{s.label}</button>
); );
} }
// ── Bestellung Detail / Bearbeiten ──────────────────────────────────────────── // ── Bestellung Detail ─────────────────────────────────────────────────────────
function BestellungDetail({ id, toast, onBack, onSaved }) { function BestellungDetail({ id, toast, onBack }) {
const [order, setOrder] = useState(null); const [order, setOrder] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [archiv, setArchiv] = useState([]); const [archiv, setArchiv] = useState([]);
const [showPicker, setShowPicker] = useState(false); const [showPicker, setShowPicker] = useState(false);
const [pickerSearch, setPickerSearch] = useState(''); const [pickerSearch,setPickerSearch]= useState('');
const [editMode, setEditMode] = useState(!id); // neue Bestellung direkt im Edit-Mode const [editMode, setEditMode] = useState(!id);
const [form, setForm] = useState({ name:'', bemerkung:'', custom_price:'', status:'warteliste' }); const [form, setForm] = useState({ name:'', bemerkung:'', custom_price:'', status:'warteliste' });
useEffect(() => { useEffect(() => {
api('/tools/kalkulator3d').then(setArchiv).catch(() => {}); api('/tools/kalkulator3d').then(setArchiv).catch(()=>{});
if (id) { if (id) {
api(`/tools/bestellungen/${id}`) api(`/tools/bestellungen/${id}`)
.then(o => { setOrder(o); setForm({ name:o.name, bemerkung:o.bemerkung, custom_price:o.custom_price??'', status:o.status }); }) .then(o => { setOrder(o); setForm({ name:o.name, bemerkung:o.bemerkung, custom_price:o.custom_price??'', status:o.status }); })
.catch(e => toast(e.message,'error')) .catch(e => toast(e.message,'error'))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
} else { } else { setLoading(false); }
setLoading(false);
}
}, [id]); }, [id]);
const totals = useMemo(() => { const totals = useMemo(() => {
const items = order?.items || []; const items = order?.items || [];
return { return {
freundschaft: items.reduce((s,i) => s + i.preis_freundschaft * i.stueckzahl, 0), freundschaft: items.reduce((s,i) => s + i.preis_freundschaft * i.stueckzahl, 0),
normal: items.reduce((s,i) => s + i.preis_normal * i.stueckzahl, 0), normal: items.reduce((s,i) => s + i.preis_normal * i.stueckzahl, 0),
auftrag: items.reduce((s,i) => s + i.preis_auftrag * i.stueckzahl, 0), auftrag: items.reduce((s,i) => s + i.preis_auftrag * i.stueckzahl, 0),
custom_sum: items.reduce((s,i) => s + (i.custom_price != null ? i.custom_price * i.stueckzahl : 0), 0),
has_any_custom: items.some(i => i.custom_price != null),
}; };
}, [order]); }, [order]);
const saveOrder = async () => { const saveOrder = async () => {
if (!form.name.trim()) { toast('Name erforderlich','error'); return; } if (!form.name.trim()) { toast('Name erforderlich','error'); return; }
try { try {
const payload = { ...form, custom_price: form.custom_price !== '' ? parseFloat(form.custom_price) : null }; const payload = { ...form, custom_price: form.custom_price!=='' ? parseFloat(form.custom_price) : null };
if (id) { if (id) {
const updated = await api(`/tools/bestellungen/${id}`, { method:'PUT', body:payload }); const u = await api(`/tools/bestellungen/${id}`,{method:'PUT',body:payload});
setOrder(updated); setEditMode(false); toast('Gespeichert'); setOrder(u); setEditMode(false); toast('Gespeichert');
} else { } else {
const created = await api('/tools/bestellungen', { body:payload }); const created = await api('/tools/bestellungen',{body:payload});
toast('Bestellung angelegt'); toast('Bestellung angelegt');
onSaved(created.id); onBack(created.id);
} }
} catch(e) { toast(e.message,'error'); } } catch(e) { toast(e.message,'error'); }
}; };
const addItem = async calcId => { const addItem = async calcId => {
try { try { const u = await api(`/tools/bestellungen/${id}/items`,{body:{calculation_id:calcId,stueckzahl:1}}); setOrder(u); setShowPicker(false); toast('Hinzugefügt'); }
const updated = await api(`/tools/bestellungen/${id}/items`, { body:{ calculation_id:calcId, stueckzahl:1 } }); catch(e) { toast(e.message,'error'); }
setOrder(updated); setShowPicker(false); toast('Hinzugefügt');
} catch(e) { toast(e.message,'error'); }
}; };
const updateQty = async (itemId, qty) => { const updateItem = async (itemId, patch) => {
if (qty < 1) return; try { const u = await api(`/tools/bestellungen/${id}/items/${itemId}`,{method:'PUT',body:patch}); setOrder(u); }
try { const u = await api(`/tools/bestellungen/${id}/items/${itemId}`, { method:'PUT', body:{ stueckzahl:qty } }); setOrder(u); }
catch(e) { toast(e.message,'error'); } catch(e) { toast(e.message,'error'); }
}; };
const removeItem = async itemId => { const removeItem = async itemId => {
try { const u = await api(`/tools/bestellungen/${id}/items/${itemId}`, { method:'DELETE' }); setOrder(u); toast('Entfernt'); } try { const u = await api(`/tools/bestellungen/${id}/items/${itemId}`,{method:'DELETE'}); setOrder(u); toast('Entfernt'); }
catch(e) { toast(e.message,'error'); } catch(e) { toast(e.message,'error'); }
}; };
const cycleStatus = async () => { const cycleOrderStatus = async () => {
const states = ['warteliste','in_arbeit','fertig']; const next = STATUS_KEYS[(STATUS_KEYS.indexOf(order.status)+1) % STATUS_KEYS.length];
const next = states[(states.indexOf(order.status)+1) % states.length]; try { const u = await api(`/tools/bestellungen/${id}`,{method:'PUT',body:{...order,status:next}}); setOrder(u); }
try { const u = await api(`/tools/bestellungen/${id}`, { method:'PUT', body:{...order, status:next} }); setOrder(u); }
catch(e) { toast(e.message,'error'); } catch(e) { toast(e.message,'error'); }
}; };
const cycleItemStatus = item => {
const next = STATUS_KEYS[(STATUS_KEYS.indexOf(item.status||'warteliste')+1) % STATUS_KEYS.length];
updateItem(item.id, { status: next });
};
const filteredArchiv = pickerSearch const filteredArchiv = pickerSearch
? archiv.filter(a => a.name.toLowerCase().includes(pickerSearch.toLowerCase())) ? archiv.filter(a => a.name.toLowerCase().includes(pickerSearch.toLowerCase()))
: archiv; : archiv;
if (loading) return <div style={{ padding:24, color:'rgba(255,255,255,0.3)', fontFamily:'monospace' }}>Lädt</div>; if (loading) return <div style={{padding:24,color:'rgba(255,255,255,0.3)',fontFamily:'monospace'}}>Lädt</div>;
return ( return (
<div style={{ padding:'14px 14px 90px', maxWidth:860 }}> <div style={{ padding:'14px 14px 90px', maxWidth:860 }}>
{/* Header */} {/* Header */}
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:18 }}> <div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:16 }}>
<button onClick={onBack} style={{ background:'transparent', border:'none', cursor:'pointer', padding:4 }}> <button onClick={() => onBack(null)} style={{ background:'transparent', border:'none', cursor:'pointer', padding:4 }}>
<ChevronIcon size={20} color="rgba(255,255,255,0.5)" dir="left" /> <ChevronIcon size={20} color="rgba(255,255,255,0.5)" dir="left"/>
</button> </button>
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:18, flex:1, margin:0 }}> <h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:18, flex:1, margin:0 }}>
{id ? (editMode ? 'Bearbeiten' : order?.name) : 'Neue Bestellung'} {id ? (editMode?'Bearbeiten':order?.name) : 'Neue Bestellung'}
</h1> </h1>
{order && !editMode && <StatusBadge status={order.status} onClick={cycleStatus}/>} {order && !editMode && <StatusBadge status={order.status} onClick={cycleOrderStatus}/>}
{order && !editMode && ( {order && !editMode && (
<button onClick={() => setEditMode(true)} style={{ background:'transparent', border:'none', cursor:'pointer', padding:4 }}> <button onClick={()=>setEditMode(true)} style={{background:'transparent',border:'none',cursor:'pointer',padding:4}}>
<EditIcon size={18} color="rgba(255,255,255,0.5)"/> <EditIcon size={18} color="rgba(255,255,255,0.5)"/>
</button> </button>
)} )}
@@ -123,175 +125,202 @@ function BestellungDetail({ id, toast, onBack, onSaved }) {
{/* Formular */} {/* Formular */}
{(editMode || !id) && ( {(editMode || !id) && (
<div style={{ ...S.card, marginBottom:12 }}> <div style={{...S.card,marginBottom:12}}>
<div style={{ marginBottom:10 }}> <div style={{marginBottom:10}}>
<label style={{ ...S.head, display:'block', marginBottom:4 }}>NAME / PERSON</label> <label style={{...S.head,display:'block',marginBottom:4}}>NAME / PERSON</label>
<input value={form.name} onChange={e=>setForm(f=>({...f,name:e.target.value}))} <input value={form.name} onChange={e=>setForm(f=>({...f,name:e.target.value}))}
placeholder="z.B. Max Mustermann" placeholder="z.B. Max Mustermann" style={{...S.inp,fontSize:15}}/>
style={{ ...S.inp, fontSize:15 }} />
</div> </div>
<div style={{ marginBottom:10 }}> <div style={{marginBottom:10}}>
<label style={{ ...S.head, display:'block', marginBottom:4 }}>BEMERKUNG</label> <label style={{...S.head,display:'block',marginBottom:4}}>BEMERKUNG</label>
<textarea value={form.bemerkung} onChange={e=>setForm(f=>({...f,bemerkung:e.target.value}))} <textarea value={form.bemerkung} onChange={e=>setForm(f=>({...f,bemerkung:e.target.value}))}
placeholder="Hinweise, Lieferung, Material…" rows={3} placeholder="Hinweise, Lieferung…" rows={2}
style={{ ...S.inp, resize:'vertical', lineHeight:1.6, fontSize:14, padding:'9px 12px' }} /> style={{...S.inp,resize:'vertical',lineHeight:1.6,fontSize:14,padding:'9px 12px'}}/>
</div> </div>
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, marginBottom:12 }}> <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10,marginBottom:12}}>
<div> <div>
<label style={{ ...S.head, display:'block', marginBottom:4 }}>STATUS</label> <label style={{...S.head,display:'block',marginBottom:4}}>STATUS</label>
<select value={form.status} onChange={e=>setForm(f=>({...f,status:e.target.value}))} <select value={form.status} onChange={e=>setForm(f=>({...f,status:e.target.value}))} style={{...S.inp,fontSize:14}}>
style={{ ...S.inp, fontSize:14 }}> {Object.entries(STATUS).map(([k,v])=><option key={k} value={k} style={{background:'#1a1a1e'}}>{v.label}</option>)}
{Object.entries(STATUS).map(([k,v]) => <option key={k} value={k} style={{background:'#1a1a1e'}}>{v.label}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label style={{ ...S.head, display:'block', marginBottom:4 }}>FESTPREIS ()</label> <label style={{...S.head,display:'block',marginBottom:4}}>GESAMT-FESTPREIS ()</label>
<input type="number" value={form.custom_price} onChange={e=>setForm(f=>({...f,custom_price:e.target.value}))} <input type="number" value={form.custom_price} onChange={e=>setForm(f=>({...f,custom_price:e.target.value}))}
placeholder="Optional" step="0.01" placeholder="Optional" step="0.01" style={{...S.inp,fontSize:15}}/>
style={{ ...S.inp, fontSize:15 }} />
</div> </div>
</div> </div>
<div style={{ display:'flex', gap:8 }}> <div style={{display:'flex',gap:8}}>
<button onClick={saveOrder} style={{ ...S.btn('#4ecdc4'), flex:1, textAlign:'center', padding:'10px 0' }}> <button onClick={saveOrder} style={{...S.btn('#4ecdc4'),flex:1,textAlign:'center',padding:'10px 0'}}>
{id ? '✓ Speichern' : '+ Bestellung anlegen'} {id?'✓ Speichern':'+ Bestellung anlegen'}
</button> </button>
{id && <button onClick={()=>setEditMode(false)} style={{ ...S.btn('#ff6b9d'), padding:'10px 14px' }}>Abbrechen</button>} {id && <button onClick={()=>setEditMode(false)} style={{...S.btn('#ff6b9d'),padding:'10px 14px'}}>Abbrechen</button>}
</div> </div>
</div> </div>
)} )}
{/* Nur bei bestehender Bestellung: Items + Summen */}
{order && !editMode && ( {order && !editMode && (
<> <>
{/* Bemerkung anzeigen */}
{order.bemerkung && ( {order.bemerkung && (
<div style={{ ...S.card, marginBottom:10, padding:'10px 14px' }}> <div style={{...S.card,marginBottom:10,padding:'10px 14px'}}>
<div style={{ color:'rgba(255,255,255,0.45)', fontFamily:'monospace', fontSize:12, lineHeight:1.6 }}>{order.bemerkung}</div> <div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:12,lineHeight:1.6}}>{order.bemerkung}</div>
</div> </div>
)} )}
{/* Positionen */} {/* Positionen */}
<div style={{ ...S.card, marginBottom:10 }}> <div style={{...S.card,marginBottom:10}}>
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:12 }}> <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
<div style={S.head}>POSITIONEN</div> <div style={S.head}>POSITIONEN</div>
<button onClick={() => setShowPicker(true)} style={{ ...S.btn('#4ecdc4',true), display:'flex', alignItems:'center', gap:5 }}> <button onClick={()=>{setShowPicker(true);setPickerSearch('');}}
style={{...S.btn('#4ecdc4',true),display:'flex',alignItems:'center',gap:5}}>
<PlusIcon size={13} color="#4ecdc4"/> Hinzufügen <PlusIcon size={13} color="#4ecdc4"/> Hinzufügen
</button> </button>
</div> </div>
{order.items.length === 0 && ( {order.items.length===0 && (
<div style={{ color:'rgba(255,255,255,0.2)', fontFamily:'monospace', fontSize:12, padding:'8px 0' }}> <div style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:12,padding:'6px 0'}}>
Noch keine Positionen. Archiv-Einträge hinzufügen. Noch keine Positionen.
</div> </div>
)} )}
{order.items.map(item => ( {order.items.map(item => (
<div key={item.id} style={{ borderBottom:'1px solid rgba(255,255,255,0.05)', paddingBottom:10, marginBottom:10 }}> <div key={item.id} style={{borderBottom:'1px solid rgba(255,255,255,0.05)',paddingBottom:12,marginBottom:12}}>
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:6 }}> {/* Name + Status + Löschen */}
<span style={{ color:'#fff', fontFamily:'monospace', fontSize:13 }}>{item.calc_name}</span> <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
<button onClick={() => removeItem(item.id)} style={{ background:'transparent', border:'none', cursor:'pointer', padding:'0 2px' }}> <span style={{color:'#fff',fontFamily:'monospace',fontSize:13,flex:1}}>{item.calc_name}</span>
<XIcon size={15} color="rgba(255,255,255,0.3)"/> <div style={{display:'flex',gap:6,alignItems:'center'}}>
</button> <StatusBadge status={item.status||'warteliste'} onClick={()=>cycleItemStatus(item)} small/>
<button onClick={()=>removeItem(item.id)} style={{background:'transparent',border:'none',cursor:'pointer',padding:2}}>
<XIcon size={14} color="rgba(255,255,255,0.3)"/>
</button>
</div>
</div> </div>
<div style={{ display:'flex', alignItems:'center', gap:12 }}>
{/* Stückzahl */} {/* Stückzahl + Festpreis */}
<div style={{ display:'flex', alignItems:'center', gap:0, background:'rgba(255,255,255,0.05)', borderRadius:7, border:'1px solid rgba(255,255,255,0.1)', overflow:'hidden' }}> <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:8,marginBottom:8}}>
<button onClick={() => updateQty(item.id, item.stueckzahl-1)} style={{ padding:'5px 10px', background:'transparent', border:'none', color:'rgba(255,255,255,0.6)', fontSize:16, cursor:'pointer' }}></button> <div>
<span style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, minWidth:28, textAlign:'center' }}>{item.stueckzahl}</span> <div style={{...S.head,marginBottom:4}}>STÜCKZAHL</div>
<button onClick={() => updateQty(item.id, item.stueckzahl+1)} style={{ padding:'5px 10px', background:'transparent', border:'none', color:'rgba(255,255,255,0.6)', fontSize:16, cursor:'pointer' }}>+</button> <div style={{display:'flex',alignItems:'center',background:'rgba(255,255,255,0.05)',borderRadius:7,border:'1px solid rgba(255,255,255,0.1)',overflow:'hidden'}}>
<button onClick={()=>item.stueckzahl>1&&updateItem(item.id,{stueckzahl:item.stueckzahl-1})}
style={{padding:'6px 10px',background:'transparent',border:'none',color:'rgba(255,255,255,0.6)',fontSize:18,cursor:'pointer'}}></button>
<span style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:14,flex:1,textAlign:'center'}}>{item.stueckzahl}</span>
<button onClick={()=>updateItem(item.id,{stueckzahl:item.stueckzahl+1})}
style={{padding:'6px 10px',background:'transparent',border:'none',color:'rgba(255,255,255,0.6)',fontSize:18,cursor:'pointer'}}>+</button>
</div>
</div> </div>
<span style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:10 }}>× Stk.</span> <div>
{/* Preise */} <div style={{...S.head,marginBottom:4}}>FESTPREIS/STK ()</div>
<div style={{ display:'flex', gap:8, marginLeft:'auto' }}> <input type="number" step="0.01" min="0"
{PREISE.map(p => ( defaultValue={item.custom_price??''}
<div key={p.key} style={{ textAlign:'right' }}> placeholder=""
<div style={{ fontSize:9, color:'rgba(255,255,255,0.25)', fontFamily:'monospace' }}>{p.emoji}</div> onBlur={e=>{
<div style={{ color:p.c, fontFamily:"'Space Mono',monospace", fontSize:11, fontWeight:700 }}> const v = e.target.value==='' ? null : parseFloat(e.target.value);
{(item[`preis_${p.key}`] * item.stueckzahl).toFixed(2)} updateItem(item.id, {custom_price:v});
</div> }}
style={{...S.inp,fontSize:14,padding:'8px 10px'}}/>
</div>
</div>
{/* Preiszeile */}
<div style={{display:'flex',gap:8,flexWrap:'wrap'}}>
{PREISE.map(p=>(
<div key={p.key} style={{textAlign:'center',minWidth:60}}>
<div style={{fontSize:10}}>{p.emoji}</div>
<div style={{color:p.c,fontFamily:"'Space Mono',monospace",fontSize:11,fontWeight:700}}>
{(item[`preis_${p.key}`]*item.stueckzahl).toFixed(2)}
</div> </div>
))} </div>
</div> ))}
{item.custom_price!=null && (
<div style={{textAlign:'center',minWidth:60,marginLeft:'auto'}}>
<div style={{color:'rgba(255,255,255,0.3)',fontSize:9,fontFamily:'monospace'}}>FEST</div>
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:12,fontWeight:700}}>
{(item.custom_price*item.stueckzahl).toFixed(2)}
</div>
</div>
)}
</div> </div>
</div> </div>
))} ))}
</div> </div>
{/* Summen */} {/* Summen */}
<div style={{ ...S.card, marginBottom:10 }}> {order.items.length > 0 && (
<div style={S.head}>GESAMTSUMME</div> <div style={{...S.card,marginBottom:10}}>
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:8, marginBottom: order.custom_price ? 12 : 0 }}> <div style={S.head}>GESAMTSUMME</div>
{PREISE.map(p => ( <div style={{display:'grid',gridTemplateColumns:'1fr 1fr 1fr',gap:8,marginBottom:totals.has_any_custom||order.custom_price?12:0}}>
<div key={p.key} style={{ background:`${p.c}10`, border:`1px solid ${p.c}30`, borderRadius:9, padding:'10px 8px', textAlign:'center' }}> {PREISE.map(p=>(
<div style={{ fontSize:16, marginBottom:3 }}>{p.emoji}</div> <div key={p.key} style={{background:`${p.c}10`,border:`1px solid ${p.c}30`,borderRadius:9,padding:'10px 8px',textAlign:'center'}}>
<div style={{ color:'rgba(255,255,255,0.35)', fontSize:9, fontFamily:'monospace', marginBottom:3 }}>{p.label}</div> <div style={{fontSize:16,marginBottom:2}}>{p.emoji}</div>
<div style={{ color:p.c, fontFamily:"'Space Mono',monospace", fontSize:16, fontWeight:700 }}> <div style={{color:'rgba(255,255,255,0.35)',fontSize:9,fontFamily:'monospace',marginBottom:3}}>{p.label}</div>
{totals[p.key].toFixed(2)} <div style={{color:p.c,fontFamily:"'Space Mono',monospace",fontSize:16,fontWeight:700}}>
{totals[p.key].toFixed(2)}
</div>
</div> </div>
</div> ))}
))}
</div>
{order.custom_price != null && (
<div style={{ marginTop:10, padding:'10px 14px', background:'rgba(255,255,255,0.04)', border:'1px solid rgba(255,255,255,0.1)', borderRadius:8, display:'flex', justifyContent:'space-between', alignItems:'center' }}>
<span style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:12 }}>Festpreis</span>
<span style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:18, fontWeight:700 }}>
{parseFloat(order.custom_price).toFixed(2)}
</span>
</div> </div>
)} {totals.has_any_custom && (
</div> <div style={{padding:'10px 14px',background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.1)',borderRadius:8,display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:12}}> Positions-Festpreise</span>
<span style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:18,fontWeight:700}}>
{totals.custom_sum.toFixed(2)}
</span>
</div>
)}
{order.custom_price != null && (
<div style={{padding:'10px 14px',background:'rgba(255,255,255,0.06)',border:'1px solid rgba(255,255,255,0.15)',borderRadius:8,display:'flex',justifyContent:'space-between',alignItems:'center'}}>
<span style={{color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:12}}>Gesamt-Festpreis</span>
<span style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:20,fontWeight:700}}>
{parseFloat(order.custom_price).toFixed(2)}
</span>
</div>
)}
</div>
)}
{/* Archiv-Picker Modal */} {/* Archiv-Picker */}
{showPicker && ( {showPicker && (
<div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.8)', zIndex:5000, <div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.8)',zIndex:5000,
display:'flex', alignItems:'flex-end', justifyContent:'center' }} display:'flex',alignItems:'flex-end',justifyContent:'center'}}
onClick={e => e.target===e.currentTarget && setShowPicker(false)}> onClick={e=>e.target===e.currentTarget&&setShowPicker(false)}>
<div style={{ background:'#1a1a1e', borderRadius:'16px 16px 0 0', width:'100%', maxWidth:600, <div style={{background:'#1a1a1e',borderRadius:'16px 16px 0 0',width:'100%',maxWidth:600,
maxHeight:'75vh', display:'flex', flexDirection:'column', maxHeight:'75vh',display:'flex',flexDirection:'column',border:'1px solid rgba(255,255,255,0.1)'}}>
border:'1px solid rgba(255,255,255,0.1)' }}> <div style={{padding:'16px 16px 10px'}}>
<div style={{ padding:'16px 16px 10px' }}> <div style={{width:36,height:4,background:'rgba(255,255,255,0.15)',borderRadius:2,margin:'0 auto 14px'}}/>
<div style={{ width:36, height:4, background:'rgba(255,255,255,0.15)', borderRadius:2, margin:'0 auto 14px' }}/> <div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:14,fontWeight:700,marginBottom:10}}>Archiv-Eintrag wählen</div>
<div style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:14, fontWeight:700, marginBottom:10 }}> <div style={{position:'relative'}}>
Archiv-Eintrag wählen <span style={{position:'absolute',left:10,top:'50%',transform:'translateY(-50%)'}}>
</div>
<div style={{ position:'relative' }}>
<span style={{ position:'absolute', left:10, top:'50%', transform:'translateY(-50%)' }}>
<SearchIcon size={14} color="rgba(255,255,255,0.3)"/> <SearchIcon size={14} color="rgba(255,255,255,0.3)"/>
</span> </span>
<input value={pickerSearch} onChange={e=>setPickerSearch(e.target.value)} <input value={pickerSearch} onChange={e=>setPickerSearch(e.target.value)}
placeholder="Suchen…" placeholder="Suchen…" style={{...S.inp,paddingLeft:32,fontSize:14}} autoFocus/>
style={{ ...S.inp, paddingLeft:32, fontSize:14 }} autoFocus />
</div> </div>
</div> </div>
<div style={{ overflowY:'auto', flex:1, padding:'0 8px 16px' }}> <div style={{overflowY:'auto',flex:1,padding:'0 8px 8px'}}>
{filteredArchiv.length === 0 && ( {filteredArchiv.map(a=>(
<div style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:12, padding:'12px 8px' }}>Keine Treffer.</div> <button key={a.id} onClick={()=>addItem(a.id)} style={{
)} width:'100%',padding:'12px',background:'transparent',border:'none',
{filteredArchiv.map(a => ( borderBottom:'1px solid rgba(255,255,255,0.05)',
<button key={a.id} onClick={() => addItem(a.id)} style={{ display:'flex',justifyContent:'space-between',alignItems:'center',cursor:'pointer',
width:'100%', padding:'12px', background:'transparent',
border:'none', borderBottom:'1px solid rgba(255,255,255,0.05)',
display:'flex', justifyContent:'space-between', alignItems:'center', cursor:'pointer',
}}> }}>
<div style={{ textAlign:'left' }}> <div style={{textAlign:'left'}}>
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:13 }}>{a.name}</div> <div style={{color:'#fff',fontFamily:'monospace',fontSize:13}}>{a.name}</div>
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:10, marginTop:2 }}> <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,marginTop:2}}>{a.gramm}g · {a.stunden}h</div>
{a.gramm}g · {a.stunden}h
</div>
</div> </div>
<div style={{ display:'flex', gap:8 }}> <div style={{display:'flex',gap:8}}>
{[['💖',a.preis_freundschaft,'#ff6b9d'],['🤝',a.preis_normal,'#4ecdc4'],['💼',a.preis_auftrag,'#ffe66d']].map(([e,v,c])=>( {PREISE.map(p=>(
<div key={e} style={{ textAlign:'right' }}> <div key={p.key} style={{textAlign:'right'}}>
<div style={{ fontSize:10 }}>{e}</div> <div style={{fontSize:10}}>{p.emoji}</div>
<div style={{ color:c, fontFamily:"'Space Mono',monospace", fontSize:11 }}>{v.toFixed(2)}</div> <div style={{color:p.c,fontFamily:"'Space Mono',monospace",fontSize:11}}>{a[`preis_${p.key}`].toFixed(2)}</div>
</div> </div>
))} ))}
</div> </div>
</button> </button>
))} ))}
{filteredArchiv.length===0 && <div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:12,padding:'12px 8px'}}>Keine Treffer.</div>}
</div> </div>
<div style={{ padding:'8px 16px 24px' }}> <div style={{padding:'8px 16px 24px'}}>
<button onClick={() => setShowPicker(false)} style={{ ...S.btn('#ff6b9d'), width:'100%', textAlign:'center', padding:'10px 0' }}>Schließen</button> <button onClick={()=>setShowPicker(false)} style={{...S.btn('#ff6b9d'),width:'100%',textAlign:'center',padding:'10px 0'}}>Schließen</button>
</div> </div>
</div> </div>
</div> </div>
@@ -302,22 +331,21 @@ function BestellungDetail({ id, toast, onBack, onSaved }) {
); );
} }
// ── Bestellungen Übersicht ──────────────────────────────────────────────────── // ── Übersicht ─────────────────────────────────────────────────────────────────
export default function Bestellungen({ toast, mobile }) { export default function Bestellungen({ toast, mobile }) {
const [orders, setOrders] = useState([]); const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [filterStatus, setFilter] = useState('alle'); const [filterStatus,setFilter] = useState('alle');
const [openId, setOpenId] = useState(null); const [openId, setOpenId] = useState(null);
const [delId, setDelId] = useState(null); const [delId, setDelId] = useState(null);
const [newMode, setNewMode] = useState(false); const [newMode, setNewMode] = useState(false);
const load = () => { const load = () => {
setLoading(true); setLoading(true);
api('/tools/bestellungen').then(setOrders).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false)); api('/tools/bestellungen').then(setOrders).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false));
}; };
useEffect(()=>{ load(); },[]);
useEffect(() => { load(); }, []);
const del = async id => { const del = async id => {
try { await api(`/tools/bestellungen/${id}`,{method:'DELETE'}); setOrders(p=>p.filter(o=>o.id!==id)); toast('Gelöscht'); } try { await api(`/tools/bestellungen/${id}`,{method:'DELETE'}); setOrders(p=>p.filter(o=>o.id!==id)); toast('Gelöscht'); }
@@ -325,94 +353,117 @@ export default function Bestellungen({ toast, mobile }) {
}; };
const filtered = orders.filter(o => { const filtered = orders.filter(o => {
const matchSearch = !search.trim() || const ms = !search.trim() || o.name.toLowerCase().includes(search.toLowerCase());
o.name.toLowerCase().includes(search.toLowerCase()); const mf = filterStatus==='alle' || o.status===filterStatus;
const matchStatus = filterStatus === 'alle' || o.status === filterStatus; return ms && mf;
return matchSearch && matchStatus;
}); });
// Detail-Ansicht
if (openId !== null || newMode) { if (openId !== null || newMode) {
return ( return (
<BestellungDetail <BestellungDetail
id={newMode ? null : openId} id={newMode ? null : openId}
toast={toast} toast={toast}
onBack={() => { setOpenId(null); setNewMode(false); load(); }} onBack={savedId => { setNewMode(false); if (savedId) setOpenId(savedId); else { setOpenId(null); load(); } }}
onSaved={id => { setNewMode(false); setOpenId(id); load(); }}
/> />
); );
} }
return ( return (
<div style={{ padding: mobile ? '14px 14px 90px' : '36px 44px', maxWidth:860 }}> <div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:860}}>
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:14 }}> <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}>
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile?18:22, margin:0 }}>Bestellungen</h1> <h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?18:22,margin:0}}>Bestellungen</h1>
<button onClick={() => setNewMode(true)} style={{ ...S.btn('#4ecdc4'), display:'flex', alignItems:'center', gap:6 }}> <button onClick={()=>{setNewMode(true);setOpenId(null);}} style={{...S.btn('#4ecdc4'),display:'flex',alignItems:'center',gap:6}}>
<PlusIcon size={14} color="#4ecdc4"/> Neu <PlusIcon size={14} color="#4ecdc4"/> Neu
</button> </button>
</div> </div>
{/* Suche */} {/* Suche */}
<div style={{ position:'relative', marginBottom:10 }}> <div style={{position:'relative',marginBottom:10}}>
<span style={{ position:'absolute', left:12, top:'50%', transform:'translateY(-50%)' }}> <span style={{position:'absolute',left:12,top:'50%',transform:'translateY(-50%)'}}>
<SearchIcon size={14} color="rgba(255,255,255,0.3)"/> <SearchIcon size={14} color="rgba(255,255,255,0.3)"/>
</span> </span>
<input value={search} onChange={e=>setSearch(e.target.value)} <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Name suchen…"
placeholder="Name suchen…" style={{...S.inp,paddingLeft:34,fontSize:15}}/>
style={{ ...S.inp, paddingLeft:34, fontSize:15 }} /> {search && <button onClick={()=>setSearch('')} style={{position:'absolute',right:10,top:'50%',transform:'translateY(-50%)',background:'transparent',border:'none',color:'rgba(255,255,255,0.3)',cursor:'pointer',fontSize:16}}></button>}
{search && <button onClick={()=>setSearch('')} style={{ position:'absolute', right:10, top:'50%', transform:'translateY(-50%)', background:'transparent', border:'none', color:'rgba(255,255,255,0.3)', cursor:'pointer', fontSize:16 }}></button>}
</div> </div>
{/* Status-Filter */} {/* Status-Filter */}
<div style={{ display:'flex', gap:6, marginBottom:14, flexWrap:'wrap' }}> <div style={{display:'flex',gap:6,marginBottom:14,flexWrap:'wrap'}}>
{[['alle','Alle','rgba(255,255,255,0.6)','rgba(255,255,255,0.12)'], ...Object.entries(STATUS).map(([k,v])=>[k,v.label,v.color,v.bg])].map(([k,l,c,bg]) => ( {[['alle','Alle','rgba(255,255,255,0.6)','rgba(255,255,255,0.1)'],...Object.entries(STATUS).map(([k,v])=>[k,v.label,v.color,v.bg])].map(([k,l,c,bg])=>(
<button key={k} onClick={() => setFilter(k)} style={{ <button key={k} onClick={()=>setFilter(k)} style={{
padding:'5px 12px', borderRadius:20, border:`1px solid ${filterStatus===k?c:'rgba(255,255,255,0.1)'}`, padding:'5px 12px',borderRadius:20,border:`1px solid ${filterStatus===k?c:'rgba(255,255,255,0.1)'}`,
background: filterStatus===k ? bg : 'transparent', background:filterStatus===k?bg:'transparent',
color: filterStatus===k ? c : 'rgba(255,255,255,0.4)', color:filterStatus===k?c:'rgba(255,255,255,0.4)',
fontFamily:'monospace', fontSize:11, cursor:'pointer', fontFamily:'monospace',fontSize:11,cursor:'pointer',
}}>{l}</button> }}>{l}</button>
))} ))}
</div> </div>
<p style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:10, marginBottom:12 }}> <p style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:10,marginBottom:12}}>
{filtered.length} von {orders.length} Bestellungen {filtered.length} von {orders.length} Bestellungen
</p> </p>
{loading && <div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace' }}>Lädt</div>} {loading && <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace'}}>Lädt</div>}
{!loading && orders.length === 0 && ( {!loading && orders.length===0 && (
<div style={{ ...S.card, textAlign:'center', padding:40 }}> <div style={{...S.card,textAlign:'center',padding:40}}>
<div style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:12 }}>Noch keine Bestellungen.</div> <div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:12}}>Noch keine Bestellungen.</div>
</div> </div>
)} )}
{filtered.map(order => ( {filtered.map(order => {
<div key={order.id} style={{ ...S.card, marginBottom:8, padding:'12px 14px' }}> const done = order.items_done || 0;
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:8 }}> const total = order.item_count || 0;
<button onClick={() => setOpenId(order.id)} style={{ background:'transparent', border:'none', cursor:'pointer', textAlign:'left', padding:0 }}> const customSum = order.custom_price_sum || 0;
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:14, fontWeight:700 }}>{order.name}</div> const allDone = total > 0 && done === total;
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:10, marginTop:2 }}> return (
{order.item_count} Position{order.item_count !== 1 ? 'en' : ''} · {new Date(order.created_at).toLocaleDateString('de-DE')} <div key={order.id} style={{...S.card,marginBottom:8,padding:'12px 14px'}}>
</div> <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',marginBottom:6}}>
</button> <button onClick={()=>setOpenId(order.id)} style={{background:'transparent',border:'none',cursor:'pointer',textAlign:'left',padding:0,flex:1}}>
<StatusBadge status={order.status} small/> <div style={{color:'#fff',fontFamily:'monospace',fontSize:14,fontWeight:700}}>{order.name}</div>
</div> <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
<div style={{ display:'flex', gap:6, justifyContent:'flex-end' }}> {new Date(order.created_at).toLocaleDateString('de-DE')}
<button onClick={() => setOpenId(order.id)} style={S.btn('#4ecdc4',true)}>Öffnen</button> </div>
{delId === order.id ? (
<div style={{ display:'flex', gap:5, alignItems:'center' }}>
<button onClick={() => del(order.id)} style={S.btn('#ff6b9d',true)}> Ja</button>
<button onClick={() => setDelId(null)} style={S.btn('rgba(255,255,255,0.4)',true)}>Nein</button>
</div>
) : (
<button onClick={() => setDelId(order.id)} style={S.btn('#ff6b9d',true)}>
<TrashIcon size={13} color="#ff6b9d"/>
</button> </button>
)} <StatusBadge status={order.status} small/>
</div>
{/* Positions-Fortschritt + Festpreis-Summe */}
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
<div style={{display:'flex',alignItems:'center',gap:6}}>
<div style={{
fontSize:10,fontFamily:'monospace',
color: allDone?'#6bcb77':'rgba(255,255,255,0.4)',
background: allDone?'rgba(107,203,119,0.12)':'rgba(255,255,255,0.05)',
border:`1px solid ${allDone?'rgba(107,203,119,0.3)':'rgba(255,255,255,0.08)'}`,
borderRadius:12,padding:'2px 8px',
}}>
{done}/{total} fertig
</div>
</div>
{customSum > 0 && (
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:"'Space Mono',monospace",fontSize:12}}>
<span style={{color:'#fff',fontWeight:700}}>{customSum.toFixed(2)}</span>
</div>
)}
</div>
<div style={{display:'flex',gap:6,justifyContent:'flex-end'}}>
<button onClick={()=>setOpenId(order.id)} style={{...S.btn('#4ecdc4',true),flex:1,textAlign:'center'}}>Öffnen</button>
{delId===order.id?(
<div style={{display:'flex',gap:5,alignItems:'center'}}>
<button onClick={()=>del(order.id)} style={S.btn('#ff6b9d',true)}> Ja</button>
<button onClick={()=>setDelId(null)} style={S.btn('rgba(255,255,255,0.4)',true)}>Nein</button>
</div>
):(
<button onClick={()=>setDelId(order.id)} style={S.btn('#ff6b9d',true)}>
<TrashIcon size={13} color="#ff6b9d"/>
</button>
)}
</div>
</div> </div>
</div> );
))} })}
</div> </div>
); );
} }