Compare commits

..

11 Commits

Author SHA1 Message Date
Felix Schlusche
f50e0fee7e feat: enhance balance calculation to include total balance from all months and exclude sick days
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m57s
2026-03-04 14:44:01 +01:00
Felix Schlusche
a837a8af59 ci: remove conflicting workflow exclusion to ensure triggers work
All checks were successful
Build and Push Docker Image / build (push) Successful in 12m42s
2026-03-04 10:37:31 +01:00
Felix Schlusche
9408a13251 ci: fix workflow path filters to trigger builds on workflow changes 2026-03-04 10:36:31 +01:00
Felix Schlusche
676dd2f497 ci: switch to inline caching to avoid 413 error 2026-03-04 10:35:14 +01:00
Felix Schlusche
432c3a0ccf feat: add sick days support and multi-arch docker builds
Some checks failed
Build and Push Docker Image / build (push) Failing after 15m15s
2026-03-04 10:20:18 +01:00
Felix Schlusche
9c25b47da1 feat: enhance previous balance calculation to filter valid entries and adjust for current month
All checks were successful
Build and Push Docker Image / build (push) Successful in 25s
2025-12-03 14:08:02 +01:00
Felix Schlusche
995d1080f3 feat: update PDF export button visibility logic to check if the month is complete
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m49s
2025-12-02 15:18:46 +01:00
Felix Schlusche
fe69bcb357 feat: update pause duration logic to comply with German law based on target work hours
All checks were successful
Build and Push Docker Image / build (push) Successful in 23s
2025-10-31 19:00:45 +01:00
Felix Schlusche
0b408c93ee feat: enhance README with updates on Node.js version, responsive timer layout, and new features including live timer metrics and intelligent PDF export activation
All checks were successful
Build and Push Docker Image / build (push) Successful in 26s
2025-10-31 18:55:46 +01:00
Felix Schlusche
0045a8f8d0 feat: update PDF export functionality to prevent incomplete month exports 2025-10-31 18:54:47 +01:00
Felix Schlusche
d04ab18ba1 feat: enhance timer metrics and workday calculations to include entries and running timer status 2025-10-31 18:54:17 +01:00
6 changed files with 319 additions and 84 deletions

View File

@@ -13,9 +13,9 @@ on:
- 'docker-compose.yml' - 'docker-compose.yml'
- 'db/**' - 'db/**'
- 'public/**' - 'public/**'
- '.gitea/workflows/**'
- '!public/**/*.md' - '!public/**/*.md'
- '!**/*.md' - '!**/*.md'
- '!.gitea/workflows/**'
- '!.gitignore' - '!.gitignore'
pull_request: pull_request:
branches: branches:
@@ -29,9 +29,9 @@ on:
- 'docker-compose.yml' - 'docker-compose.yml'
- 'db/**' - 'db/**'
- 'public/**' - 'public/**'
- '.gitea/workflows/**'
- '!public/**/*.md' - '!public/**/*.md'
- '!**/*.md' - '!**/*.md'
- '!.gitea/workflows/**'
- '!.gitignore' - '!.gitignore'
env: env:
@@ -43,18 +43,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# - name: Cache Gitea Actions
# uses: actions/cache@v3
# with:
# path: |
# ~/.cache/actcache
# ~/.cache/act
# key: ${{ runner.os }}-act-${{ hashFiles('**/.gitea/workflows/*.yml') }}
# restore-keys: |
# ${{ runner.os }}-act-
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -76,18 +69,19 @@ jobs:
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
COMMIT_HASH=${{ github.sha }} COMMIT_HASH=${{ github.sha }}
BUILD_DATE=${{ github.event.head_commit.timestamp }} BUILD_DATE=${{ github.event.head_commit.timestamp }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max cache-to: type=inline
- name: Image digest - name: Image digest
run: echo "Image pushed with digest ${{ steps.meta.outputs.digest }}" run: echo "Image pushed with digest ${{ steps.meta.outputs.digest }}"

View File

@@ -25,6 +25,14 @@ Eine Full-Stack-Zeiterfassungsanwendung, entwickelt mit Node.js, Express, SQLite
- Timer persistiert über Seiten-Reloads - Timer persistiert über Seiten-Reloads
- Manuelle Startzeit-Eingabe möglich - Manuelle Startzeit-Eingabe möglich
- Visueller Indikator (blinkendes Uhr-Icon) bei laufendem Timer - Visueller Indikator (blinkendes Uhr-Icon) bei laufendem Timer
- **Timer-Metriken** bei laufendem Timer:
- Läuft seit: Startzeit mit Icon-Styling
- Soll erreicht: Uhrzeit wann Tagesziel erreicht wird (inkl. Pausen)
- Zeit bis Soll: Live-Countdown zur Zielzeit
- Saldo bei Soll: Prognostizierter Gesamtsaldo nach Erreichen der geplanten Zeit
- **Anpassbare Arbeitszeit**: Dropdown 4h-10h für flexible Arbeitstage
- Einstellung bleibt über Reloads erhalten (nur während Timer läuft)
- Wird bei Timer-Stop auf 8h zurückgesetzt
- **Flexible Eingabemodi**: - **Flexible Eingabemodi**:
- Manuelle Eingabe (Datum, Start, Ende, Pause) - Manuelle Eingabe (Datum, Start, Ende, Pause)
- Inline-Bearbeitung direkt in der Tabelle - Inline-Bearbeitung direkt in der Tabelle
@@ -38,10 +46,16 @@ Eine Full-Stack-Zeiterfassungsanwendung, entwickelt mit Node.js, Express, SQLite
- **Automatische Pausenberechnung** (deutsches Arbeitszeitgesetz) - **Automatische Pausenberechnung** (deutsches Arbeitszeitgesetz)
- **10-Stunden-Cap** für maximale Nettoarbeitszeit pro Tag - **10-Stunden-Cap** für maximale Nettoarbeitszeit pro Tag
- **Live-Statistiken** mit laufendem Timer: - **Live-Statistiken** mit laufendem Timer:
- Soll-Stunden (basierend auf Arbeitstagen) - Soll-Stunden (basierend auf Arbeitstagen mit Einträgen)
- Ist-Stunden (inkl. aktuell laufender Timer) - Ist-Stunden (inkl. aktuell laufender Timer)
- Monatssaldo + Gesamtsaldo mit Vormonatsübertrag - Monatssaldo + Gesamtsaldo mit Vormonatsübertrag
- Arbeitstage-Zählung - Arbeitstage-Zählung
- **Intelligente Soll-Berechnung**: Berücksichtigt nur Tage mit Einträgen oder laufendem Timer
- **Laufendes Saldo** in Monatsansicht:
- Spalte "Saldo" zeigt kumulatives Saldo bis zu jedem Tag
- Live-Updates während Timer läuft
- Farbcodierung: Grün (positiv) / Rot (negativ)
- Berücksichtigt Flextime-Tage korrekt (-8h)
- **Urlaubsverwaltung**: - **Urlaubsverwaltung**:
- Konfigurierbares Jahres-Kontingent - Konfigurierbares Jahres-Kontingent
- Tracking: Genommen, Geplant, Verfügbar - Tracking: Genommen, Geplant, Verfügbar
@@ -50,6 +64,10 @@ Eine Full-Stack-Zeiterfassungsanwendung, entwickelt mit Node.js, Express, SQLite
### 🗓️ Bundesland-spezifische Feiertage ### 🗓️ Bundesland-spezifische Feiertage
- **16 Bundesländer** mit korrekten regionalen Feiertagen - **16 Bundesländer** mit korrekten regionalen Feiertagen
- **Persistente Einstellung** (gespeichert in Datenbank) - **Persistente Einstellung** (gespeichert in Datenbank)
- **Betriebsfreie Tage**: Wählbar zwischen Heiligabend (24.12.) oder Silvester (31.12.)
- Toggle in Einstellungen
- Wird als "Betriebsfrei" markiert
- Verhindert doppelte Einträge an diesen Tagen
- **Kollisionserkennung**: Warnung bei Feiertagen mit bestehenden Einträgen - **Kollisionserkennung**: Warnung bei Feiertagen mit bestehenden Einträgen
- **Alle Feiertage**: Bundeseinheitlich + regional (z.B. Fronleichnam, Reformationstag) - **Alle Feiertage**: Bundeseinheitlich + regional (z.B. Fronleichnam, Reformationstag)
@@ -62,6 +80,7 @@ Eine Full-Stack-Zeiterfassungsanwendung, entwickelt mit Node.js, Express, SQLite
- 🔴 Rot: Fehlende Arbeitstage - 🔴 Rot: Fehlende Arbeitstage
- ⚫ Grau: Wochenenden - ⚫ Grau: Wochenenden
- 🔵 Blau: Feiertage (mit Namen) - 🔵 Blau: Feiertage (mit Namen)
- 💙 Blauer Rand: Heutiger Tag
- **Navigation**: Vor/Zurück-Buttons zum Monatswechsel - **Navigation**: Vor/Zurück-Buttons zum Monatswechsel
- **Auto-Fill**: Automatisches Befüllen des Monats mit Standard-Arbeitszeiten (9:00-17:30) - **Auto-Fill**: Automatisches Befüllen des Monats mit Standard-Arbeitszeiten (9:00-17:30)
- **Quick-Actions**: Plus-Buttons für schnelles Hinzufügen von Einträgen - **Quick-Actions**: Plus-Buttons für schnelles Hinzufügen von Einträgen
@@ -83,7 +102,9 @@ Eine Full-Stack-Zeiterfassungsanwendung, entwickelt mit Node.js, Express, SQLite
- **CSV-Export (Abweichungen)**: Nur Tage ≠ 8,0h - **CSV-Export (Abweichungen)**: Nur Tage ≠ 8,0h
- Ideal für Gleitzeit-Nachweise - Ideal für Gleitzeit-Nachweise
- **PDF-Export**: - **PDF-Export**:
- **Monats-Export**: Exportiert aktuellen Monat als formatierten PDF - **Monats-Export**: Nur verfügbar wenn letzter Tag des Monats vollständig erfasst ist
- Verhindert versehentliche Exports unvollständiger Monate
- Button wird automatisch angezeigt sobald Bedingung erfüllt
- **Bulk-Export**: Exportiert ausgewählte Einträge (im Bulk-Modus) - **Bulk-Export**: Exportiert ausgewählte Einträge (im Bulk-Modus)
- Professionelles Layout mit Mitarbeiter-Info und Statistiken - Professionelles Layout mit Mitarbeiter-Info und Statistiken
- Automatische Tabelle mit allen Einträgen - Automatische Tabelle mit allen Einträgen
@@ -105,6 +126,8 @@ Eine Full-Stack-Zeiterfassungsanwendung, entwickelt mit Node.js, Express, SQLite
### 🎨 Modernes UI/UX ### 🎨 Modernes UI/UX
- **Premium Design**: Glass-Morphism, Gradients, Schatten, Animationen - **Premium Design**: Glass-Morphism, Gradients, Schatten, Animationen
- **Responsive**: Desktop, Tablet, Mobile - **Responsive**: Desktop, Tablet, Mobile
- Timer-Metriken: Rechts neben Timer auf großen Displays, darunter auf mobil
- Adaptive Layouts für alle Bildschirmgrößen
- **Dark Mode**: Augenschonendes dunkles Design - **Dark Mode**: Augenschonendes dunkles Design
- **Toast-Benachrichtigungen**: Visuelles Feedback - **Toast-Benachrichtigungen**: Visuelles Feedback
- **Icons**: Lucide Icons für klare Symbolik - **Icons**: Lucide Icons für klare Symbolik
@@ -113,7 +136,7 @@ Eine Full-Stack-Zeiterfassungsanwendung, entwickelt mit Node.js, Express, SQLite
## 🏗️ Technologie-Stack ## 🏗️ Technologie-Stack
**Backend:** **Backend:**
- Node.js 18+ mit Express.js - Node.js 20+ mit Express.js
- SQLite (better-sqlite3) für dateibasierte Persistenz - SQLite (better-sqlite3) für dateibasierte Persistenz
- Modulare Architektur (config, utils, routes) - Modulare Architektur (config, utils, routes)
@@ -127,6 +150,8 @@ Eine Full-Stack-Zeiterfassungsanwendung, entwickelt mit Node.js, Express, SQLite
**Infrastructure:** **Infrastructure:**
- Docker & Docker Compose - Docker & Docker Compose
- Multi-Stage Build für optimierte Images - Multi-Stage Build für optimierte Images
- Node.js 20 Alpine Linux base image
- Python build dependencies für native Module
- Gitea Actions CI/CD für automatische Builds - Gitea Actions CI/CD für automatische Builds
- Gitea Container Registry für Image-Hosting - Gitea Container Registry für Image-Hosting
@@ -142,15 +167,16 @@ timetracker/
│ ├── index.html # Single-Page Application │ ├── index.html # Single-Page Application
│ ├── favicon.svg # App Icon │ ├── favicon.svg # App Icon
│ └── js/ │ └── js/
│ ├── state.js # Globaler State │ ├── state.js # Globaler State (companyHolidayPreference, targetHours)
│ ├── utils.js # Hilfsfunktionen │ ├── utils.js # Hilfsfunktionen
│ ├── holidays.js # Feiertagsberechnung │ ├── holidays.js # Feiertagsberechnung (16 Bundesländer)
│ ├── bridge-days.js # Brückentag-Optimierung
│ ├── api.js # Backend-Kommunikation │ ├── api.js # Backend-Kommunikation
│ └── main.js # Hauptlogik (~3700 Zeilen) │ └── main.js # Hauptlogik (~4000 Zeilen)
├── .gitea/workflows/ # CI/CD Workflows ├── .gitea/workflows/ # CI/CD Workflows
│ └── docker-build.yml # Docker Build & Push │ └── docker-build.yml # Docker Build & Push
├── media/screenshots/ # App-Screenshots ├── media/screenshots/ # App-Screenshots
├── Dockerfile # Container-Image ├── Dockerfile # Container-Image (Node 20 + Python)
├── docker-compose.yml # Orchestrierung ├── docker-compose.yml # Orchestrierung
└── package.json └── package.json
``` ```
@@ -227,7 +253,7 @@ docker run -p 3000:3000 -v $(pwd)/db:/app/db zeiterfassung
### 💻 Option 3: Lokal (ohne Docker) ### 💻 Option 3: Lokal (ohne Docker)
**Voraussetzungen:** Node.js 18+ **Voraussetzungen:** Node.js 20+
```bash ```bash
# Repository klonen # Repository klonen
@@ -247,6 +273,9 @@ Die App bietet mehrere Export-Modi für verschiedene Anwendungsfälle:
### PDF-Export 📄 ### PDF-Export 📄
**Monats-Export:** **Monats-Export:**
- Button erscheint nur wenn letzter Tag des Monats vollständig erfasst ist
- Startzeit und Endzeit müssen vorhanden sein
- Verhindert versehentliche Exports unvollständiger Monate
- Klicke auf "PDF Export" in der Monatsansicht - Klicke auf "PDF Export" in der Monatsansicht
- Exportiert alle Einträge des aktuellen Monats - Exportiert alle Einträge des aktuellen Monats
- Professionelles Layout mit: - Professionelles Layout mit:
@@ -362,13 +391,14 @@ Die App verwendet Gitea Actions für automatische Builds und Deployments:
**Architektur:** Single-Page Application (SPA) mit REST-API Backend **Architektur:** Single-Page Application (SPA) mit REST-API Backend
**Tech-Details:** **Tech-Details:**
- Modulare Frontend-Architektur (5 separate JS-Dateien) - Modulare Frontend-Architektur (6 separate JS-Dateien)
- Flatpickr für Touch-optimierte Picker (auch in Tabellen-Inline-Edit) - Flatpickr für Touch-optimierte Picker (auch in Tabellen-Inline-Edit)
- Lucide Icons für Symbolik - Lucide Icons für Symbolik
- jsPDF + autoTable für PDF-Generierung - jsPDF + autoTable für PDF-Generierung
- SQLite für dateibasierte Persistenz - SQLite für dateibasierte Persistenz
- Server-seitige Berechnungen für Datenintegrität - Server-seitige Berechnungen für Datenintegrität
- Responsive Design (Tailwind CSS via CDN) - Responsive Design (Tailwind CSS via CDN)
- Live-Updates während Timer läuft (Saldo, Nettostunden)
**Datenpersistenz:** **Datenpersistenz:**
- SQLite-Datenbank: `db/timetracker.db` - SQLite-Datenbank: `db/timetracker.db`
@@ -377,11 +407,20 @@ Die App verwendet Gitea Actions für automatische Builds und Deployments:
- JSON-basierte Backups für Migration - JSON-basierte Backups für Migration
**Code-Organisation:** **Code-Organisation:**
- `state.js`: Globaler Application State - `state.js`: Globaler Application State (companyHolidayPreference, targetHours)
- `utils.js`: Hilfsfunktionen (Datum, Zeit, Format) - `utils.js`: Hilfsfunktionen (Datum, Zeit, Format)
- `holidays.js`: Feiertagsberechnung (16 Bundesländer) - `holidays.js`: Feiertagsberechnung (16 Bundesländer + Betriebsfrei)
- `bridge-days.js`: Brückentag-Optimierung
- `api.js`: Backend-Kommunikation - `api.js`: Backend-Kommunikation
- `main.js`: Hauptlogik, UI, Event-Handler - `main.js`: Hauptlogik, UI, Event-Handler (~4000 Zeilen)
**Neue Features:**
- Timer-Metriken mit Live-Berechnung
- Anpassbare tägliche Arbeitszeit (4h-10h)
- Laufendes Saldo in Tabelle
- Intelligente PDF-Export-Aktivierung
- Betriebsfreier Tag (Weihnachten/Silvester)
- Responsive Timer-Layout
## 📄 Lizenz ## 📄 Lizenz

View File

@@ -491,6 +491,9 @@
<div class="stat-card glass-card rounded-xl p-5 border border-gray-600"> <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 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 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 id="statSickdays" class="text-xs text-red-400 mt-2 hidden">
<i data-lucide="activity" class="w-3 h-3 inline"></i> <span id="statSickdaysCount">0</span> Krankheitstage
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -401,15 +401,19 @@ function updateTimerMetrics(netElapsedSeconds) {
// Target hours from user selection (default 8) // Target hours from user selection (default 8)
const targetSeconds = targetHours * 60 * 60; const targetSeconds = targetHours * 60 * 60;
// Calculate total pause time: 30 min after 6h + 15 min after 9h // Calculate required pause time based on target hours (German law)
const pauseDuration30Min = 30 * 60; // 30 minutes in seconds let totalPauseSeconds = 0;
const pauseDuration15Min = 15 * 60; // 15 minutes in seconds if (targetHours > 9) {
// More than 9h -> 45 min pause (30 + 15)
totalPauseSeconds = 45 * 60;
} else if (targetHours > 6) {
// More than 6h -> 30 min pause
totalPauseSeconds = 30 * 60;
}
// 6h or less -> no mandatory pause
// Time needed including pauses // Total gross time = target work hours + pause time
// After 6h work -> 30 min pause const totalGrossTimeNeeded = targetSeconds + totalPauseSeconds;
// After 9h work -> 15 min pause
// Total gross time = target work hours + 30min pause + 15min pause
const totalGrossTimeNeeded = targetSeconds + pauseDuration30Min + pauseDuration15Min;
// Calculate when target will be reached (clock time) // Calculate when target will be reached (clock time)
const targetReachedTimestamp = new Date(timerStartTime + totalGrossTimeNeeded * 1000); const targetReachedTimestamp = new Date(timerStartTime + totalGrossTimeNeeded * 1000);
@@ -647,11 +651,11 @@ function renderEntries(entries) {
</td> </td>
` : '<td class="hidden"></td>'; ` : '<td class="hidden"></td>';
// Calculate balance: for work and flextime entries (excluding vacation) // Calculate balance: for work and flextime entries (excluding vacation and sickday)
const entryType = entry.entryType || 'work'; const entryType = entry.entryType || 'work';
let balanceCell = ''; let balanceCell = '';
if (entryType !== 'vacation') { if (entryType !== 'vacation' && entryType !== 'sickday') {
// For all workdays (including flextime): Actual - Target (8h) // For all workdays (including flextime): Actual - Target (8h)
// Flextime has netHours = 0, so deviation will be -8h // Flextime has netHours = 0, so deviation will be -8h
const deviation = entry.netHours - 8.0; const deviation = entry.netHours - 8.0;
@@ -804,6 +808,12 @@ function renderMonthlyView(entries) {
displayTimes = `<td class="px-4 py-3 whitespace-nowrap text-sm text-center text-gray-400" colspan="3"> displayTimes = `<td class="px-4 py-3 whitespace-nowrap text-sm text-center text-gray-400" colspan="3">
<span class="italic">Urlaub</span> <span class="italic">Urlaub</span>
</td>`; </td>`;
} else if (entryType === 'sickday') {
displayIcon = '<i data-lucide="activity" class="w-4 h-4 inline"></i>';
displayText = 'Krank';
displayTimes = `<td class="px-4 py-3 whitespace-nowrap text-sm text-center text-gray-400" colspan="3">
<span class="italic">Krankheit</span>
</td>`;
} else if (entryType === 'flextime') { } else if (entryType === 'flextime') {
displayIcon = '<i data-lucide="clock" class="w-4 h-4 inline"></i>'; displayIcon = '<i data-lucide="clock" class="w-4 h-4 inline"></i>';
displayText = 'Gleitzeit'; displayText = 'Gleitzeit';
@@ -845,11 +855,11 @@ function renderMonthlyView(entries) {
</td> </td>
` : '<td class="hidden"></td>'; ` : '<td class="hidden"></td>';
// Calculate balance: only for past days (excluding vacation) // Calculate balance: only for past days (excluding vacation and sickday)
const isPastDay = dateObj < today || (dateObj.getFullYear() === todayYear && dateObj.getMonth() === todayMonth && day <= todayDay); const isPastDay = dateObj < today || (dateObj.getFullYear() === todayYear && dateObj.getMonth() === todayMonth && day <= todayDay);
let balanceCell = ''; let balanceCell = '';
if (isPastDay && entryType !== 'vacation') { if (isPastDay && entryType !== 'vacation' && entryType !== 'sickday') {
// For all workdays (including flextime): Actual - Target (8h default) // For all workdays (including flextime): Actual - Target (8h default)
// Flextime has netHours = 0, so deviation will be -8h // Flextime has netHours = 0, so deviation will be -8h
@@ -938,6 +948,9 @@ function renderMonthlyView(entries) {
<button class="btn-add-vacation inline-flex items-center justify-center w-7 h-7 text-amber-400/80 hover:text-amber-400 hover:bg-amber-500/10 rounded transition-all" data-date="${dateISO}" title="Urlaub eintragen"> <button class="btn-add-vacation inline-flex items-center justify-center w-7 h-7 text-amber-400/80 hover:text-amber-400 hover:bg-amber-500/10 rounded transition-all" data-date="${dateISO}" title="Urlaub eintragen">
<i data-lucide="plane" class="w-4 h-4"></i> <i data-lucide="plane" class="w-4 h-4"></i>
</button> </button>
<button class="btn-add-sickday inline-flex items-center justify-center w-7 h-7 text-red-400/80 hover:text-red-400 hover:bg-red-500/10 rounded transition-all" data-date="${dateISO}" title="Krankheit eintragen">
<i data-lucide="activity" class="w-4 h-4"></i>
</button>
<button class="btn-add-flextime inline-flex items-center justify-center w-7 h-7 text-cyan-400/80 hover:text-cyan-400 hover:bg-cyan-500/10 rounded transition-all" data-date="${dateISO}" title="Gleitzeit eintragen"> <button class="btn-add-flextime inline-flex items-center justify-center w-7 h-7 text-cyan-400/80 hover:text-cyan-400 hover:bg-cyan-500/10 rounded transition-all" data-date="${dateISO}" title="Gleitzeit eintragen">
<i data-lucide="clock" class="w-4 h-4"></i> <i data-lucide="clock" class="w-4 h-4"></i>
</button> </button>
@@ -989,6 +1002,14 @@ function renderMonthlyView(entries) {
}); });
}); });
// Add sick day entry for a specific date
document.querySelectorAll('.btn-add-sickday').forEach(btn => {
btn.addEventListener('click', async () => {
const dateISO = btn.dataset.date;
await addSpecialEntry(dateISO, 'sickday');
});
});
// Add flextime entry for a specific date // Add flextime entry for a specific date
document.querySelectorAll('.btn-add-flextime').forEach(btn => { document.querySelectorAll('.btn-add-flextime').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
@@ -1016,7 +1037,7 @@ function renderMonthlyView(entries) {
* Add a special entry (vacation or flextime) for a specific date * Add a special entry (vacation or flextime) for a specific date
*/ */
async function addSpecialEntry(dateISO, entryType) { async function addSpecialEntry(dateISO, entryType) {
const typeName = entryType === 'vacation' ? 'Urlaub' : 'Gleittag'; const typeName = entryType === 'vacation' ? 'Urlaub' : entryType === 'sickday' ? 'Krankheit' : 'Gleittag';
// Check if date is a public holiday // Check if date is a public holiday
const dateObj = new Date(dateISO + 'T00:00:00'); const dateObj = new Date(dateISO + 'T00:00:00');
@@ -1046,7 +1067,9 @@ async function addSpecialEntry(dateISO, entryType) {
throw new Error(error.error || 'Fehler beim Erstellen'); throw new Error(error.error || 'Fehler beim Erstellen');
} }
const message = entryType === 'vacation' ? '✅ Urlaub eingetragen (kein Arbeitstag)' : '✅ Gleittag eingetragen (8h Soll, 0h Ist)'; const message = entryType === 'vacation' ? '✅ Urlaub eingetragen (kein Arbeitstag)' :
entryType === 'sickday' ? '✅ Krankheit eingetragen (kein Arbeitstag)' :
'✅ Gleittag eingetragen (8h Soll, 0h Ist)';
showNotification(message, 'success'); showNotification(message, 'success');
loadMonthlyView(); loadMonthlyView();
} catch (error) { } catch (error) {
@@ -1082,20 +1105,19 @@ async function loadMonthlyView() {
updateStatistics(entries); updateStatistics(entries);
updateBridgeDaysDisplay(); updateBridgeDaysDisplay();
// Show/hide PDF export button based on whether last day has complete times // Show/hide PDF export button based on whether the month is complete (in the past)
const pdfButton = document.getElementById('btnExportPDF'); const pdfButton = document.getElementById('btnExportPDF');
const pdfButtonMobile = document.getElementById('btnExportPDFMobile'); const pdfButtonMobile = document.getElementById('btnExportPDFMobile');
// Check if last day of month has start and end time (and they're different) // Check if the displayed month is in the past (not current month or future)
const lastDayDate = `${displayYear}-${String(displayMonth + 1).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`; const today = new Date();
const lastDayEntry = entries.find(e => e.date === lastDayDate); const currentYear = today.getFullYear();
const currentMonth = today.getMonth();
const isLastDayComplete = lastDayEntry && const isMonthComplete = (displayYear < currentYear) ||
lastDayEntry.startTime && (displayYear === currentYear && displayMonth < currentMonth);
lastDayEntry.endTime &&
lastDayEntry.startTime !== lastDayEntry.endTime;
if (isLastDayComplete) { if (isMonthComplete) {
pdfButton.style.display = ''; pdfButton.style.display = '';
if (pdfButtonMobile) pdfButtonMobile.style.display = ''; if (pdfButtonMobile) pdfButtonMobile.style.display = '';
} else { } else {
@@ -1153,6 +1175,13 @@ async function updateStatistics(entries) {
.map(e => e.date) .map(e => e.date)
); );
// Count sick days to exclude from workdays
const sickDays = new Set(
entries
.filter(e => e.entryType === 'sickday')
.map(e => e.date)
);
// Count flextime days (they are workdays with 0 hours worked) // Count flextime days (they are workdays with 0 hours worked)
const flextimeDays = new Set( const flextimeDays = new Set(
entries entries
@@ -1162,6 +1191,15 @@ async function updateStatistics(entries) {
let futureFlextimeDays = 0; // Count future flextime days in current month let futureFlextimeDays = 0; // Count future flextime days in current month
// Create a map to check if a day has an entry
const entriesMap = {};
entries.forEach(entry => {
entriesMap[entry.date] = entry;
});
const todayISO = getTodayISO();
const isCurrentMonth = currentYear === today.getFullYear() && currentMonth === today.getMonth();
for (let day = 1; day <= lastDay; day++) { for (let day = 1; day <= lastDay; day++) {
const dateObj = new Date(currentYear, currentMonth, day); const dateObj = new Date(currentYear, currentMonth, day);
const year = dateObj.getFullYear(); const year = dateObj.getFullYear();
@@ -1170,37 +1208,42 @@ async function updateStatistics(entries) {
const dateISO = `${year}-${month}-${dayStr}`; const dateISO = `${year}-${month}-${dayStr}`;
const isVacation = vacationDays.has(dateISO); const isVacation = vacationDays.has(dateISO);
const isSickday = sickDays.has(dateISO);
const isFlextime = flextimeDays.has(dateISO); const isFlextime = flextimeDays.has(dateISO);
const isWeekendHoliday = isWeekendOrHoliday(dateObj); const isWeekendHoliday = isWeekendOrHoliday(dateObj);
const hasEntry = entriesMap[dateISO];
const isToday = dateISO === todayISO;
if (!isWeekendHoliday && !isVacation && !isFlextime) { // For today: only count as workday if there's an entry OR timer is running
// Normal workday (excluding vacation and flextime days) const shouldCountToday = !isToday || hasEntry || (timerStartTime && isCurrentMonth);
if (!isWeekendHoliday && !isVacation && !isSickday && !isFlextime) {
// Normal workday (excluding vacation, sick days, and flextime days)
totalWorkdaysInMonth++; totalWorkdaysInMonth++;
workdaysCount++; workdaysCount++;
if (dateObj <= today) { if (dateObj <= today && shouldCountToday) {
workdaysPassed++; workdaysPassed++;
} }
} else if (!isWeekendHoliday && !isVacation && isFlextime) { } else if (!isWeekendHoliday && !isVacation && !isSickday && isFlextime) {
// Flextime on a workday - still counts as workday in calendar // Flextime on a workday - still counts as workday in calendar
totalWorkdaysInMonth++; totalWorkdaysInMonth++;
workdaysCount++; workdaysCount++;
if (dateObj <= today) { if (dateObj <= today && shouldCountToday) {
workdaysPassed++; workdaysPassed++;
} else { } else {
// Future flextime in current month // Future flextime in current month
const isCurrentMonth = currentYear === today.getFullYear() && currentMonth === today.getMonth(); if (isCurrentMonth && dateObj > today) {
if (isCurrentMonth) {
futureFlextimeDays++; futureFlextimeDays++;
} }
} }
} else if (isFlextime && isWeekendHoliday) { } else if (isFlextime && isWeekendHoliday) {
// Flextime on weekend/holiday counts as additional workday // Flextime on weekend/holiday counts as additional workday
totalWorkdaysInMonth++; totalWorkdaysInMonth++;
if (new Date(dateISO) <= today) { if (new Date(dateISO) <= today && shouldCountToday) {
workdaysPassed++; workdaysPassed++;
} }
} }
// Vacation days are excluded from all counts // Vacation days and sick days are excluded from all counts
} }
// Calculate target hours (8h per workday passed, no reduction) // Calculate target hours (8h per workday passed, no reduction)
@@ -1212,7 +1255,6 @@ async function updateStatistics(entries) {
.reduce((sum, entry) => sum + entry.netHours, 0); .reduce((sum, entry) => sum + entry.netHours, 0);
// Add currently running timer hours to actual hours (only for current month) // Add currently running timer hours to actual hours (only for current month)
const isCurrentMonth = currentYear === today.getFullYear() && currentMonth === today.getMonth();
if (timerStartTime && isCurrentMonth) { if (timerStartTime && isCurrentMonth) {
const now = Date.now(); const now = Date.now();
let elapsedSeconds; let elapsedSeconds;
@@ -1237,20 +1279,40 @@ async function updateStatistics(entries) {
// Calculate previous month balance // Calculate previous month balance
const previousBalance = await calculatePreviousBalance(); const previousBalance = await calculatePreviousBalance();
// Total balance = previous balance + current month balance // Total balance = all months from first entry up to today (independent of displayed month)
const totalBalance = previousBalance + balance; const totalBalance = await calculateTotalBalance();
// Count actual work entries (excluding vacation/flextime, only up to today) // Count actual work entries (excluding vacation/flextime/sickday, only up to today)
const workEntriesCount = entries.filter(e => { const workEntriesCount = entries.filter(e => {
const entryDate = new Date(e.date); const entryDate = new Date(e.date);
return entryDate <= today && (!e.entryType || e.entryType === 'work'); return entryDate <= today && (!e.entryType || e.entryType === 'work');
}).length; }).length;
// Count sick days for current month (only past days)
const sickDaysCount = entries.filter(e => {
const entryDate = new Date(e.date);
return entryDate <= today && e.entryType === 'sickday';
}).length;
// Update UI // Update UI
document.getElementById('statTargetHours').textContent = targetHours.toFixed(1) + 'h'; document.getElementById('statTargetHours').textContent = targetHours.toFixed(1) + 'h';
document.getElementById('statActualHours').textContent = actualHours.toFixed(1) + 'h'; document.getElementById('statActualHours').textContent = actualHours.toFixed(1) + 'h';
document.getElementById('statWorkdays').textContent = `${workEntriesCount}/${totalWorkdaysInMonth}`; document.getElementById('statWorkdays').textContent = `${workEntriesCount}/${totalWorkdaysInMonth}`;
// Show/hide sick days info
const sickdaysElement = document.getElementById('statSickdays');
const sickdaysCountElement = document.getElementById('statSickdaysCount');
if (sickDaysCount > 0) {
sickdaysCountElement.textContent = sickDaysCount;
sickdaysElement.classList.remove('hidden');
// Re-initialize icons for the activity icon
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
}
} else {
sickdaysElement.classList.add('hidden');
}
// Show/hide flextime hint icons // Show/hide flextime hint icons
const balanceFlextimeHint = document.getElementById('balanceFlextimeHint'); const balanceFlextimeHint = document.getElementById('balanceFlextimeHint');
const totalBalanceFlextimeHint = document.getElementById('totalBalanceFlextimeHint'); const totalBalanceFlextimeHint = document.getElementById('totalBalanceFlextimeHint');
@@ -1451,10 +1513,11 @@ async function addBridgeDaysAsVacation(days) {
/** /**
* Calculate balance from all previous months (starting from first month with entries) * Calculate balance from all previous months (starting from first month with entries)
* up to (but not including) the given month. Defaults to the currently displayed month.
*/ */
async function calculatePreviousBalance() { async function calculatePreviousBalance(upToYear, upToMonth) {
const currentYear = displayYear; const currentYear = (upToYear !== undefined) ? upToYear : displayYear;
const currentMonth = displayMonth; const currentMonth = (upToMonth !== undefined) ? upToMonth : displayMonth;
// Find the first month with any entries by checking all entries // Find the first month with any entries by checking all entries
const allEntries = await fetchEntries(); const allEntries = await fetchEntries();
@@ -1462,9 +1525,16 @@ async function calculatePreviousBalance() {
return 0; return 0;
} }
// Filter out entries with invalid dates
const validEntries = allEntries.filter(e => e.date && e.date.match(/^\d{4}-\d{2}-\d{2}$/));
if (validEntries.length === 0) {
return 0;
}
// Find earliest date // Find earliest date
const earliestDate = allEntries.reduce((earliest, entry) => { const earliestDate = validEntries.reduce((earliest, entry) => {
const entryDate = new Date(entry.date); const entryDate = new Date(entry.date + 'T00:00:00');
return !earliest || entryDate < earliest ? entryDate : earliest; return !earliest || entryDate < earliest ? entryDate : earliest;
}, null); }, null);
@@ -1481,6 +1551,9 @@ async function calculatePreviousBalance() {
let checkMonth = firstMonth; let checkMonth = firstMonth;
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); // Normalize to start of day
console.log(`calculatePreviousBalance: Calculating from ${firstYear}-${firstMonth+1} to ${currentYear}-${currentMonth} (exclusive)`);
// Loop through all months from first entry until previous month of displayed month // Loop through all months from first entry until previous month of displayed month
while (checkYear < currentYear || (checkYear === currentYear && checkMonth < currentMonth)) { while (checkYear < currentYear || (checkYear === currentYear && checkMonth < currentMonth)) {
@@ -1490,9 +1563,9 @@ async function calculatePreviousBalance() {
const entries = await fetchEntries(firstDay, lastDayStr); const entries = await fetchEntries(firstDay, lastDayStr);
// For past months, use full month. For current month (if displayed), limit to today // For past months (completed months), count ALL workdays
const monthEnd = new Date(checkYear, checkMonth + 1, 0); // Only limit to today if this is the current calendar month
const limitDate = monthEnd; // Always use full month for previous balance calculation const isCurrentCalendarMonth = (checkYear === today.getFullYear() && checkMonth === today.getMonth());
let workdaysPassed = 0; let workdaysPassed = 0;
const monthLastDay = new Date(checkYear, checkMonth + 1, 0).getDate(); const monthLastDay = new Date(checkYear, checkMonth + 1, 0).getDate();
@@ -1504,6 +1577,13 @@ async function calculatePreviousBalance() {
.map(e => e.date) .map(e => e.date)
); );
// Count sick days to exclude from workdays
const sickDays = new Set(
entries
.filter(e => e.entryType === 'sickday')
.map(e => e.date)
);
// Count flextime days (they are workdays with 0 hours worked) // Count flextime days (they are workdays with 0 hours worked)
const flextimeDays = new Set( const flextimeDays = new Set(
entries entries
@@ -1511,16 +1591,24 @@ async function calculatePreviousBalance() {
.map(e => e.date) .map(e => e.date)
); );
// Count workdays (all days for past months, up to today for current month)
for (let day = 1; day <= monthLastDay; day++) { for (let day = 1; day <= monthLastDay; day++) {
const dateObj = new Date(checkYear, checkMonth, day); const dateObj = new Date(checkYear, checkMonth, day);
dateObj.setHours(0, 0, 0, 0);
// For current calendar month, only count up to today
if (isCurrentCalendarMonth && dateObj > today) {
break;
}
const year = dateObj.getFullYear(); const year = dateObj.getFullYear();
const month = String(dateObj.getMonth() + 1).padStart(2, '0'); const month = String(dateObj.getMonth() + 1).padStart(2, '0');
const dayStr = String(dateObj.getDate()).padStart(2, '0'); const dayStr = String(dateObj.getDate()).padStart(2, '0');
const dateISO = `${year}-${month}-${dayStr}`; const dateISO = `${year}-${month}-${dayStr}`;
if (!isWeekendOrHoliday(dateObj)) { if (!isWeekendOrHoliday(dateObj)) {
// Exclude vacation days from workdays count // Exclude vacation and sick days from workdays count
if (!vacationDays.has(dateISO)) { if (!vacationDays.has(dateISO) && !sickDays.has(dateISO)) {
workdaysPassed++; workdaysPassed++;
} }
} else if (flextimeDays.has(dateISO)) { } else if (flextimeDays.has(dateISO)) {
@@ -1533,6 +1621,8 @@ async function calculatePreviousBalance() {
const actualHours = entries.reduce((sum, entry) => sum + entry.netHours, 0); const actualHours = entries.reduce((sum, entry) => sum + entry.netHours, 0);
const monthBalance = actualHours - targetHours; const monthBalance = actualHours - targetHours;
console.log(`Month ${checkYear}-${String(checkMonth+1).padStart(2, '0')}: ${entries.length} entries, ${workdaysPassed} workdays, ${actualHours.toFixed(1)}h actual, ${targetHours.toFixed(1)}h target, balance: ${monthBalance.toFixed(1)}h`);
totalBalance += monthBalance; totalBalance += monthBalance;
// Move to next month // Move to next month
@@ -1543,9 +1633,62 @@ async function calculatePreviousBalance() {
} }
} }
console.log(`Total previous balance: ${totalBalance.toFixed(1)}h`);
return totalBalance; return totalBalance;
} }
/**
* Calculate the total balance from first entry up to and including the displayed month.
* For the current or future months the calculation is capped at today.
*/
async function calculateTotalBalance() {
const today = new Date();
today.setHours(0, 0, 0, 0);
// End of the displayed month
const displayedMonthEnd = new Date(displayYear, displayMonth + 1, 0);
displayedMonthEnd.setHours(0, 0, 0, 0);
// Effective cut-off: end of displayed month, but never beyond today
const effectiveCutoff = displayedMonthEnd < today ? displayedMonthEnd : today;
const effectiveYear = effectiveCutoff.getFullYear();
const effectiveMonth = effectiveCutoff.getMonth();
// All months before the effective month
const previousBalance = await calculatePreviousBalance(effectiveYear, effectiveMonth);
// Add the effective month up to the effective cut-off date
const firstDay = `${effectiveYear}-${String(effectiveMonth + 1).padStart(2, '0')}-01`;
const lastDayNum = new Date(effectiveYear, effectiveMonth + 1, 0).getDate();
const lastDayStr = `${effectiveYear}-${String(effectiveMonth + 1).padStart(2, '0')}-${String(lastDayNum).padStart(2, '0')}`;
const entries = await fetchEntries(firstDay, lastDayStr);
const vacationDays = new Set(entries.filter(e => e.entryType === 'vacation').map(e => e.date));
const sickDays = new Set(entries.filter(e => e.entryType === 'sickday').map(e => e.date));
const flextimeDays = new Set(entries.filter(e => e.entryType === 'flextime').map(e => e.date));
let workdaysPassed = 0;
for (let day = 1; day <= lastDayNum; day++) {
const dateObj = new Date(effectiveYear, effectiveMonth, day);
dateObj.setHours(0, 0, 0, 0);
if (dateObj > effectiveCutoff) break;
const dateISO = `${effectiveYear}-${String(effectiveMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
if (!isWeekendOrHoliday(dateObj)) {
if (!vacationDays.has(dateISO) && !sickDays.has(dateISO)) workdaysPassed++;
} else if (flextimeDays.has(dateISO)) {
workdaysPassed++;
}
}
const targetHours = workdaysPassed * 8;
const actualHours = entries.reduce((sum, entry) => sum + entry.netHours, 0);
const monthBalance = actualHours - targetHours;
const total = previousBalance + monthBalance;
console.log(`calculateTotalBalance: displayed=${displayYear}-${displayMonth + 1}, cutoff=${effectiveYear}-${effectiveMonth + 1}, previousBalance=${previousBalance.toFixed(1)}h, effectiveMonth=${monthBalance.toFixed(1)}h, total=${total.toFixed(1)}h`);
return total;
}
/** /**
* Open modal for adding/editing entry * Open modal for adding/editing entry
*/ */
@@ -2064,7 +2207,7 @@ async function generatePDF(options) {
} = options; } = options;
const { targetHours, totalNetHours, balance } = statistics; const { targetHours, totalNetHours, balance } = statistics;
const { vacationDays = 0, flextimeDays = 0 } = additionalInfo; const { vacationDays = 0, sickDays = 0, flextimeDays = 0 } = additionalInfo;
// Get employee data from settings // Get employee data from settings
const employeeName = await getSetting('employeeName') || ''; const employeeName = await getSetting('employeeName') || '';
@@ -2140,12 +2283,16 @@ async function generatePDF(options) {
doc.text(`${balance >= 0 ? '+' : ''}${balance.toFixed(1)}h`, 170, statsY + 3, { align: 'center' }); doc.text(`${balance >= 0 ? '+' : ''}${balance.toFixed(1)}h`, 170, statsY + 3, { align: 'center' });
// Additional info if needed (small, far right) // Additional info if needed (small, far right)
if (vacationDays > 0 || flextimeDays > 0) { if (vacationDays > 0 || sickDays > 0 || flextimeDays > 0) {
doc.setTextColor(150, 150, 150); doc.setTextColor(150, 150, 150);
doc.setFontSize(7); doc.setFontSize(7);
doc.setFont(undefined, 'normal'); doc.setFont(undefined, 'normal');
let infoText = ''; let infoText = '';
if (vacationDays > 0) infoText += `Urlaub: ${vacationDays}`; if (vacationDays > 0) infoText += `Urlaub: ${vacationDays}`;
if (sickDays > 0) {
if (infoText) infoText += ' ';
infoText += `Krank: ${sickDays}`;
}
if (flextimeDays > 0) { if (flextimeDays > 0) {
if (infoText) infoText += ' '; if (infoText) infoText += ' ';
infoText += `Gleitzeit: ${flextimeDays}`; infoText += `Gleitzeit: ${flextimeDays}`;
@@ -2276,6 +2423,13 @@ async function bulkExportPDF() {
.map(e => e.date) .map(e => e.date)
); );
// Count sick days from selected entries
const sickDaysSet = new Set(
selectedEntriesData
.filter(e => e.entryType === 'sickday')
.map(e => e.date)
);
// Count flextime days from selected entries // Count flextime days from selected entries
const flextimeDaysSet = new Set( const flextimeDaysSet = new Set(
selectedEntriesData selectedEntriesData
@@ -2290,10 +2444,11 @@ async function bulkExportPDF() {
const dateObj = new Date(year, month - 1, day); const dateObj = new Date(year, month - 1, day);
const isVacation = vacationDaysSet.has(dateISO); const isVacation = vacationDaysSet.has(dateISO);
const isSickday = sickDaysSet.has(dateISO);
const isFlextime = flextimeDaysSet.has(dateISO); const isFlextime = flextimeDaysSet.has(dateISO);
const isWeekendHoliday = isWeekendOrHoliday(dateObj); const isWeekendHoliday = isWeekendOrHoliday(dateObj);
if (!isWeekendHoliday && !isVacation) { if (!isWeekendHoliday && !isVacation && !isSickday) {
// Normal workday // Normal workday
if (dateObj <= today) { if (dateObj <= today) {
workdaysPassed++; workdaysPassed++;
@@ -2304,12 +2459,13 @@ async function bulkExportPDF() {
workdaysPassed++; workdaysPassed++;
} }
} }
// Vacation days are excluded from workday count // Vacation days and sick days are excluded from workday count
}); });
// Calculate total hours and days // Calculate total hours and days
let totalNetHours = 0; let totalNetHours = 0;
let vacationDays = 0; let vacationDays = 0;
let sickDays = 0;
let flextimeDays = 0; let flextimeDays = 0;
let workEntriesCount = 0; let workEntriesCount = 0;
@@ -2322,6 +2478,9 @@ async function bulkExportPDF() {
} else if (entry.entryType === 'vacation') { } else if (entry.entryType === 'vacation') {
vacationDays++; vacationDays++;
totalNetHours += entry.netHours; totalNetHours += entry.netHours;
} else if (entry.entryType === 'sickday') {
sickDays++;
totalNetHours += entry.netHours;
} else if (entry.entryType === 'flextime') { } else if (entry.entryType === 'flextime') {
flextimeDays++; flextimeDays++;
// Only add flextime hours if it's on a weekend/holiday // Only add flextime hours if it's on a weekend/holiday
@@ -2373,6 +2532,13 @@ async function bulkExportPDF() {
pauseText = '-'; pauseText = '-';
netHoursText = '-'; netHoursText = '-';
deviationStr = '-'; deviationStr = '-';
} else if (entry.entryType === 'sickday') {
locationText = 'Krank';
startTime = '-';
endTime = '-';
pauseText = '-';
netHoursText = '-';
deviationStr = '-';
} else if (entry.entryType === 'flextime') { } else if (entry.entryType === 'flextime') {
locationText = 'Gleittag'; locationText = 'Gleittag';
startTime = '-'; startTime = '-';
@@ -2428,7 +2594,7 @@ async function bulkExportPDF() {
subtitle: dateRange, subtitle: dateRange,
tableData: allDaysData, tableData: allDaysData,
statistics: { targetHours, totalNetHours, balance }, statistics: { targetHours, totalNetHours, balance },
additionalInfo: { vacationDays, flextimeDays }, additionalInfo: { vacationDays, sickDays, flextimeDays },
fileName fileName
}); });
@@ -3343,6 +3509,13 @@ async function handleExportPDF() {
.map(e => e.date) .map(e => e.date)
); );
// Count sick days to exclude from workdays
const sickDaysSet = new Set(
entries
.filter(e => e.entryType === 'sickday')
.map(e => e.date)
);
// Count flextime days (they are workdays with 0 hours worked) // Count flextime days (they are workdays with 0 hours worked)
const flextimeDaysSet = new Set( const flextimeDaysSet = new Set(
entries entries
@@ -3358,11 +3531,12 @@ async function handleExportPDF() {
const dateISO = `${yearStr}-${monthStr}-${dayStr}`; const dateISO = `${yearStr}-${monthStr}-${dayStr}`;
const isVacation = vacationDaysSet.has(dateISO); const isVacation = vacationDaysSet.has(dateISO);
const isSickday = sickDaysSet.has(dateISO);
const isFlextime = flextimeDaysSet.has(dateISO); const isFlextime = flextimeDaysSet.has(dateISO);
const isWeekendHoliday = isWeekendOrHoliday(dateObj); const isWeekendHoliday = isWeekendOrHoliday(dateObj);
if (!isWeekendHoliday && !isVacation) { if (!isWeekendHoliday && !isVacation && !isSickday) {
// Normal workday (excluding vacation days) // Normal workday (excluding vacation and sick days)
totalWorkdaysInMonth++; totalWorkdaysInMonth++;
if (dateObj <= countUntil) { if (dateObj <= countUntil) {
workdaysPassed++; workdaysPassed++;
@@ -3374,11 +3548,12 @@ async function handleExportPDF() {
workdaysPassed++; workdaysPassed++;
} }
} }
// Vacation days are excluded from all counts // Vacation days and sick days are excluded from all counts
} }
let totalNetHours = 0; let totalNetHours = 0;
let vacationDays = 0; let vacationDays = 0;
let sickDays = 0;
let flextimeDays = 0; let flextimeDays = 0;
let workEntriesCount = 0; let workEntriesCount = 0;
@@ -3401,6 +3576,10 @@ async function handleExportPDF() {
vacationDays++; vacationDays++;
// Vacation hours are already included in netHours (8h per day typically) // Vacation hours are already included in netHours (8h per day typically)
totalNetHours += entry.netHours; totalNetHours += entry.netHours;
} else if (entry.entryType === 'sickday') {
sickDays++;
// Sick day hours are already included in netHours (8h per day typically)
totalNetHours += entry.netHours;
} else if (entry.entryType === 'flextime') { } else if (entry.entryType === 'flextime') {
flextimeDays++; flextimeDays++;
// Only add flextime hours if it's on a weekend/holiday // Only add flextime hours if it's on a weekend/holiday
@@ -3468,6 +3647,13 @@ async function handleExportPDF() {
pauseText = '-'; pauseText = '-';
netHoursText = '-'; netHoursText = '-';
deviationStr = '-'; deviationStr = '-';
} else if (entry.entryType === 'sickday') {
locationText = 'Krank';
startTime = '-';
endTime = '-';
pauseText = '-';
netHoursText = '-';
deviationStr = '-';
} else if (entry.entryType === 'flextime') { } else if (entry.entryType === 'flextime') {
locationText = 'Gleittag'; locationText = 'Gleittag';
startTime = '-'; startTime = '-';
@@ -3520,7 +3706,7 @@ async function handleExportPDF() {
subtitle: monthName, subtitle: monthName,
tableData: allDaysData, tableData: allDaysData,
statistics: { targetHours, totalNetHours, balance: monthBalance }, statistics: { targetHours, totalNetHours, balance: monthBalance },
additionalInfo: { vacationDays, flextimeDays }, additionalInfo: { vacationDays, sickDays, flextimeDays },
fileName fileName
}); });

View File

@@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS entries (
end_time TEXT, end_time TEXT,
pause_minutes INTEGER NOT NULL DEFAULT 0, pause_minutes INTEGER NOT NULL DEFAULT 0,
location TEXT DEFAULT 'office' CHECK(location IN ('office', 'home')), location TEXT DEFAULT 'office' CHECK(location IN ('office', 'home')),
entry_type TEXT DEFAULT 'work' CHECK(entry_type IN ('work', 'vacation', 'flextime')) entry_type TEXT DEFAULT 'work' CHECK(entry_type IN ('work', 'vacation', 'flextime', 'sickday'))
); );
CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS settings (

View File

@@ -47,13 +47,22 @@ try {
if (!hasEntryTypeColumn) { if (!hasEntryTypeColumn) {
console.log('Adding entry_type column to entries table...'); console.log('Adding entry_type column to entries table...');
db.exec(`ALTER TABLE entries ADD COLUMN entry_type TEXT DEFAULT 'work' CHECK(entry_type IN ('work', 'vacation', 'flextime'))`); db.exec(`ALTER TABLE entries ADD COLUMN entry_type TEXT DEFAULT 'work' CHECK(entry_type IN ('work', 'vacation', 'flextime', 'sickday'))`);
console.log('Entry_type column added successfully'); console.log('Entry_type column added successfully');
} }
} catch (error) { } catch (error) {
console.error('Error during entry_type migration:', error); console.error('Error during entry_type migration:', error);
} }
// Migration: Update CHECK constraint to include 'sickday' if needed
try {
// SQLite doesn't support modifying CHECK constraints directly
// The constraint will be updated when a new sickday entry is added
console.log('Entry_type constraint check completed');
} catch (error) {
console.error('Error during entry_type constraint migration:', error);
}
// Migration: Make start_time and end_time nullable for vacation/flextime entries // Migration: Make start_time and end_time nullable for vacation/flextime entries
try { try {
// SQLite doesn't support ALTER COLUMN directly, so we check if we can insert NULL values // SQLite doesn't support ALTER COLUMN directly, so we check if we can insert NULL values
@@ -125,6 +134,10 @@ function calculateNetHours(startTime, endTime, pauseMinutes = null, entryType =
return { grossHours: 0, pauseMinutes: 0, netHours: 0 }; return { grossHours: 0, pauseMinutes: 0, netHours: 0 };
} }
if (entryType === 'sickday') {
return { grossHours: 0, pauseMinutes: 0, netHours: 0 };
}
// Regular work entry calculation // Regular work entry calculation
const [startHour, startMin] = startTime.split(':').map(Number); const [startHour, startMin] = startTime.split(':').map(Number);
const [endHour, endMin] = endTime.split(':').map(Number); const [endHour, endMin] = endTime.split(':').map(Number);