Files
timer-app/frontend/index.html
T
Kerim (portable/claudecode)andClaude Opus 5 70756f3762 Bon fuer die Kasse auf einem USB-Bondrucker ausgeben
Der Kunde soll mit einem Zettel zur Kasse gehen koennen, auf dem steht, was
er zahlen muss. Drei Wege fuehren jetzt zu einem Bon: der Knopf "Bon" auf der
Timer-Karte druckt den Zwischenstand und laesst den Timer weiterlaufen,
"Beenden" druckt den Endbeleg, und in der Historie laesst sich jeder Beleg
nachdrucken.

Der Drucker haengt per USB an einem Raspberry Pi und ist damit vom
Timer-Server aus nicht direkt erreichbar. Auf dem Pi laeuft deshalb eine
kleine Bruecke (deploy/bondrucker/, nur Python-Standardbibliothek), die
fertige ESC/POS-Daten per HTTP annimmt. Das Bon-Layout bleibt bewusst hier
im Repo, damit Aenderungen daran nicht auf dem Pi angefasst werden muessen.

Beim Beenden wird erst gespeichert und dann gedruckt. Eine leere Papierrolle
ist damit ein Papierproblem und kein Datenproblem - der Vorgang steht in der
Historie, die Oberflaeche sagt ausdruecklich, dass der Bon nicht gedruckt
wurde, und nennt den Grund.

Ohne PRINTER_HOST in der .env ist die Druckfunktion aus und die App verhaelt
sich unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 20:37:12 +02:00

840 lines
29 KiB
HTML
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.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Timer - Zeiterfassung</title>
<link rel="icon" type="image/jpeg" href="/favicon.jpg">
<style>
/* Bewusst KEIN Google-Fonts-Import: kein externer Abruf zur Laufzeit.
Der System-Font-Stack sieht praktisch identisch aus (Segoe UI /
Roboto / SF Pro). Wer Inter zwingend will: siehe DEPLOY.md. */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #2d2d44 50%, #1a1a2e 100%);
min-height: 100vh;
color: #ffffff;
display: flex;
flex-direction: column;
position: relative;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(45, 27, 105, 0.1) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
@keyframes pulse {
0%, 100% { transform: scale(1) rotate(0deg); opacity: 0.3; }
50% { transform: scale(1.1) rotate(180deg); opacity: 0.5; }
}
@media (prefers-reduced-motion: reduce) {
body::before { animation: none; }
.view { animation: none; }
}
.container {
position: relative;
z-index: 1;
flex: 1;
display: flex;
flex-direction: column;
}
header {
padding: 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
background: rgba(26, 26, 46, 0.8);
border-bottom: 1px solid rgba(45, 27, 105, 0.3);
}
.logo {
height: 50px;
width: auto;
object-fit: contain;
}
.header-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.header-user {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.45rem 0.85rem;
border-radius: 10px;
background: rgba(45, 27, 105, 0.35);
border: 1px solid rgba(255, 255, 255, 0.12);
font-size: 0.9rem;
font-weight: 600;
white-space: nowrap;
}
.header-user::before { content: '👤'; }
.header-user:empty { display: none; }
.btn {
padding: 0.625rem 1.5rem;
background: linear-gradient(135deg, #2D1B69, #3d2589);
color: white;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
font-family: inherit;
box-shadow: 0 4px 15px rgba(45, 27, 105, 0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(45, 27, 105, 0.5);
background: linear-gradient(135deg, #3d2589, #4d35a9);
}
.btn:active { transform: translateY(0); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn:focus-visible {
outline: 2px solid #a8a8ff;
outline-offset: 2px;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
box-shadow: none;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: none;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
}
.btn-danger { background: linear-gradient(135deg, #8B1538, #a01848); }
.btn-danger:hover { background: linear-gradient(135deg, #a01848, #b02058); }
.btn-start { background: linear-gradient(135deg, #10b981, #059669); }
.btn-start:hover { background: linear-gradient(135deg, #059669, #047857); }
.btn-stop { background: linear-gradient(135deg, #f59e0b, #d97706); }
.btn-stop:hover { background: linear-gradient(135deg, #d97706, #b45309); }
main {
flex: 1;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 2rem;
}
.view {
display: none;
width: 100%;
max-width: 1200px;
animation: fadeIn 0.4s ease-out;
}
.view.active { display: block; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ---------- Login ---------- */
#loginView { display: none; }
#loginView.active { display: flex; align-items: center; justify-content: center; min-height: 70vh; }
.login-container {
max-width: 450px;
width: 100%;
margin: 0 auto;
background: rgba(26, 26, 46, 0.8);
border-radius: 20px;
padding: 3rem;
border: 1px solid rgba(45, 27, 105, 0.3);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.login-logo {
width: 120px;
height: 120px;
margin: 0 auto 2rem;
display: block;
border-radius: 50%;
}
.login-container h1 {
text-align: center;
margin-bottom: 2rem;
font-size: 1.8rem;
font-weight: 600;
background: linear-gradient(135deg, #ffffff, #a8a8ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.form-group { margin-bottom: 1.5rem; }
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.9rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.8);
}
input[type="text"],
input[type="password"],
input[type="number"],
input[type="date"],
textarea {
width: 100%;
padding: 0.875rem 1.125rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
font-size: 1rem;
color: white;
font-family: inherit;
transition: all 0.3s ease;
}
input:focus,
textarea:focus {
outline: none;
border-color: #4d35a9;
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 0 0 3px rgba(45, 27, 105, 0.35);
}
input::placeholder,
textarea::placeholder { color: rgba(255, 255, 255, 0.35); }
/* Datumsfelder im Dark Theme lesbar halten */
input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1); opacity: 0.6; cursor: pointer; }
.error-message {
color: #ff8787;
font-size: 0.875rem;
margin-top: 0.5rem;
display: none;
}
.error-message.show { display: block; }
/* ---------- Timer-Uebersicht ---------- */
.timer-toolbar {
background: rgba(26, 26, 46, 0.6);
border-radius: 20px;
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid rgba(45, 27, 105, 0.3);
}
.timer-toolbar form {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.timer-toolbar input {
flex: 1;
min-width: 220px;
}
.timer-toolbar .btn { white-space: nowrap; }
.rate-hint {
margin-top: 1rem;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.6);
}
.timer-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.timer-card {
background: rgba(26, 26, 46, 0.6);
border-radius: 20px;
padding: 1.75rem;
border: 2px solid rgba(45, 27, 105, 0.4);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
text-align: center;
transition: border-color 0.3s ease;
}
.timer-card.running { border-color: rgba(16, 185, 129, 0.45); }
.timer-card.paused { border-color: rgba(245, 158, 11, 0.45); }
.timer-card-head {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.25rem;
}
.timer-name {
flex: 1;
padding: 0.5rem 0.75rem !important;
font-size: 0.95rem !important;
font-weight: 500;
text-align: left;
}
.timer-badge {
font-size: 0.75rem;
font-weight: 600;
padding: 0.35rem 0.6rem;
border-radius: 6px;
white-space: nowrap;
letter-spacing: 0.03em;
}
.timer-card.running .timer-badge { background: rgba(16, 185, 129, 0.18); color: #6ee7b7; }
.timer-card.paused .timer-badge { background: rgba(245, 158, 11, 0.18); color: #fcd34d; }
.timer-card-time {
font-size: 3.2rem;
font-weight: 300;
letter-spacing: 0.03em;
font-variant-numeric: tabular-nums;
margin-bottom: 0.75rem;
background: linear-gradient(135deg, #ffffff, #c8c8ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.timer-card-amount {
font-size: 1.8rem;
font-weight: 600;
color: #ffffff;
background: linear-gradient(135deg, #2D1B69, #3d2589);
padding: 0.6rem 1.5rem;
border-radius: 12px;
display: inline-block;
min-width: 140px;
border: 2px solid rgba(255, 255, 255, 0.18);
font-variant-numeric: tabular-nums;
}
.timer-card-since {
margin-top: 0.75rem;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.45);
}
.timer-card-actions {
display: flex;
gap: 0.5rem;
justify-content: center;
margin-top: 1.5rem;
flex-wrap: wrap;
}
/* ---------- Historie ---------- */
.history-container,
.settings-container {
background: rgba(26, 26, 46, 0.6);
border-radius: 20px;
padding: 2rem;
border: 1px solid rgba(45, 27, 105, 0.3);
}
.settings-container { max-width: 700px; margin: 0 auto; padding: 2.5rem; }
.history-container h2,
.settings-container h2 {
margin-bottom: 1.5rem;
font-size: 2rem;
font-weight: 600;
background: linear-gradient(135deg, #ffffff, #a8a8ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.filter-bar {
display: flex;
gap: 1rem;
align-items: flex-end;
flex-wrap: wrap;
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.filter-field { display: flex; flex-direction: column; gap: 0.4rem; }
.filter-field label {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.6);
font-weight: 500;
}
.filter-field input { min-width: 150px; padding: 0.6rem 0.9rem !important; font-size: 0.9rem !important; }
.filter-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.history-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: rgba(45, 27, 105, 0.2);
padding: 1.5rem;
border-radius: 15px;
border: 1px solid rgba(45, 27, 105, 0.3);
}
.stat-card h3 {
font-size: 0.8rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-card p {
font-size: 1.9rem;
font-weight: 600;
color: #ffffff;
font-variant-numeric: tabular-nums;
}
.history-list { max-height: 55vh; overflow-y: auto; }
.history-item {
background: rgba(255, 255, 255, 0.05);
padding: 1.1rem 1.25rem;
border-radius: 12px;
margin-bottom: 0.75rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.25s ease;
}
.history-item:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(45, 27, 105, 0.4);
}
.history-item-info { flex: 1; min-width: 0; }
.history-item-name {
display: block;
font-size: 1.05rem;
font-weight: 600;
margin-bottom: 0.15rem;
}
.history-item-name.muted { color: rgba(255, 255, 255, 0.4); font-weight: 400; font-style: italic; }
.history-item-date { font-size: 0.85rem; color: rgba(255, 255, 255, 0.55); }
.history-item-duration {
font-size: 1.05rem;
font-weight: 500;
font-variant-numeric: tabular-nums;
margin-top: 0.15rem;
}
.history-item-right { display: flex; align-items: center; gap: 0.75rem; }
.history-item-amount {
font-size: 1.4rem;
font-weight: 600;
background: linear-gradient(135deg, #2D1B69, #3d2589);
padding: 0.5rem 1rem;
border-radius: 8px;
min-width: 100px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.15);
font-variant-numeric: tabular-nums;
}
.history-footer { text-align: center; margin-top: 1rem; }
.empty-state {
text-align: center;
padding: 3rem;
color: rgba(255, 255, 255, 0.5);
}
.empty-state-icon { font-size: 4rem; margin-bottom: 1rem; opacity: 0.3; }
/* ---------- Einstellungen ---------- */
.settings-description { color: rgba(255, 255, 255, 0.7); margin-bottom: 2rem; font-size: 0.95rem; }
.settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 2rem;
}
.form-group small {
display: block;
margin-top: 0.5rem;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.5);
}
.settings-preview {
background: rgba(45, 27, 105, 0.2);
padding: 1.5rem;
border-radius: 15px;
margin-bottom: 2rem;
border: 1px solid rgba(45, 27, 105, 0.3);
text-align: center;
}
.settings-preview h3 {
font-size: 0.9rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.settings-preview p { font-size: 1.7rem; font-weight: 600; margin-bottom: 0.5rem; }
.preview-examples {
font-size: 0.95rem !important;
font-weight: 400 !important;
color: rgba(255, 255, 255, 0.7) !important;
margin-top: 1rem !important;
}
/* ---------- Bondrucker ---------- */
.settings-section {
border-top: 1px solid rgba(255, 255, 255, 0.12);
margin: 2rem 0;
padding-top: 1.5rem;
}
.settings-section h3 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.35rem;
}
.settings-section > p {
color: rgba(255, 255, 255, 0.7);
font-size: 0.9rem;
margin-bottom: 1.25rem;
}
.settings-section textarea {
box-sizing: border-box;
resize: vertical;
min-height: 4.5rem;
line-height: 1.5;
}
.form-group + .form-group { margin-top: 1.25rem; }
.checkbox-row {
display: flex;
align-items: flex-start;
gap: 0.75rem;
margin-top: 1.25rem;
}
.checkbox-row input[type="checkbox"] {
width: 1.15rem;
height: 1.15rem;
margin-top: 0.15rem;
flex-shrink: 0;
accent-color: #6d4aff;
}
.checkbox-row label { font-weight: 500; cursor: pointer; }
.checkbox-row small { display: block; color: rgba(255, 255, 255, 0.5); font-size: 0.8rem; margin-top: 0.25rem; }
.printer-state {
font-size: 0.9rem;
padding: 0.75rem 1rem;
border-radius: 10px;
margin-bottom: 1rem;
background: rgba(255, 255, 255, 0.05);
border-left: 3px solid rgba(255, 255, 255, 0.25);
}
.printer-state.ok { border-left-color: #22c55e; }
.printer-state.error { border-left-color: #ef4444; }
.printer-state.off { border-left-color: rgba(255, 255, 255, 0.2); color: rgba(255, 255, 255, 0.6); }
.settings-actions { display: flex; gap: 1rem; margin-bottom: 1.5rem; }
.settings-actions .btn { flex: 1; padding: 1rem; }
.settings-info {
background: rgba(255, 255, 255, 0.05);
padding: 1rem;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.settings-info p { font-size: 0.875rem; color: rgba(255, 255, 255, 0.7); margin: 0; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%) translateY(200%);
background: rgba(26, 26, 46, 0.97);
border: 1px solid rgba(45, 27, 105, 0.6);
color: #fff;
padding: 1rem 1.75rem;
border-radius: 12px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
z-index: 100;
transition: transform 0.35s ease, opacity 0.35s ease;
opacity: 0;
max-width: 90vw;
font-size: 0.95rem;
text-align: center;
/* Meldungen zum Bondruck sind zweizeilig - Umbrueche sollen stehen bleiben. */
white-space: pre-line;
}
.toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
.toast.success { border-color: rgba(16, 185, 129, 0.7); }
.toast.error { border-color: rgba(220, 38, 38, 0.8); }
/* ---------- Mobil ---------- */
@media (max-width: 768px) {
header { padding: 1rem; }
main { padding: 1rem; }
.logo { height: 38px; }
.header-actions { gap: 0.5rem; width: 100%; }
.header-actions .btn { flex: 1; padding: 0.6rem 0.75rem; font-size: 0.85rem; }
.login-container { padding: 2rem 1.5rem; }
.timer-grid { grid-template-columns: 1fr; }
.timer-card-time { font-size: 2.6rem; }
.settings-grid { grid-template-columns: 1fr; }
.settings-actions { flex-direction: column; }
.history-item { flex-direction: column; align-items: stretch; text-align: center; }
.history-item-right { justify-content: center; }
.filter-field, .filter-field input { width: 100%; }
}
</style>
</head>
<body>
<div class="container">
<header id="header" style="display: none;">
<img src="/logo.png" alt="Logo" class="logo">
<div class="header-actions">
<span id="currentUser" class="header-user" title="Angemeldet"></span>
<button class="btn btn-secondary" onclick="showTimer()">⏱️ Timer</button>
<button class="btn btn-secondary" onclick="showHistory()">📊 Historie</button>
<button class="btn btn-secondary" onclick="showSettings()">⚙️ Einstellungen</button>
<button class="btn btn-secondary" onclick="logout()">🚪 Abmelden</button>
</div>
</header>
<main>
<!-- Anmeldung -->
<div id="loginView" class="view active">
<div class="login-container">
<img src="/favicon.jpg" alt="" class="login-logo">
<h1>Zeiterfassung</h1>
<!-- Anmeldung ausschliesslich ueber Authentik (SSO) -->
<a id="ssoLoginBtn" href="/auth/login" class="btn"
style="width: 100%; padding: 1rem; text-decoration: none; box-sizing: border-box;">🔐 Anmelden mit Authentik</a>
<div class="error-message" id="loginError" style="margin-top: 1rem;">
Anmeldung derzeit nicht möglich Authentik ist nicht erreichbar.
</div>
</div>
</div>
<!-- Timer-Uebersicht -->
<div id="timerView" class="view">
<div class="timer-toolbar">
<form id="newTimerForm">
<input type="text" id="newTimerName" maxlength="80"
placeholder="Name oder Platz (optional) - z. B. „Tisch 2" oder Herr Müller"">
<button type="submit" class="btn btn-start">▶ Neuer Timer</button>
</form>
<div class="rate-hint" id="rateHint">Tarif wird geladen …</div>
</div>
<div class="timer-grid" id="timerList"></div>
<div class="empty-state" id="timerEmpty" style="display: none;">
<div class="empty-state-icon">⏱️</div>
<p>Kein Timer läuft gerade.<br>Oben einen neuen starten.</p>
</div>
</div>
<!-- Historie -->
<div id="historyView" class="view">
<div class="history-container">
<h2>📊 Historie</h2>
<div class="filter-bar">
<div class="filter-field">
<label for="filterFrom">Von</label>
<input type="date" id="filterFrom">
</div>
<div class="filter-field">
<label for="filterTo">Bis</label>
<input type="date" id="filterTo">
</div>
<div class="filter-field">
<label for="filterName">Name</label>
<input type="text" id="filterName" placeholder="Kunde / Platz">
</div>
<div class="filter-actions">
<button class="btn btn-secondary btn-sm" onclick="filterToday()">Heute</button>
<button class="btn btn-secondary btn-sm" onclick="filterThisMonth()">Dieser Monat</button>
<button class="btn btn-secondary btn-sm" onclick="resetFilter()">Alle</button>
<button class="btn btn-sm" onclick="exportCsv()">⬇ CSV-Export</button>
</div>
</div>
<div class="history-stats" id="historyStats"></div>
<div class="history-list" id="historyList"></div>
<div class="history-footer">
<button class="btn btn-secondary" id="historyMore"
onclick="loadMoreHistory()" style="display: none;">Weitere laden</button>
</div>
</div>
</div>
<!-- Einstellungen -->
<div id="settingsView" class="view">
<div class="settings-container">
<h2>⚙️ Einstellungen</h2>
<p class="settings-description">Der Tarif gilt für alle Geräte gemeinsam.</p>
<form id="settingsForm">
<div class="settings-grid">
<div class="form-group">
<label for="rateAmount">Betrag (€)</label>
<input type="number" id="rateAmount" min="0.5" max="10000" step="0.5" value="10" required>
<small>Euro pro Intervall</small>
</div>
<div class="form-group">
<label for="rateInterval">Intervall (Minuten)</label>
<input type="number" id="rateInterval" min="1" max="600" step="1" value="10" required>
<small>Jede angefangene Einheit wird voll berechnet</small>
</div>
</div>
<div class="settings-preview">
<h3>Vorschau</h3>
<p id="settingsPreview">€10 pro angefangene 10 Minuten</p>
<p class="preview-examples" id="previewExamples"></p>
</div>
<div class="settings-section">
<h3>🖨️ Bon für die Kasse</h3>
<p>Was auf dem Zettel steht, den der Kunde mit zur Kasse nimmt.
Betrag, Zeiten und Tarif kommen automatisch dazu.</p>
<div id="printerState" class="printer-state">Druckerzustand wird geprüft …</div>
<div class="form-group">
<label for="receiptHeader">Kopfzeilen</label>
<textarea id="receiptHeader" maxlength="240" rows="3"
placeholder="Handy-Insel&#10;Hauptstraße 12, 12345 Musterstadt&#10;Tel. 01234 / 56789"></textarea>
<small>Erste Zeile wird groß gedruckt. Eine Zeile je Zeilenumbruch, maximal 240 Zeichen.</small>
</div>
<div class="form-group">
<label for="receiptFooter">Fußzeilen</label>
<textarea id="receiptFooter" maxlength="240" rows="2"
placeholder="Bitte an der Kasse bezahlen."></textarea>
<small>Steht unter dem Betrag, maximal 240 Zeichen.</small>
</div>
<div class="checkbox-row">
<input type="checkbox" id="printAutoOnStop" checked>
<label for="printAutoOnStop">Bon beim Beenden automatisch drucken
<small>Aus: der Bon wird nur über die Knöpfe 🖨 gedruckt.</small>
</label>
</div>
<div class="settings-actions" style="margin-top: 1.25rem;">
<button type="button" class="btn btn-secondary" id="printerTestBtn"
onclick="testPrint()">🖨️ Probedruck</button>
<button type="button" class="btn btn-secondary"
onclick="refreshPrinterStatus()">🔄 Drucker prüfen</button>
</div>
</div>
<div class="settings-actions">
<button type="submit" class="btn">💾 Speichern</button>
<button type="button" class="btn btn-secondary" onclick="resetSettings()">🔄 Zurücksetzen</button>
</div>
</form>
<div class="settings-info">
<p><strong>Hinweis:</strong> Eine Tarifänderung gilt ab sofort für laufende und neue Timer.
Bereits gespeicherte Einträge in der Historie behalten den Tarif, der beim Beenden galt.</p>
</div>
</div>
</div>
</main>
</div>
<div class="toast" id="toast"></div>
<script src="/app.js?v=20260904-bon"></script>
</body>
</html>