Dateien nach "frontend/src/tools" hochladen
This commit is contained in:
418
frontend/src/tools/bestellungen.jsx
Normal file
418
frontend/src/tools/bestellungen.jsx
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { api, S } from '../lib.js';
|
||||||
|
import { SearchIcon, PlusIcon, XIcon, EditIcon, TrashIcon, CheckIcon, 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 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 / Bearbeiten ────────────────────────────────────────────
|
||||||
|
function BestellungDetail({ id, toast, onBack, onSaved }) {
|
||||||
|
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); // neue Bestellung direkt im Edit-Mode
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
}, [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 updated = await api(`/tools/bestellungen/${id}`, { method:'PUT', body:payload });
|
||||||
|
setOrder(updated); setEditMode(false); toast('Gespeichert');
|
||||||
|
} else {
|
||||||
|
const created = await api('/tools/bestellungen', { body:payload });
|
||||||
|
toast('Bestellung angelegt');
|
||||||
|
onSaved(created.id);
|
||||||
|
}
|
||||||
|
} catch(e) { toast(e.message,'error'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const addItem = async calcId => {
|
||||||
|
try {
|
||||||
|
const updated = await api(`/tools/bestellungen/${id}/items`, { body:{ calculation_id:calcId, stueckzahl:1 } });
|
||||||
|
setOrder(updated); setShowPicker(false); toast('Hinzugefügt');
|
||||||
|
} catch(e) { toast(e.message,'error'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateQty = async (itemId, qty) => {
|
||||||
|
if (qty < 1) return;
|
||||||
|
try { const u = await api(`/tools/bestellungen/${id}/items/${itemId}`, { method:'PUT', body:{ stueckzahl:qty } }); 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 cycleStatus = async () => {
|
||||||
|
const states = ['warteliste','in_arbeit','fertig'];
|
||||||
|
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); }
|
||||||
|
catch(e) { toast(e.message,'error'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
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:18 }}>
|
||||||
|
<button onClick={onBack} 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={cycleStatus}/>}
|
||||||
|
{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, Material…" rows={3}
|
||||||
|
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 }}>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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Nur bei bestehender Bestellung: Items + Summen */}
|
||||||
|
{order && !editMode && (
|
||||||
|
<>
|
||||||
|
{/* Bemerkung anzeigen */}
|
||||||
|
{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)} 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:'8px 0' }}>
|
||||||
|
Noch keine Positionen. Archiv-Einträge hinzufügen.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{order.items.map(item => (
|
||||||
|
<div key={item.id} style={{ borderBottom:'1px solid rgba(255,255,255,0.05)', paddingBottom:10, marginBottom:10 }}>
|
||||||
|
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:6 }}>
|
||||||
|
<span style={{ color:'#fff', fontFamily:'monospace', fontSize:13 }}>{item.calc_name}</span>
|
||||||
|
<button onClick={() => removeItem(item.id)} style={{ background:'transparent', border:'none', cursor:'pointer', padding:'0 2px' }}>
|
||||||
|
<XIcon size={15} color="rgba(255,255,255,0.3)"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display:'flex', alignItems:'center', gap:12 }}>
|
||||||
|
{/* Stückzahl */}
|
||||||
|
<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' }}>
|
||||||
|
<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>
|
||||||
|
<span style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, minWidth:28, textAlign:'center' }}>{item.stueckzahl}</span>
|
||||||
|
<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:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:10 }}>× Stk.</span>
|
||||||
|
{/* Preise */}
|
||||||
|
<div style={{ display:'flex', gap:8, marginLeft:'auto' }}>
|
||||||
|
{PREISE.map(p => (
|
||||||
|
<div key={p.key} style={{ textAlign:'right' }}>
|
||||||
|
<div style={{ fontSize:9, color:'rgba(255,255,255,0.25)', fontFamily:'monospace' }}>{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>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summen */}
|
||||||
|
<div style={{ ...S.card, marginBottom:10 }}>
|
||||||
|
<div style={S.head}>GESAMTSUMME</div>
|
||||||
|
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:8, marginBottom: 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:3 }}>{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>
|
||||||
|
{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>
|
||||||
|
|
||||||
|
{/* Archiv-Picker Modal */}
|
||||||
|
{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 16px' }}>
|
||||||
|
{filteredArchiv.length === 0 && (
|
||||||
|
<div style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:12, padding:'12px 8px' }}>Keine Treffer.</div>
|
||||||
|
)}
|
||||||
|
{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 }}>
|
||||||
|
{[['💖',a.preis_freundschaft,'#ff6b9d'],['🤝',a.preis_normal,'#4ecdc4'],['💼',a.preis_auftrag,'#ffe66d']].map(([e,v,c])=>(
|
||||||
|
<div key={e} style={{ textAlign:'right' }}>
|
||||||
|
<div style={{ fontSize:10 }}>{e}</div>
|
||||||
|
<div style={{ color:c, fontFamily:"'Space Mono',monospace", fontSize:11 }}>{v.toFixed(2)}€</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bestellungen Ü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 matchSearch = !search.trim() ||
|
||||||
|
o.name.toLowerCase().includes(search.toLowerCase());
|
||||||
|
const matchStatus = filterStatus === 'alle' || o.status === filterStatus;
|
||||||
|
return matchSearch && matchStatus;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detail-Ansicht
|
||||||
|
if (openId !== null || newMode) {
|
||||||
|
return (
|
||||||
|
<BestellungDetail
|
||||||
|
id={newMode ? null : openId}
|
||||||
|
toast={toast}
|
||||||
|
onBack={() => { setOpenId(null); setNewMode(false); load(); }}
|
||||||
|
onSaved={id => { setNewMode(false); setOpenId(id); 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)} 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.12)'], ...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 => (
|
||||||
|
<div key={order.id} style={{ ...S.card, marginBottom:8, padding:'12px 14px' }}>
|
||||||
|
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:8 }}>
|
||||||
|
<button onClick={() => setOpenId(order.id)} style={{ background:'transparent', border:'none', cursor:'pointer', textAlign:'left', padding:0 }}>
|
||||||
|
<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 }}>
|
||||||
|
{order.item_count} Position{order.item_count !== 1 ? 'en' : ''} · {new Date(order.created_at).toLocaleDateString('de-DE')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<StatusBadge status={order.status} small/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display:'flex', gap:6, justifyContent:'flex-end' }}>
|
||||||
|
<button onClick={() => setOpenId(order.id)} style={S.btn('#4ecdc4',true)}>Ö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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user