/** * 3D-Druck Kostenkalkulation — Frontend-Logik. * * - Live-Berechnung bei jeder Eingabe (via POST /calculate) * - Automatisches Speichern in YAML (debounced 500ms via POST /save_config) * - Dark-Mode-Umschaltung (persistiert in YAML) * - Export als TXT (POST /export -> Blob-Download) */ let saveTimeout = null; /** Sammelt alle Formularwerte anhand ihrer data-var Attribute. */ function getFormData() { const data = {}; document.querySelectorAll('[data-var]').forEach(el => { const key = el.dataset.var; const type = el.dataset.type || 'text'; let val = el.value; if (type === 'number') val = parseFloat(val) || 0; else if (type === 'checkbox') val = el.checked; data[key] = val; }); data.dark_mode = document.documentElement.getAttribute('data-bs-theme') === 'dark'; return data; } /** Sendet Formulardaten an /calculate und aktualisiert die Ergebnisanzeige. */ async function recalculate() { const data = getFormData(); try { const res = await fetch('/calculate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); const calc = await res.json(); renderResult(calc); } catch (e) { console.error('Calculate error', e); } } /** Baut die textuelle Ergebnisanzeige aus den JSON-Berechnungswerten. */ function renderResult(calc) { const lines = [ `Materialkosten: ${String(calc.materialkosten.toFixed(2)).padStart(10)} EUR`, `Stromkosten: ${String(calc.stromkosten.toFixed(2)).padStart(10)} EUR`, `Abschreibung Drucker: ${String(calc.abschreibung.toFixed(2)).padStart(10)} EUR`, `Arbeitskosten: ${String(calc.arbeitskosten.toFixed(2)).padStart(10)} EUR`, `${'\u2500'.repeat(40)}`, `Gesamtkosten: ${String(calc.gesamtkosten_netto.toFixed(2)).padStart(10)} EUR`, '', `Gewinn (${((document.querySelector('[data-var="gewinnmarge"]')?.value) || 20)}%): ${String(calc.gewinn.toFixed(2)).padStart(10)} EUR`, `${'\u2500'.repeat(40)}`, `Verkaufspreis (netto): ${String(calc.verkaufspreis.toFixed(2)).padStart(10)} EUR`, ]; if (calc.mwst_ausweisen) { lines.push(`MwSt. (19%): ${String(calc.mwst.toFixed(2)).padStart(10)} EUR`); } lines.push(`${'\u2550'.repeat(40)}`); lines.push(`GESAMT: ${String(calc.gesamt_brutto.toFixed(2)).padStart(10)} EUR`); document.getElementById('result-text').textContent = lines.join('\n'); const details = [ `Druckzeit: ${calc.druckzeit_stunden} Stunden`, `Materialgewicht: ${calc.material_kg} kg`, `Abschreibung pro Stunde: ${calc.abschreibung_pro_stunde} EUR`, `Kosten pro Gramm: ${calc.kosten_pro_gramm} EUR`, `Stromkosten pro Stunde: ${calc.strom_pro_stunde} EUR`, ]; document.getElementById('detail-text').textContent = details.join('\n'); } /** Plant ein Auto-Save in die YAML-Konfiguration (debounced 500ms). */ function scheduleSave() { clearTimeout(saveTimeout); saveTimeout = setTimeout(async () => { const data = getFormData(); try { await fetch('/save_config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } catch (e) { console.error('Save error', e); } }, 500); } /** Wird bei jeder Eingabe aufgerufen: speichert + berechnet neu. */ async function onInputChange() { scheduleSave(); await recalculate(); } /** Schaltet zwischen Light- und Dark-Mode um und persistiert die Einstellung. */ function toggleDarkMode() { const html = document.documentElement; const isDark = html.getAttribute('data-bs-theme') === 'dark'; const newTheme = isDark ? 'light' : 'dark'; html.setAttribute('data-bs-theme', newTheme); const btn = document.getElementById('dark-mode-btn'); const icon = btn.querySelector('i'); icon.className = `ti ti-${isDark ? 'sun' : 'moon'} me-1`; btn.childNodes[2].textContent = isDark ? 'Light' : 'Dark'; scheduleSave(); } /** Sendet die aktuellen Werte an /export und lost einen TXT-Download aus. */ async function exportData() { const data = getFormData(); try { const res = await fetch('/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'kalkulation.txt'; a.click(); URL.revokeObjectURL(url); showNotification('Export erfolgreich!'); } catch (e) { console.error('Export error', e); } } /** Zeigt eine kurze Toast-Notification oben rechts an. */ function showNotification(msg) { const container = document.getElementById('notification'); document.getElementById('notification-msg').textContent = msg; container.style.display = 'block'; setTimeout(() => { container.style.display = 'none'; }, 2500); } /** Logo hochladen via AJAX. */ async function uploadLogo() { const input = document.getElementById('logo-upload'); if (!input.files || !input.files[0]) { showNotification('Bitte eine Datei auswaehlen'); return; } const formData = new FormData(); formData.append('logo', input.files[0]); try { const res = await fetch('/upload_logo', { method: 'POST', body: formData }); const data = await res.json(); if (data.status === 'ok') { showNotification('Logo gespeichert!'); setTimeout(() => location.reload(), 800); } } catch (e) { showNotification('Fehler beim Hochladen'); } } /** Logo loeschen. */ async function deleteLogo() { try { const res = await fetch('/delete_logo', { method: 'POST' }); const data = await res.json(); if (data.status === 'ok') { showNotification('Logo entfernt'); setTimeout(() => location.reload(), 800); } } catch (e) { showNotification('Fehler beim Loeschen'); } } /** Initiale Berechnung beim Seitenladen. */ document.addEventListener('DOMContentLoaded', () => { recalculate(); });