504 lines
27 KiB
JavaScript
504 lines
27 KiB
JavaScript
import { useState, useEffect, useMemo } from 'react';
|
||
import { api, S } from '../lib.js';
|
||
import { SearchIcon, PlusIcon, XIcon, EditIcon, TrashIcon, ChevronIcon } from '../icons.jsx';
|
||
|
||
const STATUS = {
|
||
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)' },
|
||
fertig: { label:'Fertig', color:'#6bcb77', bg:'rgba(107,203,119,0.12)' },
|
||
};
|
||
const STATUS_KEYS = ['warteliste','in_arbeit','fertig'];
|
||
|
||
const PREISE = [
|
||
{ key:'freundschaft', emoji:'💖', label:'Freundschaft', c:'#ff6b9d' },
|
||
{ key:'normal', emoji:'🤝', label:'Normal', c:'#4ecdc4' },
|
||
{ key:'auftrag', emoji:'💼', label:'Auftrag', c:'#ffe66d' },
|
||
];
|
||
|
||
function StatusBadge({ status, onClick, small }) {
|
||
const s = STATUS[status] || STATUS.warteliste;
|
||
return (
|
||
<button onClick={onClick} style={{
|
||
background:s.bg, border:`1px solid ${s.color}44`,
|
||
borderRadius:20, padding:small?'3px 10px':'5px 14px',
|
||
color:s.color, fontFamily:'monospace', fontSize:small?10:11,
|
||
cursor:onClick?'pointer':'default', whiteSpace:'nowrap',
|
||
}}>{s.label}</button>
|
||
);
|
||
}
|
||
|
||
// ── Bestellung Detail ─────────────────────────────────────────────────────────
|
||
function BestellungDetail({ id, toast, onBack }) {
|
||
const [order, setOrder] = useState(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [archiv, setArchiv] = useState([]);
|
||
const [showPicker, setShowPicker] = useState(false);
|
||
const [pickerSearch,setPickerSearch]= useState('');
|
||
const [editMode, setEditMode] = useState(!id);
|
||
const [form, setForm] = useState({ name:'', bemerkung:'', custom_price:'', status:'warteliste' });
|
||
|
||
useEffect(() => {
|
||
api('/tools/kalkulator3d').then(setArchiv).catch(()=>{});
|
||
if (id) {
|
||
api(`/tools/bestellungen/${id}`)
|
||
.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'))
|
||
.finally(() => setLoading(false));
|
||
} else { setLoading(false); }
|
||
}, [id]);
|
||
|
||
const totals = useMemo(() => {
|
||
const items = order?.items || [];
|
||
return {
|
||
freundschaft: items.reduce((s,i) => s + i.preis_freundschaft * 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),
|
||
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]);
|
||
|
||
const saveOrder = async () => {
|
||
if (!form.name.trim()) { toast('Name erforderlich','error'); return; }
|
||
try {
|
||
const payload = { ...form, custom_price: form.custom_price!=='' ? parseFloat(form.custom_price) : null };
|
||
if (id) {
|
||
const u = await api(`/tools/bestellungen/${id}`,{method:'PUT',body:payload});
|
||
setOrder(u); setEditMode(false); toast('Gespeichert');
|
||
} else {
|
||
const created = await api('/tools/bestellungen',{body:payload});
|
||
toast('Bestellung angelegt');
|
||
onBack(created.id);
|
||
}
|
||
} catch(e) { toast(e.message,'error'); }
|
||
};
|
||
|
||
const addItem = async calcId => {
|
||
try { const u = await api(`/tools/bestellungen/${id}/items`,{body:{calculation_id:calcId,stueckzahl:1}}); setOrder(u); setShowPicker(false); toast('Hinzugefügt'); }
|
||
catch(e) { toast(e.message,'error'); }
|
||
};
|
||
|
||
const updateItem = async (itemId, patch) => {
|
||
try { const u = await api(`/tools/bestellungen/${id}/items/${itemId}`,{method:'PUT',body:patch}); setOrder(u); }
|
||
catch(e) { toast(e.message,'error'); }
|
||
};
|
||
|
||
const removeItem = async itemId => {
|
||
try { const u = await api(`/tools/bestellungen/${id}/items/${itemId}`,{method:'DELETE'}); setOrder(u); toast('Entfernt'); }
|
||
catch(e) { toast(e.message,'error'); }
|
||
};
|
||
|
||
const cycleOrderStatus = async () => {
|
||
const next = STATUS_KEYS[(STATUS_KEYS.indexOf(order.status)+1) % STATUS_KEYS.length];
|
||
try { const u = await api(`/tools/bestellungen/${id}`,{method:'PUT',body:{...order,status:next}}); setOrder(u); }
|
||
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
|
||
? archiv.filter(a => a.name.toLowerCase().includes(pickerSearch.toLowerCase()))
|
||
: archiv;
|
||
|
||
if (loading) return <div style={{padding:24,color:'rgba(255,255,255,0.3)',fontFamily:'monospace'}}>Lädt…</div>;
|
||
|
||
return (
|
||
<div style={{ padding:'14px 14px 90px', maxWidth:860 }}>
|
||
{/* Header */}
|
||
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:16 }}>
|
||
<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"/>
|
||
</button>
|
||
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:18, flex:1, margin:0 }}>
|
||
{id ? (editMode?'Bearbeiten':order?.name) : 'Neue Bestellung'}
|
||
</h1>
|
||
{order && !editMode && <StatusBadge status={order.status} onClick={cycleOrderStatus}/>}
|
||
{order && !editMode && (
|
||
<button onClick={()=>setEditMode(true)} style={{background:'transparent',border:'none',cursor:'pointer',padding:4}}>
|
||
<EditIcon size={18} color="rgba(255,255,255,0.5)"/>
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Formular */}
|
||
{(editMode || !id) && (
|
||
<div style={{...S.card,marginBottom:12}}>
|
||
<div style={{marginBottom:10}}>
|
||
<label style={{...S.head,display:'block',marginBottom:4}}>NAME / PERSON</label>
|
||
<input value={form.name} onChange={e=>setForm(f=>({...f,name:e.target.value}))}
|
||
placeholder="z.B. Max Mustermann" style={{...S.inp,fontSize:15}}/>
|
||
</div>
|
||
<div style={{marginBottom:10}}>
|
||
<label style={{...S.head,display:'block',marginBottom:4}}>BEMERKUNG</label>
|
||
<textarea value={form.bemerkung} onChange={e=>setForm(f=>({...f,bemerkung:e.target.value}))}
|
||
placeholder="Hinweise, Lieferung…" rows={2}
|
||
style={{...S.inp,resize:'vertical',lineHeight:1.6,fontSize:14,padding:'9px 12px'}}/>
|
||
</div>
|
||
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10,marginBottom:12}}>
|
||
<div>
|
||
<label style={{...S.head,display:'block',marginBottom:4}}>STATUS</label>
|
||
<select value={form.status} onChange={e=>setForm(f=>({...f,status:e.target.value}))} style={{...S.inp,fontSize:14}}>
|
||
{Object.entries(STATUS).map(([k,v])=><option key={k} value={k} style={{background:'#1a1a1e'}}>{v.label}</option>)}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<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}))}
|
||
placeholder="Optional" step="0.01" style={{...S.inp,fontSize:15}}/>
|
||
</div>
|
||
</div>
|
||
<div style={{display:'flex',gap:8}}>
|
||
<button onClick={saveOrder} style={{...S.btn('#4ecdc4'),flex:1,textAlign:'center',padding:'10px 0'}}>
|
||
{id?'✓ Speichern':'+ Bestellung anlegen'}
|
||
</button>
|
||
{id && <button onClick={()=>setEditMode(false)} style={{...S.btn('#ff6b9d'),padding:'10px 14px'}}>Abbrechen</button>}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{order && !editMode && (
|
||
<>
|
||
{order.bemerkung && (
|
||
<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>
|
||
)}
|
||
|
||
{/* Positionen */}
|
||
<div style={{...S.card,marginBottom:10}}>
|
||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
|
||
<div style={S.head}>POSITIONEN</div>
|
||
<button onClick={()=>{setShowPicker(true);setPickerSearch('');}}
|
||
style={{...S.btn('#4ecdc4',true),display:'flex',alignItems:'center',gap:5}}>
|
||
<PlusIcon size={13} color="#4ecdc4"/> Hinzufügen
|
||
</button>
|
||
</div>
|
||
|
||
{order.items.length===0 && (
|
||
<div style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:12,padding:'6px 0'}}>
|
||
Noch keine Positionen.
|
||
</div>
|
||
)}
|
||
|
||
{order.items.map(item => (
|
||
<div key={item.id} style={{borderBottom:'1px solid rgba(255,255,255,0.05)',paddingBottom:12,marginBottom:12}}>
|
||
{/* Name + Status + Löschen */}
|
||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
|
||
<span style={{color:'#fff',fontFamily:'monospace',fontSize:13,flex:1}}>{item.calc_name}</span>
|
||
<div style={{display:'flex',gap:6,alignItems:'center'}}>
|
||
<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>
|
||
|
||
{/* Stückzahl gesamt + Festpreis */}
|
||
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:8,marginBottom:10}}>
|
||
<div>
|
||
<div style={{...S.head,marginBottom:4}}>STÜCKZAHL GESAMT</div>
|
||
<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>
|
||
<div style={{...S.head,marginBottom:4}}>FESTPREIS/STK (€)</div>
|
||
<input type="number" step="0.01" min="0"
|
||
defaultValue={item.custom_price??''}
|
||
placeholder="–"
|
||
onBlur={e=>{
|
||
const v = e.target.value==='' ? null : parseFloat(e.target.value);
|
||
updateItem(item.id, {custom_price:v});
|
||
}}
|
||
style={{...S.inp,fontSize:14,padding:'8px 10px'}}/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Status pro Stück */}
|
||
<div style={{marginBottom:10}}>
|
||
<div style={{...S.head,marginBottom:6}}>STATUS PRO STÜCK</div>
|
||
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr 1fr',gap:6}}>
|
||
{[
|
||
['warteliste','Warteliste','#ffe66d', item.qty_warteliste||0],
|
||
['in_arbeit', 'In Arbeit', '#4ecdc4', item.qty_in_arbeit||0],
|
||
['fertig', 'Fertig', '#6bcb77', item.qty_fertig||0],
|
||
].map(([key, label, color, qty]) => {
|
||
const others = item.stueckzahl - qty;
|
||
return (
|
||
<div key={key} style={{background:`${color}10`,border:`1px solid ${color}30`,borderRadius:8,padding:'8px 6px',textAlign:'center'}}>
|
||
<div style={{color:'rgba(255,255,255,0.35)',fontSize:9,fontFamily:'monospace',marginBottom:6}}>{label.toUpperCase()}</div>
|
||
<div style={{display:'flex',alignItems:'center',justifyContent:'center',gap:4}}>
|
||
<button
|
||
onClick={()=>qty>0&&updateItem(item.id,{[`qty_${key}`]:qty-1})}
|
||
style={{width:22,height:22,borderRadius:5,background:'rgba(0,0,0,0.3)',border:'none',color:qty>0?color:'rgba(255,255,255,0.15)',fontSize:16,cursor:qty>0?'pointer':'default',lineHeight:1}}>−</button>
|
||
<span style={{color,fontFamily:"'Space Mono',monospace",fontSize:16,fontWeight:700,minWidth:20,textAlign:'center'}}>{qty}</span>
|
||
<button
|
||
onClick={()=>{const sum=(item.qty_warteliste||0)+(item.qty_in_arbeit||0)+(item.qty_fertig||0);sum<item.stueckzahl&&updateItem(item.id,{[`qty_${key}`]:qty+1})}}
|
||
style={{width:22,height:22,borderRadius:5,background:'rgba(0,0,0,0.3)',border:'none',color:color,fontSize:16,cursor:'pointer',lineHeight:1}}>+</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
{/* Verbleibend ohne Status */}
|
||
{(() => { const assigned=(item.qty_warteliste||0)+(item.qty_in_arbeit||0)+(item.qty_fertig||0); return assigned<item.stueckzahl ? (
|
||
<div style={{marginTop:6,color:'rgba(255,230,109,0.7)',fontFamily:'monospace',fontSize:10,textAlign:'center'}}>
|
||
⚠ {item.stueckzahl-assigned} Stück noch keinem Status zugewiesen
|
||
</div>
|
||
) : null; })()}
|
||
</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>
|
||
))}
|
||
{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>
|
||
|
||
{/* Summen */}
|
||
{order.items.length > 0 && (
|
||
<div style={{...S.card,marginBottom:10}}>
|
||
<div style={S.head}>GESAMTSUMME</div>
|
||
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr 1fr',gap:8,marginBottom:totals.has_any_custom||order.custom_price?12:0}}>
|
||
{PREISE.map(p=>(
|
||
<div key={p.key} style={{background:`${p.c}10`,border:`1px solid ${p.c}30`,borderRadius:9,padding:'10px 8px',textAlign:'center'}}>
|
||
<div style={{fontSize:16,marginBottom:2}}>{p.emoji}</div>
|
||
<div style={{color:'rgba(255,255,255,0.35)',fontSize:9,fontFamily:'monospace',marginBottom:3}}>{p.label}</div>
|
||
<div style={{color:p.c,fontFamily:"'Space Mono',monospace",fontSize:16,fontWeight:700}}>
|
||
{totals[p.key].toFixed(2)}€
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{totals.has_any_custom && (
|
||
<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 */}
|
||
{showPicker && (
|
||
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.8)',zIndex:5000,
|
||
display:'flex',alignItems:'flex-end',justifyContent:'center'}}
|
||
onClick={e=>e.target===e.currentTarget&&setShowPicker(false)}>
|
||
<div style={{background:'#1a1a1e',borderRadius:'16px 16px 0 0',width:'100%',maxWidth:600,
|
||
maxHeight:'75vh',display:'flex',flexDirection:'column',border:'1px solid rgba(255,255,255,0.1)'}}>
|
||
<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={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:14,fontWeight:700,marginBottom:10}}>Archiv-Eintrag wählen</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)"/>
|
||
</span>
|
||
<input value={pickerSearch} onChange={e=>setPickerSearch(e.target.value)}
|
||
placeholder="Suchen…" style={{...S.inp,paddingLeft:32,fontSize:14}} autoFocus/>
|
||
</div>
|
||
</div>
|
||
<div style={{overflowY:'auto',flex:1,padding:'0 8px 8px'}}>
|
||
{filteredArchiv.map(a=>(
|
||
<button key={a.id} onClick={()=>addItem(a.id)} style={{
|
||
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={{color:'#fff',fontFamily:'monospace',fontSize:13}}>{a.name}</div>
|
||
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,marginTop:2}}>{a.gramm}g · {a.stunden}h</div>
|
||
</div>
|
||
<div style={{display:'flex',gap:8}}>
|
||
{PREISE.map(p=>(
|
||
<div key={p.key} style={{textAlign:'right'}}>
|
||
<div style={{fontSize:10}}>{p.emoji}</div>
|
||
<div style={{color:p.c,fontFamily:"'Space Mono',monospace",fontSize:11}}>{a[`preis_${p.key}`].toFixed(2)}€</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</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 style={{padding:'8px 16px 24px'}}>
|
||
<button onClick={()=>setShowPicker(false)} style={{...S.btn('#ff6b9d'),width:'100%',textAlign:'center',padding:'10px 0'}}>Schließen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Übersicht ─────────────────────────────────────────────────────────────────
|
||
export default function Bestellungen({ toast, mobile }) {
|
||
const [orders, setOrders] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [search, setSearch] = useState('');
|
||
const [filterStatus,setFilter] = useState('alle');
|
||
const [openId, setOpenId] = useState(null);
|
||
const [delId, setDelId] = useState(null);
|
||
const [newMode, setNewMode] = useState(false);
|
||
|
||
const load = () => {
|
||
setLoading(true);
|
||
api('/tools/bestellungen').then(setOrders).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false));
|
||
};
|
||
useEffect(()=>{ load(); },[]);
|
||
|
||
const del = async id => {
|
||
try { await api(`/tools/bestellungen/${id}`,{method:'DELETE'}); setOrders(p=>p.filter(o=>o.id!==id)); toast('Gelöscht'); }
|
||
catch(e) { toast(e.message,'error'); } finally { setDelId(null); }
|
||
};
|
||
|
||
const filtered = orders.filter(o => {
|
||
const ms = !search.trim() || o.name.toLowerCase().includes(search.toLowerCase());
|
||
const mf = filterStatus==='alle' || o.status===filterStatus;
|
||
return ms && mf;
|
||
});
|
||
|
||
if (openId !== null || newMode) {
|
||
return (
|
||
<BestellungDetail
|
||
id={newMode ? null : openId}
|
||
toast={toast}
|
||
onBack={savedId => { setNewMode(false); if (savedId) setOpenId(savedId); else { setOpenId(null); load(); } }}
|
||
/>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:860}}>
|
||
<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>
|
||
<button onClick={()=>{setNewMode(true);setOpenId(null);}} style={{...S.btn('#4ecdc4'),display:'flex',alignItems:'center',gap:6}}>
|
||
<PlusIcon size={14} color="#4ecdc4"/> Neu
|
||
</button>
|
||
</div>
|
||
|
||
{/* Suche */}
|
||
<div style={{position:'relative',marginBottom:10}}>
|
||
<span style={{position:'absolute',left:12,top:'50%',transform:'translateY(-50%)'}}>
|
||
<SearchIcon size={14} color="rgba(255,255,255,0.3)"/>
|
||
</span>
|
||
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Name suchen…"
|
||
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>}
|
||
</div>
|
||
|
||
{/* Status-Filter */}
|
||
<div style={{display:'flex',gap:6,marginBottom:14,flexWrap:'wrap'}}>
|
||
{[['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={{
|
||
padding:'5px 12px',borderRadius:20,border:`1px solid ${filterStatus===k?c:'rgba(255,255,255,0.1)'}`,
|
||
background:filterStatus===k?bg:'transparent',
|
||
color:filterStatus===k?c:'rgba(255,255,255,0.4)',
|
||
fontFamily:'monospace',fontSize:11,cursor:'pointer',
|
||
}}>{l}</button>
|
||
))}
|
||
</div>
|
||
|
||
<p style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:10,marginBottom:12}}>
|
||
{filtered.length} von {orders.length} Bestellungen
|
||
</p>
|
||
|
||
{loading && <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace'}}>Lädt…</div>}
|
||
|
||
{!loading && orders.length===0 && (
|
||
<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>
|
||
)}
|
||
|
||
{filtered.map(order => {
|
||
const done = order.qty_fertig_total || 0;
|
||
const total = order.qty_total || 0;
|
||
const customSum = order.custom_price_sum || 0;
|
||
const allDone = total > 0 && done === total;
|
||
return (
|
||
<div key={order.id} style={{...S.card,marginBottom:8,padding:'12px 14px'}}>
|
||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',marginBottom:6}}>
|
||
<button onClick={()=>setOpenId(order.id)} style={{background:'transparent',border:'none',cursor:'pointer',textAlign:'left',padding:0,flex:1}}>
|
||
<div style={{color:'#fff',fontFamily:'monospace',fontSize:14,fontWeight:700}}>{order.name}</div>
|
||
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
|
||
{new Date(order.created_at).toLocaleDateString('de-DE')}
|
||
</div>
|
||
</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>
|
||
);
|
||
}
|