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

286 lines
15 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 { useState, useMemo, useEffect } from 'react';
import { api, S } from '../lib.js';
const DEFAULTS = {
materialpreis_pro_gramm: 0.01, stromverbrauch_kw: 0.15,
strompreis_pro_kwh: 0.38, druckerpreis: 550,
gesamtdruckstunden: 5000, verschleiss_pro_stunde: 0.06,
};
// Farbfaktor: jede weitere Farbe +0.1
const getFarbFaktor = n => Math.round((1.0 + Math.max(0, n-1) * 0.1) * 100) / 100;
const STUFEN = [
{ key:'f', emoji:'💖', label:'Freundschaft', mult:1.0, c:'#ff6b9d' },
{ key:'n', emoji:'🤝', label:'Normal', mult:1.5, c:'#4ecdc4' },
{ key:'a', emoji:'💼', label:'Auftrag', mult:2.5, c:'#ffe66d' },
];
// Stepper-Eingabe kein Tippen nötig, gut für Mobile
function Stepper({ label, value, onChange, step, unit, min=0, hint }) {
const dec = () => onChange(Math.max(min, Math.round((value - step) * 10000) / 10000));
const inc = () => onChange(Math.round((value + step) * 10000) / 10000);
return (
<div>
<div style={{ color:'rgba(255,255,255,0.4)', fontSize:10, fontFamily:'monospace', marginBottom:5, letterSpacing:1 }}>
{label.toUpperCase()}{unit ? ` (${unit})` : ''}
</div>
<div style={{ display:'flex', alignItems:'center', background:'rgba(255,255,255,0.05)', borderRadius:8, border:'1px solid rgba(255,255,255,0.1)', overflow:'hidden' }}>
<button onClick={dec} style={{ padding:'10px 12px', background:'transparent', border:'none', color:'rgba(255,255,255,0.6)', fontSize:20, cursor:'pointer', flexShrink:0, lineHeight:1 }}></button>
<input
type="number" value={value} step={step}
onChange={e => onChange(parseFloat(e.target.value) || 0)}
style={{ flex:1, background:'transparent', border:'none', color:'#fff', fontSize:15, fontFamily:"'Space Mono',monospace", textAlign:'center', outline:'none', minWidth:0, padding:'10px 4px' }}
/>
<button onClick={inc} style={{ padding:'10px 12px', background:'transparent', border:'none', color:'rgba(255,255,255,0.6)', fontSize:20, cursor:'pointer', flexShrink:0, lineHeight:1 }}>+</button>
</div>
{hint && <div style={{ color:'rgba(255,255,255,0.2)', fontSize:10, fontFamily:'monospace', marginTop:3 }}>{hint}</div>}
</div>
);
}
function SmallInput({ label, value, onChange, step=0.01 }) {
return (
<div>
<div style={{ color:'rgba(255,255,255,0.35)', fontSize:9, fontFamily:'monospace', marginBottom:3, letterSpacing:0.5 }}>{label}</div>
<input type="number" value={value} step={step}
onChange={e => onChange(parseFloat(e.target.value)||0)}
style={{ ...S.inp, padding:'7px 8px', fontSize:13 }}
/>
</div>
);
}
export default function Kalkulator3D({ toast, editData, onEditDone, mobile }) {
const [gramm, setGramm] = useState(editData?.gramm ?? 50);
const [stunden, setStunden] = useState(editData?.stunden ?? 3);
const [farben, setFarben] = useState(editData?.farben ?? 1);
const [vars, setVars] = useState(editData ? {
materialpreis_pro_gramm: editData.materialpreis_pro_gramm,
stromverbrauch_kw: editData.stromverbrauch_kw,
strompreis_pro_kwh: editData.strompreis_pro_kwh,
druckerpreis: editData.druckerpreis,
gesamtdruckstunden: editData.gesamtdruckstunden,
verschleiss_pro_stunde: editData.verschleiss_pro_stunde,
} : { ...DEFAULTS });
const [showAdv, setShowAdv] = useState(false);
const [name, setName] = useState(editData?.name ?? '');
const [saving, setSaving] = useState(false);
const sv = (k,v) => setVars(p => ({ ...p, [k]:v }));
const calc = useMemo(() => {
const f = getFarbFaktor(farben);
const mat = gramm * vars.materialpreis_pro_gramm * f;
const str = stunden * vars.stromverbrauch_kw * vars.strompreis_pro_kwh;
const ver = stunden * vars.verschleiss_pro_stunde;
return { mat, str, ver, grund: mat + str + ver };
}, [gramm, stunden, farben, vars]);
const save = async () => {
if (!name.trim()) { toast('Bitte einen Namen eingeben','error'); return; }
setSaving(true);
try {
const payload = { name, gramm, stunden, farben, ...vars,
preis_freundschaft: calc.grund * 1.0,
preis_normal: calc.grund * 1.5,
preis_auftrag: calc.grund * 2.5 };
if (editData?.id) {
await api(`/tools/kalkulator3d/${editData.id}`, { method:'PUT', body:payload });
toast('Aktualisiert ✓'); onEditDone?.();
} else {
await api('/tools/kalkulator3d', { body:payload });
toast('Gespeichert ✓'); setName('');
}
} catch(e) { toast(e.message,'error'); } finally { setSaving(false); }
};
const pad = mobile ? '14px 14px 90px' : '36px 44px';
return (
<div style={{ padding:pad, maxWidth:860 }}>
{/* Header */}
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:20 }}>
<div>
{editData && <div style={{ color:'#ffe66d', fontSize:10, fontFamily:'monospace', marginBottom:4 }}> {editData.name}</div>}
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile?18:22, margin:0 }}>3D-Kalkulator</h1>
</div>
{editData && <button onClick={onEditDone} style={S.btn('#ff6b9d',true)}> Abbrechen</button>}
</div>
{/* Haupteingaben */}
<div style={{ ...S.card, marginBottom:10 }}>
<div style={{ display:'flex', flexDirection:'column', gap:10, marginBottom:10 }}>
<Stepper label="Gewicht" value={gramm} onChange={setGramm} step={1} unit="g" min={0} />
<Stepper label="Druckdauer" value={stunden} onChange={setStunden} step={0.5} unit="h" min={0} />
</div>
<Stepper label="Anzahl Farben" value={farben}
onChange={v => setFarben(Math.max(1, Math.round(v)))}
step={1} min={1}
hint={`Farbfaktor: ×${getFarbFaktor(farben).toFixed(2)}`} />
{/* Erweiterte Einstellungen */}
<button onClick={()=>setShowAdv(v=>!v)} style={{
width:'100%', marginTop:12, background:'rgba(255,255,255,0.02)',
border:'1px solid rgba(255,255,255,0.06)', borderRadius:7,
padding:'9px 12px', color:'rgba(255,255,255,0.35)', cursor:'pointer',
fontFamily:'monospace', fontSize:11, textAlign:'left',
display:'flex', alignItems:'center', gap:7,
}}>
<span style={{ display:'inline-block', transform:showAdv?'rotate(90deg)':'rotate(0)', transition:'transform 0.18s' }}></span>
Erweiterte Einstellungen
<span style={{ marginLeft:'auto', fontSize:9, background:'rgba(78,205,196,0.1)', borderRadius:3, padding:'2px 6px', color:'#4ecdc4' }}>
{Object.entries(vars).some(([k,v])=>v!==DEFAULTS[k])?'GEÄNDERT':'STANDARD'}
</span>
</button>
{showAdv && (
<div style={{ marginTop:10, background:'rgba(0,0,0,0.2)', border:'1px solid rgba(255,255,255,0.05)', borderRadius:9, padding:12 }}>
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10 }}>
<SmallInput label="Material €/g" value={vars.materialpreis_pro_gramm} onChange={v=>sv('materialpreis_pro_gramm',v)} step={0.001}/>
<SmallInput label="Strom kW" value={vars.stromverbrauch_kw} onChange={v=>sv('stromverbrauch_kw',v)} step={0.01}/>
<SmallInput label="Strom €/kWh" value={vars.strompreis_pro_kwh} onChange={v=>sv('strompreis_pro_kwh',v)} step={0.01}/>
<SmallInput label="Drucker €" value={vars.druckerpreis} onChange={v=>sv('druckerpreis',v)} step={10}/>
<SmallInput label="Lebensdauer h" value={vars.gesamtdruckstunden} onChange={v=>sv('gesamtdruckstunden',v)} step={100}/>
<SmallInput label="Verschleiß €/h" value={vars.verschleiss_pro_stunde} onChange={v=>sv('verschleiss_pro_stunde',v)} step={0.01}/>
</div>
<button onClick={()=>setVars({...DEFAULTS})} style={{ ...S.btn('#ff6b9d',true), marginTop:10 }}> Standard</button>
</div>
)}
</div>
{/* Ergebnis kompakt */}
<div style={{ ...S.card, marginBottom:10 }}>
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:0 }}>
{[['Material',calc.mat,'#a78bfa'],['Strom',calc.str,'#60a5fa'],['Verschleiß',calc.ver,'#f59e0b'],['Grundkosten',calc.grund,'#4ecdc4']].map(([l,v,c],i)=>(
<div key={l} style={{
padding:'10px 14px',
borderBottom: i<2 ? '1px solid rgba(255,255,255,0.05)' : 'none',
borderRight: i%2===0 ? '1px solid rgba(255,255,255,0.05)' : 'none',
}}>
<div style={{ color:'rgba(255,255,255,0.3)', fontSize:9, fontFamily:'monospace', marginBottom:2 }}>{l.toUpperCase()}</div>
<div style={{ color:c, fontSize:i===3?18:14, fontFamily:"'Space Mono',monospace", fontWeight:i===3?700:400 }}>
{v.toFixed(2)}
</div>
</div>
))}
</div>
</div>
{/* Preis-Cards */}
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:8, marginBottom:12 }}>
{STUFEN.map((s,i) => (
<div key={s.key} style={{
background:`${s.c}10`, border:`1px solid ${s.c}30`,
borderRadius:10, padding:'12px 10px', textAlign:'center',
transform: i===1?'scale(1.04)':'scale(1)',
}}>
<div style={{ fontSize:20, marginBottom:3 }}>{s.emoji}</div>
<div style={{ color:'rgba(255,255,255,0.35)', fontSize:9, fontFamily:'monospace', marginBottom:4 }}>{s.label}</div>
<div style={{ color:s.c, fontSize:mobile?16:20, fontFamily:"'Space Mono',monospace", fontWeight:700 }}>
{(calc.grund*s.mult).toFixed(2)}
</div>
<div style={{ color:'rgba(255,255,255,0.2)', fontSize:8, fontFamily:'monospace', marginTop:2 }}>×{s.mult}</div>
</div>
))}
</div>
{/* Speichern */}
<div style={S.card}>
<div style={{ display:'flex', gap:8 }}>
<input value={name} onChange={e=>setName(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&save()}
placeholder='Name z.B. "Halterung Müller"'
style={{ ...S.inp, flex:1, fontSize:15 }}
/>
<button onClick={save} disabled={saving} style={{
background:'linear-gradient(135deg,#4ecdc4,#45b7d1)', border:'none',
borderRadius:8, padding:'0 16px', color:'#0d0d0f',
fontFamily:"'Space Mono',monospace", fontWeight:700,
fontSize:12, cursor:saving?'default':'pointer', opacity:saving?0.7:1, flexShrink:0,
}}>{saving?'…':editData?.id?'✓ Update':'↓ Speichern'}</button>
</div>
</div>
</div>
);
}
// ── Gespeicherte Berechnungen ─────────────────────────────────────────────────
export function SavedList({ toast, mobile }) {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [delId, setDelId] = useState(null);
const [editData, setEditData] = useState(null);
useEffect(() => {
api('/tools/kalkulator3d').then(setItems).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false));
}, []);
const del = async id => {
try { await api(`/tools/kalkulator3d/${id}`,{method:'DELETE'}); setItems(p=>p.filter(i=>i.id!==id)); toast('Gelöscht'); }
catch(e) { toast(e.message,'error'); } finally { setDelId(null); }
};
if (editData) return (
<Kalkulator3D toast={toast} mobile={mobile} editData={editData} onEditDone={() => {
setEditData(null);
api('/tools/kalkulator3d').then(setItems).catch(()=>{});
}} />
);
return (
<div style={{ padding: mobile?'14px 14px 90px':'36px 44px', maxWidth:900 }}>
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile?18:22, marginBottom:6 }}>Gespeichert</h1>
<p style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:11, marginBottom:18 }}>{items.length} Einträge</p>
{loading && <div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace' }}>Lädt</div>}
{!loading && items.length===0 && (
<div style={{ ...S.card, textAlign:'center', padding:40 }}>
<div style={{ fontSize:28, marginBottom:10 }}></div>
<div style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:12 }}>Noch keine Berechnungen.</div>
</div>
)}
{items.map(item => (
<div key={item.id} style={{ ...S.card, marginBottom:8, padding:'12px 14px' }}>
{/* Name + Datum */}
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:10 }}>
<div>
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:13, fontWeight:700 }}>{item.name}</div>
<div style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:10, marginTop:2 }}>
{item.gramm}g · {item.stunden}h · {item.farben} Farbe{item.farben>1?'n':''}
</div>
</div>
<div style={{ color:'rgba(255,255,255,0.2)', fontFamily:'monospace', fontSize:10 }}>
{new Date(item.created_at).toLocaleDateString('de-DE')}
</div>
</div>
{/* Preise */}
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:6, marginBottom:10 }}>
{[['💖',item.preis_freundschaft,'#ff6b9d'],['🤝',item.preis_normal,'#4ecdc4'],['💼',item.preis_auftrag,'#ffe66d']].map(([e,v,c])=>(
<div key={e} style={{ background:`${c}10`, border:`1px solid ${c}25`, borderRadius:8, padding:'8px 6px', textAlign:'center' }}>
<div style={{ fontSize:14 }}>{e}</div>
<div style={{ color:c, fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700 }}>{v.toFixed(2)}</div>
</div>
))}
</div>
{/* Aktionen */}
{delId===item.id ? (
<div style={{ display:'flex', gap:6, alignItems:'center' }}>
<span style={{ color:'rgba(255,255,255,0.35)', fontSize:11, fontFamily:'monospace', flex:1 }}>Wirklich löschen?</span>
<button onClick={()=>del(item.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>
) : (
<div style={{ display:'flex', gap:6 }}>
<button onClick={()=>setEditData(item)} style={{ ...S.btn('#4ecdc4',true), flex:1, textAlign:'center' }}> Bearbeiten</button>
<button onClick={()=>setDelId(item.id)} style={{ ...S.btn('#ff6b9d',true), flex:1, textAlign:'center' }}> Löschen</button>
</div>
)}
</div>
))}
</div>
);
}