Files
timer-app/frontend/index.html
T
Kerim (portable/claudecode)andClaude Opus 5 f6f2df308c Versatz im Tarif-Raster beheben, Hinweis ohne Drucker richtigstellen
Die Regel ".form-group + .form-group { margin-top: 1.25rem }" aus dem letzten
Umbau traf auch das zweite Feld im Tarif-Raster: "Intervall (Minuten)" stand
dadurch 20 Pixel tiefer als "Betrag". Die Regel war ueberdies ueberfluessig -
.form-group bringt bereits margin-bottom mit -, deshalb ersatzlos raus.

Der Hinweis bei fehlendem Drucker verwies noch auf PRINTER_HOST in der .env.
Seit der Einrichtung aus der Oberflaeche ist das der falsche Weg; jetzt steht
dort, was die Folge ist (keine Bon-Knoepfe) und wo der Knopf sitzt.

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

945 lines
34 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;
}
.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);
}
.setup-box {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.25rem;
margin-top: 1rem;
}
.setup-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 1rem;
}
@media (max-width: 600px) {
.setup-grid { grid-template-columns: 1fr; }
}
.setup-hint {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.55);
margin-top: 1rem;
line-height: 1.5;
}
/* Protokoll der Einrichtung - Schritt fuer Schritt, damit im Fehlerfall
sichtbar ist, wie weit es gekommen ist. */
.setup-log {
margin-top: 1rem;
font-size: 0.85rem;
display: none;
}
.setup-log.show { display: block; }
.setup-step {
display: flex;
gap: 0.65rem;
padding: 0.5rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.setup-step:last-child { border-bottom: none; }
.setup-step-icon { flex-shrink: 0; }
.setup-step-name { font-weight: 600; }
.setup-step-detail {
color: rgba(255, 255, 255, 0.6);
white-space: pre-wrap;
word-break: break-word;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 0.78rem;
margin-top: 0.3rem;
max-height: 11rem;
overflow-y: auto;
}
.setup-step.fail .setup-step-name { color: #f87171; }
.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>
<button type="button" class="btn btn-secondary"
onclick="toggleSetup()" id="setupToggleBtn">🔧 Drucker einrichten</button>
</div>
<div class="setup-box" id="setupBox" style="display: none;">
<h3 style="margin-bottom: 0.35rem;">Neuen Drucker-Pi einrichten</h3>
<p style="color: rgba(255,255,255,0.7); font-size: 0.9rem; margin-bottom: 1.25rem;">
Richtet den Druckdienst auf einem frisch aufgesetzten Raspberry Pi
vollständig ein und übernimmt ihn anschließend. Der Pi braucht nur
SSH, Python 3 und den Drucker am USB-Anschluss.
</p>
<div class="setup-grid">
<div class="form-group">
<label for="setupHost">Adresse des Pi</label>
<input type="text" id="setupHost" placeholder="192.168.8.157" autocomplete="off">
</div>
<div class="form-group">
<label for="setupPort">SSH-Port</label>
<input type="number" id="setupPort" value="22" min="1" max="65535">
</div>
</div>
<div class="setup-grid" style="margin-top: 1rem;">
<div class="form-group">
<label for="setupUser">Benutzer</label>
<input type="text" id="setupUser" value="pi" autocomplete="off">
<small>Muss auf dem Pi sudo dürfen</small>
</div>
<div class="form-group">
<label for="setupPassword">Passwort</label>
<input type="password" id="setupPassword" autocomplete="new-password">
<small>Wird auch für sudo verwendet</small>
</div>
</div>
<div class="settings-actions" style="margin-top: 1.25rem; margin-bottom: 0;">
<button type="button" class="btn" id="setupStartBtn"
onclick="startProvision()">🚀 Jetzt einrichten</button>
</div>
<p class="setup-hint">
Das Passwort wird einmal verwendet und danach verworfen — gespeichert
wird nur ein neu erzeugtes Drucker-Token. Ein vorhandener Druckdienst
auf demselben Pi wird dabei überschrieben.
</p>
<div class="setup-log" id="setupLog"></div>
</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-setup2"></script>
</body>
</html>