Initial commit

This commit is contained in:
Felix Schlusche
2025-10-23 00:32:06 +02:00
commit 9310cb2b8f
1010 changed files with 641360 additions and 0 deletions

1885
public/app.js Normal file

File diff suppressed because it is too large Load Diff

442
public/index.html Normal file
View File

@@ -0,0 +1,442 @@
<!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>
<!-- 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>
/* 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: background-color 0.2s;
}
.editable-cell:hover {
background-color: #374151;
}
.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;
}
/* 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: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
gap: 12px;
animation: slideIn 0.3s ease-out;
font-size: 14px;
font-weight: 500;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
.toast.hiding {
animation: slideOut 0.3s ease-in forwards;
}
.toast-success {
background-color: #10b981;
color: white;
}
.toast-error {
background-color: #ef4444;
color: white;
}
.toast-info {
background-color: #3b82f6;
color: white;
}
.toast-icon {
font-size: 20px;
flex-shrink: 0;
}
</style>
</head>
<body class="bg-gray-900 min-h-screen">
<!-- Toast Container -->
<div id="toastContainer" class="toast-container"></div>
<div class="container mx-auto px-4 py-8 max-w-6xl">
<!-- Header -->
<div class="bg-gray-800 rounded-lg shadow-md p-6 mb-6 border border-gray-700">
<h1 class="text-3xl font-bold text-gray-100 mb-4">⏱️ Zeiterfassung</h1>
<!-- Start/Stop Timer Section -->
<div class="mb-6 p-4 bg-gradient-to-r from-gray-700 to-gray-800 rounded-lg border border-gray-600">
<div class="flex flex-wrap items-center justify-between gap-4">
<div>
<div class="text-sm text-gray-400 mb-1">Heutige Arbeitszeit</div>
<div id="timerDisplay" class="text-4xl font-bold text-gray-100">00:00:00</div>
<div id="timerStatus" class="text-sm text-gray-400 mt-1">Nicht gestartet</div>
</div>
<div class="flex gap-3">
<button id="btnStartWork"
class="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-2xl shadow-md" title="Start">
▶️
</button>
<button id="btnStopWork" disabled
class="px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-2xl shadow-md disabled:opacity-50 disabled:cursor-not-allowed" title="Stop">
⏹️
</button>
</div>
</div>
</div>
<!-- Date Range Filter -->
<div class="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-3">
<button id="btnFilter"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-xl" title="Filtern">
🔍
</button>
<button id="btnClearFilter"
class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors text-xl" title="Filter zurücksetzen">
</button>
<button id="btnExport"
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-xl" title="Export (alle)">
📥
</button>
<button id="btnExportDeviations"
class="px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors text-xl" title="Export (nur Abweichungen)">
⚠️
</button>
</div>
</div>
</div>
<!-- Statistics -->
<div class="mb-6 bg-gray-800 rounded-lg shadow-md p-6 border border-gray-700">
<h2 class="text-xl font-bold text-gray-100 mb-4">📊 Statistiken</h2>
<!-- Current Month Stats -->
<div class="mb-4">
<h3 class="text-sm font-semibold text-gray-300 mb-2">Aktueller Monat</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="bg-gray-700 rounded-lg p-4 border border-gray-600">
<div class="text-sm text-gray-400 mb-1">Soll</div>
<div id="statTargetHours" class="text-2xl font-bold text-gray-100">0h</div>
</div>
<div class="bg-gray-700 rounded-lg p-4 border border-gray-600">
<div class="text-sm text-gray-400 mb-1">Ist</div>
<div id="statActualHours" class="text-2xl font-bold text-gray-100">0h</div>
</div>
<div class="bg-gray-700 rounded-lg p-4 border border-gray-600">
<div class="text-sm text-gray-400 mb-1">Saldo (Monat)</div>
<div id="statBalance" class="text-2xl font-bold text-gray-100">0h</div>
</div>
<div class="bg-gray-700 rounded-lg p-4 border border-gray-600">
<div class="text-sm text-gray-400 mb-1">Arbeitstage</div>
<div id="statWorkdays" class="text-2xl font-bold text-gray-100">0</div>
</div>
</div>
</div>
<!-- Total Balance -->
<div class="bg-gradient-to-r from-gray-700 to-gray-600 rounded-lg p-4 border border-gray-600">
<div class="flex justify-between items-center">
<div>
<div class="text-sm text-gray-300 mb-1">Gesamt-Saldo (inkl. Vormonat)</div>
<div id="statTotalBalance" class="text-3xl font-bold text-gray-100">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-6 bg-gray-800 rounded-lg shadow-md p-4 border border-gray-700">
<div class="flex items-center justify-between">
<div class="flex gap-3">
<button id="btnAddEntry"
class="px-4 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors text-xl" title="Neuer Eintrag">
</button>
<button id="btnAutoFill"
class="px-4 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-xl" title="Monat ausfüllen (8h)">
🔄
</button>
<button id="btnToggleBulkEdit"
class="px-4 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors text-xl" title="Mehrfachauswahl aktivieren">
☑️
</button>
</div>
<div class="flex items-center gap-4">
<button id="btnPrevMonth"
class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors text-xl">
</button>
<h2 id="currentMonthDisplay" class="text-2xl font-bold text-gray-100 min-w-[200px] text-center">
<!-- Month name will be inserted here -->
</h2>
<button id="btnNextMonth"
class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors text-xl">
</button>
</div>
<div class="w-48"></div> <!-- Spacer for alignment -->
</div>
</div>
<!-- Bulk Edit Actions Bar -->
<div id="bulkEditBar" class="hidden mb-6 bg-amber-900 rounded-lg shadow-md p-4 border border-amber-700">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<span id="selectedCount" class="text-white font-semibold">0 ausgewählt</span>
<button id="btnSelectAll"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-semibold">
Alle auswählen
</button>
<button id="btnDeselectAll"
class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors text-sm font-semibold">
Auswahl aufheben
</button>
</div>
<div class="flex gap-3">
<button id="btnBulkSetOffice"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-semibold" title="Alle auf Präsenz setzen">
🏢 Präsenz
</button>
<button id="btnBulkSetHome"
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm font-semibold" title="Alle auf Home Office setzen">
🏠 Home Office
</button>
<button id="btnBulkDelete"
class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-semibold" title="Ausgewählte löschen">
🗑️ Löschen
</button>
</div>
</div>
</div>
<!-- Entries Table -->
<div class="bg-gray-800 rounded-lg shadow-md overflow-hidden border border-gray-700">
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-700 border-b border-gray-600">
<tr>
<th id="checkboxHeader" class="hidden px-2 py-3 text-center text-xs font-medium text-gray-400 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-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Tag</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Datum</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Start</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Ende</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Pause (Min)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Netto (Std)</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">Ort</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody id="entriesTableBody" class="bg-gray-800 divide-y divide-gray-700">
<!-- Entries will be inserted here dynamically -->
</tbody>
</table>
</div>
<!-- Empty State -->
<div id="emptyState" class="hidden p-12 text-center">
<p class="text-gray-400 text-lg">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-colors font-semibold flex items-center justify-center gap-2">
🏢 Präsenz
</button>
<button type="button" id="btnLocationHome"
class="flex-1 px-4 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors font-semibold flex items-center justify-center gap-2">
🏠 Home Office
</button>
</div>
<input type="hidden" id="modalLocation" value="office">
</div>
<!-- Buttons -->
<div class="flex gap-3 mt-6">
<button type="submit"
class="flex-1 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-semibold">
Speichern
</button>
<button type="button" id="btnCancelModal"
class="flex-1 px-6 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition-colors font-semibold">
Abbrechen
</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>
<!-- App Logic -->
<script src="app.js"></script>
</body>
</html>