Compare commits
2 Commits
c17801e86c
...
3f36ec3cc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f36ec3cc7 | ||
|
|
defc0f3161 |
71
README.md
71
README.md
@@ -166,15 +166,36 @@ Die App implementiert deutsches Arbeitszeitgesetz (ArbZG):
|
|||||||
|
|
||||||
## 🚀 Installation & Ausführung
|
## 🚀 Installation & Ausführung
|
||||||
|
|
||||||
### 🐳 Option 1: Docker Compose (Empfohlen)
|
### <EFBFBD> Option 1: Vorgefertigtes Docker Image (Empfohlen)
|
||||||
|
|
||||||
**Voraussetzungen:** Docker & Docker Compose
|
**Voraussetzungen:** Docker (& Docker Compose optional)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Repository klonen
|
# Image pullen (public registry, kein Login nötig)
|
||||||
git clone https://gitea.fx-se.de/maggot/timetracker.git
|
docker pull gitea.fx-se.de/maggot/timetracker:latest
|
||||||
cd timetracker
|
|
||||||
|
|
||||||
|
# Container starten
|
||||||
|
docker run -d \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-v $(pwd)/db:/app/db \
|
||||||
|
--name timetracker \
|
||||||
|
gitea.fx-se.de/maggot/timetracker:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Oder mit docker-compose.yml:**
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: gitea.fx-se.de/maggot/timetracker:latest
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./db:/app/db
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
# Starten
|
# Starten
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
@@ -190,39 +211,13 @@ docker-compose down -v
|
|||||||
|
|
||||||
**App läuft auf:** `http://localhost:3000`
|
**App läuft auf:** `http://localhost:3000`
|
||||||
|
|
||||||
### 🐋 Option 2: Vorgefertigtes Docker Image (Gitea Registry)
|
### 🔨 Option 2: Docker (manuell bauen)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Login zur Registry
|
# Repository klonen
|
||||||
docker login gitea.fx-se.de
|
git clone https://gitea.fx-se.de/maggot/timetracker.git
|
||||||
|
cd timetracker
|
||||||
|
|
||||||
# Image pullen
|
|
||||||
docker pull gitea.fx-se.de/maggot/timetracker:latest
|
|
||||||
|
|
||||||
# Container starten
|
|
||||||
docker run -d \
|
|
||||||
-p 3000:3000 \
|
|
||||||
-v $(pwd)/db:/app/db \
|
|
||||||
--name timetracker \
|
|
||||||
gitea.fx-se.de/maggot/timetracker:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
**docker-compose.yml mit Registry-Image:**
|
|
||||||
```yaml
|
|
||||||
version: '3.8'
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
image: gitea.fx-se.de/maggot/timetracker:latest
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
volumes:
|
|
||||||
- ./db:/app/db
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔨 Option 3: Docker (manuell bauen)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Image bauen
|
# Image bauen
|
||||||
docker build -t zeiterfassung .
|
docker build -t zeiterfassung .
|
||||||
|
|
||||||
@@ -230,11 +225,15 @@ docker build -t zeiterfassung .
|
|||||||
docker run -p 3000:3000 -v $(pwd)/db:/app/db zeiterfassung
|
docker run -p 3000:3000 -v $(pwd)/db:/app/db zeiterfassung
|
||||||
```
|
```
|
||||||
|
|
||||||
### 💻 Option 4: Lokal (ohne Docker)
|
### 💻 Option 3: Lokal (ohne Docker)
|
||||||
|
|
||||||
**Voraussetzungen:** Node.js 18+
|
**Voraussetzungen:** Node.js 18+
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Repository klonen
|
||||||
|
git clone https://gitea.fx-se.de/maggot/timetracker.git
|
||||||
|
cd timetracker
|
||||||
|
|
||||||
npm install
|
npm install
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -549,6 +549,14 @@ function updateTimer() {
|
|||||||
document.getElementById('timerStatus').textContent = `Läuft seit ${timerStartTimeString} - Pause (${remainingMinutes} Min)`;
|
document.getElementById('timerStatus').textContent = `Läuft seit ${timerStartTimeString} - Pause (${remainingMinutes} Min)`;
|
||||||
} else {
|
} else {
|
||||||
elapsed = Math.floor((now - timerStartTime) / 1000) - timerPausedDuration;
|
elapsed = Math.floor((now - timerStartTime) / 1000) - timerPausedDuration;
|
||||||
|
|
||||||
|
// Check if 10h net time reached - auto stop timer
|
||||||
|
const netHours = elapsed / 3600;
|
||||||
|
if (netHours >= 10) {
|
||||||
|
showNotification('🛑 Maximale Arbeitszeit (10h netto) erreicht. Timer wird automatisch gestoppt.', 'warning');
|
||||||
|
stopWork();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('timerDisplay').textContent = formatDuration(elapsed);
|
document.getElementById('timerDisplay').textContent = formatDuration(elapsed);
|
||||||
@@ -1686,6 +1694,13 @@ async function handleFormSubmit(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate max 10h net time
|
||||||
|
const netHours = calculateNetHours(startTime, endTime, pauseMinutes);
|
||||||
|
if (netHours > 10) {
|
||||||
|
showNotification('❌ Maximale Arbeitszeit überschritten! Netto-Arbeitszeit darf maximal 10,0h betragen.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentEditingId) {
|
if (currentEditingId) {
|
||||||
// Update existing entry
|
// Update existing entry
|
||||||
const success = await updateEntry(currentEditingId, date, startTime, endTime, pauseMinutes, location);
|
const success = await updateEntry(currentEditingId, date, startTime, endTime, pauseMinutes, location);
|
||||||
@@ -1705,6 +1720,35 @@ async function handleFormSubmit(e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate net hours from times and pause
|
||||||
|
*/
|
||||||
|
function calculateNetHours(startTime, endTime, pauseMinutes) {
|
||||||
|
const [startHour, startMin] = startTime.split(':').map(Number);
|
||||||
|
const [endHour, endMin] = endTime.split(':').map(Number);
|
||||||
|
|
||||||
|
const startMinutes = startHour * 60 + startMin;
|
||||||
|
const endMinutes = endHour * 60 + endMin;
|
||||||
|
|
||||||
|
let grossMinutes = endMinutes - startMinutes;
|
||||||
|
if (grossMinutes < 0) grossMinutes += 24 * 60; // Handle overnight
|
||||||
|
|
||||||
|
const grossHours = grossMinutes / 60;
|
||||||
|
|
||||||
|
// Calculate pause if not provided
|
||||||
|
let pause = pauseMinutes || 0;
|
||||||
|
if (pauseMinutes === null || pauseMinutes === undefined) {
|
||||||
|
if (grossHours >= 9) {
|
||||||
|
pause = 45;
|
||||||
|
} else if (grossHours >= 6) {
|
||||||
|
pause = 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const netHours = grossHours - (pause / 60);
|
||||||
|
return netHours;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle delete button click
|
* Handle delete button click
|
||||||
*/
|
*/
|
||||||
@@ -2872,6 +2916,15 @@ function handleCellClick(e) {
|
|||||||
pauseToSend = null; // Trigger auto-calculation on server
|
pauseToSend = null; // Trigger auto-calculation on server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate max 10h net time
|
||||||
|
const netHours = calculateNetHours(values.startTime, values.endTime, pauseToSend);
|
||||||
|
if (netHours > 10) {
|
||||||
|
showNotification('❌ Maximale Arbeitszeit überschritten! Netto-Arbeitszeit darf maximal 10,0h betragen.', 'error');
|
||||||
|
cell.classList.remove('editing');
|
||||||
|
cell.innerHTML = originalContent;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Update via API
|
// Update via API
|
||||||
const success = await updateEntry(
|
const success = await updateEntry(
|
||||||
entryId,
|
entryId,
|
||||||
|
|||||||
Reference in New Issue
Block a user