V1.1 Verschiedene Anpassungen
This commit is contained in:
469
DSGVO-Dokumentation.md
Normal file
469
DSGVO-Dokumentation.md
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
# DSGVO-Dokumentation
|
||||||
|
## Stundenerfassungssystem
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Verantwortlicher und Kontaktdaten
|
||||||
|
|
||||||
|
**Verantwortlicher für die Datenverarbeitung:**
|
||||||
|
|
||||||
|
SDS Systemtechnik
|
||||||
|
Rudolf-Diesel-Str. 7
|
||||||
|
75365 Calw
|
||||||
|
info@sds-systemtechnik.de
|
||||||
|
+497051931540
|
||||||
|
|
||||||
|
**Datenschutzbeauftragter (falls vorhanden):**
|
||||||
|
[Name]
|
||||||
|
[E-Mail]
|
||||||
|
[Telefon]
|
||||||
|
|
||||||
|
**Kontakt für Datenschutzanfragen:**
|
||||||
|
Carsten Graf
|
||||||
|
Mechatronik-Ingenieur / IT-Infrastruktur
|
||||||
|
SDS Systemtechnik
|
||||||
|
carsten.graf@sds-systemtechnik.de
|
||||||
|
+4970519315416
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Zweck der Datenverarbeitung
|
||||||
|
|
||||||
|
Das Stundenerfassungssystem dient der Erfassung, Verwaltung und Dokumentation von Arbeitszeiten der Mitarbeiter. Die Verarbeitung personenbezogener Daten erfolgt zu folgenden Zwecken:
|
||||||
|
|
||||||
|
- **Zeiterfassung**: Erfassung von Arbeitsbeginn, Arbeitsende, Pausen und Gesamtarbeitszeit
|
||||||
|
- **Arbeitszeitverwaltung**: Berechnung von Überstunden, Urlaubstagen und Fehlzeiten
|
||||||
|
- **Lohnabrechnung**: Bereitstellung von Daten für die Lohn- und Gehaltsabrechnung
|
||||||
|
- **Projektabrechnung**: Zuordnung von Arbeitszeiten zu Projekten und Projektnummern
|
||||||
|
- **Compliance**: Einhaltung arbeitsrechtlicher Vorschriften (z.B. Arbeitszeitgesetz)
|
||||||
|
- **Authentifizierung**: Sicherstellung des Zugriffs nur für berechtigte Personen
|
||||||
|
- **Automatische Zeiterfassung**: IP-basierte automatische Erfassung von Arbeitsbeginn und -ende (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Rechtsgrundlage
|
||||||
|
|
||||||
|
Die Verarbeitung personenbezogener Daten erfolgt auf Grundlage der folgenden Rechtsgrundlagen nach der DSGVO:
|
||||||
|
|
||||||
|
- **Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung)**: Erfüllung des Arbeitsvertrags, insbesondere Erfassung der Arbeitszeit zur Lohnabrechnung
|
||||||
|
- **Art. 6 Abs. 1 lit. c DSGVO (Rechtliche Verpflichtung)**: Erfüllung gesetzlicher Verpflichtungen (z.B. Arbeitszeitgesetz, Aufbewahrungspflichten nach HGB/GoBD)
|
||||||
|
- **Art. 6 Abs. 1 lit. f DSGVO (Berechtigtes Interesse)**: Betriebswirtschaftliche Interessen (Projektabrechnung, Arbeitszeitoptimierung)
|
||||||
|
|
||||||
|
**Besondere Kategorien personenbezogener Daten:**
|
||||||
|
- **Krankheitsdaten** (sick_status): Art. 9 Abs. 2 lit. b DSGVO (Arbeitsrechtliche Verpflichtungen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Erfasste Datenkategorien
|
||||||
|
|
||||||
|
### 4.1 Benutzerdaten (Tabelle: `users`)
|
||||||
|
|
||||||
|
| Datenfeld | Beschreibung | Kategorie |
|
||||||
|
|-----------|--------------|-----------|
|
||||||
|
| `id` | Eindeutige Benutzer-ID (automatisch generiert) | Identifikationsdaten |
|
||||||
|
| `username` | Benutzername für Login | Authentifizierung |
|
||||||
|
| `password` | Passwort (gehasht mit bcrypt) | Authentifizierung |
|
||||||
|
| `firstname` | Vorname | Identifikationsdaten |
|
||||||
|
| `lastname` | Nachname | Identifikationsdaten |
|
||||||
|
| `personalnummer` | Personalnummer des Mitarbeiters | Identifikationsdaten |
|
||||||
|
| `role` | Benutzerrolle(n) als JSON-Array (z.B. "mitarbeiter", "verwaltung", "admin") | Berechtigungsdaten |
|
||||||
|
| `wochenstunden` | Soll-Arbeitsstunden pro Woche | Arbeitszeitdaten |
|
||||||
|
| `urlaubstage` | Anzahl der Urlaubstage pro Jahr | Arbeitszeitdaten |
|
||||||
|
| `overtime_offset_hours` | Manuelle Korrektur der Überstunden durch Verwaltung | Arbeitszeitdaten |
|
||||||
|
| `vacation_offset_days` | Manuelle Korrektur der Urlaubstage durch Verwaltung | Arbeitszeitdaten |
|
||||||
|
| `ping_ip` | IP-Adresse für automatische Zeiterfassung (optional) | Technische Daten |
|
||||||
|
| `last_week_start` | Letzte bearbeitete Woche (für UI-Navigation) | Systemdaten |
|
||||||
|
| `created_at` | Zeitstempel der Benutzererstellung | Metadaten |
|
||||||
|
|
||||||
|
### 4.2 Zeiterfassungsdaten (Tabelle: `timesheet_entries`)
|
||||||
|
|
||||||
|
| Datenfeld | Beschreibung | Kategorie |
|
||||||
|
|-----------|--------------|-----------|
|
||||||
|
| `id` | Eindeutige Eintrags-ID | Identifikationsdaten |
|
||||||
|
| `user_id` | Verknüpfung zum Benutzer | Identifikationsdaten |
|
||||||
|
| `date` | Datum des Arbeitstages (Format: YYYY-MM-DD) | Zeitstempel |
|
||||||
|
| `start_time` | Arbeitsbeginn (Format: HH:MM) | Zeitstempel |
|
||||||
|
| `end_time` | Arbeitsende (Format: HH:MM) | Zeitstempel |
|
||||||
|
| `pause_start_time` | Pausenbeginn (Format: HH:MM, optional) | Zeitstempel |
|
||||||
|
| `pause_end_time` | Pausenende (Format: HH:MM, optional) | Zeitstempel |
|
||||||
|
| `break_minutes` | Pausenzeit in Minuten | Arbeitszeitdaten |
|
||||||
|
| `total_hours` | Berechnete Gesamtarbeitszeit in Stunden | Arbeitszeitdaten |
|
||||||
|
| `activity1_desc` bis `activity5_desc` | Beschreibung der Tätigkeit (bis zu 5 pro Tag) | Tätigkeitsdaten |
|
||||||
|
| `activity1_hours` bis `activity5_hours` | Stunden für die jeweilige Tätigkeit | Tätigkeitsdaten |
|
||||||
|
| `activity1_project_number` bis `activity5_project_number` | Projektnummer für die jeweilige Tätigkeit | Projektbezogene Daten |
|
||||||
|
| `notes` | Freitext-Notizen zum Arbeitstag | Tätigkeitsdaten |
|
||||||
|
| `overtime_taken_hours` | Verbrauchte Überstunden an diesem Tag | Arbeitszeitdaten |
|
||||||
|
| `vacation_type` | Urlaubstyp ("full" = ganzer Tag, "half" = halber Tag, null = kein Urlaub) | Arbeitszeitdaten |
|
||||||
|
| `sick_status` | Krankheitsstatus (0 = nicht krank, 1 = krank) | Gesundheitsdaten |
|
||||||
|
| `weekend_travel` | Wochenend-Reise (0 = nein, 1 = ja) | Arbeitszeitdaten |
|
||||||
|
| `applied_weekend_percentage` | Angewendeter Wochenend-Prozentsatz für Berechnung | Arbeitszeitdaten |
|
||||||
|
| `status` | Status des Eintrags ("offen", "eingereicht") | Metadaten |
|
||||||
|
| `created_at` | Zeitstempel der Erstellung | Metadaten |
|
||||||
|
| `updated_at` | Zeitstempel der letzten Änderung | Metadaten |
|
||||||
|
|
||||||
|
### 4.3 Wochenstundenzettel (Tabelle: `weekly_timesheets`)
|
||||||
|
|
||||||
|
| Datenfeld | Beschreibung | Kategorie |
|
||||||
|
|-----------|--------------|-----------|
|
||||||
|
| `id` | Eindeutige Stundenzettel-ID | Identifikationsdaten |
|
||||||
|
| `user_id` | Verknüpfung zum Benutzer | Identifikationsdaten |
|
||||||
|
| `week_start` | Startdatum der Woche (Format: YYYY-MM-DD) | Zeitstempel |
|
||||||
|
| `week_end` | Enddatum der Woche (Format: YYYY-MM-DD) | Zeitstempel |
|
||||||
|
| `version` | Versionsnummer des Stundenzettels (bei Änderungen) | Metadaten |
|
||||||
|
| `version_reason` | Grund für die neue Version (Freitext) | Metadaten |
|
||||||
|
| `status` | Status ("eingereicht", "bearbeitet") | Metadaten |
|
||||||
|
| `submitted_at` | Zeitstempel der Einreichung | Metadaten |
|
||||||
|
| `reviewed_by` | ID des prüfenden Verwaltungsmitarbeiters | Identifikationsdaten |
|
||||||
|
| `reviewed_at` | Zeitstempel der Prüfung | Metadaten |
|
||||||
|
| `pdf_downloaded_at` | Zeitstempel des PDF-Downloads | Metadaten |
|
||||||
|
| `pdf_downloaded_by` | ID des Benutzers, der das PDF heruntergeladen hat | Identifikationsdaten |
|
||||||
|
| `admin_comment` | Kommentar der Verwaltung zum Stundenzettel | Metadaten |
|
||||||
|
|
||||||
|
### 4.4 IP-basierte Zeiterfassung (Tabelle: `ping_status`)
|
||||||
|
|
||||||
|
| Datenfeld | Beschreibung | Kategorie |
|
||||||
|
|-----------|--------------|-----------|
|
||||||
|
| `user_id` | Verknüpfung zum Benutzer | Identifikationsdaten |
|
||||||
|
| `date` | Datum (Format: YYYY-MM-DD) | Zeitstempel |
|
||||||
|
| `last_successful_ping` | Zeitstempel des letzten erfolgreichen Pings | Technische Daten |
|
||||||
|
| `failed_ping_count` | Anzahl fehlgeschlagener Pings | Technische Daten |
|
||||||
|
| `start_time_set` | Flag, ob Start-Zeit bereits gesetzt wurde (0/1) | Technische Daten |
|
||||||
|
| `first_failed_ping_time` | Zeitstempel des ersten fehlgeschlagenen Pings | Technische Daten |
|
||||||
|
|
||||||
|
### 4.5 LDAP-Synchronisation (Tabelle: `ldap_sync_log`)
|
||||||
|
|
||||||
|
| Datenfeld | Beschreibung | Kategorie |
|
||||||
|
|-----------|--------------|-----------|
|
||||||
|
| `id` | Eindeutige Log-ID | Identifikationsdaten |
|
||||||
|
| `sync_type` | Typ der Synchronisation ("manual" oder "automatic") | Metadaten |
|
||||||
|
| `status` | Status ("success", "error") | Metadaten |
|
||||||
|
| `users_synced` | Anzahl synchronisierter Benutzer | Metadaten |
|
||||||
|
| `error_message` | Fehlermeldung bei Fehlern (optional) | Metadaten |
|
||||||
|
| `sync_started_at` | Zeitstempel des Synchronisationsbeginns | Metadaten |
|
||||||
|
| `sync_completed_at` | Zeitstempel des Synchronisationsendes | Metadaten |
|
||||||
|
|
||||||
|
**LDAP-Konfiguration (Tabelle: `ldap_config`):**
|
||||||
|
Diese Tabelle enthält keine personenbezogenen Daten, sondern nur technische Konfigurationsdaten für die LDAP-Verbindung.
|
||||||
|
|
||||||
|
**LDAP-Synchronisierte Daten:**
|
||||||
|
Bei aktivierter LDAP-Integration werden folgende Daten aus dem Active Directory synchronisiert:
|
||||||
|
- `username` (sAMAccountName)
|
||||||
|
- `firstname` (givenName)
|
||||||
|
- `lastname` (sn)
|
||||||
|
|
||||||
|
### 4.6 Session-Daten (Express-Session)
|
||||||
|
|
||||||
|
| Datenfeld | Beschreibung | Speicherdauer |
|
||||||
|
|-----------|--------------|---------------|
|
||||||
|
| `userId` | ID des angemeldeten Benutzers | 24 Stunden (Standard) oder 30 Tage (bei "Angemeldet bleiben") |
|
||||||
|
| `roles` | Array der Benutzerrollen | 24 Stunden (Standard) oder 30 Tage (bei "Angemeldet bleiben") |
|
||||||
|
| `currentRole` | Aktuell ausgewählte Rolle | 24 Stunden (Standard) oder 30 Tage (bei "Angemeldet bleiben") |
|
||||||
|
|
||||||
|
**Cookie-Informationen:**
|
||||||
|
- Cookie-Name: `connect.sid`
|
||||||
|
- Speicherort: Client-Browser
|
||||||
|
- Verschlüsselung: Session-Daten werden serverseitig gespeichert, Cookie enthält nur Session-ID
|
||||||
|
|
||||||
|
### 4.7 Systemdaten (keine personenbezogenen Daten)
|
||||||
|
|
||||||
|
**Tabelle: `public_holidays`**
|
||||||
|
- `date`: Feiertagsdatum
|
||||||
|
- `name`: Name des Feiertags
|
||||||
|
|
||||||
|
**Tabelle: `system_options`**
|
||||||
|
- `saturday_percentage`: Prozentsatz für Samstagsarbeit
|
||||||
|
- `sunday_percentage`: Prozentsatz für Sonntagsarbeit
|
||||||
|
- `checkin_root_url`: Basis-URL für Check-in/Check-out Links
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Datenquellen
|
||||||
|
|
||||||
|
Die personenbezogenen Daten stammen aus folgenden Quellen:
|
||||||
|
|
||||||
|
1. **Direkte Eingabe durch Mitarbeiter**: Zeiterfassungsdaten, Tätigkeitsbeschreibungen, Projektnummern, Notizen
|
||||||
|
2. **Automatische Erfassung**: IP-basierte Zeiterfassung (optional, wenn `ping_ip` konfiguriert ist)
|
||||||
|
3. **LDAP/Active Directory**: Bei aktivierter LDAP-Integration werden Benutzername, Vorname und Nachname aus dem Active Directory synchronisiert
|
||||||
|
4. **Administrative Eingabe**: Personalnummer, Wochenstunden, Urlaubstage, Überstunden-Offset, Urlaubstage-Offset durch Verwaltung/Admin
|
||||||
|
5. **Automatische Berechnung**: Gesamtstunden, Überstunden werden automatisch aus den eingegebenen Daten berechnet
|
||||||
|
6. **Systemgeneriert**: IDs, Zeitstempel (created_at, updated_at), Session-Daten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Empfänger der Daten
|
||||||
|
|
||||||
|
Die personenbezogenen Daten werden folgenden Empfängern zugänglich gemacht:
|
||||||
|
|
||||||
|
### 6.1 Interne Empfänger
|
||||||
|
|
||||||
|
- **Mitarbeiter**: Zugriff auf eigene Zeiterfassungsdaten über das Dashboard
|
||||||
|
- **Verwaltungsmitarbeiter**: Zugriff auf alle eingereichten Stundenzettel zur Prüfung und PDF-Generierung
|
||||||
|
- **Administratoren**: Vollzugriff auf alle Daten zur Systemverwaltung und Benutzerverwaltung
|
||||||
|
- **IT-Administration**: Technischer Zugriff für Wartung und Support (nur bei Bedarf)
|
||||||
|
|
||||||
|
### 6.2 Externe Empfänger
|
||||||
|
|
||||||
|
- **Lohnbuchhaltung**: PDF-Stundenzettel werden für die Lohnabrechnung verwendet (manueller Export)
|
||||||
|
- **Steuerberater/Wirtschaftsprüfer**: Bei Prüfungen können Daten eingesehen werden (gemäß gesetzlichen Aufbewahrungspflichten)
|
||||||
|
|
||||||
|
### 6.3 Keine Weitergabe an Dritte
|
||||||
|
|
||||||
|
Die Daten werden nicht an Dritte (außerhalb der oben genannten Empfänger) weitergegeben. Es erfolgt keine Datenweitergabe an:
|
||||||
|
- Externe Dienstleister (außer bei expliziter Auftragsverarbeitung)
|
||||||
|
- Marketing-Unternehmen
|
||||||
|
- Soziale Netzwerke
|
||||||
|
- Andere Unternehmen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Speicherdauer und Löschung
|
||||||
|
|
||||||
|
### 7.1 Speicherdauer
|
||||||
|
|
||||||
|
Die personenbezogenen Daten werden für folgende Zeiträume gespeichert:
|
||||||
|
|
||||||
|
- **Zeiterfassungsdaten**: Mindestens 10 Jahre gemäß § 257 HGB (Aufbewahrungspflicht für Handelsbücher und Belege)
|
||||||
|
- **Lohnabrechnungsrelevante Daten**: 10 Jahre gemäß § 147 AO (Abgabenordnung)
|
||||||
|
- **Benutzerdaten**: Solange der Mitarbeiter im Unternehmen beschäftigt ist, danach gemäß Aufbewahrungspflichten
|
||||||
|
- **Session-Daten**: Werden automatisch nach Ablauf der Cookie-Laufzeit gelöscht (24 Stunden oder 30 Tage)
|
||||||
|
- **LDAP-Sync-Logs**: 1 Jahr (technische Logs)
|
||||||
|
|
||||||
|
### 7.2 Löschung
|
||||||
|
|
||||||
|
Die Löschung erfolgt:
|
||||||
|
- **Automatisch**: Session-Daten nach Ablauf der Cookie-Laufzeit
|
||||||
|
- **Manuell**: Bei Beendigung des Arbeitsverhältnisses, jedoch unter Beachtung der gesetzlichen Aufbewahrungspflichten
|
||||||
|
- **Auf Anfrage**: Gemäß den Betroffenenrechten (siehe Abschnitt 8), soweit keine gesetzlichen Aufbewahrungspflichten entgegenstehen
|
||||||
|
|
||||||
|
**Hinweis**: Auch nach Beendigung des Arbeitsverhältnisses können Daten aufgrund gesetzlicher Aufbewahrungspflichten (HGB, AO) nicht sofort gelöscht werden. Nach Ablauf der Aufbewahrungsfristen erfolgt die Löschung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Betroffenenrechte
|
||||||
|
|
||||||
|
Jede betroffene Person hat folgende Rechte nach der DSGVO:
|
||||||
|
|
||||||
|
### 8.1 Auskunftsrecht (Art. 15 DSGVO)
|
||||||
|
|
||||||
|
Sie haben das Recht, Auskunft über die zu Ihrer Person gespeicherten Daten zu erhalten. Dies umfasst:
|
||||||
|
- Kategorien der verarbeiteten Daten
|
||||||
|
- Zweck der Verarbeitung
|
||||||
|
- Empfänger der Daten
|
||||||
|
- Geplante Speicherdauer
|
||||||
|
- Herkunft der Daten
|
||||||
|
|
||||||
|
### 8.2 Recht auf Berichtigung (Art. 16 DSGVO)
|
||||||
|
|
||||||
|
Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollständigung unvollständiger Daten zu verlangen.
|
||||||
|
|
||||||
|
### 8.3 Recht auf Löschung (Art. 17 DSGVO)
|
||||||
|
|
||||||
|
Sie haben das Recht, die Löschung Ihrer Daten zu verlangen, sofern:
|
||||||
|
- Die Daten für den ursprünglichen Zweck nicht mehr benötigt werden
|
||||||
|
- Sie Ihre Einwilligung widerrufen haben
|
||||||
|
- Die Daten unrechtmäßig verarbeitet wurden
|
||||||
|
- Keine gesetzlichen Aufbewahrungspflichten entgegenstehen
|
||||||
|
|
||||||
|
**Wichtig**: Aufgrund gesetzlicher Aufbewahrungspflichten (HGB, AO) können Zeiterfassungsdaten für 10 Jahre nicht gelöscht werden.
|
||||||
|
|
||||||
|
### 8.4 Recht auf Einschränkung der Verarbeitung (Art. 18 DSGVO)
|
||||||
|
|
||||||
|
Sie haben das Recht, die Einschränkung der Verarbeitung zu verlangen, wenn:
|
||||||
|
- Die Richtigkeit der Daten bestritten wird
|
||||||
|
- Die Verarbeitung unrechtmäßig ist
|
||||||
|
- Die Daten nicht mehr benötigt werden, aber für Rechtsansprüche erforderlich sind
|
||||||
|
|
||||||
|
### 8.5 Recht auf Datenübertragbarkeit (Art. 20 DSGVO)
|
||||||
|
|
||||||
|
Sie haben das Recht, Ihre Daten in einem strukturierten, gängigen und maschinenlesbaren Format zu erhalten und an einen anderen Verantwortlichen zu übermitteln.
|
||||||
|
|
||||||
|
### 8.6 Widerspruchsrecht (Art. 21 DSGVO)
|
||||||
|
|
||||||
|
Sie haben das Recht, der Verarbeitung Ihrer Daten zu widersprechen, wenn die Verarbeitung auf Art. 6 Abs. 1 lit. f DSGVO (Berechtigtes Interesse) beruht.
|
||||||
|
|
||||||
|
**Hinweis**: Da die Zeiterfassung primär zur Erfüllung arbeitsvertraglicher und gesetzlicher Verpflichtungen erfolgt, ist ein Widerspruch in der Regel nicht möglich.
|
||||||
|
|
||||||
|
### 8.7 Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
|
||||||
|
|
||||||
|
Falls die Datenverarbeitung auf einer Einwilligung beruht, haben Sie das Recht, diese jederzeit zu widerrufen.
|
||||||
|
|
||||||
|
### 8.8 Beschwerderecht (Art. 77 DSGVO)
|
||||||
|
|
||||||
|
Sie haben das Recht, eine Beschwerde bei einer Aufsichtsbehörde einzulegen, insbesondere in dem Mitgliedstaat, in dem Sie sich aufhalten, arbeiten oder in dem der mutmaßliche Verstoß aufgetreten ist.
|
||||||
|
|
||||||
|
**Zuständige Aufsichtsbehörde:**
|
||||||
|
Der Landesbeauftragte für den Datenschutz und die Informationsfreiheit Baden-Württemberg
|
||||||
|
[Adresse]
|
||||||
|
[E-Mail]
|
||||||
|
[Telefon]
|
||||||
|
|
||||||
|
### 8.9 Ausübung der Rechte
|
||||||
|
|
||||||
|
Um Ihre Rechte auszuüben, wenden Sie sich bitte an:
|
||||||
|
|
||||||
|
Carsten Graf
|
||||||
|
Mechatronik-Ingenieur / IT-Infrastruktur
|
||||||
|
SDS Systemtechnik
|
||||||
|
[E-Mail]
|
||||||
|
[Telefon]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Technische und organisatorische Maßnahmen (TOM)
|
||||||
|
|
||||||
|
Zur Gewährleistung der Datensicherheit wurden folgende technische und organisatorische Maßnahmen implementiert:
|
||||||
|
|
||||||
|
### 9.1 Zugriffskontrolle
|
||||||
|
|
||||||
|
- **Authentifizierung**: Passwort-basierte Authentifizierung mit bcrypt-Hashing (10 Runden)
|
||||||
|
- **Autorisierung**: Rollenbasierte Zugriffskontrolle (Mitarbeiter, Verwaltung, Admin)
|
||||||
|
- **Session-Management**: Express-Session mit serverseitiger Speicherung, Session-IDs in verschlüsselten Cookies
|
||||||
|
- **Automatische Abmeldung**: Session-Cookies laufen nach 24 Stunden (Standard) oder 30 Tagen (bei "Angemeldet bleiben") ab
|
||||||
|
|
||||||
|
### 9.2 Verschlüsselung
|
||||||
|
|
||||||
|
- **Passwörter**: Bcrypt-Hashing mit Salt (10 Runden)
|
||||||
|
- **Datenübertragung**: Empfohlen wird die Verwendung von HTTPS (TLS/SSL) über einen Reverse Proxy (z.B. nginx)
|
||||||
|
- **Datenbank**: SQLite-Datenbankdatei sollte auf einem gesicherten Server gespeichert werden
|
||||||
|
|
||||||
|
### 9.3 Datensicherung
|
||||||
|
|
||||||
|
- **Backups**: Regelmäßige Backups der SQLite-Datenbankdatei (`stundenerfassung.db`)
|
||||||
|
- **Backup-Speicherung**: Backups sollten an einem separaten, gesicherten Ort gespeichert werden
|
||||||
|
|
||||||
|
### 9.4 Integrität
|
||||||
|
|
||||||
|
- **Datenbank-Constraints**: Foreign Keys und Constraints zur Gewährleistung der Datenintegrität
|
||||||
|
- **Validierung**: Eingabevalidierung auf Server- und Client-Seite
|
||||||
|
- **Versionierung**: Versionsverwaltung bei Stundenzetteln zur Nachvollziehbarkeit von Änderungen
|
||||||
|
|
||||||
|
### 9.5 Verfügbarkeit
|
||||||
|
|
||||||
|
- **Server-Monitoring**: Überwachung der Server-Verfügbarkeit
|
||||||
|
- **Fehlerbehandlung**: Umfassende Fehlerbehandlung und Logging
|
||||||
|
|
||||||
|
### 9.6 Trennbarkeit
|
||||||
|
|
||||||
|
- **Datenbank-Isolation**: Jeder Benutzer kann nur auf seine eigenen Daten zugreifen (außer Verwaltung/Admin)
|
||||||
|
- **Rollenbasierte Ansichten**: Unterschiedliche Ansichten je nach Benutzerrolle
|
||||||
|
|
||||||
|
### 9.7 Pseudonymisierung
|
||||||
|
|
||||||
|
- **Benutzer-IDs**: Interne Verwendung von numerischen IDs statt Namen für Datenbankverknüpfungen
|
||||||
|
- **Session-IDs**: Verwendung von zufälligen Session-IDs statt direkter Benutzeridentifikation in Cookies
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Besondere Verarbeitungssituationen
|
||||||
|
|
||||||
|
### 10.1 IP-basierte automatische Zeiterfassung
|
||||||
|
|
||||||
|
**Funktionsweise:**
|
||||||
|
- Mitarbeiter können optional eine IP-Adresse (`ping_ip`) hinterlegen
|
||||||
|
- Das System pingt diese IP-Adresse jede Minute
|
||||||
|
- Bei erfolgreichem Ping wird automatisch die Start-Zeit gesetzt
|
||||||
|
- Bei 3 fehlgeschlagenen Pings wird automatisch die End-Zeit gesetzt
|
||||||
|
|
||||||
|
**Erfasste Daten:**
|
||||||
|
- IP-Adresse des Mitarbeiters (freiwillig hinterlegt)
|
||||||
|
- Ping-Status (erfolgreich/fehlgeschlagen)
|
||||||
|
- Zeitstempel der Pings
|
||||||
|
- Automatisch generierte Start- und End-Zeiten
|
||||||
|
|
||||||
|
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) - Erfassung der Arbeitszeit
|
||||||
|
|
||||||
|
**Hinweis:** Die IP-Adresse wird nur für die automatische Zeiterfassung verwendet und nicht an Dritte weitergegeben.
|
||||||
|
|
||||||
|
### 10.2 LDAP/Active Directory Integration
|
||||||
|
|
||||||
|
**Funktionsweise:**
|
||||||
|
- Optional kann eine LDAP/Active Directory-Verbindung konfiguriert werden
|
||||||
|
- Benutzer werden automatisch oder manuell aus dem Active Directory synchronisiert
|
||||||
|
- Synchronisierte Daten: Benutzername, Vorname, Nachname
|
||||||
|
|
||||||
|
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) - Bereitstellung des Systems für Mitarbeiter
|
||||||
|
|
||||||
|
**Hinweis:** Die LDAP-Konfiguration (URL, Bind-DN, Passwort) wird verschlüsselt in der Datenbank gespeichert. Die Synchronisation erfolgt nur innerhalb des Unternehmensnetzwerks.
|
||||||
|
|
||||||
|
### 10.3 PDF-Generierung
|
||||||
|
|
||||||
|
**Funktionsweise:**
|
||||||
|
- Verwaltungsmitarbeiter können PDF-Stundenzettel generieren
|
||||||
|
- PDFs enthalten alle Zeiterfassungsdaten einer Woche
|
||||||
|
- PDFs werden für die Lohnabrechnung verwendet
|
||||||
|
|
||||||
|
**Enthaltene Daten:**
|
||||||
|
- Mitarbeitername und Personalnummer
|
||||||
|
- Zeitraum (Woche)
|
||||||
|
- Alle Tageseinträge mit Start, Ende, Pause, Stunden
|
||||||
|
- Tätigkeiten mit Projektnummern
|
||||||
|
- Überstunden, Urlaub, Feiertage
|
||||||
|
- Gesamtstundensumme
|
||||||
|
|
||||||
|
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) - Lohnabrechnung
|
||||||
|
|
||||||
|
**Hinweis:** PDFs werden nur intern verwendet und nicht an Dritte weitergegeben (außer Lohnbuchhaltung).
|
||||||
|
|
||||||
|
### 10.4 QR-Code Check-in/Check-out
|
||||||
|
|
||||||
|
**Funktionsweise:**
|
||||||
|
- Jeder Mitarbeiter erhält einen persönlichen QR-Code
|
||||||
|
- QR-Code enthält die Benutzer-ID
|
||||||
|
- Scan des QR-Codes führt zu Check-in oder Check-out
|
||||||
|
|
||||||
|
**Erfasste Daten:**
|
||||||
|
- Benutzer-ID (aus QR-Code)
|
||||||
|
- Zeitstempel des Scans
|
||||||
|
- Automatisch gesetzte Start- oder End-Zeit
|
||||||
|
|
||||||
|
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) - Erfassung der Arbeitszeit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Datenübertragung in Drittländer
|
||||||
|
|
||||||
|
**Aktuell erfolgt keine Datenübertragung in Drittländer.**
|
||||||
|
|
||||||
|
Alle Daten werden auf Servern innerhalb der Europäischen Union (EU) oder des Europäischen Wirtschaftsraums (EWR) gespeichert und verarbeitet.
|
||||||
|
|
||||||
|
Falls in Zukunft eine Datenübertragung in Drittländer erforderlich sein sollte, wird dies nur unter Einhaltung der DSGVO-Anforderungen (z.B. Standardvertragsklauseln, Angemessenheitsbeschluss) erfolgen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Automatisierte Entscheidungsfindung und Profiling
|
||||||
|
|
||||||
|
**Es erfolgt keine automatisierte Entscheidungsfindung (Art. 22 DSGVO) oder Profiling.**
|
||||||
|
|
||||||
|
Die automatische Berechnung von Arbeitszeiten und Überstunden dient lediglich der Unterstützung der manuellen Prüfung durch die Verwaltung und stellt keine automatisierte Entscheidung dar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Änderungen dieser Dokumentation
|
||||||
|
|
||||||
|
Diese DSGVO-Dokumentation wird bei Änderungen der Datenverarbeitung aktualisiert. Die aktuelle Version ist immer im System verfügbar.
|
||||||
|
|
||||||
|
**Letzte Aktualisierung:** [Datum]
|
||||||
|
|
||||||
|
**Version:** 1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Anhang: Datenbank-Schema-Übersicht
|
||||||
|
|
||||||
|
### Tabellen mit personenbezogenen Daten:
|
||||||
|
|
||||||
|
1. **users**: Benutzerdaten, Authentifizierung, Arbeitszeitkonfiguration
|
||||||
|
2. **timesheet_entries**: Tageseinträge mit Zeiten, Tätigkeiten, Projektnummern
|
||||||
|
3. **weekly_timesheets**: Eingereichte Wochenstundenzettel
|
||||||
|
4. **ping_status**: IP-basierte Zeiterfassungsdaten
|
||||||
|
5. **ldap_sync_log**: Synchronisationsprotokolle (enthält Anzahl synchronisierter Benutzer)
|
||||||
|
|
||||||
|
### Tabellen ohne personenbezogene Daten:
|
||||||
|
|
||||||
|
1. **public_holidays**: Feiertage (Baden-Württemberg)
|
||||||
|
2. **system_options**: Systemweite Einstellungen
|
||||||
|
3. **ldap_config**: LDAP-Konfiguration (keine personenbezogenen Daten)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ende der DSGVO-Dokumentation**
|
||||||
14
README.md
14
README.md
@@ -163,13 +163,13 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
|||||||
```
|
```
|
||||||
stundenerfassung/
|
stundenerfassung/
|
||||||
├── routes/ # API-Routen
|
├── routes/ # API-Routen
|
||||||
│ ├── admin.js # Admin-Funktionen
|
│ ├── admin-routes.js # Admin-Funktionen
|
||||||
│ ├── admin-ldap.js # LDAP-Verwaltung
|
│ ├── admin-ldap-routes.js # LDAP-Verwaltung
|
||||||
│ ├── auth.js # Authentifizierung
|
│ ├── auth-routes.js # Authentifizierung
|
||||||
│ ├── dashboard.js # Dashboard-Routen
|
│ ├── dashboard-routes.js # Dashboard-Routen
|
||||||
│ ├── timesheet.js # Stundenerfassung
|
│ ├── timesheet-routes.js # Stundenerfassung
|
||||||
│ ├── user.js # Benutzer-APIs
|
│ ├── user-routes.js # Benutzer-APIs
|
||||||
│ └── verwaltung.js # Verwaltungs-Funktionen
|
│ └── verwaltung-routes.js # Verwaltungs-Funktionen
|
||||||
├── services/ # Services
|
├── services/ # Services
|
||||||
│ ├── feiertage-service.js # Feiertags-API
|
│ ├── feiertage-service.js # Feiertags-API
|
||||||
│ ├── ldap-service.js # LDAP-Service
|
│ ├── ldap-service.js # LDAP-Service
|
||||||
|
|||||||
18
Stundenregeln.txt
Normal file
18
Stundenregeln.txt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
- Standarterfassung: (Ende - Start ) - Pause
|
||||||
|
|
||||||
|
- Überstunden nehmen
|
||||||
|
- Wenn Überstunden eingetragen > (Wochenarbeitszeit / Arbeitstage) -> Muss noch Start und Ende und Pause eingetragen werden
|
||||||
|
- Wenn Überstunden = (Wochenarbeitszeit / Arbeitstage) -> Tag als ausgefüllt zu betrachten
|
||||||
|
|
||||||
|
- Urlaub
|
||||||
|
- Wird als ausgefüllt betrachtet
|
||||||
|
- Stunden werden als (Wochenarbeitszeit / Arbeitstage) gerechnet
|
||||||
|
- Wird von Verbleibendem Urlaub abgezogen
|
||||||
|
|
||||||
|
- Krank
|
||||||
|
- Wird als ausgefüllt betrachtet
|
||||||
|
- Stunden werden als (Wochenarbeitszeit / Arbeitstage) gerechnt
|
||||||
|
|
||||||
|
- Feiertage
|
||||||
|
- Wird als ausgefüllt betrachtet
|
||||||
|
- Stunden werden als (Wochenarbeitszeit / Arbeitstage) gerechnet
|
||||||
11
Stunderfassung todo.txt
Normal file
11
Stunderfassung todo.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
- Mitarbeiter Name in den QR code Sheets -> DONE
|
||||||
|
- Pause vorbelegen (einstellbar in der Admin) -> DONE Wird anhand der Gesetztlichen vorgaben berechnet
|
||||||
|
- Offset für die Verwaltung für Urlaubstage -> DONE
|
||||||
|
- Stunden pro Tag und wie viele Tage arbeit
|
||||||
|
- Reisen für Wochenende -> DONE
|
||||||
|
- LDAP Prüfung
|
||||||
|
- DSGVO Sicherheit
|
||||||
|
- Feiertage müssen als ausgefüllt zählen -> DONE
|
||||||
|
- Mitarbeiter sollen PDF ansehen können. -> DONE
|
||||||
|
- Wenn bereits heruntergeladen wurde und neue version da ist Meldung an Verwaltung. -> DONE Muss getestet werden
|
||||||
|
-
|
||||||
23
database.js
23
database.js
@@ -172,6 +172,21 @@ function initDatabase() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Migration: Wochenend-Reise und angewendeter Wochenend-Prozentsatz hinzufügen
|
||||||
|
db.run(`ALTER TABLE timesheet_entries ADD COLUMN weekend_travel INTEGER DEFAULT 0`, (err) => {
|
||||||
|
// Fehler ignorieren wenn Spalte bereits existiert
|
||||||
|
if (err && !err.message.includes('duplicate column')) {
|
||||||
|
console.warn('Warnung beim Hinzufügen der Spalte weekend_travel:', err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
db.run(`ALTER TABLE timesheet_entries ADD COLUMN applied_weekend_percentage REAL DEFAULT NULL`, (err) => {
|
||||||
|
// Fehler ignorieren wenn Spalte bereits existiert
|
||||||
|
if (err && !err.message.includes('duplicate column')) {
|
||||||
|
console.warn('Warnung beim Hinzufügen der Spalte applied_weekend_percentage:', err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Migration: User-Felder hinzufügen (Personalnummer, Wochenstunden, Urlaubstage)
|
// Migration: User-Felder hinzufügen (Personalnummer, Wochenstunden, Urlaubstage)
|
||||||
db.run(`ALTER TABLE users ADD COLUMN personalnummer TEXT`, (err) => {
|
db.run(`ALTER TABLE users ADD COLUMN personalnummer TEXT`, (err) => {
|
||||||
// Fehler ignorieren wenn Spalte bereits existiert
|
// Fehler ignorieren wenn Spalte bereits existiert
|
||||||
@@ -193,6 +208,14 @@ function initDatabase() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Migration: Urlaubstage-Offset (manuelle Korrektur durch Verwaltung)
|
||||||
|
db.run(`ALTER TABLE users ADD COLUMN vacation_offset_days REAL DEFAULT 0`, (err) => {
|
||||||
|
// Fehler ignorieren wenn Spalte bereits existiert
|
||||||
|
if (err && !err.message.includes('duplicate column')) {
|
||||||
|
console.warn('Warnung beim Hinzufügen der Spalte vacation_offset_days:', err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Migration: ping_ip Spalte hinzufügen
|
// Migration: ping_ip Spalte hinzufügen
|
||||||
db.run(`ALTER TABLE users ADD COLUMN ping_ip TEXT`, (err) => {
|
db.run(`ALTER TABLE users ADD COLUMN ping_ip TEXT`, (err) => {
|
||||||
// Fehler ignorieren wenn Spalte bereits existiert
|
// Fehler ignorieren wenn Spalte bereits existiert
|
||||||
|
|||||||
@@ -1002,6 +1002,17 @@ table input[type="text"] {
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-version-warning {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #fee;
|
||||||
|
border: 1px solid #e74c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.dashboard-layout {
|
.dashboard-layout {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ let currentEntries = {};
|
|||||||
let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-DD)
|
let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-DD)
|
||||||
let userWochenstunden = 0; // Wochenstunden des Users
|
let userWochenstunden = 0; // Wochenstunden des Users
|
||||||
let weekendPercentages = { saturday: 100, sunday: 100 }; // Wochenend-Prozentsätze (100% = normal)
|
let weekendPercentages = { saturday: 100, sunday: 100 }; // Wochenend-Prozentsätze (100% = normal)
|
||||||
|
let latestSubmittedTimesheetId = null; // ID der neuesten eingereichten Version
|
||||||
|
|
||||||
// Wochenend-Prozentsätze laden
|
// Wochenend-Prozentsätze laden
|
||||||
async function loadWeekendPercentages() {
|
async function loadWeekendPercentages() {
|
||||||
@@ -206,6 +207,21 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
} else {
|
} else {
|
||||||
console.error('Submit-Button nicht gefunden beim Initialisieren!');
|
console.error('Submit-Button nicht gefunden beim Initialisieren!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Event-Listener für PDF-Button
|
||||||
|
const viewPdfButton = document.getElementById('viewPdfBtn');
|
||||||
|
if (viewPdfButton) {
|
||||||
|
viewPdfButton.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.disabled && latestSubmittedTimesheetId) {
|
||||||
|
// Öffne PDF in neuem Tab
|
||||||
|
window.open(`/api/timesheet/pdf/${latestSubmittedTimesheetId}?inline=true`, '_blank');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('PDF-Button Event-Listener gesetzt');
|
||||||
|
} else {
|
||||||
|
console.error('PDF-Button nicht gefunden beim Initialisieren!');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Letzte Woche auf dem Server speichern
|
// Letzte Woche auf dem Server speichern
|
||||||
@@ -303,14 +319,19 @@ async function loadWeek() {
|
|||||||
const parts = currentWeekStart.split('-');
|
const parts = currentWeekStart.split('-');
|
||||||
const weekEndDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10) + 6);
|
const weekEndDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10) + 6);
|
||||||
const weekEnd = weekEndDate.getFullYear() + '-' + String(weekEndDate.getMonth() + 1).padStart(2, '0') + '-' + String(weekEndDate.getDate()).padStart(2, '0');
|
const weekEnd = weekEndDate.getFullYear() + '-' + String(weekEndDate.getMonth() + 1).padStart(2, '0') + '-' + String(weekEndDate.getDate()).padStart(2, '0');
|
||||||
const [entriesResponse, holidaysResponse] = await Promise.all([
|
const [entriesResponse, holidaysResponse, latestSubmittedResponse] = await Promise.all([
|
||||||
fetch(`/api/timesheet/week/${currentWeekStart}`),
|
fetch(`/api/timesheet/week/${currentWeekStart}`),
|
||||||
fetch(`/api/timesheet/holidays?week_start=${currentWeekStart}&week_end=${weekEnd}`)
|
fetch(`/api/timesheet/holidays?week_start=${currentWeekStart}&week_end=${weekEnd}`),
|
||||||
|
fetch(`/api/timesheet/latest-submitted/${currentWeekStart}`)
|
||||||
]);
|
]);
|
||||||
const entries = await entriesResponse.json();
|
const entries = await entriesResponse.json();
|
||||||
const holidaysData = await holidaysResponse.json();
|
const holidaysData = await holidaysResponse.json();
|
||||||
|
const latestSubmittedData = await latestSubmittedResponse.json();
|
||||||
currentHolidayDates = new Set(holidaysData.dates || []);
|
currentHolidayDates = new Set(holidaysData.dates || []);
|
||||||
|
|
||||||
|
// Speichere die neueste eingereichte Timesheet-ID
|
||||||
|
latestSubmittedTimesheetId = latestSubmittedData.timesheetId || null;
|
||||||
|
|
||||||
// Entries in Object umwandeln für schnellen Zugriff
|
// Entries in Object umwandeln für schnellen Zugriff
|
||||||
currentEntries = {};
|
currentEntries = {};
|
||||||
let weekIsSubmitted = false;
|
let weekIsSubmitted = false;
|
||||||
@@ -382,6 +403,9 @@ function renderWeek() {
|
|||||||
const vacationType = entry.vacation_type || '';
|
const vacationType = entry.vacation_type || '';
|
||||||
const sickStatus = entry.sick_status || false;
|
const sickStatus = entry.sick_status || false;
|
||||||
const isHoliday = currentHolidayDates.has(dateStr);
|
const isHoliday = currentHolidayDates.has(dateStr);
|
||||||
|
const weekendTravel = entry.weekend_travel || false;
|
||||||
|
const appliedWeekendPercentage = entry.applied_weekend_percentage;
|
||||||
|
const isWeekend = (i >= 5); // Samstag (5) oder Sonntag (6)
|
||||||
|
|
||||||
// Tätigkeiten laden
|
// Tätigkeiten laden
|
||||||
const activities = [
|
const activities = [
|
||||||
@@ -413,10 +437,18 @@ function renderWeek() {
|
|||||||
hoursToAdd = 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden)
|
hoursToAdd = 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden)
|
||||||
} else {
|
} else {
|
||||||
hoursToAdd = hours || 0;
|
hoursToAdd = hours || 0;
|
||||||
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Urlaub/Krankheit)
|
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
|
||||||
const weekendPercentage = getWeekendPercentage(date);
|
if (isWeekend && weekendTravel && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) {
|
||||||
if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) {
|
// Verwende gespeicherten Prozentsatz falls vorhanden, sonst aktuellen
|
||||||
hoursToAdd = hours * (weekendPercentage / 100);
|
let weekendPercentage = 100;
|
||||||
|
if (appliedWeekendPercentage !== null && appliedWeekendPercentage !== undefined) {
|
||||||
|
weekendPercentage = appliedWeekendPercentage;
|
||||||
|
} else {
|
||||||
|
weekendPercentage = getWeekendPercentage(date);
|
||||||
|
}
|
||||||
|
if (weekendPercentage >= 100) {
|
||||||
|
hoursToAdd = hours * (weekendPercentage / 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
totalHours += hoursToAdd;
|
totalHours += hoursToAdd;
|
||||||
@@ -537,20 +569,17 @@ function renderWeek() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sick-control">
|
<div class="sick-control">
|
||||||
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleSickStatus('${dateStr}')" style="margin-right: 5px;">
|
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleSickStatus('${dateStr}')" style="margin-right: 5px; ${sickStatus ? 'background-color: #e74c3c; color: white;' : ''}">
|
||||||
Krank
|
Krank
|
||||||
</button>
|
</button>
|
||||||
<div id="sick-checkbox-${dateStr}" style="display: ${sickStatus ? 'inline-block' : 'none'};">
|
|
||||||
<input type="checkbox"
|
|
||||||
data-date="${dateStr}"
|
|
||||||
data-field="sick_status"
|
|
||||||
${sickStatus ? 'checked' : ''}
|
|
||||||
${disabled}
|
|
||||||
onchange="saveEntry(this); updateOvertimeDisplay();"
|
|
||||||
style="margin-left: 5px;"
|
|
||||||
class="sick-checkbox">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
${isWeekend ? `
|
||||||
|
<div class="travel-control">
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleWeekendTravel('${dateStr}')" style="margin-right: 5px; ${weekendTravel ? 'background-color: #28a745; color: white;' : ''}">
|
||||||
|
Reise
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -671,8 +700,8 @@ function updateOvertimeDisplay() {
|
|||||||
// Prüfe Urlaub-Status und Krank-Status
|
// Prüfe Urlaub-Status und Krank-Status
|
||||||
const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`);
|
const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`);
|
||||||
const vacationType = vacationSelect ? vacationSelect.value : (currentEntries[dateStr]?.vacation_type || '');
|
const vacationType = vacationSelect ? vacationSelect.value : (currentEntries[dateStr]?.vacation_type || '');
|
||||||
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
|
// Für sick_status: Wert aus currentEntries lesen (da keine Checkbox mehr vorhanden)
|
||||||
const sickStatus = sickCheckbox ? sickCheckbox.checked : (currentEntries[dateStr]?.sick_status || false);
|
const sickStatus = currentEntries[dateStr]?.sick_status || false;
|
||||||
|
|
||||||
// Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind
|
// Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind
|
||||||
const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`);
|
const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`);
|
||||||
@@ -703,20 +732,46 @@ function updateOvertimeDisplay() {
|
|||||||
const end = new Date(`2000-01-01T${endTime}`);
|
const end = new Date(`2000-01-01T${endTime}`);
|
||||||
const diffMs = end - start;
|
const diffMs = end - start;
|
||||||
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
|
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
|
||||||
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
|
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
|
||||||
const weekendPercentage = getWeekendPercentage(date);
|
const dayOfWeek = date.getDay();
|
||||||
|
const isWeekend = (dayOfWeek === 6 || dayOfWeek === 0);
|
||||||
|
// Für weekend_travel: Wert aus currentEntries lesen (da keine Checkbox mehr vorhanden)
|
||||||
|
const weekendTravel = currentEntries[dateStr]?.weekend_travel || false;
|
||||||
let adjustedHours = hours;
|
let adjustedHours = hours;
|
||||||
if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) {
|
if (isWeekend && weekendTravel && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) {
|
||||||
adjustedHours = hours * (weekendPercentage / 100);
|
// Verwende gespeicherten Prozentsatz falls vorhanden, sonst aktuellen
|
||||||
|
let weekendPercentage = 100;
|
||||||
|
const entry = currentEntries[dateStr] || {};
|
||||||
|
if (entry.applied_weekend_percentage !== null && entry.applied_weekend_percentage !== undefined) {
|
||||||
|
weekendPercentage = entry.applied_weekend_percentage;
|
||||||
|
} else {
|
||||||
|
weekendPercentage = getWeekendPercentage(date);
|
||||||
|
}
|
||||||
|
if (weekendPercentage >= 100) {
|
||||||
|
adjustedHours = hours * (weekendPercentage / 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
totalHours += adjustedHours;
|
totalHours += adjustedHours;
|
||||||
} else if (currentEntries[dateStr]?.total_hours && !isFullDayOvertime) {
|
} else if (currentEntries[dateStr]?.total_hours && !isFullDayOvertime) {
|
||||||
// Fallback auf gespeicherte Werte
|
// Fallback auf gespeicherte Werte
|
||||||
let hours = parseFloat(currentEntries[dateStr].total_hours) || 0;
|
let hours = parseFloat(currentEntries[dateStr].total_hours) || 0;
|
||||||
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
|
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
|
||||||
const weekendPercentage = getWeekendPercentage(date);
|
const dayOfWeek = date.getDay();
|
||||||
if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus) {
|
const isWeekend = (dayOfWeek === 6 || dayOfWeek === 0);
|
||||||
hours = hours * (weekendPercentage / 100);
|
// Für weekend_travel: Wert aus currentEntries lesen (da keine Checkbox mehr vorhanden)
|
||||||
|
const weekendTravel = currentEntries[dateStr]?.weekend_travel || false;
|
||||||
|
if (isWeekend && weekendTravel && hours > 0 && vacationType !== 'full' && !sickStatus) {
|
||||||
|
// Verwende gespeicherten Prozentsatz falls vorhanden, sonst aktuellen
|
||||||
|
const entry = currentEntries[dateStr] || {};
|
||||||
|
let weekendPercentage = 100;
|
||||||
|
if (entry.applied_weekend_percentage !== null && entry.applied_weekend_percentage !== undefined) {
|
||||||
|
weekendPercentage = entry.applied_weekend_percentage;
|
||||||
|
} else {
|
||||||
|
weekendPercentage = getWeekendPercentage(date);
|
||||||
|
}
|
||||||
|
if (weekendPercentage >= 100) {
|
||||||
|
hours = hours * (weekendPercentage / 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
totalHours += hours;
|
totalHours += hours;
|
||||||
}
|
}
|
||||||
@@ -759,20 +814,46 @@ function updateOvertimeDisplay() {
|
|||||||
const end = new Date(`2000-01-01T${endTime}`);
|
const end = new Date(`2000-01-01T${endTime}`);
|
||||||
const diffMs = end - start;
|
const diffMs = end - start;
|
||||||
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
|
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
|
||||||
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
|
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
|
||||||
const weekendPercentage = getWeekendPercentage(date);
|
const dayOfWeek = date.getDay();
|
||||||
|
const isWeekend = (dayOfWeek === 6 || dayOfWeek === 0);
|
||||||
|
const travelCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="weekend_travel"]`);
|
||||||
|
const weekendTravel = travelCheckbox ? travelCheckbox.checked : (currentEntries[dateStr]?.weekend_travel || false);
|
||||||
let adjustedHours = hours;
|
let adjustedHours = hours;
|
||||||
if (weekendPercentage >= 100 && hours > 0 && !isFullDayOvertime) {
|
if (isWeekend && weekendTravel && hours > 0 && !isFullDayOvertime) {
|
||||||
adjustedHours = hours * (weekendPercentage / 100);
|
// Verwende gespeicherten Prozentsatz falls vorhanden, sonst aktuellen
|
||||||
|
const entry = currentEntries[dateStr] || {};
|
||||||
|
let weekendPercentage = 100;
|
||||||
|
if (entry.applied_weekend_percentage !== null && entry.applied_weekend_percentage !== undefined) {
|
||||||
|
weekendPercentage = entry.applied_weekend_percentage;
|
||||||
|
} else {
|
||||||
|
weekendPercentage = getWeekendPercentage(date);
|
||||||
|
}
|
||||||
|
if (weekendPercentage >= 100) {
|
||||||
|
adjustedHours = hours * (weekendPercentage / 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
totalHours += adjustedHours;
|
totalHours += adjustedHours;
|
||||||
} else if (currentEntries[dateStr]?.total_hours) {
|
} else if (currentEntries[dateStr]?.total_hours) {
|
||||||
// Fallback auf gespeicherte Werte
|
// Fallback auf gespeicherte Werte
|
||||||
let hours = parseFloat(currentEntries[dateStr].total_hours) || 0;
|
let hours = parseFloat(currentEntries[dateStr].total_hours) || 0;
|
||||||
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
|
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
|
||||||
const weekendPercentage = getWeekendPercentage(date);
|
const dayOfWeek = date.getDay();
|
||||||
if (weekendPercentage >= 100 && hours > 0 && !isFullDayOvertime) {
|
const isWeekend = (dayOfWeek === 6 || dayOfWeek === 0);
|
||||||
hours = hours * (weekendPercentage / 100);
|
const travelCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="weekend_travel"]`);
|
||||||
|
const weekendTravel = travelCheckbox ? travelCheckbox.checked : (currentEntries[dateStr]?.weekend_travel || false);
|
||||||
|
if (isWeekend && weekendTravel && hours > 0 && !isFullDayOvertime) {
|
||||||
|
// Verwende gespeicherten Prozentsatz falls vorhanden, sonst aktuellen
|
||||||
|
const entry = currentEntries[dateStr] || {};
|
||||||
|
let weekendPercentage = 100;
|
||||||
|
if (entry.applied_weekend_percentage !== null && entry.applied_weekend_percentage !== undefined) {
|
||||||
|
weekendPercentage = entry.applied_weekend_percentage;
|
||||||
|
} else {
|
||||||
|
weekendPercentage = getWeekendPercentage(date);
|
||||||
|
}
|
||||||
|
if (weekendPercentage >= 100) {
|
||||||
|
hours = hours * (weekendPercentage / 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
totalHours += hours;
|
totalHours += hours;
|
||||||
}
|
}
|
||||||
@@ -893,6 +974,27 @@ function handleOvertimeChange(dateStr, overtimeHours) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Berechnet die gesetzlich erforderliche Mindestpause basierend auf der Arbeitszeit
|
||||||
|
function calculateRequiredBreakMinutes(startTime, endTime) {
|
||||||
|
if (!startTime || !endTime) return null;
|
||||||
|
|
||||||
|
// Berechne Arbeitszeit in Stunden
|
||||||
|
const [startHours, startMinutes] = startTime.split(':').map(Number);
|
||||||
|
const [endHours, endMinutes] = endTime.split(':').map(Number);
|
||||||
|
const startTotalMinutes = startHours * 60 + startMinutes;
|
||||||
|
const endTotalMinutes = endHours * 60 + endMinutes;
|
||||||
|
const workMinutes = endTotalMinutes - startTotalMinutes;
|
||||||
|
const workHours = workMinutes / 60;
|
||||||
|
|
||||||
|
// Gesetzliche Mindestpause bestimmen
|
||||||
|
if (workHours > 9) {
|
||||||
|
return 45; // Mehr als 9 Stunden: 45 Minuten
|
||||||
|
} else if (workHours >= 6) {
|
||||||
|
return 30; // 6 bis 9 Stunden: 30 Minuten
|
||||||
|
}
|
||||||
|
return 0; // Weniger als 6 Stunden: keine gesetzliche Pause erforderlich
|
||||||
|
}
|
||||||
|
|
||||||
// Eintrag speichern
|
// Eintrag speichern
|
||||||
async function saveEntry(input) {
|
async function saveEntry(input) {
|
||||||
const date = input.dataset.date;
|
const date = input.dataset.date;
|
||||||
@@ -936,11 +1038,27 @@ async function saveEntry(input) {
|
|||||||
// Wichtig: Leere Strings werden zu null konvertiert, aber ein Wert sollte vorhanden sein
|
// Wichtig: Leere Strings werden zu null konvertiert, aber ein Wert sollte vorhanden sein
|
||||||
const start_time = actualStartTime;
|
const start_time = actualStartTime;
|
||||||
const end_time = actualEndTime;
|
const end_time = actualEndTime;
|
||||||
const break_minutes = breakInput && breakInput.value ? (parseInt(breakInput.value) || 0) : (parseInt(currentEntries[date].break_minutes) || 0);
|
let break_minutes = breakInput && breakInput.value ? (parseInt(breakInput.value) || 0) : (parseInt(currentEntries[date].break_minutes) || 0);
|
||||||
|
|
||||||
|
// Automatische Vorbelegung der Pausenzeiten basierend auf gesetzlichen Vorgaben
|
||||||
|
// Wird ausgelöst, wenn start_time oder end_time geändert werden
|
||||||
|
if ((input.dataset.field === 'start_time' || input.dataset.field === 'end_time') && start_time && end_time) {
|
||||||
|
const requiredBreakMinutes = calculateRequiredBreakMinutes(start_time, end_time);
|
||||||
|
if (requiredBreakMinutes !== null && requiredBreakMinutes > break_minutes) {
|
||||||
|
// Setze den höheren Wert (gesetzliche Mindestpause)
|
||||||
|
break_minutes = requiredBreakMinutes;
|
||||||
|
// Aktualisiere das Input-Feld im DOM
|
||||||
|
if (breakInput) {
|
||||||
|
breakInput.value = break_minutes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const notes = notesInput ? (notesInput.value || '') : (currentEntries[date].notes || '');
|
const notes = notesInput ? (notesInput.value || '') : (currentEntries[date].notes || '');
|
||||||
const vacation_type = vacationSelect && vacationSelect.value ? vacationSelect.value : (currentEntries[date].vacation_type || null);
|
const vacation_type = vacationSelect && vacationSelect.value ? vacationSelect.value : (currentEntries[date].vacation_type || null);
|
||||||
const overtime_taken_hours = overtimeInput && overtimeInput.value ? overtimeInput.value : (currentEntries[date].overtime_taken_hours || null);
|
const overtime_taken_hours = overtimeInput && overtimeInput.value ? overtimeInput.value : (currentEntries[date].overtime_taken_hours || null);
|
||||||
const sick_status = sickCheckbox ? (sickCheckbox.checked ? true : false) : (currentEntries[date].sick_status || false);
|
// Für sick_status und weekend_travel: Wert aus currentEntries lesen (da keine Checkboxen mehr vorhanden)
|
||||||
|
const sick_status = (input.dataset.field === 'sick_status') ? (value === 'true' || value === true || value === '1' || value === 1) : (currentEntries[date].sick_status || false);
|
||||||
|
const weekend_travel = (input.dataset.field === 'weekend_travel') ? (value === 'true' || value === true || value === '1' || value === 1) : (currentEntries[date].weekend_travel || false);
|
||||||
|
|
||||||
// Activity-Felder aus DOM lesen
|
// Activity-Felder aus DOM lesen
|
||||||
const activities = [];
|
const activities = [];
|
||||||
@@ -964,6 +1082,7 @@ async function saveEntry(input) {
|
|||||||
currentEntries[date].vacation_type = vacation_type;
|
currentEntries[date].vacation_type = vacation_type;
|
||||||
currentEntries[date].overtime_taken_hours = overtime_taken_hours;
|
currentEntries[date].overtime_taken_hours = overtime_taken_hours;
|
||||||
currentEntries[date].sick_status = sick_status;
|
currentEntries[date].sick_status = sick_status;
|
||||||
|
currentEntries[date].weekend_travel = weekend_travel;
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
currentEntries[date][`activity${i}_desc`] = activities[i-1].desc;
|
currentEntries[date][`activity${i}_desc`] = activities[i-1].desc;
|
||||||
currentEntries[date][`activity${i}_hours`] = activities[i-1].hours;
|
currentEntries[date][`activity${i}_hours`] = activities[i-1].hours;
|
||||||
@@ -1002,7 +1121,8 @@ async function saveEntry(input) {
|
|||||||
activity5_project_number: activities[4].projectNumber,
|
activity5_project_number: activities[4].projectNumber,
|
||||||
overtime_taken_hours: overtime_taken_hours,
|
overtime_taken_hours: overtime_taken_hours,
|
||||||
vacation_type: vacation_type,
|
vacation_type: vacation_type,
|
||||||
sick_status: sick_status
|
sick_status: sick_status,
|
||||||
|
weekend_travel: weekend_travel
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1063,6 +1183,9 @@ function checkWeekComplete() {
|
|||||||
date.setDate(date.getDate() + i);
|
date.setDate(date.getDate() + i);
|
||||||
const dateStr = formatDate(date);
|
const dateStr = formatDate(date);
|
||||||
|
|
||||||
|
// Prüfe ob Feiertag
|
||||||
|
const isHoliday = currentHolidayDates.has(dateStr);
|
||||||
|
|
||||||
// Prüfe Urlaub-Status und Krank-Status
|
// Prüfe Urlaub-Status und Krank-Status
|
||||||
const entry = currentEntries[dateStr] || {};
|
const entry = currentEntries[dateStr] || {};
|
||||||
const vacationType = entry.vacation_type;
|
const vacationType = entry.vacation_type;
|
||||||
@@ -1071,9 +1194,9 @@ function checkWeekComplete() {
|
|||||||
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
|
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
|
||||||
const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false);
|
const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false);
|
||||||
|
|
||||||
// Wenn ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
|
// Wenn Feiertag, ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
|
||||||
if (vacationValue === 'full' || sickStatus) {
|
if (isHoliday || vacationValue === 'full' || sickStatus) {
|
||||||
continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank)
|
continue; // Tag ist ausgefüllt (Feiertag, ganzer Tag Urlaub oder Krank)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe ob 8 Überstunden eingetragen sind (dann ist der Tag auch ausgefüllt)
|
// Prüfe ob 8 Überstunden eingetragen sind (dann ist der Tag auch ausgefüllt)
|
||||||
@@ -1132,6 +1255,18 @@ function checkWeekComplete() {
|
|||||||
|
|
||||||
console.log(`Submit-Button Status: disabled=${submitButton.disabled}, allWeekdaysFilled=${allWeekdaysFilled}, weekIsSubmitted=${weekIsSubmitted}`);
|
console.log(`Submit-Button Status: disabled=${submitButton.disabled}, allWeekdaysFilled=${allWeekdaysFilled}, weekIsSubmitted=${weekIsSubmitted}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PDF-Button Status aktualisieren
|
||||||
|
const viewPdfButton = document.getElementById('viewPdfBtn');
|
||||||
|
if (viewPdfButton) {
|
||||||
|
// Button aktivieren wenn eine eingereichte Version existiert
|
||||||
|
viewPdfButton.disabled = !latestSubmittedTimesheetId;
|
||||||
|
if (latestSubmittedTimesheetId) {
|
||||||
|
viewPdfButton.title = 'Letzte eingereichte Version anzeigen';
|
||||||
|
} else {
|
||||||
|
viewPdfButton.title = 'Keine eingereichte Version verfügbar';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Globaler Handler für onclick-Attribut (im HTML)
|
// Globaler Handler für onclick-Attribut (im HTML)
|
||||||
@@ -1185,6 +1320,9 @@ async function submitWeek() {
|
|||||||
const weekday = getWeekday(dateStr);
|
const weekday = getWeekday(dateStr);
|
||||||
const dateDisplay = formatDateDE(dateStr);
|
const dateDisplay = formatDateDE(dateStr);
|
||||||
|
|
||||||
|
// Prüfe ob Feiertag
|
||||||
|
const isHoliday = currentHolidayDates.has(dateStr);
|
||||||
|
|
||||||
// Prüfe Urlaub-Status und Krank-Status
|
// Prüfe Urlaub-Status und Krank-Status
|
||||||
const entry = currentEntries[dateStr] || {};
|
const entry = currentEntries[dateStr] || {};
|
||||||
const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`);
|
const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`);
|
||||||
@@ -1192,9 +1330,9 @@ async function submitWeek() {
|
|||||||
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
|
const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`);
|
||||||
const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false);
|
const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false);
|
||||||
|
|
||||||
// Wenn ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
|
// Wenn Feiertag, ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
|
||||||
if (vacationValue === 'full' || sickStatus) {
|
if (isHoliday || vacationValue === 'full' || sickStatus) {
|
||||||
continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank)
|
continue; // Tag ist ausgefüllt (Feiertag, ganzer Tag Urlaub oder Krank)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe ob 8 Überstunden eingetragen sind (dann ist der Tag auch ausgefüllt, Start/Ende nicht nötig)
|
// Prüfe ob 8 Überstunden eingetragen sind (dann ist der Tag auch ausgefüllt, Start/Ende nicht nötig)
|
||||||
@@ -1474,37 +1612,69 @@ function toggleVacationSelect(dateStr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Krank-Status ein-/ausblenden
|
// Wochenend-Reise-Status umschalten
|
||||||
function toggleSickStatus(dateStr) {
|
function toggleWeekendTravel(dateStr) {
|
||||||
const checkboxDiv = document.getElementById(`sick-checkbox-${dateStr}`);
|
const button = document.querySelector(`button[onclick="toggleWeekendTravel('${dateStr}')"]`);
|
||||||
if (checkboxDiv) {
|
if (!button) return;
|
||||||
if (checkboxDiv.style.display === 'none' || !checkboxDiv.style.display) {
|
|
||||||
checkboxDiv.style.display = 'inline-block';
|
// Aktuellen Status aus currentEntries lesen
|
||||||
const checkbox = checkboxDiv.querySelector('input[type="checkbox"]');
|
const currentTravelStatus = currentEntries[dateStr]?.weekend_travel || false;
|
||||||
if (checkbox) {
|
const newStatus = !currentTravelStatus;
|
||||||
// Prüfe aktuellen Status aus currentEntries
|
|
||||||
const currentSickStatus = currentEntries[dateStr]?.sick_status || false;
|
// Status in currentEntries aktualisieren
|
||||||
checkbox.checked = currentSickStatus || true; // Wenn nicht gesetzt, auf true setzen
|
if (!currentEntries[dateStr]) {
|
||||||
checkbox.focus();
|
currentEntries[dateStr] = { date: dateStr };
|
||||||
// Sofort speichern wenn aktiviert
|
|
||||||
if (!currentSickStatus) {
|
|
||||||
saveEntry(checkbox);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Wert löschen wenn ausgeblendet
|
|
||||||
const checkbox = checkboxDiv.querySelector('input[type="checkbox"]');
|
|
||||||
if (checkbox) {
|
|
||||||
checkbox.checked = false;
|
|
||||||
// Speichern
|
|
||||||
if (currentEntries[dateStr]) {
|
|
||||||
currentEntries[dateStr].sick_status = false;
|
|
||||||
saveEntry(checkbox);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkboxDiv.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
currentEntries[dateStr].weekend_travel = newStatus;
|
||||||
|
|
||||||
|
// Button-Stil aktualisieren
|
||||||
|
if (newStatus) {
|
||||||
|
button.style.backgroundColor = '#28a745';
|
||||||
|
button.style.color = 'white';
|
||||||
|
} else {
|
||||||
|
button.style.backgroundColor = '';
|
||||||
|
button.style.color = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speichere den Wert (erstellen ein temporäres Input-Element für saveEntry)
|
||||||
|
const tempInput = document.createElement('input');
|
||||||
|
tempInput.dataset.date = dateStr;
|
||||||
|
tempInput.dataset.field = 'weekend_travel';
|
||||||
|
tempInput.value = newStatus;
|
||||||
|
saveEntry(tempInput);
|
||||||
|
updateOvertimeDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSickStatus(dateStr) {
|
||||||
|
const button = document.querySelector(`button[onclick="toggleSickStatus('${dateStr}')"]`);
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
// Aktuellen Status aus currentEntries lesen
|
||||||
|
const currentSickStatus = currentEntries[dateStr]?.sick_status || false;
|
||||||
|
const newStatus = !currentSickStatus;
|
||||||
|
|
||||||
|
// Status in currentEntries aktualisieren
|
||||||
|
if (!currentEntries[dateStr]) {
|
||||||
|
currentEntries[dateStr] = { date: dateStr };
|
||||||
|
}
|
||||||
|
currentEntries[dateStr].sick_status = newStatus;
|
||||||
|
|
||||||
|
// Button-Stil aktualisieren
|
||||||
|
if (newStatus) {
|
||||||
|
button.style.backgroundColor = '#e74c3c';
|
||||||
|
button.style.color = 'white';
|
||||||
|
} else {
|
||||||
|
button.style.backgroundColor = '';
|
||||||
|
button.style.color = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speichere den Wert (erstellen ein temporäres Input-Element für saveEntry)
|
||||||
|
const tempInput = document.createElement('input');
|
||||||
|
tempInput.dataset.date = dateStr;
|
||||||
|
tempInput.dataset.field = 'sick_status';
|
||||||
|
tempInput.value = newStatus;
|
||||||
|
saveEntry(tempInput);
|
||||||
|
updateOvertimeDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping-IP laden
|
// Ping-IP laden
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const { db } = require('../database');
|
|||||||
const { requireAuth, requireVerwaltung } = require('../middleware/auth');
|
const { requireAuth, requireVerwaltung } = require('../middleware/auth');
|
||||||
const { generatePDF } = require('../services/pdf-service');
|
const { generatePDF } = require('../services/pdf-service');
|
||||||
const { getHolidaysForDateRange } = require('../services/feiertage-service');
|
const { getHolidaysForDateRange } = require('../services/feiertage-service');
|
||||||
|
const { hasRole } = require('../helpers/utils');
|
||||||
|
|
||||||
// Routes registrieren
|
// Routes registrieren
|
||||||
function registerTimesheetRoutes(app) {
|
function registerTimesheetRoutes(app) {
|
||||||
@@ -16,7 +17,7 @@ function registerTimesheetRoutes(app) {
|
|||||||
activity3_desc, activity3_hours, activity3_project_number,
|
activity3_desc, activity3_hours, activity3_project_number,
|
||||||
activity4_desc, activity4_hours, activity4_project_number,
|
activity4_desc, activity4_hours, activity4_project_number,
|
||||||
activity5_desc, activity5_hours, activity5_project_number,
|
activity5_desc, activity5_hours, activity5_project_number,
|
||||||
overtime_taken_hours, vacation_type, sick_status
|
overtime_taken_hours, vacation_type, sick_status, weekend_travel
|
||||||
} = req.body;
|
} = req.body;
|
||||||
const userId = req.session.userId;
|
const userId = req.session.userId;
|
||||||
|
|
||||||
@@ -27,6 +28,10 @@ function registerTimesheetRoutes(app) {
|
|||||||
// Normalisiere sick_status: Boolean oder 1/0 zu Boolean
|
// Normalisiere sick_status: Boolean oder 1/0 zu Boolean
|
||||||
const isSick = sick_status === true || sick_status === 1 || sick_status === 'true' || sick_status === '1';
|
const isSick = sick_status === true || sick_status === 1 || sick_status === 'true' || sick_status === '1';
|
||||||
|
|
||||||
|
// Normalisiere weekend_travel: Boolean oder 1/0 zu Integer
|
||||||
|
const isWeekendTravel = weekend_travel === true || weekend_travel === 1 || weekend_travel === 'true' || weekend_travel === '1';
|
||||||
|
const weekendTravelValue = isWeekendTravel ? 1 : 0;
|
||||||
|
|
||||||
// Wochenend-Prozentsätze laden
|
// Wochenend-Prozentsätze laden
|
||||||
db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => {
|
db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -66,6 +71,11 @@ function registerTimesheetRoutes(app) {
|
|||||||
isFullDayOvertime = true;
|
isFullDayOvertime = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüfe ob es ein Wochenendtag ist
|
||||||
|
const dateObj = new Date(date);
|
||||||
|
const dayOfWeek = dateObj.getDay();
|
||||||
|
const isWeekend = (dayOfWeek === 6 || dayOfWeek === 0); // 6 = Samstag, 0 = Sonntag
|
||||||
|
|
||||||
// Gesamtstunden berechnen (aus Start- und Endzeit, nicht aus Tätigkeiten)
|
// Gesamtstunden berechnen (aus Start- und Endzeit, nicht aus Tätigkeiten)
|
||||||
// Wenn ganzer Tag Urlaub oder Krank, dann zählt dieser als 8 Stunden normale Arbeitszeit
|
// Wenn ganzer Tag Urlaub oder Krank, dann zählt dieser als 8 Stunden normale Arbeitszeit
|
||||||
let total_hours = 0;
|
let total_hours = 0;
|
||||||
@@ -77,6 +87,7 @@ function registerTimesheetRoutes(app) {
|
|||||||
let finalActivity5Desc = activity5_desc;
|
let finalActivity5Desc = activity5_desc;
|
||||||
let finalStartTime = normalizedStartTime;
|
let finalStartTime = normalizedStartTime;
|
||||||
let finalEndTime = normalizedEndTime;
|
let finalEndTime = normalizedEndTime;
|
||||||
|
let appliedWeekendPercentage = null; // Wird gesetzt wenn Wochenend-Prozentsatz angewendet wird
|
||||||
|
|
||||||
// Überstunden-Logik: Bei vollem Tag Überstunden
|
// Überstunden-Logik: Bei vollem Tag Überstunden
|
||||||
if (isFullDayOvertime) {
|
if (isFullDayOvertime) {
|
||||||
@@ -95,10 +106,14 @@ function registerTimesheetRoutes(app) {
|
|||||||
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
||||||
const diffMs = end - start;
|
const diffMs = end - start;
|
||||||
total_hours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60);
|
total_hours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60);
|
||||||
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Urlaub/Krankheit)
|
|
||||||
const weekendPercentage = getWeekendPercentage(date);
|
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
|
||||||
if (weekendPercentage >= 100 && total_hours > 0 && !isSick && vacation_type !== 'full') {
|
if (isWeekend && isWeekendTravel && total_hours > 0 && !isSick && vacation_type !== 'full') {
|
||||||
total_hours = total_hours * (weekendPercentage / 100);
|
const weekendPercentage = getWeekendPercentage(date);
|
||||||
|
if (weekendPercentage >= 100) {
|
||||||
|
total_hours = total_hours * (weekendPercentage / 100);
|
||||||
|
appliedWeekendPercentage = weekendPercentage; // Speichere den angewendeten Prozentsatz
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Bei halbem Tag Urlaub: total_hours bleibt die tatsächlich gearbeiteten Stunden
|
// Bei halbem Tag Urlaub: total_hours bleibt die tatsächlich gearbeiteten Stunden
|
||||||
// Die 4 Stunden Urlaub werden nur in der Überstunden-Berechnung hinzugezählt
|
// Die 4 Stunden Urlaub werden nur in der Überstunden-Berechnung hinzugezählt
|
||||||
@@ -108,9 +123,34 @@ function registerTimesheetRoutes(app) {
|
|||||||
// Sie werden über overtime_taken_hours in der PDF angezeigt
|
// Sie werden über overtime_taken_hours in der PDF angezeigt
|
||||||
|
|
||||||
// Prüfen ob Eintrag existiert - verwende den neuesten Eintrag falls mehrere existieren
|
// Prüfen ob Eintrag existiert - verwende den neuesten Eintrag falls mehrere existieren
|
||||||
db.get('SELECT id FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
|
db.get('SELECT id, applied_weekend_percentage FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
|
||||||
[userId, date], (err, row) => {
|
[userId, date], (err, row) => {
|
||||||
if (row) {
|
if (row) {
|
||||||
|
// Wenn bereits ein gespeicherter Prozentsatz existiert, diesen verwenden (historische Einträge bleiben unverändert)
|
||||||
|
let finalAppliedPercentage = appliedWeekendPercentage;
|
||||||
|
if (row.applied_weekend_percentage !== null && row.applied_weekend_percentage !== undefined) {
|
||||||
|
// Verwende den gespeicherten Prozentsatz, aber nur wenn weekend_travel aktiviert ist
|
||||||
|
if (isWeekendTravel && isWeekend) {
|
||||||
|
finalAppliedPercentage = row.applied_weekend_percentage;
|
||||||
|
// Berechne total_hours neu mit gespeichertem Prozentsatz, falls nötig
|
||||||
|
if (normalizedStartTime && normalizedEndTime && !isSick && vacation_type !== 'full' && !isFullDayOvertime) {
|
||||||
|
const start = new Date(`2000-01-01T${normalizedStartTime}`);
|
||||||
|
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
||||||
|
const diffMs = end - start;
|
||||||
|
const baseHours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60);
|
||||||
|
if (baseHours > 0 && finalAppliedPercentage >= 100) {
|
||||||
|
total_hours = baseHours * (finalAppliedPercentage / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Wenn weekend_travel nicht aktiviert ist, aber ein gespeicherter Prozentsatz existiert, behalte ihn
|
||||||
|
finalAppliedPercentage = row.applied_weekend_percentage;
|
||||||
|
}
|
||||||
|
} else if (isWeekendTravel && isWeekend) {
|
||||||
|
// Neuer Eintrag mit weekend_travel - speichere den aktuellen Prozentsatz
|
||||||
|
finalAppliedPercentage = appliedWeekendPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
db.run(`UPDATE timesheet_entries
|
db.run(`UPDATE timesheet_entries
|
||||||
SET start_time = ?, end_time = ?, break_minutes = ?, total_hours = ?, notes = ?,
|
SET start_time = ?, end_time = ?, break_minutes = ?, total_hours = ?, notes = ?,
|
||||||
@@ -120,6 +160,7 @@ function registerTimesheetRoutes(app) {
|
|||||||
activity4_desc = ?, activity4_hours = ?, activity4_project_number = ?,
|
activity4_desc = ?, activity4_hours = ?, activity4_project_number = ?,
|
||||||
activity5_desc = ?, activity5_hours = ?, activity5_project_number = ?,
|
activity5_desc = ?, activity5_hours = ?, activity5_project_number = ?,
|
||||||
overtime_taken_hours = ?, vacation_type = ?, sick_status = ?,
|
overtime_taken_hours = ?, vacation_type = ?, sick_status = ?,
|
||||||
|
weekend_travel = ?, applied_weekend_percentage = ?,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
[
|
[
|
||||||
@@ -132,6 +173,8 @@ function registerTimesheetRoutes(app) {
|
|||||||
overtime_taken_hours ? parseFloat(overtime_taken_hours) : null,
|
overtime_taken_hours ? parseFloat(overtime_taken_hours) : null,
|
||||||
vacation_type || null,
|
vacation_type || null,
|
||||||
isSick ? 1 : 0,
|
isSick ? 1 : 0,
|
||||||
|
weekendTravelValue,
|
||||||
|
finalAppliedPercentage,
|
||||||
row.id
|
row.id
|
||||||
],
|
],
|
||||||
(err) => {
|
(err) => {
|
||||||
@@ -150,8 +193,8 @@ function registerTimesheetRoutes(app) {
|
|||||||
activity3_desc, activity3_hours, activity3_project_number,
|
activity3_desc, activity3_hours, activity3_project_number,
|
||||||
activity4_desc, activity4_hours, activity4_project_number,
|
activity4_desc, activity4_hours, activity4_project_number,
|
||||||
activity5_desc, activity5_hours, activity5_project_number,
|
activity5_desc, activity5_hours, activity5_project_number,
|
||||||
overtime_taken_hours, vacation_type, sick_status)
|
overtime_taken_hours, vacation_type, sick_status, weekend_travel, applied_weekend_percentage)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
userId, date, finalStartTime, finalEndTime, break_minutes, total_hours, notes,
|
userId, date, finalStartTime, finalEndTime, break_minutes, total_hours, notes,
|
||||||
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
|
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
|
||||||
@@ -161,7 +204,9 @@ function registerTimesheetRoutes(app) {
|
|||||||
finalActivity5Desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null,
|
finalActivity5Desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null,
|
||||||
overtime_taken_hours ? parseFloat(overtime_taken_hours) : null,
|
overtime_taken_hours ? parseFloat(overtime_taken_hours) : null,
|
||||||
vacation_type || null,
|
vacation_type || null,
|
||||||
isSick ? 1 : 0
|
isSick ? 1 : 0,
|
||||||
|
weekendTravelValue,
|
||||||
|
appliedWeekendPercentage
|
||||||
],
|
],
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -368,6 +413,40 @@ function registerTimesheetRoutes(app) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API: Neueste eingereichte Version für eine Woche abrufen
|
||||||
|
app.get('/api/timesheet/latest-submitted/:weekStart', requireAuth, (req, res) => {
|
||||||
|
const userId = req.session.userId;
|
||||||
|
const weekStart = req.params.weekStart;
|
||||||
|
|
||||||
|
// Berechne Wochenende
|
||||||
|
const startDate = new Date(weekStart);
|
||||||
|
const endDate = new Date(startDate);
|
||||||
|
endDate.setDate(endDate.getDate() + 6);
|
||||||
|
const weekEnd = endDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Hole die neueste eingereichte Version für diese Woche
|
||||||
|
db.get(`SELECT id, version, submitted_at FROM weekly_timesheets
|
||||||
|
WHERE user_id = ? AND week_start = ? AND week_end = ?
|
||||||
|
ORDER BY version DESC LIMIT 1`,
|
||||||
|
[userId, weekStart, weekEnd],
|
||||||
|
(err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Fehler beim Abrufen der neuesten Version:', err);
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Abrufen der Version' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
res.json({
|
||||||
|
timesheetId: result.id,
|
||||||
|
version: result.version,
|
||||||
|
submitted_at: result.submitted_at
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.json({ timesheetId: null, version: null, submitted_at: null });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// API: PDF Download-Info abrufen
|
// API: PDF Download-Info abrufen
|
||||||
app.get('/api/timesheet/download-info/:id', requireVerwaltung, (req, res) => {
|
app.get('/api/timesheet/download-info/:id', requireVerwaltung, (req, res) => {
|
||||||
const timesheetId = req.params.id;
|
const timesheetId = req.params.id;
|
||||||
@@ -398,9 +477,24 @@ function registerTimesheetRoutes(app) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// API: PDF generieren
|
// API: PDF generieren
|
||||||
app.get('/api/timesheet/pdf/:id', requireVerwaltung, (req, res) => {
|
app.get('/api/timesheet/pdf/:id', requireAuth, (req, res) => {
|
||||||
const timesheetId = req.params.id;
|
const timesheetId = req.params.id;
|
||||||
generatePDF(timesheetId, req, res);
|
const userId = req.session.userId;
|
||||||
|
const isVerwaltung = hasRole(req, 'verwaltung') || hasRole(req, 'admin');
|
||||||
|
|
||||||
|
// Prüfe ob User Verwaltung/Admin ist oder ob das Timesheet dem User gehört
|
||||||
|
db.get(`SELECT user_id FROM weekly_timesheets WHERE id = ?`, [timesheetId], (err, timesheet) => {
|
||||||
|
if (err || !timesheet) {
|
||||||
|
return res.status(404).send('Stundenzettel nicht gefunden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zugriff erlauben wenn Verwaltung/Admin ODER wenn Timesheet dem User gehört
|
||||||
|
if (isVerwaltung || timesheet.user_id === userId) {
|
||||||
|
generatePDF(timesheetId, req, res);
|
||||||
|
} else {
|
||||||
|
res.status(403).send('Zugriff verweigert');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ function registerUserRoutes(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// User-Daten abrufen
|
// User-Daten abrufen
|
||||||
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => {
|
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours, vacation_offset_days FROM users WHERE id = ?', [userId], (err, user) => {
|
||||||
if (err || !user) {
|
if (err || !user) {
|
||||||
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
||||||
}
|
}
|
||||||
@@ -231,6 +231,7 @@ function registerUserRoutes(app) {
|
|||||||
const wochenstunden = user.wochenstunden || 0;
|
const wochenstunden = user.wochenstunden || 0;
|
||||||
const urlaubstage = user.urlaubstage || 0;
|
const urlaubstage = user.urlaubstage || 0;
|
||||||
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
||||||
|
const vacationOffsetDays = user.vacation_offset_days ? parseFloat(user.vacation_offset_days) : 0;
|
||||||
|
|
||||||
// Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte)
|
// Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte)
|
||||||
const { getCalendarWeek } = require('../helpers/utils');
|
const { getCalendarWeek } = require('../helpers/utils');
|
||||||
@@ -282,14 +283,15 @@ function registerUserRoutes(app) {
|
|||||||
if (!weeks || weeks.length === 0) {
|
if (!weeks || weeks.length === 0) {
|
||||||
return res.json({
|
return res.json({
|
||||||
currentOvertime: overtimeOffsetHours,
|
currentOvertime: overtimeOffsetHours,
|
||||||
remainingVacation: urlaubstage,
|
remainingVacation: urlaubstage + vacationOffsetDays,
|
||||||
totalOvertimeHours: 0,
|
totalOvertimeHours: 0,
|
||||||
totalOvertimeTaken: 0,
|
totalOvertimeTaken: 0,
|
||||||
totalVacationDays: 0,
|
totalVacationDays: 0,
|
||||||
plannedVacationDays: plannedVacationDays,
|
plannedVacationDays: plannedVacationDays,
|
||||||
plannedWeeks: plannedWeeks,
|
plannedWeeks: plannedWeeks,
|
||||||
urlaubstage: urlaubstage,
|
urlaubstage: urlaubstage,
|
||||||
overtimeOffsetHours: overtimeOffsetHours
|
overtimeOffsetHours: overtimeOffsetHours,
|
||||||
|
vacationOffsetDays: vacationOffsetDays
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +383,7 @@ function registerUserRoutes(app) {
|
|||||||
processedWeeks++;
|
processedWeeks++;
|
||||||
if (processedWeeks === weeks.length && !hasError) {
|
if (processedWeeks === weeks.length && !hasError) {
|
||||||
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
|
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
|
||||||
const remainingVacation = urlaubstage - totalVacationDays;
|
const remainingVacation = urlaubstage - totalVacationDays + vacationOffsetDays;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
currentOvertime: currentOvertime,
|
currentOvertime: currentOvertime,
|
||||||
@@ -392,7 +394,8 @@ function registerUserRoutes(app) {
|
|||||||
plannedVacationDays: plannedVacationDays,
|
plannedVacationDays: plannedVacationDays,
|
||||||
plannedWeeks: plannedWeeks,
|
plannedWeeks: plannedWeeks,
|
||||||
urlaubstage: urlaubstage,
|
urlaubstage: urlaubstage,
|
||||||
overtimeOffsetHours: overtimeOffsetHours
|
overtimeOffsetHours: overtimeOffsetHours,
|
||||||
|
vacationOffsetDays: vacationOffsetDays
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return; // Überspringe diese Woche
|
return; // Überspringe diese Woche
|
||||||
@@ -479,7 +482,7 @@ function registerUserRoutes(app) {
|
|||||||
// weekOvertimeHours enthält bereits die korrekte Berechnung pro Woche (wie im Frontend)
|
// weekOvertimeHours enthält bereits die korrekte Berechnung pro Woche (wie im Frontend)
|
||||||
// weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden)
|
// weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden)
|
||||||
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
|
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
|
||||||
const remainingVacation = urlaubstage - totalVacationDays;
|
const remainingVacation = urlaubstage - totalVacationDays + vacationOffsetDays;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
currentOvertime: currentOvertime,
|
currentOvertime: currentOvertime,
|
||||||
@@ -490,7 +493,8 @@ function registerUserRoutes(app) {
|
|||||||
plannedVacationDays: plannedVacationDays,
|
plannedVacationDays: plannedVacationDays,
|
||||||
plannedWeeks: plannedWeeks,
|
plannedWeeks: plannedWeeks,
|
||||||
urlaubstage: urlaubstage,
|
urlaubstage: urlaubstage,
|
||||||
overtimeOffsetHours: overtimeOffsetHours
|
overtimeOffsetHours: overtimeOffsetHours,
|
||||||
|
vacationOffsetDays: vacationOffsetDays
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}); // getHolidaysForDateRange.then
|
}); // getHolidaysForDateRange.then
|
||||||
@@ -12,7 +12,7 @@ function registerVerwaltungRoutes(app) {
|
|||||||
// Verwaltungs-Bereich
|
// Verwaltungs-Bereich
|
||||||
app.get('/verwaltung', requireVerwaltung, (req, res) => {
|
app.get('/verwaltung', requireVerwaltung, (req, res) => {
|
||||||
db.all(`
|
db.all(`
|
||||||
SELECT wt.*, u.firstname, u.lastname, u.username, u.personalnummer, u.wochenstunden, u.urlaubstage, u.overtime_offset_hours,
|
SELECT wt.*, u.firstname, u.lastname, u.username, u.personalnummer, u.wochenstunden, u.urlaubstage, u.overtime_offset_hours, u.vacation_offset_days,
|
||||||
dl.firstname as downloaded_by_firstname,
|
dl.firstname as downloaded_by_firstname,
|
||||||
dl.lastname as downloaded_by_lastname,
|
dl.lastname as downloaded_by_lastname,
|
||||||
(SELECT COUNT(*) FROM weekly_timesheets wt2
|
(SELECT COUNT(*) FROM weekly_timesheets wt2
|
||||||
@@ -44,7 +44,8 @@ function registerVerwaltungRoutes(app) {
|
|||||||
personalnummer: ts.personalnummer,
|
personalnummer: ts.personalnummer,
|
||||||
wochenstunden: ts.wochenstunden,
|
wochenstunden: ts.wochenstunden,
|
||||||
urlaubstage: ts.urlaubstage,
|
urlaubstage: ts.urlaubstage,
|
||||||
overtime_offset_hours: ts.overtime_offset_hours
|
overtime_offset_hours: ts.overtime_offset_hours,
|
||||||
|
vacation_offset_days: ts.vacation_offset_days
|
||||||
},
|
},
|
||||||
weeks: {}
|
weeks: {}
|
||||||
};
|
};
|
||||||
@@ -64,6 +65,38 @@ function registerVerwaltungRoutes(app) {
|
|||||||
groupedByEmployee[userId].weeks[weekKey].versions.push(ts);
|
groupedByEmployee[userId].weeks[weekKey].versions.push(ts);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Prüfe für jede Woche, ob nach dem letzten Download eine neue Version eingereicht wurde
|
||||||
|
Object.values(groupedByEmployee).forEach(employee => {
|
||||||
|
Object.values(employee.weeks).forEach(week => {
|
||||||
|
// Finde die neueste Version mit pdf_downloaded_at (letzter Download)
|
||||||
|
let lastDownloadTime = null;
|
||||||
|
week.versions.forEach(version => {
|
||||||
|
if (version.pdf_downloaded_at) {
|
||||||
|
const downloadTime = new Date(version.pdf_downloaded_at).getTime();
|
||||||
|
if (!lastDownloadTime || downloadTime > lastDownloadTime) {
|
||||||
|
lastDownloadTime = downloadTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prüfe, ob es eine Version gibt, die nach dem letzten Download eingereicht wurde
|
||||||
|
let hasNewVersionAfterDownload = false;
|
||||||
|
if (lastDownloadTime) {
|
||||||
|
week.versions.forEach(version => {
|
||||||
|
if (version.submitted_at) {
|
||||||
|
const submittedTime = new Date(version.submitted_at).getTime();
|
||||||
|
if (submittedTime > lastDownloadTime) {
|
||||||
|
hasNewVersionAfterDownload = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setze Flag auf dem week-Objekt
|
||||||
|
week.has_new_version_after_download = hasNewVersionAfterDownload;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Sortierung: Mitarbeiter nach Name, Wochen nach Datum (neueste zuerst)
|
// Sortierung: Mitarbeiter nach Name, Wochen nach Datum (neueste zuerst)
|
||||||
const sortedEmployees = Object.values(groupedByEmployee).map(employee => {
|
const sortedEmployees = Object.values(groupedByEmployee).map(employee => {
|
||||||
// Wochen innerhalb jedes Mitarbeiters sortieren
|
// Wochen innerhalb jedes Mitarbeiters sortieren
|
||||||
@@ -114,6 +147,26 @@ function registerVerwaltungRoutes(app) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API: Urlaubstage-Offset für einen User setzen (positiv/negativ)
|
||||||
|
app.put('/api/verwaltung/user/:id/vacation-offset', requireVerwaltung, (req, res) => {
|
||||||
|
const userId = req.params.id;
|
||||||
|
const raw = req.body ? req.body.vacation_offset_days : undefined;
|
||||||
|
|
||||||
|
// Leere Eingabe => 0
|
||||||
|
const normalized = (raw === '' || raw === null || raw === undefined) ? 0 : parseFloat(raw);
|
||||||
|
if (!Number.isFinite(normalized)) {
|
||||||
|
return res.status(400).json({ error: 'Ungültiger Urlaubstage-Offset' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run('UPDATE users SET vacation_offset_days = ? WHERE id = ?', [normalized, userId], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Fehler beim Speichern des Urlaubstage-Offsets:', err);
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Speichern des Urlaubstage-Offsets' });
|
||||||
|
}
|
||||||
|
res.json({ success: true, vacation_offset_days: normalized });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// API: Krankheitstage für einen User im aktuellen Jahr abrufen
|
// API: Krankheitstage für einen User im aktuellen Jahr abrufen
|
||||||
app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => {
|
app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
@@ -164,7 +217,7 @@ function registerVerwaltungRoutes(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// User-Daten abrufen
|
// User-Daten abrufen
|
||||||
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => {
|
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours, vacation_offset_days FROM users WHERE id = ?', [userId], (err, user) => {
|
||||||
if (err || !user) {
|
if (err || !user) {
|
||||||
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
||||||
}
|
}
|
||||||
@@ -172,6 +225,7 @@ function registerVerwaltungRoutes(app) {
|
|||||||
const wochenstunden = user.wochenstunden || 0;
|
const wochenstunden = user.wochenstunden || 0;
|
||||||
const urlaubstage = user.urlaubstage || 0;
|
const urlaubstage = user.urlaubstage || 0;
|
||||||
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
||||||
|
const vacationOffsetDays = user.vacation_offset_days ? parseFloat(user.vacation_offset_days) : 0;
|
||||||
|
|
||||||
// Einträge für die Woche abrufen
|
// Einträge für die Woche abrufen
|
||||||
db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type, sick_status
|
db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type, sick_status
|
||||||
@@ -251,7 +305,7 @@ function registerVerwaltungRoutes(app) {
|
|||||||
const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours;
|
const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours;
|
||||||
|
|
||||||
// Verbleibende Urlaubstage
|
// Verbleibende Urlaubstage
|
||||||
const remainingVacation = urlaubstage - vacationDays;
|
const remainingVacation = urlaubstage - vacationDays + vacationOffsetDays;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
wochenstunden,
|
wochenstunden,
|
||||||
@@ -264,6 +318,7 @@ function registerVerwaltungRoutes(app) {
|
|||||||
overtimeOffsetHours,
|
overtimeOffsetHours,
|
||||||
remainingOvertimeWithOffset,
|
remainingOvertimeWithOffset,
|
||||||
vacationDays,
|
vacationDays,
|
||||||
|
vacationOffsetDays,
|
||||||
remainingVacation,
|
remainingVacation,
|
||||||
sickDays,
|
sickDays,
|
||||||
workdays
|
workdays
|
||||||
14
server.js
14
server.js
@@ -30,13 +30,13 @@ app.use(session({
|
|||||||
initDatabase();
|
initDatabase();
|
||||||
|
|
||||||
// Routes importieren und registrieren
|
// Routes importieren und registrieren
|
||||||
const registerAuthRoutes = require('./routes/auth');
|
const registerAuthRoutes = require('./routes/auth-routes');
|
||||||
const registerDashboardRoutes = require('./routes/dashboard');
|
const registerDashboardRoutes = require('./routes/dashboard-routes');
|
||||||
const registerUserRoutes = require('./routes/user');
|
const registerUserRoutes = require('./routes/user-routes');
|
||||||
const registerAdminRoutes = require('./routes/admin');
|
const registerAdminRoutes = require('./routes/admin-routes');
|
||||||
const registerAdminLDAPRoutes = require('./routes/admin-ldap');
|
const registerAdminLDAPRoutes = require('./routes/admin-ldap-routes');
|
||||||
const registerVerwaltungRoutes = require('./routes/verwaltung');
|
const registerVerwaltungRoutes = require('./routes/verwaltung-routes');
|
||||||
const registerTimesheetRoutes = require('./routes/timesheet');
|
const registerTimesheetRoutes = require('./routes/timesheet-routes');
|
||||||
|
|
||||||
// Services importieren
|
// Services importieren
|
||||||
const { setupPingService } = require('./services/ping-service');
|
const { setupPingService } = require('./services/ping-service');
|
||||||
|
|||||||
@@ -618,6 +618,12 @@ async function generateCheckinCheckoutQRPDF(req, res, urlType = 'internal') {
|
|||||||
doc.fontSize(18).text(title, { align: 'center' });
|
doc.fontSize(18).text(title, { align: 'center' });
|
||||||
doc.moveDown(1.5);
|
doc.moveDown(1.5);
|
||||||
|
|
||||||
|
const displayFirst = (req.session.firstname || '').trim();
|
||||||
|
const displayLast = (req.session.lastname || '').trim();
|
||||||
|
const displayName = [displayFirst, displayLast].filter(Boolean).join(' ') || 'Mitarbeiter';
|
||||||
|
doc.fontSize(12).font('Helvetica').text('Mitarbeiter: ' + displayName, { align: 'center' });
|
||||||
|
doc.moveDown(0.5);
|
||||||
|
|
||||||
const topY = doc.y;
|
const topY = doc.y;
|
||||||
doc.image(checkinQRBuffer, left1, topY, { width: qrSize, height: qrSize });
|
doc.image(checkinQRBuffer, left1, topY, { width: qrSize, height: qrSize });
|
||||||
doc.image(checkoutQRBuffer, left2, topY, { width: qrSize, height: qrSize });
|
doc.image(checkoutQRBuffer, left2, topY, { width: qrSize, height: qrSize });
|
||||||
|
|||||||
@@ -55,7 +55,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="submitWeek" class="btn btn-success" onclick="window.submitWeekHandler(event)" disabled>Woche abschicken</button>
|
<div style="display: flex; gap: 10px; align-items: center;">
|
||||||
|
<button id="submitWeek" class="btn btn-success" onclick="window.submitWeekHandler(event)" disabled>Woche abschicken</button>
|
||||||
|
<button id="viewPdfBtn" class="btn btn-info" disabled>PDF anzeigen</button>
|
||||||
|
</div>
|
||||||
<p class="help-text">Stunden werden automatisch gespeichert. Am Ende der Woche können Sie die Stunden abschicken.</p>
|
<p class="help-text">Stunden werden automatisch gespeichert. Am Ende der Woche können Sie die Stunden abschicken.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -111,6 +111,25 @@
|
|||||||
Speichern
|
Speichern
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
||||||
|
<strong>Urlaubstage-Offset:</strong>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.5"
|
||||||
|
class="vacation-offset-input"
|
||||||
|
data-user-id="<%= employee.user.id %>"
|
||||||
|
value="<%= (employee.user.vacation_offset_days !== undefined && employee.user.vacation_offset_days !== null) ? employee.user.vacation_offset_days : 0 %>"
|
||||||
|
style="width: 90px; padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px;"
|
||||||
|
title="Manuelle Korrektur (positiv oder negativ) in Tagen" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-success btn-sm save-vacation-offset-btn"
|
||||||
|
data-user-id="<%= employee.user.id %>"
|
||||||
|
style="padding: 6px 10px; white-space: nowrap;"
|
||||||
|
title="Urlaubstage-Offset speichern">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div style="display: inline-block; margin-right: 20px;">
|
<div style="display: inline-block; margin-right: 20px;">
|
||||||
<strong>Kalenderwochen:</strong> <span><%= employee.weeks.length %></span>
|
<strong>Kalenderwochen:</strong> <span><%= employee.weeks.length %></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,6 +173,11 @@
|
|||||||
<div class="week-versions-info" style="margin-top: 5px;">
|
<div class="week-versions-info" style="margin-top: 5px;">
|
||||||
<span class="version-count"><%= week.total_versions %> Version<%= week.total_versions !== 1 ? 'en' : '' %></span>
|
<span class="version-count"><%= week.total_versions %> Version<%= week.total_versions !== 1 ? 'en' : '' %></span>
|
||||||
</div>
|
</div>
|
||||||
|
<% if (week.has_new_version_after_download) { %>
|
||||||
|
<div class="new-version-warning" style="margin-top: 10px;">
|
||||||
|
<strong>ACHTUNG: Neue version eingereicht</strong>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-sm toggle-versions-btn" data-employee-index="<%= employeeIndex %>" data-week-index="<%= weekIndex %>">
|
<button class="btn btn-secondary btn-sm toggle-versions-btn" data-employee-index="<%= employeeIndex %>" data-week-index="<%= weekIndex %>">
|
||||||
<span class="toggle-icon">▼</span> Versionen
|
<span class="toggle-icon">▼</span> Versionen
|
||||||
@@ -169,7 +193,6 @@
|
|||||||
<th>Eingereicht am</th>
|
<th>Eingereicht am</th>
|
||||||
<th>Grund</th>
|
<th>Grund</th>
|
||||||
<th>Kommentar</th>
|
<th>Kommentar</th>
|
||||||
<th>Status</th>
|
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -225,7 +248,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><span class="status-badge status-<%= ts.status %>"><%= ts.status %></span></td>
|
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-info btn-sm toggle-pdf-btn" data-timesheet-id="<%= ts.id %>">
|
<button class="btn btn-info btn-sm toggle-pdf-btn" data-timesheet-id="<%= ts.id %>">
|
||||||
<span class="arrow-icon">▶</span> PDF anzeigen
|
<span class="arrow-icon">▶</span> PDF anzeigen
|
||||||
@@ -236,7 +258,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="pdf-preview-row" data-timesheet-id="<%= ts.id %>" style="display: none;">
|
<tr class="pdf-preview-row" data-timesheet-id="<%= ts.id %>" style="display: none;">
|
||||||
<td colspan="6">
|
<td colspan="5">
|
||||||
<div class="pdf-preview-container">
|
<div class="pdf-preview-container">
|
||||||
<div class="pdf-preview-header">
|
<div class="pdf-preview-header">
|
||||||
<h3>PDF-Vorschau: <%= employee.user.firstname %> <%= employee.user.lastname %> - Version <%= ts.version || 1 %> - <%= new Date(ts.week_start).toLocaleDateString('de-DE') %> bis <%= new Date(ts.week_end).toLocaleDateString('de-DE') %></h3>
|
<h3>PDF-Vorschau: <%= employee.user.firstname %> <%= employee.user.lastname %> - Version <%= ts.version || 1 %> - <%= new Date(ts.week_start).toLocaleDateString('de-DE') %> bis <%= new Date(ts.week_end).toLocaleDateString('de-DE') %></h3>
|
||||||
@@ -317,6 +339,12 @@
|
|||||||
${data.remainingVacation !== undefined ? `<span style="color: #28a745;">(verbleibend: ${data.remainingVacation.toFixed(1)} Tage)</span>` : ''}
|
${data.remainingVacation !== undefined ? `<span style="color: #28a745;">(verbleibend: ${data.remainingVacation.toFixed(1)} Tage)</span>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
if (data.vacationOffsetDays !== undefined && data.vacationOffsetDays !== 0) {
|
||||||
|
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
|
||||||
|
<strong>Urlaubstage-Offset:</strong> <span>${Number(data.vacationOffsetDays).toFixed(1)} Tag${Math.abs(data.vacationOffsetDays) !== 1 ? 'e' : ''}</span>
|
||||||
|
${data.remainingVacation !== undefined ? `<span style="color: #28a745;">(verbleibend inkl. Offset: ${Number(data.remainingVacation).toFixed(1)} Tage)</span>` : ''}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
if (data.sickDays !== undefined && data.sickDays > 0) {
|
if (data.sickDays !== undefined && data.sickDays > 0) {
|
||||||
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
|
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
|
||||||
<strong>Krankheitstage:</strong> <span style="color: #e74c3c;">${data.sickDays} Tag${data.sickDays !== 1 ? 'e' : ''}</span>
|
<strong>Krankheitstage:</strong> <span style="color: #e74c3c;">${data.sickDays} Tag${data.sickDays !== 1 ? 'e' : ''}</span>
|
||||||
@@ -432,6 +460,68 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Urlaubstage-Offset speichern
|
||||||
|
document.querySelectorAll('.save-vacation-offset-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async function() {
|
||||||
|
const userId = this.dataset.userId;
|
||||||
|
const input = document.querySelector(`.vacation-offset-input[data-user-id="${userId}"]`);
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
const originalText = this.textContent;
|
||||||
|
this.disabled = true;
|
||||||
|
this.textContent = '...';
|
||||||
|
|
||||||
|
// leere Eingabe => 0 (Backend macht das auch, aber UI soll sauber sein)
|
||||||
|
const raw = (input.value || '').trim();
|
||||||
|
const value = raw === '' ? '' : Number(raw);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/verwaltung/user/${userId}/vacation-offset`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ vacation_offset_days: value })
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (!resp.ok) {
|
||||||
|
alert(data.error || 'Fehler beim Speichern des Offsets');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalisiere Input auf Zahl (Backend gibt number zurück)
|
||||||
|
input.value = (data.vacation_offset_days !== undefined && data.vacation_offset_days !== null)
|
||||||
|
? Number(data.vacation_offset_days)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Stats für diesen User neu laden
|
||||||
|
const statDivs = document.querySelectorAll(`.group-stats[data-user-id="${userId}"]`);
|
||||||
|
statDivs.forEach(div => {
|
||||||
|
// loading indicator optional wieder anzeigen
|
||||||
|
const loading = div.querySelector('.stats-loading');
|
||||||
|
if (loading) {
|
||||||
|
loading.style.display = 'inline-block';
|
||||||
|
loading.style.color = '#666';
|
||||||
|
loading.textContent = 'Lade Statistiken...';
|
||||||
|
}
|
||||||
|
loadStatsForDiv(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.textContent = '✓';
|
||||||
|
setTimeout(() => {
|
||||||
|
this.textContent = originalText;
|
||||||
|
this.disabled = false;
|
||||||
|
}, 900);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Speichern des Offsets:', e);
|
||||||
|
alert('Fehler beim Speichern des Offsets');
|
||||||
|
} finally {
|
||||||
|
if (this.textContent === '...') {
|
||||||
|
this.textContent = originalText;
|
||||||
|
this.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Mitarbeiter-Gruppen auf-/zuklappen (zeigt/versteckt Wochen)
|
// Mitarbeiter-Gruppen auf-/zuklappen (zeigt/versteckt Wochen)
|
||||||
document.querySelectorAll('.toggle-employee-btn').forEach(btn => {
|
document.querySelectorAll('.toggle-employee-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', function() {
|
btn.addEventListener('click', function() {
|
||||||
|
|||||||
Reference in New Issue
Block a user