Files
dickendock/frontend/src/tools/bestellungen.jsx

811 lines
45 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<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 { 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(d => setArchiv(d.own || d || [])).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 <div style={{padding:24,color:'rgba(255,255,255,0.6)',fontFamily:'monospace'}}>Lädt</div>;
// 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 (
<div style={{ padding:'14px 14px 90px', maxWidth:860 }}>
<ConfirmDialog/>
{/* Header nur Zurück, Titel, Status, Edit */}
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:12 }}>
<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, minWidth:0,
overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
{id ? (editMode?'Bearbeiten':order?.name) : 'Neue Bestellung'}
</h1>
{order && !editMode && !order.bezahlt && (
<StatusBadge status={order.status} onClick={cycleOrderStatus}/>
)}
{order && !editMode && !order.bezahlt && (
<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>
{/* Bezahlt-Zeile immer eigene Card unter dem Header */}
{order && !editMode && (
<div style={{marginBottom:12,padding:'10px 14px',
background:!!order.bezahlt?'rgba(192,132,252,0.07)':'rgba(255,255,255,0.02)',
border:`1px solid ${!!order.bezahlt?'rgba(192,132,252,0.25)':'rgba(255,255,255,0.07)'}`,
borderRadius:10}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
<button onClick={toggleBezahlt} style={{
background:!!order.bezahlt?BEZAHLT.bg:'rgba(255,255,255,0.05)',
border:`1px solid ${!!order.bezahlt?BEZAHLT.color+'55':'rgba(255,255,255,0.12)'}`,
borderRadius:20,padding:'5px 14px',
color:!!order.bezahlt?BEZAHLT.color:'rgba(255,255,255,0.45)',
fontFamily:'monospace',fontSize:11,cursor:'pointer',
}}>
{!!order.bezahlt?'💜 Bezahlt':'○ Als bezahlt markieren'}
</button>
{!!order.bezahlt && order.bezahlt_am && (
<span style={{color:BEZAHLT.color,fontFamily:'monospace',fontSize:11}}>
am {new Date(order.bezahlt_am).toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'})}
</span>
)}
</div>
{!!order.bezahlt && (
<div style={{color:'rgba(192,132,252,0.5)',fontFamily:'monospace',fontSize:10,marginTop:5}}>
🔒 Bearbeitung gesperrt.
</div>
)}
</div>
)}
{/* Formular */}
{(!id || editMode) && !order?.bezahlt && (
<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:'flex',flexDirection:'column',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>
) : null}
{/* Positionen */}
<div style={{...S.card,marginBottom:10}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
<div style={S.head}>POSITIONEN</div>
{!order.bezahlt && (
<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.5)',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={itemDisplayStatus(item)} onClick={!!order.bezahlt?undefined:()=>cycleItemStatus(item)} small/>
{!order.bezahlt && (
<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>
{/* Druckzeit der Position */}
{item.stunden > 0 && (
<div style={{marginBottom:8,display:'flex',alignItems:'center',gap:6}}>
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11}}> Druckzeit:</span>
<span style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:11,fontWeight:700}}>
{fmtH(item.stunden)} × {item.stueckzahl} = {fmtH(item.stunden * item.stueckzahl)}
</span>
</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={()=>!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'}}></button>
<span style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:14,flex:1,textAlign:'center'}}>{item.stueckzahl}</span>
<button onClick={()=>!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'}}>+</button>
</div>
</div>
<div>
<div style={{...S.head,marginBottom:4}}>FESTPREIS/STK ()</div>
<div style={{display:'flex',gap:6,alignItems:'stretch'}}>
<input type="number" step="0.01" min="0"
key={item.custom_price ?? 'none'}
defaultValue={item.custom_price??''}
placeholder=""
readOnly={!!order.bezahlt}
onBlur={e=>{
if (!!order.bezahlt) return;
const v = e.target.value==='' ? null : parseFloat(e.target.value);
updateItem(item.id, {custom_price:v});
}}
style={{...S.inp,fontSize:14,padding:'8px 10px',flex:1,
opacity:!!order.bezahlt?0.5:1,
cursor:!!order.bezahlt?'not-allowed':'text'}}/>
{item.custom_price!=null && !order.bezahlt && (
<button
onClick={()=>updateItem(item.id,{custom_price:null})}
title="Festpreis zurücksetzen"
style={{...S.btn('#ff6b9d',true),padding:'0 10px',flexShrink:0,fontSize:14}}></button>
)}
</div>
</div>
</div>
{/* Status pro Stück */}
<div style={{marginBottom:10,opacity:!!order.bezahlt?0.5:1,pointerEvents:!!order.bezahlt?'none':'auto'}}>
<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 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 (
<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.6)',fontSize:9,fontFamily:'monospace',marginBottom:6}}>{label.toUpperCase()}</div>
<div style={{display:'flex',alignItems:'center',justifyContent:'center',gap:4}}>
<button onClick={handleDec}
style={{width:22,height:22,borderRadius:5,background:'rgba(0,0,0,0.3)',border:'none',color:canDec?color:'rgba(255,255,255,0.45)',fontSize:16,cursor:canDec?'pointer':'default',lineHeight:1}}></button>
<span style={{color,fontFamily:"'Space Mono',monospace",fontSize:16,fontWeight:700,minWidth:20,textAlign:'center'}}>{qty}</span>
<button onClick={handleInc}
style={{width:22,height:22,borderRadius:5,background:'rgba(0,0,0,0.3)',border:'none',color:canInc?color:'rgba(255,255,255,0.45)',fontSize:16,cursor:canInc?'pointer':'default',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',alignItems:'center'}}>
{PREISE.map(p=>{
const canSelect = !order.bezahlt && item.custom_price == null;
return (
<div key={p.key}
onClick={() => canSelect && updateItem(item.id, {custom_price: item[`preis_${p.key}`]})}
title={canSelect ? `${p.label}-Preis als Festpreis/Stk. übernehmen` : undefined}
style={{textAlign:'center',minWidth:60,padding:'4px 6px',borderRadius:7,
cursor:canSelect?'pointer':'default',
background:canSelect?`${p.c}08`:'transparent',
border:`1px solid ${canSelect?p.c+'20':'transparent'}`,
transition:'background 0.15s'}}>
<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>
{canSelect && <div style={{color:`${p.c}70`,fontSize:7,fontFamily:'monospace',marginTop:1}}> übernehmen</div>}
</div>
);
})}
{item.custom_price!=null && (
<div style={{textAlign:'center',minWidth:60,marginLeft:'auto'}}>
<div style={{color:'rgba(255,255,255,0.6)',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=>{
const canSelect = !order.bezahlt && order.custom_price == null && !totals.has_any_custom;
return (
<div key={p.key}
onClick={() => canSelect && api(`/tools/bestellungen/${id}`,{method:'PUT',body:{custom_price:totals[p.key]}}).then(u=>{setOrder(u);setForm(f=>({...f,custom_price:totals[p.key].toFixed(2)}));toast(`${p.label}-Preis übernommen`);}).catch(e=>toast(e.message,'error'))}
title={canSelect ? `${p.label}-Preis als Festpreis übernehmen` : undefined}
style={{background:`${p.c}10`,border:`1px solid ${p.c}30`,borderRadius:9,padding:'10px 8px',textAlign:'center',
cursor:canSelect?'pointer':'default',
transition:'background 0.15s',
...(canSelect?{':hover':{background:`${p.c}20`}}:{})}}>
<div style={{fontSize:16,marginBottom:2}}>{p.emoji}</div>
<div style={{color:'rgba(255,255,255,0.6)',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>
{canSelect && <div style={{color:`${p.c}80`,fontSize:8,fontFamily:'monospace',marginTop:3}}> übernehmen</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>
)}
{totals.stunden_total > 0 && (
<div style={{padding:'10px 14px',background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.08)',borderRadius:8,display:'flex',justifyContent:'space-between',alignItems:'center'}}>
<span style={{color:'rgba(255,255,255,0.55)',fontFamily:'monospace',fontSize:12}}> Gesamt-Druckzeit</span>
<span style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:16,fontWeight:700}}>
{fmtH(totals.stunden_total)}
</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:window.innerWidth>=768?'center':'flex-end',
justifyContent:'center',
padding:window.innerWidth>=768?24:0}}
onClick={e=>e.target===e.currentTarget&&setShowPicker(false)}>
<div style={{background:'#1a1a1e',
borderRadius:window.innerWidth>=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'}}>
<div style={{padding:'16px 16px 10px'}}>
{window.innerWidth<768&&<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.6)',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.55)',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>
);
}
// ── 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(d => {
const items = d.own || d || [];
setItem(items.find(i => i.id === calcId) || null);
}).catch(() => {});
}, [calcId]);
if (!item) return (
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.8)',zIndex:6000,
display:'flex',alignItems:'center',justifyContent:'center'}} onClick={onClose}>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:13}}>Lädt</div>
</div>
);
// 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 (
<div style={overlayStyle} onClick={e=>e.target===e.currentTarget&&onClose()}>
{lightbox && <div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.95)',zIndex:7000,
display:'flex',alignItems:'center',justifyContent:'center'}} onClick={()=>setLightbox(false)}>
<img src={item.image} alt="" style={{maxWidth:'95%',maxHeight:'90vh',borderRadius:10,objectFit:'contain'}}/>
</div>}
<div style={dialogStyle}>
<div style={{padding:'16px 20px 0'}}>
{mobile && <div style={{width:36,height:4,background:'rgba(255,255,255,0.15)',borderRadius:2,margin:'0 auto 14px'}}/>}
<div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',marginBottom:14}}>
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?15:17,fontWeight:700}}>{item.name}</div>
<button onClick={onClose} style={{background:'rgba(255,255,255,0.06)',border:'1px solid rgba(255,255,255,0.1)',
borderRadius:6, color:'rgba(255,255,255,0.6)', fontSize:14, cursor:'pointer', padding:'4px 10px', lineHeight:1, flexShrink:0, marginLeft:12}}></button>
</div>
</div>
<div style={{padding:'0 20px 28px'}}>
{item.image && (
<img src={item.image} alt={item.name} onClick={()=>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)'}}/>
)}
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr 1fr',gap:8,marginBottom:14}}>
{[['Gewicht',`${item.gramm}g`],['Dauer',`${item.stunden}h`],['Farben',item.farben]].map(([l,v])=>(
<div key={l} style={{background:'rgba(255,255,255,0.04)',borderRadius:8,padding:'10px 12px'}}>
<div style={{color:'rgba(255,255,255,0.55)',fontFamily:'monospace',fontSize:9,marginBottom:4}}>{l.toUpperCase()}</div>
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:14,fontWeight:700}}>{v}</div>
</div>
))}
</div>
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr 1fr',gap:8,marginBottom:14}}>
{[['💖',item.preis_freundschaft,'#ff6b9d'],['🤝',item.preis_normal,'#4ecdc4'],['💼',item.preis_auftrag,'#ffe66d']].map(([e,v,color])=>(
<div key={e} style={{background:`${color}10`,border:`1px solid ${color}25`,borderRadius:8,padding:'10px 8px',textAlign:'center'}}>
<div style={{fontSize:16,marginBottom:4}}>{e}</div>
<div style={{color,fontFamily:"'Space Mono',monospace",fontSize:mobile?14:16,fontWeight:700}}>{v.toFixed(2)}</div>
</div>
))}
</div>
{item.bemerkung && (
<div style={{background:'rgba(255,255,255,0.03)',border:'1px solid rgba(255,255,255,0.07)',
borderRadius:8,padding:'12px 14px'}}>
<div style={{color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:12,lineHeight:1.7}}>{item.bemerkung}</div>
</div>
)}
</div>
</div>
</div>
);
}
// ── Ü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 (
<BestellungDetail
id={newMode ? null : openId}
toast={toast}
onBack={(savedId, jump) => { setNewMode(false); if (savedId) { setOpenId(savedId); } else { setOpenId(null); load(); } }}
/>
);
}
return (
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:860}}>
<ConfirmDialog/>
{archivItem && <ArchivModal calcId={archivItem} onClose={()=>setArchivItem(null)}/>}
<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>
<div style={{display:'flex',gap:8}}>
<button onClick={() => 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'}}></button>
<button onClick={()=>{setNewMode(true);setOpenId(null);}} style={{...S.btn('#4ecdc4'),display:'flex',alignItems:'center',gap:6}}>
<PlusIcon size={14} color="#4ecdc4"/> Neu
</button>
</div>
</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 oder Modell 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.6)',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)'],
['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])=>(
<button key={k} onClick={()=>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}</button>
))}
</div>
<p style={{color:'rgba(255,255,255,0.55)',fontFamily:'monospace',fontSize:10,marginBottom:12}}>
{filtered.length} von {orders.length} Bestellungen
</p>
{loading && <div style={{color:'rgba(255,255,255,0.6)',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.55)',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.6)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
{new Date(order.created_at).toLocaleDateString('de-DE')}
</div>
</button>
<div style={{display:'flex',gap:5,alignItems:'center'}}>
{!!order.bezahlt && (
<span style={{background:BEZAHLT.bg,border:`1px solid ${BEZAHLT.color}44`,
borderRadius:20,padding:'3px 10px',color:BEZAHLT.color,fontFamily:'monospace',fontSize:10}}>
💜 Bezahlt
</span>
)}
{!order.bezahlt && <StatusBadge status={order.status} small/>}
</div>
</div>
{/* Positions-Fortschritt + Zeit + Festpreis-Summe */}
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8,flexWrap:'wrap',gap:4}}>
<div style={{display:'flex',alignItems:'center',gap:6,flexWrap:'wrap'}}>
<div style={{
fontSize:10,fontFamily:'monospace',
color: allDone?'#6bcb77':'rgba(255,255,255,0.5)',
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>
{order.stunden_total > 0 && (
<div style={{fontSize:10,fontFamily:'monospace',color:'rgba(255,255,255,0.5)',
background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.08)',
borderRadius:12,padding:'2px 8px'}}>
{fmtH(order.stunden_total)}
</div>
)}
</div>
{(order.custom_price != null || customSum > 0) && (
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:"'Space Mono',monospace",fontSize:12}}>
<span style={{color:'#fff',fontWeight:700}}>
{order.custom_price != null
? parseFloat(order.custom_price).toFixed(2)
: customSum.toFixed(2)}
</span>
</div>
)}
</div>
{/* Positionen */}
{order.items && order.items.length > 0 && (
<div style={{marginBottom:10,borderTop:'1px solid rgba(255,255,255,0.06)',paddingTop:8}}>
{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 (
<div key={item.id} style={{display:'flex',alignItems:'center',gap:8,padding:'5px 0',
borderBottom:'1px solid rgba(255,255,255,0.04)'}}>
{/* Status dot */}
<span style={{width:7,height:7,borderRadius:'50%',background:sc,flexShrink:0,display:'inline-block'}}/>
{/* Name klickbar wenn calculation_id vorhanden */}
{item.calculation_id ? (
<button onClick={e=>{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}
</button>
) : (
<span style={{color:'rgba(255,255,255,0.75)',fontFamily:'monospace',fontSize:12,flex:1,
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{item.calc_name}</span>
)}
{/* Stückzahl + Druckzeit */}
<span style={{color:'rgba(255,255,255,0.55)',fontFamily:'monospace',fontSize:11,flexShrink:0}}>
×{item.stueckzahl}{item.stunden>0?` · ${fmtH(item.stunden*item.stueckzahl)}`:''}
</span>
{/* Status Zähler kompakt */}
<div style={{display:'flex',gap:3,flexShrink:0}}>
{w>0&&<span style={{background:'rgba(255,230,109,0.15)',borderRadius:4,padding:'1px 5px',
color:'#ffe66d',fontFamily:'monospace',fontSize:10}}>{w}W</span>}
{a>0&&<span style={{background:'rgba(78,205,196,0.15)',borderRadius:4,padding:'1px 5px',
color:'#4ecdc4',fontFamily:'monospace',fontSize:10}}>{a}A</span>}
{f>0&&<span style={{background:'rgba(107,203,119,0.15)',borderRadius:4,padding:'1px 5px',
color:'#6bcb77',fontFamily:'monospace',fontSize:10}}>{f}F</span>}
</div>
{/* Preis */}
{item.custom_price!=null && (
<span style={{color:'rgba(255,255,255,0.6)',fontFamily:"'Space Mono',monospace",fontSize:11,flexShrink:0}}>
{(item.custom_price*item.stueckzahl).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>
<button onClick={()=>del(order.id)} style={S.btn('#ff6b9d',true)}>
<TrashIcon size={13} color="#ff6b9d"/>
</button>
</div>
</div>
);
})}
</div>
);
}