diff --git a/frontend/src/tools/bestellungen.jsx b/frontend/src/tools/bestellungen.jsx
new file mode 100644
index 0000000..1e6ec87
--- /dev/null
+++ b/frontend/src/tools/bestellungen.jsx
@@ -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 (
+
+ );
+}
+
+// ── 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
Lädt…
;
+
+ return (
+
+ {/* Header */}
+
+
+
+ {id ? (editMode ? 'Bearbeiten' : order?.name) : 'Neue Bestellung'}
+
+ {order && !editMode && }
+ {order && !editMode && (
+
+ )}
+
+
+ {/* Formular */}
+ {(editMode || !id) && (
+
+
+
+ setForm(f=>({...f,name:e.target.value}))}
+ placeholder="z.B. Max Mustermann"
+ style={{ ...S.inp, fontSize:15 }} />
+
+
+
+
+
+
+
+
+
+
+
+ setForm(f=>({...f,custom_price:e.target.value}))}
+ placeholder="Optional" step="0.01"
+ style={{ ...S.inp, fontSize:15 }} />
+
+
+
+
+ {id && }
+
+
+ )}
+
+ {/* Nur bei bestehender Bestellung: Items + Summen */}
+ {order && !editMode && (
+ <>
+ {/* Bemerkung anzeigen */}
+ {order.bemerkung && (
+
+ )}
+
+ {/* Positionen */}
+
+
+
POSITIONEN
+
+
+
+ {order.items.length === 0 && (
+
+ Noch keine Positionen. Archiv-Einträge hinzufügen.
+
+ )}
+
+ {order.items.map(item => (
+
+
+ {item.calc_name}
+
+
+
+ {/* Stückzahl */}
+
+
+ {item.stueckzahl}
+
+
+
× Stk.
+ {/* Preise */}
+
+ {PREISE.map(p => (
+
+
{p.emoji}
+
+ {(item[`preis_${p.key}`] * item.stueckzahl).toFixed(2)}€
+
+
+ ))}
+
+
+
+ ))}
+
+
+ {/* Summen */}
+
+
GESAMTSUMME
+
+ {PREISE.map(p => (
+
+
{p.emoji}
+
{p.label}
+
+ {totals[p.key].toFixed(2)}€
+
+
+ ))}
+
+ {order.custom_price != null && (
+
+ Festpreis
+
+ {parseFloat(order.custom_price).toFixed(2)} €
+
+
+ )}
+
+
+ {/* Archiv-Picker Modal */}
+ {showPicker && (
+
e.target===e.currentTarget && setShowPicker(false)}>
+
+
+
+
+ Archiv-Eintrag wählen
+
+
+
+
+
+ setPickerSearch(e.target.value)}
+ placeholder="Suchen…"
+ style={{ ...S.inp, paddingLeft:32, fontSize:14 }} autoFocus />
+
+
+
+ {filteredArchiv.length === 0 && (
+
Keine Treffer.
+ )}
+ {filteredArchiv.map(a => (
+
+ ))}
+
+
+
+
+
+
+ )}
+ >
+ )}
+
+ );
+}
+
+// ── 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 (
+ { setOpenId(null); setNewMode(false); load(); }}
+ onSaved={id => { setNewMode(false); setOpenId(id); load(); }}
+ />
+ );
+ }
+
+ return (
+
+
+
Bestellungen
+
+
+
+ {/* Suche */}
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Name suchen…"
+ style={{ ...S.inp, paddingLeft:34, fontSize:15 }} />
+ {search && }
+
+
+ {/* Status-Filter */}
+
+ {[['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]) => (
+
+ ))}
+
+
+
+ {filtered.length} von {orders.length} Bestellungen
+
+
+ {loading &&
Lädt…
}
+
+ {!loading && orders.length === 0 && (
+
+
Noch keine Bestellungen.
+
+ )}
+
+ {filtered.map(order => (
+
+
+
+
+
+
+
+ {delId === order.id ? (
+
+
+
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+ );
+}