/** * 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; let calcTimeout = 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. * Auf Seiten ohne Ergebnisblock (Einstellungen, Rechnung) entfaellt beides. */ async function recalculate() { if (!document.getElementById('result-text')) return; 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 ziel = document.getElementById('result-text'); if (!ziel) return; const eur = (v) => `${String(v.toFixed(2)).padStart(10)} EUR`; const marge = `Gewinn (${calc.gewinnmarge}%):`.padEnd(23); const lines = [ `PRO STUCK`, `Materialkosten: ${eur(calc.materialkosten)}`, `Stromkosten: ${eur(calc.stromkosten)}`, `Abschreibung Drucker: ${eur(calc.abschreibung)}`, `Arbeitskosten: ${eur(calc.arbeitskosten)}`, `${'\u2500'.repeat(40)}`, `Gesamtkosten: ${eur(calc.gesamtkosten_netto)}`, '', `${marge}${eur(calc.gewinn)}`, `${'\u2500'.repeat(40)}`, `Verkaufspreis (netto): ${eur(calc.verkaufspreis)}`, ]; if (calc.mwst_ausweisen) { lines.push(`MwSt. (19%): ${eur(calc.mwst)}`); } lines.push(`Preis pro Stuck: ${eur(calc.gesamt_brutto)}`); // Auftragssumme: bei Stueckzahl 1 identisch, aber explizit ausgewiesen, // damit der Endbetrag nicht mit dem Stueckpreis verwechselt wird. lines.push(''); lines.push(`AUFTRAG (${calc.stueckzahl} Stuck)`); lines.push(`Auftrag netto: ${eur(calc.gesamt_netto)}`); if (calc.mwst_ausweisen) { lines.push(`MwSt. (19%): ${eur(calc.gesamt_mwst)}`); } lines.push(`${'\u2550'.repeat(40)}`); lines.push(`GESAMT: ${eur(calc.gesamt_endpreis)}`); ziel.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`, ]; const detailZiel = document.getElementById('detail-text'); if (detailZiel) detailZiel.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. * Beides entprellt, damit Tippen nicht pro Zeichen einen Request ausloest. */ function onInputChange() { scheduleSave(); clearTimeout(calcTimeout); calcTimeout = setTimeout(recalculate, 150); } /** 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); // Icon spiegelt den NEUEN Zustand: dunkel -> Sonne (zum Zurueckschalten). const icon = document.getElementById('dark-mode-icon'); if (icon) { icon.className = `ti ti-${newTheme === 'dark' ? 'sun' : 'moon'} icon icon-1`; } 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(); });