904 lines
39 KiB
HTML
904 lines
39 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de" class="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Zeiterfassung</title>
|
|
|
|
<!-- Favicon -->
|
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
|
|
<!-- Tailwind CSS via CDN -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<!-- Flatpickr CSS -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
|
|
|
<!-- Custom Flatpickr Mobile Theme for Tumbler Style -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/dark.css">
|
|
|
|
<style>
|
|
/* Premium Background with Texture */
|
|
body {
|
|
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
|
background-attachment: fixed;
|
|
position: relative;
|
|
}
|
|
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-image:
|
|
radial-gradient(circle at 20% 50%, rgba(59, 130, 246, 0.05) 0%, transparent 50%),
|
|
radial-gradient(circle at 80% 80%, rgba(139, 92, 246, 0.05) 0%, transparent 50%),
|
|
radial-gradient(circle at 40% 20%, rgba(16, 185, 129, 0.03) 0%, transparent 50%);
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
/* Glass morphism effect */
|
|
.glass-card {
|
|
background: rgba(30, 41, 59, 0.7);
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
box-shadow:
|
|
0 8px 32px 0 rgba(0, 0, 0, 0.37),
|
|
inset 0 1px 0 0 rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
/* Premium card with elevation */
|
|
.premium-card {
|
|
background: linear-gradient(135deg, rgba(51, 65, 85, 0.9) 0%, rgba(30, 41, 59, 0.9) 100%);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
box-shadow:
|
|
0 20px 60px -15px rgba(0, 0, 0, 0.5),
|
|
0 8px 20px -8px rgba(0, 0, 0, 0.3),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.premium-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow:
|
|
0 30px 80px -20px rgba(0, 0, 0, 0.6),
|
|
0 12px 30px -10px rgba(0, 0, 0, 0.4),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
/* Gradient text */
|
|
.gradient-text {
|
|
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 50%, #34d399 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
/* Elevated button styles */
|
|
.btn-elevated {
|
|
box-shadow:
|
|
0 4px 14px 0 rgba(0, 0, 0, 0.4),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.btn-elevated::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: -100%;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
transition: left 0.5s;
|
|
}
|
|
|
|
.btn-elevated:hover::before {
|
|
left: 100%;
|
|
}
|
|
|
|
.btn-elevated:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow:
|
|
0 8px 20px 0 rgba(0, 0, 0, 0.5),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
.btn-elevated:active {
|
|
transform: translateY(0);
|
|
box-shadow:
|
|
0 2px 8px 0 rgba(0, 0, 0, 0.3),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
/* Table with depth */
|
|
.premium-table {
|
|
background: linear-gradient(180deg, rgba(31, 41, 55, 0.95) 0%, rgba(17, 24, 39, 0.95) 100%);
|
|
backdrop-filter: blur(10px);
|
|
box-shadow:
|
|
0 10px 40px -10px rgba(0, 0, 0, 0.5),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
/* Custom styles for better tumbler/wheel experience */
|
|
.flatpickr-time input {
|
|
font-size: 1.2rem;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
/* Mobile-friendly time picker styles */
|
|
@media (min-width: 768px) {
|
|
.flatpickr-time {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
}
|
|
}
|
|
|
|
/* Editable cell styles */
|
|
.editable-cell {
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.editable-cell::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 50%;
|
|
width: 0;
|
|
height: 2px;
|
|
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
|
transition: all 0.3s ease;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.editable-cell:hover::after {
|
|
width: 80%;
|
|
}
|
|
|
|
.editable-cell:hover {
|
|
background: linear-gradient(180deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.05) 100%);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.editable-cell.editing {
|
|
padding: 0;
|
|
}
|
|
|
|
.cell-input {
|
|
width: 100%;
|
|
padding: 0.5rem;
|
|
border: 2px solid #3b82f6;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.875rem;
|
|
background-color: #1f2937;
|
|
color: #f3f4f6;
|
|
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
|
|
}
|
|
|
|
/* Toast Notification Styles */
|
|
.toast-container {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
z-index: 1000;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.toast {
|
|
padding: 16px 20px;
|
|
border-radius: 12px;
|
|
box-shadow:
|
|
0 10px 40px rgba(0, 0, 0, 0.3),
|
|
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
animation: slideIn 0.3s ease-out;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(400px) scale(0.9);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateX(0) scale(1);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes slideOut {
|
|
from {
|
|
transform: translateX(0) scale(1);
|
|
opacity: 1;
|
|
}
|
|
to {
|
|
transform: translateX(400px) scale(0.9);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.toast.hiding {
|
|
animation: slideOut 0.3s ease-in forwards;
|
|
}
|
|
|
|
.toast-success {
|
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.95), rgba(5, 150, 105, 0.95));
|
|
color: white;
|
|
}
|
|
|
|
.toast-error {
|
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.95), rgba(220, 38, 38, 0.95));
|
|
color: white;
|
|
}
|
|
|
|
.toast-info {
|
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.95), rgba(37, 99, 235, 0.95));
|
|
color: white;
|
|
}
|
|
|
|
.toast-icon {
|
|
font-size: 20px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Details/Summary chevron rotation */
|
|
details[open] .details-chevron {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
details summary {
|
|
list-style: none;
|
|
}
|
|
|
|
details summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
/* Blink animation for running timer icon */
|
|
@keyframes blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.3; }
|
|
}
|
|
|
|
.timer-running-icon {
|
|
animation: blink 2s ease-in-out infinite;
|
|
filter: drop-shadow(0 0 8px rgba(59, 130, 246, 0.6));
|
|
}
|
|
|
|
/* Stat card glow effect */
|
|
.stat-card {
|
|
position: relative;
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -50%;
|
|
left: -50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.stat-card:hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-4px) scale(1.02);
|
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
/* Input fields with premium styling */
|
|
input[type="text"],
|
|
input[type="number"],
|
|
select {
|
|
background: rgba(31, 41, 55, 0.8) !important;
|
|
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
|
box-shadow:
|
|
inset 0 2px 4px rgba(0, 0, 0, 0.3),
|
|
0 0 0 1px rgba(255, 255, 255, 0.05) !important;
|
|
transition: all 0.3s ease !important;
|
|
}
|
|
|
|
input[type="text"]:focus,
|
|
input[type="number"]:focus,
|
|
select:focus {
|
|
border-color: rgba(59, 130, 246, 0.5) !important;
|
|
box-shadow:
|
|
inset 0 2px 4px rgba(0, 0, 0, 0.3),
|
|
0 0 0 3px rgba(59, 130, 246, 0.1),
|
|
0 0 20px rgba(59, 130, 246, 0.2) !important;
|
|
outline: none !important;
|
|
}
|
|
|
|
/* Scrollbar styling */
|
|
::-webkit-scrollbar {
|
|
width: 10px;
|
|
height: 10px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: rgba(15, 23, 42, 0.5);
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: linear-gradient(180deg, #475569, #334155);
|
|
border-radius: 5px;
|
|
border: 2px solid rgba(15, 23, 42, 0.5);
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: linear-gradient(180deg, #64748b, #475569);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen relative">
|
|
<!-- Toast Container -->
|
|
<div id="toastContainer" class="toast-container"></div>
|
|
|
|
<div class="container mx-auto px-4 py-8 max-w-6xl relative z-10">
|
|
<!-- Header -->
|
|
<div class="premium-card rounded-xl p-6 mb-8">
|
|
<h1 class="text-4xl font-bold mb-4 flex items-center gap-3 text-white">
|
|
<i data-lucide="clock" class="w-10 h-10 text-white"></i>
|
|
Zeiterfassung
|
|
</h1>
|
|
|
|
<!-- Start/Stop Timer Section -->
|
|
<div class="mb-6 p-6 glass-card rounded-xl">
|
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-2 font-medium">Heutige Arbeitszeit</div>
|
|
<div id="timerDisplay" class="text-5xl font-bold text-white">00:00:00</div>
|
|
<button id="timerStatus" class="text-sm text-blue-400 hover:text-blue-300 mt-2 underline cursor-pointer transition-all" title="Startzeit manuell eingeben">
|
|
Nicht gestartet
|
|
</button>
|
|
<input type="text" id="manualStartTimeInput" class="hidden">
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button id="btnStartWork"
|
|
class="btn-elevated inline-flex items-center justify-center gap-2 px-8 py-4 bg-gradient-to-r from-green-600 to-green-500 text-white rounded-xl font-semibold text-lg" title="Start">
|
|
<i data-lucide="play" class="w-6 h-6"></i>
|
|
<span>Start</span>
|
|
</button>
|
|
<button id="btnStopWork" disabled
|
|
class="btn-elevated inline-flex items-center justify-center gap-2 px-8 py-4 bg-gradient-to-r from-red-600 to-red-500 text-white rounded-xl font-semibold text-lg disabled:opacity-50 disabled:cursor-not-allowed" title="Stop">
|
|
<i data-lucide="square" class="w-6 h-6"></i>
|
|
<span>Stop</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manual Start Time Picker Modal -->
|
|
<div id="manualTimePickerModal" class="hidden fixed inset-0 bg-black bg-opacity-70 backdrop-blur-sm flex items-center justify-center z-50">
|
|
<div class="premium-card rounded-2xl p-8 max-w-sm w-full mx-4">
|
|
<h3 class="text-xl font-bold text-gray-100 mb-6">Startzeit eingeben</h3>
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Zeit (HH:MM)</label>
|
|
<input type="text" id="manualTimeInput"
|
|
class="w-full px-4 py-3 bg-gray-700 text-gray-100 border border-gray-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 text-lg"
|
|
placeholder="09:00" readonly>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button id="btnConfirmManualTime"
|
|
class="btn-elevated flex-1 px-4 py-3 bg-gradient-to-r from-blue-600 to-blue-500 text-white rounded-xl font-semibold">
|
|
Bestätigen
|
|
</button>
|
|
<button id="btnCancelManualTime"
|
|
class="flex-1 px-4 py-2.5 bg-gray-700 text-gray-100 rounded-lg hover:bg-gray-600 transition-all duration-200 font-medium">
|
|
Abbrechen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CSV Filter & Export Section (Collapsible) -->
|
|
<details class="mt-4 pt-4 border-t border-gray-700">
|
|
<summary class="cursor-pointer text-gray-300 hover:text-gray-100 font-medium flex items-center gap-2 select-none">
|
|
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform details-chevron"></i>
|
|
<i data-lucide="filter" class="w-5 h-5 text-blue-400"></i>
|
|
<span>CSV Filter & Export</span>
|
|
</summary>
|
|
<div class="mt-4 flex flex-wrap gap-4 items-end">
|
|
<div class="flex-1 min-w-[200px]">
|
|
<label for="filterFrom" class="block text-sm font-medium text-gray-300 mb-1">Von</label>
|
|
<input type="text" id="filterFrom"
|
|
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="DD.MM.YYYY">
|
|
</div>
|
|
|
|
<div class="flex-1 min-w-[200px]">
|
|
<label for="filterTo" class="block text-sm font-medium text-gray-300 mb-1">Bis</label>
|
|
<input type="text" id="filterTo"
|
|
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="DD.MM.YYYY">
|
|
</div>
|
|
|
|
<div class="flex gap-2">
|
|
<button id="btnFilter"
|
|
class="inline-flex items-center justify-center w-10 h-10 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all duration-200 shadow-sm" title="Filtern">
|
|
<i data-lucide="search" class="w-5 h-5"></i>
|
|
</button>
|
|
|
|
<button id="btnClearFilter"
|
|
class="inline-flex items-center justify-center w-10 h-10 bg-gray-700 text-gray-100 rounded-lg hover:bg-gray-600 transition-all duration-200 shadow-sm" title="Filter zurücksetzen">
|
|
<i data-lucide="x-circle" class="w-5 h-5"></i>
|
|
</button>
|
|
|
|
<button id="btnExport"
|
|
class="inline-flex items-center justify-center w-10 h-10 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-all duration-200 shadow-sm" title="Export (alle)">
|
|
<i data-lucide="download" class="w-5 h-5"></i>
|
|
</button>
|
|
|
|
<button id="btnExportDeviations"
|
|
class="inline-flex items-center justify-center w-10 h-10 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-all duration-200 shadow-sm" title="Export (nur Abweichungen)">
|
|
<i data-lucide="alert-circle" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- Settings Section (Collapsible) -->
|
|
<details class="mt-4 pt-4 border-t border-gray-700">
|
|
<summary class="cursor-pointer text-gray-300 hover:text-gray-100 font-medium flex items-center gap-2 select-none">
|
|
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform details-chevron"></i>
|
|
<i data-lucide="settings" class="w-5 h-5 text-purple-400"></i>
|
|
<span>Einstellungen</span>
|
|
</summary>
|
|
<div class="mt-4 mb-2 text-right">
|
|
<span id="versionInfo" class="text-xs text-gray-500 font-mono"></span>
|
|
</div>
|
|
<div class="mt-4 flex flex-wrap gap-4 items-center">
|
|
<div class="flex-1 min-w-[200px]">
|
|
<label for="employeeName" class="block text-sm font-medium text-gray-300 mb-1">Mitarbeitername</label>
|
|
<input type="text" id="employeeName" placeholder="Max Mustermann"
|
|
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
</div>
|
|
<div class="flex-1 min-w-[200px]">
|
|
<label for="employeeId" class="block text-sm font-medium text-gray-300 mb-1">Personalnummer</label>
|
|
<input type="text" id="employeeId" placeholder="12345"
|
|
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 flex flex-wrap gap-4 items-center">
|
|
<div class="flex-1 min-w-[200px]">
|
|
<label for="bundeslandSelect" class="block text-sm font-medium text-gray-300 mb-1">Bundesland (Feiertage)</label>
|
|
<select id="bundeslandSelect"
|
|
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
<option value="BW">Baden-Württemberg</option>
|
|
<option value="BY">Bayern</option>
|
|
<option value="BE">Berlin</option>
|
|
<option value="BB">Brandenburg</option>
|
|
<option value="HB">Bremen</option>
|
|
<option value="HH">Hamburg</option>
|
|
<option value="HE">Hessen</option>
|
|
<option value="MV">Mecklenburg-Vorpommern</option>
|
|
<option value="NI">Niedersachsen</option>
|
|
<option value="NW">Nordrhein-Westfalen</option>
|
|
<option value="RP">Rheinland-Pfalz</option>
|
|
<option value="SL">Saarland</option>
|
|
<option value="SN">Sachsen</option>
|
|
<option value="ST">Sachsen-Anhalt</option>
|
|
<option value="SH">Schleswig-Holstein</option>
|
|
<option value="TH">Thüringen</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex-1 min-w-[200px]">
|
|
<label for="vacationDaysInput" class="block text-sm font-medium text-gray-300 mb-1">Urlaubstage pro Jahr</label>
|
|
<input type="number" id="vacationDaysInput" min="0" max="50" value="30"
|
|
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Database Export/Import -->
|
|
<div class="mt-6 pt-4 border-t border-gray-600">
|
|
<h3 class="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2">
|
|
<i data-lucide="database" class="w-4 h-4 text-purple-400"></i>
|
|
Datenbank Verwaltung
|
|
</h3>
|
|
<div class="flex flex-wrap gap-3">
|
|
<button id="btnExportDB" class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
|
<i data-lucide="download" class="w-4 h-4"></i>
|
|
Datenbank exportieren
|
|
</button>
|
|
<button id="btnImportDB" class="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
|
|
<i data-lucide="upload" class="w-4 h-4"></i>
|
|
Datenbank importieren
|
|
</button>
|
|
<input type="file" id="importDBFile" accept=".json" class="hidden">
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-2">
|
|
<i data-lucide="info" class="w-3 h-3 inline"></i>
|
|
Export erstellt eine JSON-Datei mit allen Einträgen und Einstellungen. Import überschreibt alle vorhandenen Daten.
|
|
</p>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
<!-- Statistics -->
|
|
<div class="mb-8 premium-card rounded-xl p-6">
|
|
<h2 class="text-2xl font-bold text-gray-100 mb-6 flex items-center gap-2">
|
|
<i data-lucide="bar-chart-3" class="w-7 h-7 text-blue-400"></i>
|
|
Statistiken
|
|
</h2>
|
|
|
|
<!-- Current Month Stats -->
|
|
<div class="mb-6">
|
|
<h3 class="text-sm font-semibold text-gray-300 mb-3 uppercase tracking-wider">Aktueller Monat</h3>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div class="stat-card glass-card rounded-xl p-5 border border-gray-600">
|
|
<div class="text-xs text-gray-400 mb-2 uppercase tracking-wide">Soll</div>
|
|
<div id="statTargetHours" class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-cyan-400">0h</div>
|
|
</div>
|
|
<div class="stat-card glass-card rounded-xl p-5 border border-gray-600">
|
|
<div class="text-xs text-gray-400 mb-2 uppercase tracking-wide">Ist</div>
|
|
<div id="statActualHours" class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-emerald-400">0h</div>
|
|
</div>
|
|
<div class="stat-card glass-card rounded-xl p-5 border border-gray-600">
|
|
<div class="text-xs text-gray-400 mb-2 uppercase tracking-wide flex items-center gap-1">
|
|
Saldo (Monat)
|
|
<span id="balanceFlextimeHint" class="hidden text-cyan-400 cursor-help" title="">
|
|
<i data-lucide="info" class="w-3 h-3"></i>
|
|
</span>
|
|
</div>
|
|
<div id="statBalance" class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-400">0h</div>
|
|
</div>
|
|
<div class="stat-card glass-card rounded-xl p-5 border border-gray-600">
|
|
<div class="text-xs text-gray-400 mb-2 uppercase tracking-wide">Arbeitstage</div>
|
|
<div id="statWorkdays" class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-400">0</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vacation Stats -->
|
|
<div class="mb-6">
|
|
<h3 id="vacationYearLabel" class="text-sm font-semibold text-gray-300 mb-3 flex items-center gap-2 uppercase tracking-wider">
|
|
<i data-lucide="plane" class="w-4 h-4 text-amber-400"></i>
|
|
Urlaub 2025
|
|
</h3>
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div class="stat-card glass-card rounded-xl p-5 border border-yellow-600/30">
|
|
<div class="text-xs text-yellow-300 mb-2 uppercase tracking-wide">Genommen</div>
|
|
<div id="statVacationTaken" class="text-3xl font-bold text-yellow-100">0</div>
|
|
</div>
|
|
<div class="stat-card glass-card rounded-xl p-5 border border-cyan-600/30">
|
|
<div class="text-xs text-cyan-300 mb-2 uppercase tracking-wide">Geplant</div>
|
|
<div id="statVacationPlanned" class="text-3xl font-bold text-cyan-100">0</div>
|
|
</div>
|
|
<div class="stat-card glass-card rounded-xl p-5 border border-green-600/30">
|
|
<div class="text-xs text-green-300 mb-2 uppercase tracking-wide">Verfügbar</div>
|
|
<div id="statVacationRemaining" class="text-3xl font-bold text-green-100">0 / 30</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Total Balance -->
|
|
<div class="glass-card rounded-xl p-6 border-2 border-purple-500/30 shadow-lg shadow-purple-500/20">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<div class="text-sm text-gray-300 mb-2 uppercase tracking-wide font-semibold flex items-center gap-1">
|
|
Gesamt-Saldo (inkl. Vormonat)
|
|
<span id="totalBalanceFlextimeHint" class="hidden text-cyan-400 cursor-help" title="">
|
|
<i data-lucide="info" class="w-3 h-3"></i>
|
|
</span>
|
|
</div>
|
|
<div id="statTotalBalance" class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 via-pink-400 to-blue-400">0h</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<div class="text-xs text-gray-400">Übertrag Vormonat</div>
|
|
<div id="statPreviousBalance" class="text-lg font-semibold text-gray-300">0h</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Month Navigation -->
|
|
<div class="mb-8 glass-card rounded-xl p-5 border border-gray-700/50">
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 items-center gap-4">
|
|
<!-- Left: Action Buttons (hidden on mobile, shown on desktop at start) -->
|
|
<div class="hidden lg:flex gap-3 flex-wrap justify-start order-2 lg:order-1">
|
|
<button id="btnToggleBulkEdit"
|
|
class="btn-elevated inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-gray-700 to-gray-600 text-gray-100 rounded-xl font-semibold" title="Mehrfachauswahl aktivieren">
|
|
<i data-lucide="check-square" class="w-5 h-5"></i>
|
|
<span>Auswahl</span>
|
|
</button>
|
|
<button id="btnAutoFill"
|
|
class="btn-elevated inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-indigo-600 to-indigo-500 text-white rounded-xl font-semibold" title="Monat ausfüllen (8h)">
|
|
<i data-lucide="calendar-check" class="w-5 h-5"></i>
|
|
<span>Ausfüllen</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Center: Month Navigation (always visible and centered) -->
|
|
<div id="monthNavigation" class="flex items-center justify-center gap-4 order-1 lg:order-2">
|
|
<button id="btnPrevMonth"
|
|
class="btn-elevated inline-flex items-center justify-center w-12 h-12 bg-gradient-to-br from-gray-700 to-gray-600 text-gray-100 rounded-xl">
|
|
<i data-lucide="chevron-left" class="w-6 h-6"></i>
|
|
</button>
|
|
<h2 id="currentMonthDisplay" class="text-2xl font-bold text-white min-w-[200px] text-center">
|
|
<!-- Month name will be inserted here -->
|
|
</h2>
|
|
<button id="btnNextMonth"
|
|
class="btn-elevated inline-flex items-center justify-center w-12 h-12 bg-gradient-to-br from-gray-700 to-gray-600 text-gray-100 rounded-xl">
|
|
<i data-lucide="chevron-right" class="w-6 h-6"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Right: Export Button (hidden on mobile, shown on desktop at end) -->
|
|
<div class="hidden lg:flex justify-end order-3">
|
|
<button id="btnExportPDF"
|
|
class="btn-elevated inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-red-600 to-red-500 text-white rounded-xl font-semibold" title="Monat als PDF exportieren">
|
|
<i data-lucide="file-text" class="w-5 h-5"></i>
|
|
<span>PDF Export</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Mobile Action Buttons (shown only on mobile, below navigation) -->
|
|
<div class="flex lg:hidden gap-3 flex-wrap justify-center order-3">
|
|
<button onclick="document.getElementById('btnToggleBulkEdit').click()"
|
|
class="btn-elevated inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-gray-700 to-gray-600 text-gray-100 rounded-xl font-semibold" title="Mehrfachauswahl aktivieren">
|
|
<i data-lucide="check-square" class="w-5 h-5"></i>
|
|
<span>Auswahl</span>
|
|
</button>
|
|
<button onclick="document.getElementById('btnAutoFill').click()"
|
|
class="btn-elevated inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-indigo-600 to-indigo-500 text-white rounded-xl font-semibold" title="Monat ausfüllen (8h)">
|
|
<i data-lucide="calendar-check" class="w-5 h-5"></i>
|
|
<span>Ausfüllen</span>
|
|
</button>
|
|
<button onclick="document.getElementById('btnExportPDF').click()"
|
|
class="btn-elevated inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-red-600 to-red-500 text-white rounded-xl font-semibold" title="Monat als PDF exportieren">
|
|
<i data-lucide="file-text" class="w-5 h-5"></i>
|
|
<span>PDF Export</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bulk Edit Actions Bar -->
|
|
<div id="bulkEditBar" class="hidden mb-6 bg-gray-800 rounded-lg shadow p-4 border border-gray-700">
|
|
<div class="flex items-center justify-between flex-wrap gap-3">
|
|
<div class="flex items-center gap-3">
|
|
<span id="selectedCount" class="text-gray-300 font-medium">0 ausgewählt</span>
|
|
<button id="btnSelectAll"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-gray-700 text-gray-100 rounded-lg hover:bg-gray-600 transition-all duration-200 text-sm font-medium">
|
|
<i data-lucide="check-check" class="w-4 h-4"></i>
|
|
<span>Alle</span>
|
|
</button>
|
|
<button id="btnDeselectAll"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-gray-700 text-gray-100 rounded-lg hover:bg-gray-600 transition-all duration-200 text-sm font-medium">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
<span>Keine</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex gap-2 flex-wrap">
|
|
<button id="btnBulkSetOffice"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all duration-200 text-sm font-medium shadow-sm" title="Alle auf Präsenz setzen">
|
|
<i data-lucide="building-2" class="w-4 h-4"></i>
|
|
<span>Präsenz</span>
|
|
</button>
|
|
<button id="btnBulkSetHome"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-all duration-200 text-sm font-medium shadow-sm" title="Alle auf Home Office setzen">
|
|
<i data-lucide="home" class="w-4 h-4"></i>
|
|
<span>Home</span>
|
|
</button>
|
|
<button id="btnBulkSetVacation"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-all duration-200 text-sm font-medium shadow-sm" title="Urlaub eintragen">
|
|
<i data-lucide="plane" class="w-4 h-4"></i>
|
|
<span>Urlaub</span>
|
|
</button>
|
|
<button id="btnBulkSetFlextime"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-cyan-600 text-white rounded-lg hover:bg-cyan-700 transition-all duration-200 text-sm font-medium shadow-sm" title="Gleittage eintragen">
|
|
<i data-lucide="clock" class="w-4 h-4"></i>
|
|
<span>Gleitzeit</span>
|
|
</button>
|
|
<button id="btnBulkDelete"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-all duration-200 text-sm font-medium shadow-sm" title="Ausgewählte löschen">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
<span>Löschen</span>
|
|
</button>
|
|
<button id="btnBulkExportPDF"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-all duration-200 text-sm font-medium shadow-sm" title="Ausgewählte als PDF exportieren">
|
|
<i data-lucide="file-text" class="w-4 h-4"></i>
|
|
<span>PDF Export</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Entries Table -->
|
|
<div class="premium-table rounded-xl overflow-hidden border border-gray-700/50 shadow-2xl">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead class="bg-gradient-to-r from-gray-800 to-gray-700 border-b-2 border-blue-500/30">
|
|
<tr>
|
|
<th id="checkboxHeader" class="hidden px-2 py-4 text-center text-xs font-bold text-gray-300 uppercase tracking-wider">
|
|
<input type="checkbox" id="masterCheckbox" class="w-5 h-5 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500" title="Alle auswählen/abwählen">
|
|
</th>
|
|
<th class="px-2 py-4 text-left text-xs font-bold text-gray-300 uppercase tracking-wider">Tag</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-300 uppercase tracking-wider">Datum</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-300 uppercase tracking-wider">Start</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-300 uppercase tracking-wider">Ende</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-300 uppercase tracking-wider">Pause (Min)</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-300 uppercase tracking-wider">Netto (Std)</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-gray-300 uppercase tracking-wider">Ort</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-gray-300 uppercase tracking-wider">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="entriesTableBody" class="divide-y divide-gray-700/50">
|
|
<!-- Entries will be inserted here dynamically -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div id="emptyState" class="hidden p-16 text-center">
|
|
<i data-lucide="inbox" class="w-16 h-16 mx-auto mb-4 text-gray-600"></i>
|
|
<p class="text-gray-400 text-xl font-semibold">Keine Einträge vorhanden.</p>
|
|
<p class="text-gray-500 mt-2">Klicken Sie auf "Neuer Eintrag", um zu beginnen.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal for Add/Edit Entry -->
|
|
<div id="entryModal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50">
|
|
<div class="bg-gray-800 border border-gray-700 rounded-lg shadow-xl max-w-md w-full p-6">
|
|
<h2 id="modalTitle" class="text-2xl font-bold text-gray-100 mb-4">Neuer Eintrag</h2>
|
|
|
|
<form id="entryForm">
|
|
<!-- Date -->
|
|
<div class="mb-4">
|
|
<label for="modalDate" class="block text-sm font-medium text-gray-300 mb-1">Datum</label>
|
|
<input type="text" id="modalDate" required
|
|
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="DD.MM.YYYY">
|
|
</div>
|
|
|
|
<!-- Start Time -->
|
|
<div class="mb-4">
|
|
<label for="modalStartTime" class="block text-sm font-medium text-gray-300 mb-1">Startzeit</label>
|
|
<input type="text" id="modalStartTime" required
|
|
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="HH:MM">
|
|
</div>
|
|
|
|
<!-- End Time -->
|
|
<div class="mb-4">
|
|
<label for="modalEndTime" class="block text-sm font-medium text-gray-300 mb-1">Endzeit</label>
|
|
<input type="text" id="modalEndTime" required
|
|
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="HH:MM">
|
|
|
|
<!-- Quick Time Buttons -->
|
|
<div class="mt-2 flex gap-2 flex-wrap">
|
|
<span class="text-xs text-gray-400 w-full mb-1">Schnellauswahl (für 9:00 Start):</span>
|
|
<button type="button" class="quick-time-btn px-3 py-1 text-xs bg-gray-700 hover:bg-gray-600 text-gray-200 rounded border border-gray-600" data-hours="6">
|
|
6h
|
|
</button>
|
|
<button type="button" class="quick-time-btn px-3 py-1 text-xs bg-gray-700 hover:bg-gray-600 text-gray-200 rounded border border-gray-600" data-hours="7">
|
|
7h
|
|
</button>
|
|
<button type="button" class="quick-time-btn px-3 py-1 text-xs bg-gray-700 hover:bg-gray-600 text-gray-200 rounded border border-gray-600" data-hours="8">
|
|
8h
|
|
</button>
|
|
<button type="button" class="quick-time-btn px-3 py-1 text-xs bg-gray-700 hover:bg-gray-600 text-gray-200 rounded border border-gray-600" data-hours="9">
|
|
9h
|
|
</button>
|
|
<button type="button" class="quick-time-btn px-3 py-1 text-xs bg-gray-700 hover:bg-gray-600 text-gray-200 rounded border border-gray-600" data-hours="10">
|
|
10h
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pause -->
|
|
<div class="mb-4">
|
|
<label for="modalPause" class="block text-sm font-medium text-gray-300 mb-1">
|
|
Pause (Minuten)
|
|
<span class="text-xs text-gray-400">- Optional, sonst automatisch berechnet</span>
|
|
</label>
|
|
<input type="number" id="modalPause" min="0" step="1"
|
|
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="Leer lassen für automatische Berechnung">
|
|
</div>
|
|
|
|
<!-- Location -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">
|
|
Arbeitsort
|
|
</label>
|
|
<div class="flex gap-3">
|
|
<button type="button" id="btnLocationOffice"
|
|
class="flex-1 px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all duration-200 font-medium flex items-center justify-center gap-2 shadow-sm">
|
|
<i data-lucide="building-2" class="w-5 h-5"></i>
|
|
<span>Präsenz</span>
|
|
</button>
|
|
<button type="button" id="btnLocationHome"
|
|
class="flex-1 px-4 py-3 bg-gray-700 text-gray-100 rounded-lg hover:bg-gray-600 transition-all duration-200 font-medium flex items-center justify-center gap-2">
|
|
<i data-lucide="home" class="w-5 h-5"></i>
|
|
<span>Home Office</span>
|
|
</button>
|
|
</div>
|
|
<input type="hidden" id="modalLocation" value="office">
|
|
</div>
|
|
|
|
<!-- Buttons -->
|
|
<div class="flex gap-3 mt-6">
|
|
<button type="submit"
|
|
class="flex-1 inline-flex items-center justify-center gap-2 px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all duration-200 font-medium shadow-sm">
|
|
<i data-lucide="check" class="w-5 h-5"></i>
|
|
<span>Speichern</span>
|
|
</button>
|
|
<button type="button" id="btnCancelModal"
|
|
class="flex-1 inline-flex items-center justify-center gap-2 px-6 py-2.5 bg-gray-700 text-gray-100 rounded-lg hover:bg-gray-600 transition-all duration-200 font-medium">
|
|
<i data-lucide="x" class="w-5 h-5"></i>
|
|
<span>Abbrechen</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Flatpickr JS -->
|
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/de.js"></script>
|
|
|
|
<!-- jsPDF for PDF export -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
|
|
|
|
<!-- Lucide Icons - Try multiple CDN sources -->
|
|
<script src="https://unpkg.com/lucide@0.294.0/dist/umd/lucide.js"
|
|
onerror="console.error('Failed to load Lucide from unpkg'); this.onerror=null; this.src='https://cdn.jsdelivr.net/npm/lucide@0.294.0/dist/umd/lucide.js'">
|
|
</script>
|
|
|
|
<!-- App Logic - Load in correct order -->
|
|
<script src="js/state.js"></script>
|
|
<script src="js/utils.js"></script>
|
|
<script src="js/holidays.js"></script>
|
|
<script src="js/api.js"></script>
|
|
<script src="js/main.js"></script>
|
|
|
|
<!-- Initialize Lucide Icons -->
|
|
<script>
|
|
// Use a more robust initialization
|
|
function initLucide() {
|
|
if (typeof lucide !== 'undefined' && lucide.createIcons) {
|
|
lucide.createIcons();
|
|
} else if (window.lucide && window.lucide.createIcons) {
|
|
window.lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
// Try multiple times
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initLucide);
|
|
} else {
|
|
setTimeout(initLucide, 100);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|