Initial commit
This commit is contained in:
1885
public/app.js
Normal file
1885
public/app.js
Normal file
File diff suppressed because it is too large
Load Diff
442
public/index.html
Normal file
442
public/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user