Files
dickendock/frontend/src/tools/Kalkulator3D.jsx
2026-05-25 12:47:51 +02:00

238 lines
14 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, useRef } from 'react';
import { api, S } from '../lib.js';
// ── Konstanten ────────────────────────────────────────────────────────────────
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 FARB = { 1: 1.0, 2: 1.1, 3: 1.2, 4: 1.3 };
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' },
];
// ── Hilfselemente ─────────────────────────────────────────────────────────────
function NInput({ label, value, onChange, step = 0.01, unit }) {
return (
<div style={{ marginBottom: 12 }}>
<label style={{ ...S.head, display: 'block', marginBottom: 3 }}>
{label.toUpperCase()} {unit && <span style={{ color: 'rgba(255,255,255,0.2)' }}>({unit})</span>}
</label>
<input type="number" value={value} step={step}
onChange={e => onChange(parseFloat(e.target.value) || 0)} style={S.inp}
onFocus={e => e.target.style.borderColor = 'rgba(78,205,196,0.5)'}
onBlur={e => e.target.style.borderColor = 'rgba(255,255,255,0.1)'}
/>
</div>
);
}
// ── Kalkulator ────────────────────────────────────────────────────────────────
export default function Kalkulator3D({ toast, editData, onEditDone, onNavigate }) {
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 = FARB[farben] || 1;
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); }
};
return (
<div style={{ padding: '36px 44px', maxWidth: 860 }}>
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24 }}>
<div>
{editData && <div style={{ color: '#ffe66d', fontSize: 11, fontFamily: 'monospace', marginBottom: 5 }}> Bearbeite: {editData.name}</div>}
<h1 style={{ color: '#fff', fontFamily: "'Space Mono',monospace", fontSize: 22, margin: 0 }}>3D-Druck Kalkulator</h1>
<p style={{ color: 'rgba(255,255,255,0.25)', fontSize: 11, fontFamily: 'monospace', marginTop: 5 }}>Echtzeit-Kalkulation · 3 Preisstufen</p>
</div>
{editData && <button onClick={onEditDone} style={S.btn('#ff6b9d')}> Abbrechen</button>}
</div>
{/* Eingaben */}
<div style={{ ...S.card, marginBottom: 12 }}>
<div style={S.head}>EINGABEN</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16 }}>
<NInput label="Gewicht" value={gramm} onChange={setGramm} step={1} unit="g" />
<NInput label="Druckdauer" value={stunden} onChange={setStunden} step={0.5} unit="h" />
<div>
<label style={{ ...S.head, display: 'block', marginBottom: 3 }}>ANZAHL FARBEN</label>
<select value={farben} onChange={e => setFarben(Number(e.target.value))} style={S.inp}>
{[1,2,3,4].map(n => <option key={n} value={n} style={{ background: '#1a1a1e' }}>{n} Farbe{n>1?'n':''} (×{FARB[n].toFixed(1)})</option>)}
</select>
</div>
</div>
{/* 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: '8px 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: 14 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
<NInput label="Material €/g" value={vars.materialpreis_pro_gramm} onChange={v=>sv('materialpreis_pro_gramm',v)} step={0.001} />
<NInput label="Strom kW" value={vars.stromverbrauch_kw} onChange={v=>sv('stromverbrauch_kw',v)} step={0.01} />
<NInput label="Strom €/kWh" value={vars.strompreis_pro_kwh} onChange={v=>sv('strompreis_pro_kwh',v)} step={0.01} />
<NInput label="Druckerpreis €" value={vars.druckerpreis} onChange={v=>sv('druckerpreis',v)} step={10} />
<NInput label="Lebensdauer h" value={vars.gesamtdruckstunden} onChange={v=>sv('gesamtdruckstunden',v)} step={100} />
<NInput 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 }}> Standardwerte</button>
</div>
)}
</div>
{/* Kostenaufschlüsselung */}
<div style={{ ...S.card, display: 'flex', padding: '12px 0', marginBottom: 12 }}>
{[['Material',calc.mat,'#a78bfa'],['Strom',calc.str,'#60a5fa'],['Verschleiß',calc.ver,'#f59e0b'],['Grundkosten',calc.grund,'#4ecdc4',true]].map(([l,v,c,bold],i)=>(
<div key={l} style={{ flex:1, padding:'5px 18px', borderLeft:i>0?'1px solid rgba(255,255,255,0.05)':'none' }}>
<div style={{ color:'rgba(255,255,255,0.28)', fontSize:10, fontFamily:'monospace', marginBottom:3 }}>{l}</div>
<div style={{ color:c, fontSize:bold?16:13, fontFamily:"'Space Mono',monospace", fontWeight:bold?700:400 }}>{v.toFixed(4)} </div>
</div>
))}
</div>
{/* Preis-Cards */}
<div style={{ display: 'flex', gap: 10, marginBottom: 16, flexWrap: 'wrap' }}>
{STUFEN.map((s,i) => (
<div key={s.key} style={{ background:`${s.c}10`, border:`1px solid ${s.c}30`, borderRadius:11,
padding:'16px 20px', flex:1, minWidth:130, transform:i===1?'scale(1.04)':'scale(1)' }}>
<div style={{ fontSize:18, marginBottom:4 }}>{s.emoji}</div>
<div style={{ color:'rgba(255,255,255,0.4)', fontSize:9, fontFamily:'monospace', marginBottom:2 }}>{s.label}</div>
<div style={{ color:s.c, fontSize:24, fontFamily:"'Space Mono',monospace", fontWeight:700 }}>{(calc.grund*s.mult).toFixed(2)} </div>
<div style={{ color:'rgba(255,255,255,0.2)', fontSize:9, fontFamily:'monospace', marginTop:2 }}>×{s.mult}</div>
</div>
))}
</div>
{/* Speichern */}
<div style={S.card}>
<div style={S.head}>BERECHNUNG SPEICHERN</div>
<div style={{ display: 'flex', gap: 10 }}>
<div style={{ flex: 1 }}>
<input value={name} onChange={e=>setName(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&save()}
placeholder='z.B. "Halterung Kunde Müller"' style={S.inp} />
</div>
<button onClick={save} disabled={saving} style={{
background:'linear-gradient(135deg,#4ecdc4,#45b7d1)', border:'none', borderRadius:7,
padding:'8px 18px', color:'#0d0d0f', fontFamily:"'Space Mono',monospace",
fontWeight:700, fontSize:12, cursor:saving?'default':'pointer', opacity:saving?0.7:1,
}}>{saving?'…':editData?.id?'✓ Aktualisieren':'↓ Speichern'}</button>
</div>
</div>
</div>
);
}
// ── Gespeicherte Berechnungen ─────────────────────────────────────────────────
export function SavedList({ toast, onNavigate }) {
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} editData={editData} onEditDone={() => {
setEditData(null);
api('/tools/kalkulator3d').then(setItems).catch(()=>{});
}} />
);
return (
<div style={{ padding:'36px 44px', maxWidth:1000 }}>
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:22, marginBottom:6 }}>Gespeicherte Berechnungen</h1>
<p style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:11, marginBottom:24 }}>{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:48 }}>
<div style={{ fontSize:28, marginBottom:10 }}></div>
<div style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:12 }}>Noch keine Berechnungen gespeichert.</div>
</div>
)}
{items.map(item => (
<div key={item.id} style={{ ...S.card, display:'flex', alignItems:'center', gap:14, marginBottom:8, padding:'14px 18px' }}>
<div style={{ flex:1 }}>
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:13, fontWeight:700, marginBottom:3 }}>{item.name}</div>
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:10 }}>
{item.gramm}g · {item.stunden}h · {item.farben} Farbe{item.farben>1?'n':''} · {new Date(item.created_at).toLocaleDateString('de-DE')}
</div>
</div>
<div style={{ display:'flex', gap:12 }}>
{[['💖',item.preis_freundschaft,'#ff6b9d'],['🤝',item.preis_normal,'#4ecdc4'],['💼',item.preis_auftrag,'#ffe66d']].map(([e,v,c])=>(
<div key={e} style={{ textAlign:'center' }}>
<div style={{ fontSize:11 }}>{e}</div>
<div style={{ color:c, fontFamily:"'Space Mono',monospace", fontSize:12, fontWeight:700 }}>{v.toFixed(2)}</div>
</div>
))}
</div>
<div style={{ display:'flex', gap:6 }}>
<button onClick={()=>setEditData(item)} style={S.btn('#4ecdc4',true)}> Bearbeiten</button>
{delId===item.id ? (
<div style={{ display:'flex', gap:5, alignItems:'center' }}>
<span style={{ color:'rgba(255,255,255,0.35)', fontSize:10, fontFamily:'monospace' }}>Sicher?</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>
) : (
<button onClick={()=>setDelId(item.id)} style={S.btn('#ff6b9d',true)}> Löschen</button>
)}
</div>
</div>
))}
</div>
);
}