feat: Abgeholt-Status + Archiv-Drawer für abgeschlossene Bestellungen

This commit is contained in:
2026-06-16 20:21:46 +02:00
parent fe6fc33165
commit ca2bdc3267
3 changed files with 115 additions and 2 deletions

View File

@@ -155,6 +155,8 @@ if (!cols.includes('bemerkung')) db.exec("ALTER TABLE calculations ADD COLUMN be
const ordCols = db.prepare("PRAGMA table_info(orders)").all().map(r => r.name);
if (!ordCols.includes('bezahlt')) db.exec("ALTER TABLE orders ADD COLUMN bezahlt INTEGER NOT NULL DEFAULT 0");
if (!ordCols.includes('bezahlt_am')) db.exec("ALTER TABLE orders ADD COLUMN bezahlt_am DATETIME");
if (!ordCols.includes('abgeholt')) db.exec("ALTER TABLE orders ADD COLUMN abgeholt INTEGER NOT NULL DEFAULT 0");
if (!ordCols.includes('abgeholt_am'))db.exec("ALTER TABLE orders ADD COLUMN abgeholt_am DATETIME");
// Migrationen order_items
// files migrations

View File

@@ -77,8 +77,16 @@ router.put('/:id', authenticate, (req, res) => {
bezahlt_am = req.body.bezahlt ? new Date().toISOString() : null;
}
db.prepare('UPDATE orders SET name=?,bemerkung=?,status=?,custom_price=?,bezahlt=?,bezahlt_am=?,updated_at=CURRENT_TIMESTAMP WHERE id=? AND user_id=?')
.run(name, bemerkung, status, custom_price, bezahlt, bezahlt_am, req.params.id, uid(req));
// Abgeholt-Status mit Datum
let abgeholt = ex.abgeholt;
let abgeholt_am = ex.abgeholt_am;
if (req.body.abgeholt !== undefined) {
abgeholt = req.body.abgeholt ? 1 : 0;
abgeholt_am = req.body.abgeholt ? new Date().toISOString() : null;
}
db.prepare('UPDATE orders SET name=?,bemerkung=?,status=?,custom_price=?,bezahlt=?,bezahlt_am=?,abgeholt=?,abgeholt_am=?,updated_at=CURRENT_TIMESTAMP WHERE id=? AND user_id=?')
.run(name, bemerkung, status, custom_price, bezahlt, bezahlt_am, abgeholt, abgeholt_am, req.params.id, uid(req));
res.json(loadOrder(req.params.id, uid(req)));
});

View File

@@ -128,6 +128,12 @@ function BestellungDetail({ id, toast, onBack }) {
api(`/tools/bestellungen/${id}`,{method:'PUT',body:{bezahlt:!order.bezahlt}})
.then(u=>{setOrder(u);toast(u.bezahlt?'Als bezahlt markiert':'Bezahlung aufgehoben');});
const toggleAbgeholt = () =>
api(`/tools/bestellungen/${id}`,{method:'PUT',body:{abgeholt:!order.abgeholt}})
.then(u=>{setOrder(u);toast(u.abgeholt?'Als abgeholt markiert':'Abholung aufgehoben');});
const isAbgeschlossen = order && !!order.bezahlt && !!order.abgeholt;
return (
<div style={{ padding:'14px 14px 90px', maxWidth:860 }}>
<ConfirmDialog/>
@@ -178,6 +184,27 @@ function BestellungDetail({ id, toast, onBack }) {
🔒 Bearbeitung gesperrt.
</div>
)}
{/* Abgeholt nur bei fertig oder bezahlt anzeigen */}
{(order.status==='fertig' || !!order.bezahlt) && (
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginTop:8,paddingTop:8,
borderTop:'1px solid rgba(255,255,255,0.06)'}}>
<button onClick={toggleAbgeholt} style={{
background:!!order.abgeholt?'rgba(251,191,36,0.15)':'rgba(255,255,255,0.05)',
border:`1px solid ${!!order.abgeholt?'rgba(251,191,36,0.5)':'rgba(255,255,255,0.12)'}`,
borderRadius:20,padding:'5px 14px',
color:!!order.abgeholt?'#fbbf24':'rgba(255,255,255,0.45)',
fontFamily:'monospace',fontSize:11,cursor:'pointer',
}}>
{!!order.abgeholt?'📦 Abgeholt':'○ Als abgeholt markieren'}
</button>
{!!order.abgeholt && order.abgeholt_am && (
<span style={{color:'#fbbf24',fontFamily:'monospace',fontSize:11}}>
am {new Date(order.abgeholt_am).toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'})}
</span>
)}
</div>
)}
</div>
)}
@@ -598,6 +625,7 @@ export default function Bestellungen({ toast, mobile }) {
const [openId, setOpenId] = useState(null);
const [newMode, setNewMode] = useState(false);
const [archivItem, setArchivItem] = useState(null);
const [archivOpen, setArchivOpen] = useState(false);
const load = () => {
setLoading(true);
@@ -612,7 +640,11 @@ export default function Bestellungen({ toast, mobile }) {
};
const sq = search.trim().toLowerCase();
// Abgeschlossen = bezahlt UND abgeholt
const isAbgeschlossen = o => !!o.bezahlt && !!o.abgeholt;
const filtered = orders.filter(o => {
if (isAbgeschlossen(o)) return false; // nie in aktiver Liste
const ms = !sq || o.name.toLowerCase().includes(sq)
|| (o.items && o.items.some(i => i.calc_name.toLowerCase().includes(sq)));
const mf = filterStatus==='alle'
@@ -623,6 +655,9 @@ export default function Bestellungen({ toast, mobile }) {
return ms && mf;
});
const abgeschlossen = orders.filter(o => isAbgeschlossen(o)
&& (!sq || o.name.toLowerCase().includes(sq)));
if (openId !== null || newMode) {
return (
<BestellungDetail
@@ -805,6 +840,74 @@ export default function Bestellungen({ toast, mobile }) {
</div>
);
})}
{/* ── Archiv-Drawer ───────────────────────────────────────────────── */}
{abgeschlossen.length > 0 && (
<div style={{marginTop:20}}>
<button onClick={()=>setArchivOpen(v=>!v)} style={{
width:'100%',background:'rgba(255,255,255,0.03)',
border:'1px solid rgba(255,255,255,0.08)',
borderRadius:10,padding:'10px 14px',cursor:'pointer',
display:'flex',alignItems:'center',justifyContent:'space-between',
}}>
<div style={{display:'flex',alignItems:'center',gap:8}}>
<span style={{fontSize:16}}></span>
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:12}}>
ABGESCHLOSSEN
</span>
<span style={{
background:'rgba(74,222,128,0.15)',border:'1px solid rgba(74,222,128,0.3)',
borderRadius:10,padding:'1px 8px',color:'#4ade80',fontFamily:'monospace',fontSize:10,
}}>{abgeschlossen.length}</span>
</div>
<span style={{color:'rgba(255,255,255,0.3)',fontSize:11,
transform:archivOpen?'rotate(90deg)':'rotate(0)',transition:'transform 0.2s'}}></span>
</button>
{archivOpen && (
<div style={{marginTop:8}}>
{abgeschlossen.map(order => {
const customSum = order.custom_price_sum || 0;
return (
<div key={order.id} style={{...S.card,marginBottom:6,padding:'10px 14px',
opacity:0.7,borderColor:'rgba(255,255,255,0.05)'}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:4}}>
<button onClick={()=>setOpenId(order.id)}
style={{background:'transparent',border:'none',cursor:'pointer',textAlign:'left',padding:0,flex:1}}>
<div style={{color:'rgba(255,255,255,0.75)',fontFamily:'monospace',fontSize:13,fontWeight:700}}>
{order.name}
</div>
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
{new Date(order.created_at).toLocaleDateString('de-DE')}
{order.abgeholt_am && ` · abgeholt ${new Date(order.abgeholt_am).toLocaleDateString('de-DE')}`}
</div>
</button>
<div style={{display:'flex',gap:5,alignItems:'center',flexShrink:0}}>
<span style={{background:'rgba(251,191,36,0.12)',border:'1px solid rgba(251,191,36,0.3)',
borderRadius:20,padding:'2px 8px',color:'#fbbf24',fontFamily:'monospace',fontSize:9}}>📦</span>
<span style={{background:BEZAHLT.bg,border:`1px solid ${BEZAHLT.color}44`,
borderRadius:20,padding:'2px 8px',color:BEZAHLT.color,fontFamily:'monospace',fontSize:9}}>💜</span>
{customSum > 0 && (
<span style={{color:'rgba(255,255,255,0.4)',fontFamily:"'Space Mono',monospace",fontSize:11}}>
{customSum.toFixed(2)}
</span>
)}
</div>
</div>
<div style={{display:'flex',gap:6,justifyContent:'flex-end',marginTop:6}}>
<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>
)}
</div>
)}
</div>
);
}