Files
3ddruckkalkulation/templates/index.html
Erik Thiele c42fecacb9 Mehrere Positionen pro Auftrag und Projektverwaltung
Bisher liess sich pro Rechnung nur ein Teil kalkulieren. Ein Objekt aus
mehreren Teilen musste in getrennten Rechnungen abgebildet werden.

Positionen:
- Die Kalkulation arbeitet mit einer Positionsliste statt einem
  Einzelteil. Bezeichnung, Material, Preis/kg, Druckzeit, Arbeitszeit
  und Stueckzahl gelten je Position -- verschiedene Teile nutzen oft
  unterschiedliche Filamente und brauchen unterschiedlich viel Nacharbeit.
- Drucker, Strom, Stundenlohn, Marge und Steuer bleiben auftragsweit.
- Jede Position wird eine eigene Rechnungszeile mit Menge, Einzelpreis
  und Zeilensumme. Bei vielen Positionen bricht das PDF auf weitere
  Seiten um.
- Die Auftragssumme ist die Summe der bereits gerundeten Zeilen, damit
  die Rechnung zeilenweise nachrechenbar bleibt.
- saubere_positionen() normalisiert jede Liste aus Config oder Client
  (Typen, Wertebereiche, Obergrenze 50); es bleibt immer mindestens
  eine Position bestehen.
- Alte Configs mit den Flachfeldern werden in eine einzelne Position
  migriert, die Altfelder entfernt.
- sammlePositionen() liefert null auf Seiten ohne Positionsliste, damit
  ein Tastendruck auf der Einstellungs- oder Rechnungsseite die
  Positionen nicht loescht.

Projekte:
- Benannte Kalkulationen lassen sich speichern und wieder laden,
  Ablage in druckkalkulation_projekte.yaml (ein Mapping fuer alle
  Projekte, Schluessel ist der Projektname).
- Gesichert werden nur PROJEKT_FELDER: Name, Positionen, Kundendaten,
  Rechnungsnummer, Rechnungs- und Leistungsdatum. Betriebswerte gehoeren
  bewusst nicht dazu -- sonst wuerde das Laden eines alten Projekts den
  aktuellen Stundenlohn oder das Steuerregime ueberschreiben.
- Neue Routen: GET /projekte sowie POST /projekte/{speichern,laden,loeschen}.
- Eigenes Lock, gleiche atomare Schreibtechnik wie die Config.
- Ueberschreiben, Laden bei ungespeicherten Aenderungen und Loeschen
  fragen jeweils nach.
- Der offene Autosave-Debounce wird vor dem Speichern durchgedrueckt,
  damit wirklich der aktuelle Stand ins Projekt geht.

Die beiden Teile stehen in einem Commit, weil die Aenderungen in app.py,
script.js und index.html ineinandergreifen und ein aufgetrennter
Zwischenstand nicht als solcher getestet wurde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 11:38:26 +02:00

163 lines
8.5 KiB
HTML

<!DOCTYPE html>
<html lang="de" data-bs-theme="{% if config.dark_mode %}dark{% else %}light{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>3D-Druck Kostenkalkulation</title>
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<style>
@import url("https://rsms.me/inter/inter.css");
</style>
</head>
<body>
<div class="page">
<header class="navbar navbar-expand-md">
<div class="container-xl">
<div class="navbar-brand navbar-brand-autodark pe-0 pe-md-3 py-2 d-flex align-items-center gap-2">
<span class="fw-bold">JET Design</span>
<span class="text-secondary" style="opacity:0.5;">|</span>
<span class="fw-semibold">3D Druck Kostenkalkulator</span>
</div>
<div class="navbar-nav flex-row order-md-last">
<div class="nav-item me-3">
<a href="{{ url_for('settings') }}" class="nav-link px-0" title="Einstellungen">
<i class="ti ti-settings icon icon-1"></i>
</a>
</div>
<div class="nav-item">
<button class="nav-link px-0" id="dark-mode-btn" onclick="toggleDarkMode()" title="Dark Mode umschalten">
<i class="ti ti-{% if config.dark_mode %}sun{% else %}moon{% endif %} icon icon-1" id="dark-mode-icon"></i>
</button>
</div>
</div>
</div>
</header>
<div class="container-xl my-4">
<div class="row g-4">
<!-- Linke Spalte: Formular -->
<div class="col-lg-6">
<form id="calc-form" onchange="onInputChange()" onkeyup="onInputChange()">
<div class="card mb-3">
<div class="card-header"><i class="ti ti-info-circle me-2"></i>Projekt</div>
<div class="card-body">
<label class="form-label">Projektname</label>
<div class="input-group">
<input type="text" class="form-control" data-var="projektName" id="projekt-name" value="{{ config.projektName }}">
<button type="button" class="btn btn-primary" onclick="projektSpeichern()" title="Projekt unter diesem Namen speichern">
<i class="ti ti-device-floppy me-1"></i>Speichern
</button>
</div>
<div class="form-text">Übergreifender Name des Auftrags. Die einzelnen Teile werden unten als Positionen erfasst.</div>
<hr class="my-3">
<label class="form-label">Gespeicherte Projekte</label>
<div class="input-group">
<select class="form-select" id="projekt-auswahl"></select>
<button type="button" class="btn btn-outline-primary" onclick="projektLaden()" title="Ausgewähltes Projekt laden">
<i class="ti ti-folder-open me-1"></i>Laden
</button>
<button type="button" class="btn btn-outline-danger" onclick="projektLoeschen()" title="Ausgewähltes Projekt löschen">
<i class="ti ti-trash"></i>
</button>
</div>
<div class="form-text" id="projekt-info">Noch keine Projekte gespeichert.</div>
</div>
</div>
<div id="positionen-liste"></div>
<button type="button" class="btn btn-outline-primary w-100 mb-3" onclick="positionHinzufuegen()">
<i class="ti ti-plus me-1"></i>Position hinzufügen
</button>
</form>
</div>
<!-- Rechte Spalte: Ergebnisse -->
<div class="col-lg-6">
<div class="card mb-3">
<div class="card-header"><i class="ti ti-chart-bar me-2"></i>Kostenaufstellung</div>
<div class="card-body p-0">
<div class="result-box p-3" id="result-text"></div>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><i class="ti ti-info-square me-2"></i>Detaillierte Informationen</div>
<div class="card-body text-secondary" id="detail-text" style="font-size:0.9rem;white-space:pre"></div>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('invoice') }}" class="btn btn-primary flex-fill">
<i class="ti ti-file-invoice me-1"></i>Rechnung
</a>
<button class="btn btn-outline-primary flex-fill" onclick="exportData()">
<i class="ti ti-download me-1"></i>Export
</button>
</div>
</div>
</div>
</div>
<div class="toast-container position-fixed top-0 end-0 p-3" id="notification" style="z-index:9999;display:none">
<div class="toast show bg-primary text-white">
<div class="toast-body fw-semibold" id="notification-msg"></div>
</div>
</div>
</div>
<template id="position-vorlage">
<div class="card mb-3 position-karte">
<div class="card-header d-flex align-items-center">
<i class="ti ti-box me-2"></i>
<span class="flex-fill">Position <span class="position-nummer"></span></span>
<button type="button" class="btn btn-sm btn-ghost-danger position-entfernen" title="Position entfernen">
<i class="ti ti-trash"></i>
</button>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label">Bezeichnung</label>
<input type="text" class="form-control" data-pos-var="name" placeholder="z.B. Halterung links">
</div>
<div class="col-md-6">
<label class="form-label">Gewicht (Gramm)</label>
<input type="number" step="0.01" min="0" class="form-control" data-pos-var="materialGewicht" data-type="number">
</div>
<div class="col-md-6">
<label class="form-label">Preis pro kg (EUR)</label>
<input type="number" step="0.01" min="0" class="form-control" data-pos-var="materialPreisProKg" data-type="number">
</div>
<div class="col-md-4">
<label class="form-label">Druckzeit (Min.)</label>
<input type="number" step="1" min="0" class="form-control" data-pos-var="druckzeit" data-type="number">
</div>
<div class="col-md-4">
<label class="form-label">Arbeitszeit (Min.)</label>
<input type="number" step="1" min="0" class="form-control" data-pos-var="arbeitszeit" data-type="number">
</div>
<div class="col-md-4">
<label class="form-label">Stückzahl</label>
<input type="number" step="1" min="1" class="form-control" data-pos-var="stueckzahl" data-type="number">
</div>
</div>
</div>
</div>
</template>
<script id="positionen-daten" type="application/json">{{ config.positionen|tojson }}</script>
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/js/tabler.min.js"></script>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>