import { useConfirm } from '../confirm.jsx';
import { useState, useEffect, useMemo } from 'react';
import { api, S } from '../lib.js';
import { SearchIcon, PlusIcon, XIcon, EditIcon, TrashIcon, ChevronIcon } from '../icons.jsx';
const fmtH = h => {
if (!h) return '0h';
const hh = Math.floor(h), mm = Math.round((h - hh) * 60);
return mm > 0 ? `${hh}h ${mm}m` : `${hh}h`;
};
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 BEZAHLT = { color:'#c084fc', bg:'rgba(192,132,252,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 (
{s.label}
);
}
// ββ Bestellung Detail βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function BestellungDetail({ id, toast, onBack }) {
const { confirm, ConfirmDialog } = useConfirm();
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(false);
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),
stunden_total: items.reduce((s,i) => s + (i.stunden||0) * 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 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, true);
}
} 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 => {
if (!await confirm('Position wirklich entfernen?')) return;
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
LΓ€dtβ¦
;
// Status aus StΓΌck-ZΓ€hlern ableiten
const itemDisplayStatus = (item) => {
const f = item.qty_fertig||0, a = item.qty_in_arbeit||0;
if (f >= item.stueckzahl && item.stueckzahl > 0) return 'fertig';
if (f > 0 || a > 0) return 'in_arbeit';
return 'warteliste';
};
const toggleBezahlt = () =>
api(`/tools/bestellungen/${id}`,{method:'PUT',body:{bezahlt:!order.bezahlt}})
.then(u=>{setOrder(u);toast(u.bezahlt?'Als bezahlt markiert':'Bezahlung aufgehoben');});
return (
{/* Header β nur ZurΓΌck, Titel, Status, Edit */}
onBack(null)} style={{ background:'transparent', border:'none', cursor:'pointer', padding:4 }}>
{id ? (editMode?'Bearbeiten':order?.name) : 'Neue Bestellung'}
{order && !editMode && !order.bezahlt && (
)}
{order && !editMode && !order.bezahlt && (
setEditMode(true)} style={{background:'transparent',border:'none',cursor:'pointer',padding:4}}>
)}
{/* Bezahlt-Zeile β immer eigene Card unter dem Header */}
{order && !editMode && (
{!!order.bezahlt?'π Bezahlt':'β Als bezahlt markieren'}
{!!order.bezahlt && order.bezahlt_am && (
am {new Date(order.bezahlt_am).toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'})}
)}
{!!order.bezahlt && (
π Bearbeitung gesperrt.
)}
)}
{/* Formular */}
{(!id || editMode) && !order?.bezahlt && (
NAME / PERSON
setForm(f=>({...f,name:e.target.value}))}
placeholder="z.B. Max Mustermann" style={{...S.inp,fontSize:15}}/>
BEMERKUNG
{id?'β Speichern':'+ Bestellung anlegen'}
{id && setEditMode(false)} style={{...S.btn('#ff6b9d'),padding:'10px 14px'}}>Abbrechen }
)}
{order && !editMode && (
<>
{order.bemerkung ? (
) : null}
{/* Positionen */}
POSITIONEN
{!order.bezahlt && (
{setShowPicker(true);setPickerSearch('');}}
style={{...S.btn('#4ecdc4',true),display:'flex',alignItems:'center',gap:5}}>
HinzufΓΌgen
)}
{order.items.length===0 && (
Noch keine Positionen.
)}
{order.items.map(item => (
{/* Name + Status + LΓΆschen */}
{item.calc_name}
cycleItemStatus(item)} small/>
{!order.bezahlt && (
removeItem(item.id)} style={{background:'transparent',border:'none',cursor:'pointer',padding:2}}>
)}
{/* Druckzeit der Position */}
{item.stunden > 0 && (
β± Druckzeit:
{fmtH(item.stunden)} Γ {item.stueckzahl} = {fmtH(item.stunden * item.stueckzahl)}
)}
{/* StΓΌckzahl gesamt + Festpreis */}
STΓCKZAHL GESAMT
!order.bezahlt&&item.stueckzahl>1&&updateItem(item.id,{stueckzahl:item.stueckzahl-1})}
style={{padding:'6px 10px',background:'transparent',border:'none',color:order.bezahlt?'rgba(255,255,255,0.15)':'rgba(255,255,255,0.6)',fontSize:18,cursor:order.bezahlt?'default':'pointer'}}>β
{item.stueckzahl}
!order.bezahlt&&updateItem(item.id,{stueckzahl:item.stueckzahl+1})}
style={{padding:'6px 10px',background:'transparent',border:'none',color:order.bezahlt?'rgba(255,255,255,0.15)':'rgba(255,255,255,0.6)',fontSize:18,cursor:order.bezahlt?'default':'pointer'}}>+
{/* Status pro StΓΌck */}
STATUS PRO STΓCK
{[
['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 w = item.qty_warteliste||0, a = item.qty_in_arbeit||0, f = item.qty_fertig||0;
// + nimmt aus Warteliste (oder In Arbeit), - gibt zurΓΌck an Warteliste
const canInc = key==='warteliste' ? false : (key==='in_arbeit' ? w>0 : w>0||a>0);
const canDec = qty > 0;
const handleInc = () => {
if (!canInc) return;
let nw=w,na=a,nf=f;
if (key==='in_arbeit') { na++; nw=Math.max(0,nw-1); }
if (key==='fertig') { nf++; if(nw>0) nw--; else na=Math.max(0,na-1); }
updateItem(item.id,{qty_warteliste:nw,qty_in_arbeit:na,qty_fertig:nf});
};
const handleDec = () => {
if (!canDec) return;
let nw=w,na=a,nf=f;
if (key==='warteliste') { nw--; }
if (key==='in_arbeit') { na--; nw++; }
if (key==='fertig') { nf--; nw++; }
updateItem(item.id,{qty_warteliste:nw,qty_in_arbeit:na,qty_fertig:nf});
};
return (
{label.toUpperCase()}
β
{qty}
+
);
})}
{/* Verbleibend ohne Status */}
{(() => { const assigned=(item.qty_warteliste||0)+(item.qty_in_arbeit||0)+(item.qty_fertig||0); return assigned
β {item.stueckzahl-assigned} StΓΌck noch keinem Status zugewiesen
) : null; })()}
{/* Preiszeile */}
{PREISE.map(p=>(
{p.emoji}
{(item[`preis_${p.key}`]*item.stueckzahl).toFixed(2)}β¬
))}
{item.custom_price!=null && (
FEST
{(item.custom_price*item.stueckzahl).toFixed(2)}β¬
)}
))}
{/* Summen */}
{order.items.length > 0 && (
GESAMTSUMME
{PREISE.map(p=>(
{p.emoji}
{p.label}
{totals[p.key].toFixed(2)}β¬
))}
{totals.has_any_custom && (
β Positions-Festpreise
{totals.custom_sum.toFixed(2)} β¬
)}
{totals.stunden_total > 0 && (
β± Gesamt-Druckzeit
{fmtH(totals.stunden_total)}
)}
{order.custom_price != null && (
Gesamt-Festpreis
{parseFloat(order.custom_price).toFixed(2)} β¬
)}
)}
{/* Archiv-Picker */}
{showPicker && (
=768?'center':'flex-end',
justifyContent:'center',
padding:window.innerWidth>=768?24:0}}
onClick={e=>e.target===e.currentTarget&&setShowPicker(false)}>
=768?16:'16px 16px 0 0',
width:'100%',maxWidth:600,
maxHeight:'80vh',display:'flex',flexDirection:'column',
border:'1px solid rgba(255,255,255,0.15)',
boxShadow:window.innerWidth>=768?'0 24px 80px rgba(0,0,0,0.6)':'none'}}>
{window.innerWidth<768&&
}
Archiv-Eintrag wΓ€hlen
setPickerSearch(e.target.value)}
placeholder="Suchenβ¦" style={{...S.inp,paddingLeft:32,fontSize:14}} autoFocus/>
{filteredArchiv.map(a=>(
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',
}}>
{a.name}
{a.gramm}g Β· {a.stunden}h
{PREISE.map(p=>(
{p.emoji}
{a[`preis_${p.key}`].toFixed(2)}β¬
))}
))}
{filteredArchiv.length===0 &&
Keine Treffer.
}
setShowPicker(false)} style={{...S.btn('#ff6b9d'),width:'100%',textAlign:'center',padding:'10px 0'}}>SchlieΓen
)}
>
)}
);
}
// ββ Archiv-Eintrag Modal βββββββββββββββββββββββββββββββββββββββββββββββββββββ
function ArchivModal({ calcId, onClose }) {
const [item, setItem] = useState(null);
const [lightbox, setLightbox] = useState(false);
const mobile = window.innerWidth < 768;
useEffect(() => {
api('/tools/kalkulator3d').then(items => {
setItem(items.find(i => i.id === calcId) || null);
}).catch(() => {});
}, [calcId]);
if (!item) return (
);
// Mobile: bottom-sheet, Desktop: centered dialog
const overlayStyle = {
position:'fixed', inset:0, background:'rgba(0,0,0,0.85)', zIndex:6000,
display:'flex',
alignItems: mobile ? 'flex-end' : 'center',
justifyContent: 'center',
padding: mobile ? 0 : 24,
};
const dialogStyle = mobile ? {
background:'#1a1a1e', borderRadius:'16px 16px 0 0', width:'100%', maxWidth:600,
maxHeight:'85vh', overflowY:'auto', border:'1px solid rgba(255,255,255,0.1)',
} : {
background:'#1a1a1e', borderRadius:16, width:'100%', maxWidth:560,
maxHeight:'90vh', overflowY:'auto', border:'1px solid rgba(255,255,255,0.15)',
boxShadow:'0 24px 80px rgba(0,0,0,0.6)',
};
return (
e.target===e.currentTarget&&onClose()}>
{lightbox &&
setLightbox(false)}>
}
{item.image && (
setLightbox(true)}
style={{width:'100%', maxHeight: mobile?180:260, objectFit:'cover', borderRadius:10, marginBottom:14,
cursor:'pointer', border:'1px solid rgba(255,255,255,0.1)'}}/>
)}
{[['Gewicht',`${item.gramm}g`],['Dauer',`${item.stunden}h`],['Farben',item.farben]].map(([l,v])=>(
))}
{[['π',item.preis_freundschaft,'#ff6b9d'],['π€',item.preis_normal,'#4ecdc4'],['πΌ',item.preis_auftrag,'#ffe66d']].map(([e,v,color])=>(
))}
{item.bemerkung && (
)}
);
}
// ββ Γbersicht βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export default function Bestellungen({ toast, mobile }) {
const { confirm, ConfirmDialog } = useConfirm();
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [filterStatus,setFilter] = useState('warteliste');
const [openId, setOpenId] = useState(null);
const [newMode, setNewMode] = useState(false);
const [archivItem, setArchivItem] = useState(null);
const load = () => {
setLoading(true);
api('/tools/bestellungen').then(setOrders).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false));
};
useEffect(()=>{ load(); },[]);
const del = async id => {
if (!await confirm('Bestellung wirklich lΓΆschen?')) return;
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 sq = search.trim().toLowerCase();
const filtered = orders.filter(o => {
const ms = !sq || o.name.toLowerCase().includes(sq)
|| (o.items && o.items.some(i => i.calc_name.toLowerCase().includes(sq)));
const mf = filterStatus==='alle'
? true
: filterStatus==='bezahlt'
? o.bezahlt
: o.status===filterStatus && !o.bezahlt;
return ms && mf;
});
if (openId !== null || newMode) {
return (
{ setNewMode(false); if (savedId) { setOpenId(savedId); } else { setOpenId(null); load(); } }}
/>
);
}
return (
{archivItem &&
setArchivItem(null)}/>}
Bestellungen
window.location.reload()} title="Neu laden"
style={{background:'transparent',border:'1px solid rgba(255,255,255,0.1)',borderRadius:8,
color:'rgba(255,255,255,0.4)',cursor:'pointer',padding:'6px 10px',fontSize:14,fontFamily:'monospace'}}>βΊ
{setNewMode(true);setOpenId(null);}} style={{...S.btn('#4ecdc4'),display:'flex',alignItems:'center',gap:6}}>
Neu
{/* Suche */}
setSearch(e.target.value)} placeholder="Name oder Modell suchenβ¦"
style={{...S.inp,paddingLeft:34,fontSize:15}}/>
{search && setSearch('')} style={{position:'absolute',right:10,top:'50%',transform:'translateY(-50%)',background:'transparent',border:'none',color:'rgba(255,255,255,0.6)',cursor:'pointer',fontSize:16}}>β }
{/* Status-Filter */}
{[
['alle', 'Alle', 'rgba(255,255,255,0.6)', 'rgba(255,255,255,0.1)'],
['warteliste', 'Warteliste', '#ffe66d', 'rgba(255,230,109,0.12)'],
['in_arbeit', 'In Arbeit', '#4ecdc4', 'rgba(78,205,196,0.12)' ],
['fertig', 'Fertig', '#6bcb77', 'rgba(107,203,119,0.12)'],
['bezahlt', 'Bezahlt', BEZAHLT.color, BEZAHLT.bg ],
].map(([k,l,col,bg])=>(
setFilter(k)} style={{
padding:'5px 12px',borderRadius:20,
border:`1px solid ${filterStatus===k?col:'rgba(255,255,255,0.1)'}`,
background:filterStatus===k?bg:'transparent',
color:filterStatus===k?col:'rgba(255,255,255,0.4)',
fontFamily:'monospace',fontSize:11,cursor:'pointer',
}}>{l}
))}
{filtered.length} von {orders.length} Bestellungen
{loading && LΓ€dtβ¦
}
{!loading && orders.length===0 && (
)}
{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 (
setOpenId(order.id)} style={{background:'transparent',border:'none',cursor:'pointer',textAlign:'left',padding:0,flex:1}}>
{order.name}
{new Date(order.created_at).toLocaleDateString('de-DE')}
{!!order.bezahlt && (
π Bezahlt
)}
{!order.bezahlt && }
{/* Positions-Fortschritt + Zeit + Festpreis-Summe */}
{done}/{total} fertig
{order.stunden_total > 0 && (
β± {fmtH(order.stunden_total)}
)}
{(order.custom_price != null || customSum > 0) && (
β
{order.custom_price != null
? parseFloat(order.custom_price).toFixed(2)
: customSum.toFixed(2)}β¬
)}
{/* Positionen */}
{order.items && order.items.length > 0 && (
{order.items.map(item => {
const f=item.qty_fertig||0, a=item.qty_in_arbeit||0, w=item.qty_warteliste||0;
const itemStatus = f>=item.stueckzahl&&item.stueckzahl>0?'fertig':f>0||a>0?'in_arbeit':'warteliste';
const sc = STATUS[itemStatus]?.color || '#ffe66d';
return (
{/* Status dot */}
{/* Name β klickbar wenn calculation_id vorhanden */}
{item.calculation_id ? (
{e.stopPropagation();setArchivItem(item.calculation_id);}}
style={{background:'transparent',border:'none',cursor:'pointer',padding:0,
color:'#4ecdc4',fontFamily:'monospace',fontSize:12,flex:1,textAlign:'left',
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',
textDecoration:'underline',textDecorationStyle:'dotted',textUnderlineOffset:2}}>
{item.calc_name}
) : (
{item.calc_name}
)}
{/* StΓΌckzahl + Druckzeit */}
Γ{item.stueckzahl}{item.stunden>0?` Β· ${fmtH(item.stunden*item.stueckzahl)}`:''}
{/* Status ZΓ€hler kompakt */}
{w>0&&{w}W }
{a>0&&{a}A }
{f>0&&{f}F }
{/* Preis */}
{item.custom_price!=null && (
{(item.custom_price*item.stueckzahl).toFixed(2)}β¬
)}
);
})}
)}
setOpenId(order.id)} style={{...S.btn('#4ecdc4',true),flex:1,textAlign:'center'}}>Γffnen
del(order.id)} style={S.btn('#ff6b9d',true)}>
);
})}
);
}