diff --git a/frontend/src/tools/kalkulator3d.jsx b/frontend/src/tools/kalkulator3d.jsx
new file mode 100644
index 0000000..bc74a93
--- /dev/null
+++ b/frontend/src/tools/kalkulator3d.jsx
@@ -0,0 +1,426 @@
+import { useState, useMemo, useEffect, useRef } 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,
+};
+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' },
+];
+
+// ββ Bild komprimieren (client-seitig, max 800px, JPEG 0.75) ββββββββββββββββββ
+function compressImage(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = e => {
+ const img = new Image();
+ img.onload = () => {
+ const MAX = 800;
+ let w = img.width, h = img.height;
+ if (w > MAX || h > MAX) {
+ if (w > h) { h = Math.round(h * MAX / w); w = MAX; }
+ else { w = Math.round(w * MAX / h); h = MAX; }
+ }
+ const canvas = document.createElement('canvas');
+ canvas.width = w; canvas.height = h;
+ canvas.getContext('2d').drawImage(img, 0, 0, w, h);
+ resolve(canvas.toDataURL('image/jpeg', 0.75));
+ };
+ img.onerror = reject;
+ img.src = e.target.result;
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+}
+
+// ββ Stepper βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+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 }}
+ />
+
+ );
+}
+
+// ββ Bild-Modal ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function ImageModal({ src, onClose }) {
+ return (
+
+

+
+ );
+}
+
+// ββ Kostenrechner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+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 [bemerkung, setBemerkung] = useState(editData?.bemerkung ?? '');
+ const [image, setImage] = useState(editData?.image ?? null);
+ const [saving, setSaving] = useState(false);
+ const fileRef = useRef(null);
+ 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 handleImagePick = async e => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ try {
+ const compressed = await compressImage(file);
+ setImage(compressed);
+ } catch { toast('Bild konnte nicht geladen werden', 'error'); }
+ e.target.value = '';
+ };
+
+ 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,
+ image, bemerkung };
+ 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(''); setBemerkung(''); setImage(null);
+ }
+ } catch(e) { toast(e.message, 'error'); } finally { setSaving(false); }
+ };
+
+ const pad = mobile ? '14px 14px 90px' : '36px 44px';
+
+ return (
+
+
+
+ {editData &&
β {editData.name}
}
+
Kostenrechner
+
+ {editData &&
}
+
+
+ {/* Eingaben */}
+
+
+
+
+ setFarben(Math.max(1, Math.round(v)))} step={1} min={1}
+ hint={`Farbfaktor: Γ${getFarbFaktor(farben).toFixed(2)}`} />
+
+
+
+ {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 */}
+
+
+ {[['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)} β¬
+
+
+ ))}
+
+
+
+ {/* Preise */}
+
+ {STUFEN.map((s, i) => (
+
+
{s.emoji}
+
{s.label}
+
+ {(calc.grund * s.mult).toFixed(2)}β¬
+
+
Γ{s.mult}
+
+ ))}
+
+
+ {/* Speichern */}
+
+
BERECHNUNG SPEICHERN
+
+ {/* Name */}
+
+
+ setName(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && save()}
+ placeholder='z.B. "Halterung MΓΌller"'
+ style={{ ...S.inp, fontSize:15 }} />
+
+
+ {/* Bemerkung */}
+
+
+
+
+ {/* Bild */}
+
+
+
+ {image ? (
+
+

+
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+}
+
+// ββ Kostenarchiv ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export function SavedList({ toast, mobile }) {
+ const [items, setItems] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [delId, setDelId] = useState(null);
+ const [editData, setEditData] = useState(null);
+ const [search, setSearch] = useState('');
+ const [lightbox, setLightbox] = 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); }
+ };
+
+ const filtered = search.trim()
+ ? items.filter(i =>
+ i.name.toLowerCase().includes(search.toLowerCase()) ||
+ (i.bemerkung && i.bemerkung.toLowerCase().includes(search.toLowerCase()))
+ )
+ : items;
+
+ if (editData) return (
+ {
+ setEditData(null);
+ api('/tools/kalkulator3d').then(setItems).catch(() => {});
+ }} />
+ );
+
+ return (
+
+ {lightbox &&
setLightbox(null)} />}
+
+ Kostenarchiv
+
+ {/* Suche */}
+
+ β
+ setSearch(e.target.value)}
+ placeholder="Name oder Bemerkung suchenβ¦"
+ style={{ ...S.inp, paddingLeft:34, fontSize:15 }} />
+ {search && (
+
+ )}
+
+
+
+ {filtered.length} von {items.length} EintrΓ€gen{search ? ` fΓΌr β${search}"` : ''}
+
+
+ {loading && LΓ€dtβ¦
}
+
+ {!loading && items.length === 0 && (
+
+
β
+
Noch keine Berechnungen.
+
+ )}
+
+ {!loading && search && filtered.length === 0 && (
+
+ )}
+
+ {filtered.map(item => (
+
+
+
+ {/* Bild Thumbnail */}
+ {item.image && (
+

setLightbox(item.image)}
+ style={{ width:72, height:72, objectFit:'cover', borderRadius:8, flexShrink:0,
+ border:'1px solid rgba(255,255,255,0.1)', cursor:'pointer' }} />
+ )}
+
+
+ {/* Name + Datum */}
+
+
{item.name}
+
+ {new Date(item.created_at).toLocaleDateString('de-DE')}
+
+
+
+ {/* Infos */}
+
+ {item.gramm}g Β· {item.stunden}h Β· {item.farben} Farbe{item.farben > 1 ? 'n' : ''}
+
+
+ {/* Bemerkung */}
+ {item.bemerkung && (
+
+ {item.bemerkung}
+
+ )}
+
+ {/* 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?
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+ ))}
+
+ );
+}