diff --git a/DSGVO-Dokumentation.md b/DSGVO-Dokumentation.md new file mode 100644 index 0000000..7161053 --- /dev/null +++ b/DSGVO-Dokumentation.md @@ -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** diff --git a/README.md b/README.md index eef8a9e..585d077 100644 --- a/README.md +++ b/README.md @@ -163,13 +163,13 @@ Nach der Installation sind folgende Benutzer verfügbar: ``` stundenerfassung/ ├── routes/ # API-Routen -│ ├── admin.js # Admin-Funktionen -│ ├── admin-ldap.js # LDAP-Verwaltung -│ ├── auth.js # Authentifizierung -│ ├── dashboard.js # Dashboard-Routen -│ ├── timesheet.js # Stundenerfassung -│ ├── user.js # Benutzer-APIs -│ └── verwaltung.js # Verwaltungs-Funktionen +│ ├── admin-routes.js # Admin-Funktionen +│ ├── admin-ldap-routes.js # LDAP-Verwaltung +│ ├── auth-routes.js # Authentifizierung +│ ├── dashboard-routes.js # Dashboard-Routen +│ ├── timesheet-routes.js # Stundenerfassung +│ ├── user-routes.js # Benutzer-APIs +│ └── verwaltung-routes.js # Verwaltungs-Funktionen ├── services/ # Services │ ├── feiertage-service.js # Feiertags-API │ ├── ldap-service.js # LDAP-Service diff --git a/Stundenregeln.txt b/Stundenregeln.txt new file mode 100644 index 0000000..9b5709c --- /dev/null +++ b/Stundenregeln.txt @@ -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 diff --git a/Stunderfassung todo.txt b/Stunderfassung todo.txt new file mode 100644 index 0000000..a84957d --- /dev/null +++ b/Stunderfassung todo.txt @@ -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 +- \ No newline at end of file diff --git a/database.js b/database.js index 3b6524a..117ca7a 100644 --- a/database.js +++ b/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) db.run(`ALTER TABLE users ADD COLUMN personalnummer TEXT`, (err) => { // 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 db.run(`ALTER TABLE users ADD COLUMN ping_ip TEXT`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert diff --git a/public/css/style.css b/public/css/style.css index 21244f2..bb4abe3 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1002,6 +1002,17 @@ table input[type="text"] { 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 */ @media (max-width: 1024px) { .dashboard-layout { diff --git a/public/js/dashboard.js b/public/js/dashboard.js index bb4b842..7c6710c 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -5,6 +5,7 @@ let currentEntries = {}; let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-DD) let userWochenstunden = 0; // Wochenstunden des Users let weekendPercentages = { saturday: 100, sunday: 100 }; // Wochenend-Prozentsätze (100% = normal) +let latestSubmittedTimesheetId = null; // ID der neuesten eingereichten Version // Wochenend-Prozentsätze laden async function loadWeekendPercentages() { @@ -206,6 +207,21 @@ document.addEventListener('DOMContentLoaded', async function() { } else { 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 @@ -303,13 +319,18 @@ async function loadWeek() { const parts = currentWeekStart.split('-'); 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 [entriesResponse, holidaysResponse] = await Promise.all([ + const [entriesResponse, holidaysResponse, latestSubmittedResponse] = await Promise.all([ 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 holidaysData = await holidaysResponse.json(); + const latestSubmittedData = await latestSubmittedResponse.json(); currentHolidayDates = new Set(holidaysData.dates || []); + + // Speichere die neueste eingereichte Timesheet-ID + latestSubmittedTimesheetId = latestSubmittedData.timesheetId || null; // Entries in Object umwandeln für schnellen Zugriff currentEntries = {}; @@ -382,6 +403,9 @@ function renderWeek() { const vacationType = entry.vacation_type || ''; const sickStatus = entry.sick_status || false; 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 const activities = [ @@ -413,10 +437,18 @@ function renderWeek() { hoursToAdd = 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden) } else { hoursToAdd = hours || 0; - // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Urlaub/Krankheit) - const weekendPercentage = getWeekendPercentage(date); - if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) { - hoursToAdd = hours * (weekendPercentage / 100); + // Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag) + if (isWeekend && weekendTravel && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) { + // Verwende gespeicherten Prozentsatz falls vorhanden, sonst aktuellen + let weekendPercentage = 100; + if (appliedWeekendPercentage !== null && appliedWeekendPercentage !== undefined) { + weekendPercentage = appliedWeekendPercentage; + } else { + weekendPercentage = getWeekendPercentage(date); + } + if (weekendPercentage >= 100) { + hoursToAdd = hours * (weekendPercentage / 100); + } } } totalHours += hoursToAdd; @@ -537,20 +569,17 @@ function renderWeek() {
- -
- -
+ ${isWeekend ? ` +
+ +
+ ` : ''} @@ -671,8 +700,8 @@ function updateOvertimeDisplay() { // Prüfe Urlaub-Status und Krank-Status const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`); const vacationType = vacationSelect ? vacationSelect.value : (currentEntries[dateStr]?.vacation_type || ''); - const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`); - const sickStatus = sickCheckbox ? sickCheckbox.checked : (currentEntries[dateStr]?.sick_status || false); + // Für sick_status: Wert aus currentEntries lesen (da keine Checkbox mehr vorhanden) + const sickStatus = currentEntries[dateStr]?.sick_status || false; // Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind 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 diffMs = end - start; const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60); - // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden) - const weekendPercentage = getWeekendPercentage(date); + // Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag) + 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; - if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) { - adjustedHours = hours * (weekendPercentage / 100); + if (isWeekend && weekendTravel && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) { + // 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; } else if (currentEntries[dateStr]?.total_hours && !isFullDayOvertime) { // Fallback auf gespeicherte Werte let hours = parseFloat(currentEntries[dateStr].total_hours) || 0; - // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden) - const weekendPercentage = getWeekendPercentage(date); - if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus) { - hours = hours * (weekendPercentage / 100); + // Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag) + 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; + 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; } @@ -759,20 +814,46 @@ function updateOvertimeDisplay() { const end = new Date(`2000-01-01T${endTime}`); const diffMs = end - start; const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60); - // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden) - const weekendPercentage = getWeekendPercentage(date); + // Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag) + 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; - if (weekendPercentage >= 100 && hours > 0 && !isFullDayOvertime) { - adjustedHours = hours * (weekendPercentage / 100); + 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) { + adjustedHours = hours * (weekendPercentage / 100); + } } totalHours += adjustedHours; } else if (currentEntries[dateStr]?.total_hours) { // Fallback auf gespeicherte Werte let hours = parseFloat(currentEntries[dateStr].total_hours) || 0; - // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden) - const weekendPercentage = getWeekendPercentage(date); - if (weekendPercentage >= 100 && hours > 0 && !isFullDayOvertime) { - hours = hours * (weekendPercentage / 100); + // Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag) + 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); + 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; } @@ -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 async function saveEntry(input) { 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 const start_time = actualStartTime; 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 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 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 const activities = []; @@ -964,6 +1082,7 @@ async function saveEntry(input) { currentEntries[date].vacation_type = vacation_type; currentEntries[date].overtime_taken_hours = overtime_taken_hours; currentEntries[date].sick_status = sick_status; + currentEntries[date].weekend_travel = weekend_travel; for (let i = 1; i <= 5; i++) { currentEntries[date][`activity${i}_desc`] = activities[i-1].desc; currentEntries[date][`activity${i}_hours`] = activities[i-1].hours; @@ -1002,7 +1121,8 @@ async function saveEntry(input) { activity5_project_number: activities[4].projectNumber, overtime_taken_hours: overtime_taken_hours, 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); const dateStr = formatDate(date); + // Prüfe ob Feiertag + const isHoliday = currentHolidayDates.has(dateStr); + // Prüfe Urlaub-Status und Krank-Status const entry = currentEntries[dateStr] || {}; const vacationType = entry.vacation_type; @@ -1071,9 +1194,9 @@ function checkWeekComplete() { const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`); const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false); - // Wenn ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten - if (vacationValue === 'full' || sickStatus) { - continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank) + // Wenn Feiertag, ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten + if (isHoliday || vacationValue === 'full' || sickStatus) { + 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) @@ -1132,6 +1255,18 @@ function checkWeekComplete() { 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) @@ -1185,6 +1320,9 @@ async function submitWeek() { const weekday = getWeekday(dateStr); const dateDisplay = formatDateDE(dateStr); + // Prüfe ob Feiertag + const isHoliday = currentHolidayDates.has(dateStr); + // Prüfe Urlaub-Status und Krank-Status const entry = currentEntries[dateStr] || {}; 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 sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false); - // Wenn ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten - if (vacationValue === 'full' || sickStatus) { - continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank) + // Wenn Feiertag, ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten + if (isHoliday || vacationValue === 'full' || sickStatus) { + 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) @@ -1474,37 +1612,69 @@ function toggleVacationSelect(dateStr) { } } -// Krank-Status ein-/ausblenden -function toggleSickStatus(dateStr) { - const checkboxDiv = document.getElementById(`sick-checkbox-${dateStr}`); - if (checkboxDiv) { - if (checkboxDiv.style.display === 'none' || !checkboxDiv.style.display) { - checkboxDiv.style.display = 'inline-block'; - const checkbox = checkboxDiv.querySelector('input[type="checkbox"]'); - if (checkbox) { - // Prüfe aktuellen Status aus currentEntries - const currentSickStatus = currentEntries[dateStr]?.sick_status || false; - checkbox.checked = currentSickStatus || true; // Wenn nicht gesetzt, auf true setzen - checkbox.focus(); - // 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'; - } +// Wochenend-Reise-Status umschalten +function toggleWeekendTravel(dateStr) { + const button = document.querySelector(`button[onclick="toggleWeekendTravel('${dateStr}')"]`); + if (!button) return; + + // Aktuellen Status aus currentEntries lesen + const currentTravelStatus = currentEntries[dateStr]?.weekend_travel || false; + const newStatus = !currentTravelStatus; + + // Status in currentEntries aktualisieren + if (!currentEntries[dateStr]) { + currentEntries[dateStr] = { date: dateStr }; } + 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 diff --git a/routes/admin-ldap.js b/routes/admin-ldap-routes.js similarity index 100% rename from routes/admin-ldap.js rename to routes/admin-ldap-routes.js diff --git a/routes/admin.js b/routes/admin-routes.js similarity index 100% rename from routes/admin.js rename to routes/admin-routes.js diff --git a/routes/auth.js b/routes/auth-routes.js similarity index 100% rename from routes/auth.js rename to routes/auth-routes.js diff --git a/routes/dashboard.js b/routes/dashboard-routes.js similarity index 100% rename from routes/dashboard.js rename to routes/dashboard-routes.js diff --git a/routes/timesheet.js b/routes/timesheet-routes.js similarity index 78% rename from routes/timesheet.js rename to routes/timesheet-routes.js index d4f819a..ad89899 100644 --- a/routes/timesheet.js +++ b/routes/timesheet-routes.js @@ -4,6 +4,7 @@ const { db } = require('../database'); const { requireAuth, requireVerwaltung } = require('../middleware/auth'); const { generatePDF } = require('../services/pdf-service'); const { getHolidaysForDateRange } = require('../services/feiertage-service'); +const { hasRole } = require('../helpers/utils'); // Routes registrieren function registerTimesheetRoutes(app) { @@ -16,7 +17,7 @@ function registerTimesheetRoutes(app) { activity3_desc, activity3_hours, activity3_project_number, activity4_desc, activity4_hours, activity4_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; const userId = req.session.userId; @@ -27,6 +28,10 @@ function registerTimesheetRoutes(app) { // Normalisiere sick_status: Boolean oder 1/0 zu Boolean 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 db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => { if (err) { @@ -66,6 +71,11 @@ function registerTimesheetRoutes(app) { 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) // Wenn ganzer Tag Urlaub oder Krank, dann zählt dieser als 8 Stunden normale Arbeitszeit let total_hours = 0; @@ -77,6 +87,7 @@ function registerTimesheetRoutes(app) { let finalActivity5Desc = activity5_desc; let finalStartTime = normalizedStartTime; let finalEndTime = normalizedEndTime; + let appliedWeekendPercentage = null; // Wird gesetzt wenn Wochenend-Prozentsatz angewendet wird // Überstunden-Logik: Bei vollem Tag Überstunden if (isFullDayOvertime) { @@ -95,10 +106,14 @@ function registerTimesheetRoutes(app) { const end = new Date(`2000-01-01T${normalizedEndTime}`); const diffMs = end - start; 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); - if (weekendPercentage >= 100 && total_hours > 0 && !isSick && vacation_type !== 'full') { - total_hours = total_hours * (weekendPercentage / 100); + + // Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag) + if (isWeekend && isWeekendTravel && total_hours > 0 && !isSick && vacation_type !== 'full') { + 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 // 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 // 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) => { 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 db.run(`UPDATE timesheet_entries SET start_time = ?, end_time = ?, break_minutes = ?, total_hours = ?, notes = ?, @@ -120,6 +160,7 @@ function registerTimesheetRoutes(app) { activity4_desc = ?, activity4_hours = ?, activity4_project_number = ?, activity5_desc = ?, activity5_hours = ?, activity5_project_number = ?, overtime_taken_hours = ?, vacation_type = ?, sick_status = ?, + weekend_travel = ?, applied_weekend_percentage = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [ @@ -132,6 +173,8 @@ function registerTimesheetRoutes(app) { overtime_taken_hours ? parseFloat(overtime_taken_hours) : null, vacation_type || null, isSick ? 1 : 0, + weekendTravelValue, + finalAppliedPercentage, row.id ], (err) => { @@ -150,8 +193,8 @@ function registerTimesheetRoutes(app) { activity3_desc, activity3_hours, activity3_project_number, activity4_desc, activity4_hours, activity4_project_number, activity5_desc, activity5_hours, activity5_project_number, - overtime_taken_hours, vacation_type, sick_status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + overtime_taken_hours, vacation_type, sick_status, weekend_travel, applied_weekend_percentage) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ userId, date, finalStartTime, finalEndTime, break_minutes, total_hours, notes, finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null, @@ -161,7 +204,9 @@ function registerTimesheetRoutes(app) { finalActivity5Desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null, overtime_taken_hours ? parseFloat(overtime_taken_hours) : null, vacation_type || null, - isSick ? 1 : 0 + isSick ? 1 : 0, + weekendTravelValue, + appliedWeekendPercentage ], (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 app.get('/api/timesheet/download-info/:id', requireVerwaltung, (req, res) => { const timesheetId = req.params.id; @@ -398,9 +477,24 @@ function registerTimesheetRoutes(app) { }); // 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; - 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'); + } + }); }); } diff --git a/routes/user.js b/routes/user-routes.js similarity index 98% rename from routes/user.js rename to routes/user-routes.js index 9d015da..e9b428f 100644 --- a/routes/user.js +++ b/routes/user-routes.js @@ -223,7 +223,7 @@ function registerUserRoutes(app) { } // 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) { 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 urlaubstage = user.urlaubstage || 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) const { getCalendarWeek } = require('../helpers/utils'); @@ -282,14 +283,15 @@ function registerUserRoutes(app) { if (!weeks || weeks.length === 0) { return res.json({ currentOvertime: overtimeOffsetHours, - remainingVacation: urlaubstage, + remainingVacation: urlaubstage + vacationOffsetDays, totalOvertimeHours: 0, totalOvertimeTaken: 0, totalVacationDays: 0, plannedVacationDays: plannedVacationDays, plannedWeeks: plannedWeeks, urlaubstage: urlaubstage, - overtimeOffsetHours: overtimeOffsetHours + overtimeOffsetHours: overtimeOffsetHours, + vacationOffsetDays: vacationOffsetDays }); } @@ -381,7 +383,7 @@ function registerUserRoutes(app) { processedWeeks++; if (processedWeeks === weeks.length && !hasError) { const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours; - const remainingVacation = urlaubstage - totalVacationDays; + const remainingVacation = urlaubstage - totalVacationDays + vacationOffsetDays; res.json({ currentOvertime: currentOvertime, @@ -392,7 +394,8 @@ function registerUserRoutes(app) { plannedVacationDays: plannedVacationDays, plannedWeeks: plannedWeeks, urlaubstage: urlaubstage, - overtimeOffsetHours: overtimeOffsetHours + overtimeOffsetHours: overtimeOffsetHours, + vacationOffsetDays: vacationOffsetDays }); } return; // Überspringe diese Woche @@ -479,7 +482,7 @@ function registerUserRoutes(app) { // 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) const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours; - const remainingVacation = urlaubstage - totalVacationDays; + const remainingVacation = urlaubstage - totalVacationDays + vacationOffsetDays; res.json({ currentOvertime: currentOvertime, @@ -490,7 +493,8 @@ function registerUserRoutes(app) { plannedVacationDays: plannedVacationDays, plannedWeeks: plannedWeeks, urlaubstage: urlaubstage, - overtimeOffsetHours: overtimeOffsetHours + overtimeOffsetHours: overtimeOffsetHours, + vacationOffsetDays: vacationOffsetDays }); } }); // getHolidaysForDateRange.then diff --git a/routes/verwaltung.js b/routes/verwaltung-routes.js similarity index 85% rename from routes/verwaltung.js rename to routes/verwaltung-routes.js index 1d7b8ad..0cfdf3a 100644 --- a/routes/verwaltung.js +++ b/routes/verwaltung-routes.js @@ -12,7 +12,7 @@ function registerVerwaltungRoutes(app) { // Verwaltungs-Bereich app.get('/verwaltung', requireVerwaltung, (req, res) => { 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.lastname as downloaded_by_lastname, (SELECT COUNT(*) FROM weekly_timesheets wt2 @@ -44,7 +44,8 @@ function registerVerwaltungRoutes(app) { personalnummer: ts.personalnummer, wochenstunden: ts.wochenstunden, 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: {} }; @@ -64,6 +65,38 @@ function registerVerwaltungRoutes(app) { 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) const sortedEmployees = Object.values(groupedByEmployee).map(employee => { // 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 app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => { const userId = req.params.id; @@ -164,7 +217,7 @@ function registerVerwaltungRoutes(app) { } // 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) { 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 urlaubstage = user.urlaubstage || 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 db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type, sick_status @@ -251,7 +305,7 @@ function registerVerwaltungRoutes(app) { const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours; // Verbleibende Urlaubstage - const remainingVacation = urlaubstage - vacationDays; + const remainingVacation = urlaubstage - vacationDays + vacationOffsetDays; res.json({ wochenstunden, @@ -264,6 +318,7 @@ function registerVerwaltungRoutes(app) { overtimeOffsetHours, remainingOvertimeWithOffset, vacationDays, + vacationOffsetDays, remainingVacation, sickDays, workdays diff --git a/server.js b/server.js index 661f7c7..9f69dc0 100644 --- a/server.js +++ b/server.js @@ -30,13 +30,13 @@ app.use(session({ initDatabase(); // Routes importieren und registrieren -const registerAuthRoutes = require('./routes/auth'); -const registerDashboardRoutes = require('./routes/dashboard'); -const registerUserRoutes = require('./routes/user'); -const registerAdminRoutes = require('./routes/admin'); -const registerAdminLDAPRoutes = require('./routes/admin-ldap'); -const registerVerwaltungRoutes = require('./routes/verwaltung'); -const registerTimesheetRoutes = require('./routes/timesheet'); +const registerAuthRoutes = require('./routes/auth-routes'); +const registerDashboardRoutes = require('./routes/dashboard-routes'); +const registerUserRoutes = require('./routes/user-routes'); +const registerAdminRoutes = require('./routes/admin-routes'); +const registerAdminLDAPRoutes = require('./routes/admin-ldap-routes'); +const registerVerwaltungRoutes = require('./routes/verwaltung-routes'); +const registerTimesheetRoutes = require('./routes/timesheet-routes'); // Services importieren const { setupPingService } = require('./services/ping-service'); diff --git a/services/pdf-service.js b/services/pdf-service.js index dcf592d..452dd13 100644 --- a/services/pdf-service.js +++ b/services/pdf-service.js @@ -618,6 +618,12 @@ async function generateCheckinCheckoutQRPDF(req, res, urlType = 'internal') { doc.fontSize(18).text(title, { align: 'center' }); 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; doc.image(checkinQRBuffer, left1, topY, { width: qrSize, height: qrSize }); doc.image(checkoutQRBuffer, left2, topY, { width: qrSize, height: qrSize }); diff --git a/views/dashboard.ejs b/views/dashboard.ejs index 869c77f..7deec38 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -55,7 +55,10 @@
- +
+ + +

Stunden werden automatisch gespeichert. Am Ende der Woche können Sie die Stunden abschicken.

diff --git a/views/verwaltung.ejs b/views/verwaltung.ejs index 7e182e5..bb21d0f 100644 --- a/views/verwaltung.ejs +++ b/views/verwaltung.ejs @@ -111,6 +111,25 @@ Speichern +
+ Urlaubstage-Offset: + + +
Kalenderwochen: <%= employee.weeks.length %>
@@ -154,6 +173,11 @@
<%= week.total_versions %> Version<%= week.total_versions !== 1 ? 'en' : '' %>
+ <% if (week.has_new_version_after_download) { %> +
+ ACHTUNG: Neue version eingereicht +
+ <% } %> - <%= ts.status %>