Der Kassierer musste den Artikel bisher von Hand in der Kasse suchen. Jetzt steht er als Strichcode unter dem Betrag und wird gescannt. CODE128 in Zeichensatz B, nicht das sonst uebliche CODE39: dort sind nur Grossbuchstaben erlaubt, ein Artikel namens "techniker" waere damit nicht darstellbar. Die Modulbreite waehlt escpos.js so gross, wie die Papierbreite zulaesst - breitere Balken liest ein Handscanner zuverlaessiger, gerade auf Thermopapier, das einen Tag in einer Hosentasche hinter sich hat. Daraus ergibt sich die Laengengrenze von 17 Zeichen bei 80 mm Papier. Artikelname und An/Aus liegen in den Einstellungen, nicht im Code: der Name haengt am Artikelstamm der Kasse und kann sich aendern, ohne dass jemand an die App muss. Zwei Faelle, die sonst still danebengingen, melden sich jetzt: - Umlaute und Sonderzeichen weist das Speichern ab, statt sie zu verschlucken und einen Code zu drucken, der einen anderen Artikel bedeutet. - Passt der Name nicht auf die Papierbreite, druckt receipt.js ihn nur in Klarschrift. Ein abgeschnittener Code wird von keinem Scanner gelesen. Geprueft auf dem Epson TM-T88IV: Zwischenbeleg und Endbeleg, ausgeschalteter Strichcode, Laengengrenze, geschweifte Klammer im Namen (wird verdoppelt, sonst liest der Drucker sie als Umschaltbefehl). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
972 lines
35 KiB
HTML
972 lines
35 KiB
HTML
<!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;
|
||
}
|
||
|
||
/* Der Farbverlauf oben faerbt den Text ueber eine durchsichtige
|
||
Schriftfarbe ein. Ein Farb-Emoji hat dabei keine Kontur, die sich
|
||
einfaerben liesse - der Browser fuellt seine gesamte Flaeche mit dem
|
||
Verlauf, und aus dem Balkendiagramm wird ein weisser Kasten.
|
||
Deshalb bekommen die Symbole ihre Schriftfarbe zurueck und malen
|
||
sich wieder selbst. */
|
||
.heading-icon {
|
||
-webkit-text-fill-color: #ffffff;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.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><span class="heading-icon">📊</span> 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><span class="heading-icon">⚙️</span> 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 Hauptstraße 12, 12345 Musterstadt 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="checkbox-row">
|
||
<input type="checkbox" id="receiptBarcodeEnabled" checked
|
||
onchange="toggleBarcodeField()">
|
||
<label for="receiptBarcodeEnabled">Strichcode für die Kasse drucken
|
||
<small>Die Kasse scannt ihn, statt den Artikel zu suchen.</small>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-group" id="barcodeArticleGroup">
|
||
<label for="receiptBarcodeArticle">Artikel in der Kasse</label>
|
||
<input type="text" id="receiptBarcodeArticle" maxlength="32"
|
||
placeholder="techniker" autocomplete="off" spellcheck="false">
|
||
<small>Genau der Name, unter dem die Kasse die Leistung führt.
|
||
Keine Umlaute – der Strichcode kann sie nicht abbilden.</small>
|
||
</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=20260912-barcode"></script>
|
||
</body>
|
||
</html>
|