Deploy auf den Timer-Server / deploy (push) Successful in 18s
Die Ueberschriften von Historie und Einstellungen faerben ihren Text ueber einen Verlauf ein: background-clip: text mit durchsichtiger Schriftfarbe. Ein Farb-Emoji hat dabei keine Kontur, die sich einfaerben liesse - der Browser fuellt seine gesamte Flaeche mit dem Verlauf. Aus dem Balkendiagramm der Historie wurde so ein weisser Kasten. Die beiden Symbole stehen jetzt in einem eigenen span, das seine Schriftfarbe zurueckbekommt und sich damit wieder selbst zeichnet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
956 lines
34 KiB
HTML
956 lines
34 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="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-emoji"></script>
|
||
</body>
|
||
</html>
|