From 246a7375c6fe8142b1b46c9f3ff35a1bf6e1c228 Mon Sep 17 00:00:00 2001 From: Dicken Date: Mon, 25 May 2026 21:30:03 +0200 Subject: [PATCH] Dateien nach "frontend/src/tools" hochladen --- frontend/src/tools/kalkulator3d.jsx | 285 ++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 frontend/src/tools/kalkulator3d.jsx diff --git a/frontend/src/tools/kalkulator3d.jsx b/frontend/src/tools/kalkulator3d.jsx new file mode 100644 index 0000000..6e7c267 --- /dev/null +++ b/frontend/src/tools/kalkulator3d.jsx @@ -0,0 +1,285 @@ +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 ( +
+
+ {label.toUpperCase()}{unit ? ` (${unit})` : ''} +
+
+ + 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' }} + /> + +
+ {hint &&
{hint}
} +
+ ); +} + +function SmallInput({ label, value, onChange, step=0.01 }) { + return ( +
+
{label}
+ onChange(parseFloat(e.target.value)||0)} + style={{ ...S.inp, padding:'7px 8px', fontSize:13 }} + /> +
+ ); +} + +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 ( +
+ + {/* Header */} +
+
+ {editData &&
✎ {editData.name}
} +

3D-Kalkulator

+
+ {editData && } +
+ + {/* Haupteingaben */} +
+
+ + +
+ + setFarben(Math.max(1, Math.round(v)))} + step={1} min={1} + hint={`Farbfaktor: ×${getFarbFaktor(farben).toFixed(2)}`} /> + + {/* Erweiterte Einstellungen */} + + {showAdv && ( +
+
+ sv('materialpreis_pro_gramm',v)} step={0.001}/> + sv('stromverbrauch_kw',v)} step={0.01}/> + sv('strompreis_pro_kwh',v)} step={0.01}/> + sv('druckerpreis',v)} step={10}/> + sv('gesamtdruckstunden',v)} step={100}/> + sv('verschleiss_pro_stunde',v)} step={0.01}/> +
+ +
+ )} +
+ + {/* Ergebnis – kompakt */} +
+
+ {[['Material',calc.mat,'#a78bfa'],['Strom',calc.str,'#60a5fa'],['Verschleiß',calc.ver,'#f59e0b'],['Grundkosten',calc.grund,'#4ecdc4']].map(([l,v,c],i)=>( +
+
{l.toUpperCase()}
+
+ {v.toFixed(2)} € +
+
+ ))} +
+
+ + {/* Preis-Cards */} +
+ {STUFEN.map((s,i) => ( +
+
{s.emoji}
+
{s.label}
+
+ {(calc.grund*s.mult).toFixed(2)}€ +
+
×{s.mult}
+
+ ))} +
+ + {/* Speichern */} +
+
+ setName(e.target.value)} + onKeyDown={e=>e.key==='Enter'&&save()} + placeholder='Name z.B. "Halterung Müller"' + style={{ ...S.inp, flex:1, fontSize:15 }} + /> + +
+
+
+ ); +} + +// ── 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 ( + { + setEditData(null); + api('/tools/kalkulator3d').then(setItems).catch(()=>{}); + }} /> + ); + + return ( +
+

Gespeichert

+

{items.length} Einträge

+ + {loading &&
Lädt…
} + + {!loading && items.length===0 && ( +
+
+
Noch keine Berechnungen.
+
+ )} + + {items.map(item => ( +
+ {/* Name + Datum */} +
+
+
{item.name}
+
+ {item.gramm}g · {item.stunden}h · {item.farben} Farbe{item.farben>1?'n':''} +
+
+
+ {new Date(item.created_at).toLocaleDateString('de-DE')} +
+
+ + {/* Preise */} +
+ {[['💖',item.preis_freundschaft,'#ff6b9d'],['🤝',item.preis_normal,'#4ecdc4'],['💼',item.preis_auftrag,'#ffe66d']].map(([e,v,c])=>( +
+
{e}
+
{v.toFixed(2)}€
+
+ ))} +
+ + {/* Aktionen */} + {delId===item.id ? ( +
+ Wirklich löschen? + + +
+ ) : ( +
+ + +
+ )} +
+ ))} +
+ ); +}