Flask-Web-App statt Tkinter: Live-Berechnung, PDF-Rechnung, Dark Mode, Tabler CSS

This commit is contained in:
Erik Thiele
2026-06-30 20:46:52 +02:00
parent fd04d3dace
commit 73631b415d
9 changed files with 1690 additions and 0 deletions

142
static/script.js Normal file
View File

@@ -0,0 +1,142 @@
/**
* 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;
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`,
`MwSt. (19%): ${String(calc.mwst.toFixed(2)).padStart(10)} EUR`,
`${'\u2550'.repeat(40)}`,
`GESAMT (brutto): ${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);
}
/** Initiale Berechnung beim Seitenladen. */
document.addEventListener('DOMContentLoaded', () => {
recalculate();
});

13
static/style.css Normal file
View File

@@ -0,0 +1,13 @@
/* Grossere Schrift in Ueberschriften */
.card-header, .navbar-brand span {
font-size: 1.1rem;
}
/* Monospace-Ergebnisblock (Kostenaufstellung) */
.result-box {
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
line-height: 1.8;
white-space: pre;
overflow-x: auto;
}