V1.0
This commit is contained in:
@@ -0,0 +1,177 @@
|
|||||||
|
---
|
||||||
|
name: MySQL-Integration mit Admin-Konfiguration
|
||||||
|
overview: Erweitere das System um MySQL-Unterstützung mit einer Admin-Oberfläche zur Konfiguration. Die Datenbank wird in einer Konfigurationsdatei gespeichert und kann im Admin-Bereich geändert werden (erfordert Server-Neustart).
|
||||||
|
todos: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# MySQL/MariaDB-Integration mit Admin-Konfiguration
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Erweitere das System um MySQL/MariaDB-Unterstützung neben SQLite3. Die Datenbank-Konfiguration wird in einer Konfigurationsdatei gespeichert und kann im Admin-Bereich verwaltet werden. Ein Wechsel erfordert einen Server-Neustart.
|
||||||
|
|
||||||
|
**Hinweis:** MariaDB ist vollständig MySQL-kompatibel und funktioniert mit derselben Schnittstelle (`mysql2` Package).
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
### Datenbank-Abstraktionsschicht
|
||||||
|
|
||||||
|
Erstelle eine Abstraktionsschicht, die sowohl SQLite als auch MySQL unterstützt:
|
||||||
|
|
||||||
|
1. **Neue Datei:** `database/db-adapter.js`
|
||||||
|
|
||||||
|
- Abstrahiert `db.get()`, `db.run()`, `db.all()` für beide Datenbanken
|
||||||
|
- Promise-basierte API (für bessere Kompatibilität)
|
||||||
|
- Automatische Fehlerbehandlung
|
||||||
|
- SQL-Dialekt-Anpassung (z.B. `AUTOINCREMENT` vs `AUTO_INCREMENT`)
|
||||||
|
|
||||||
|
2. **Neue Datei:** `database/sqlite-adapter.js`
|
||||||
|
|
||||||
|
- SQLite-spezifische Implementierung
|
||||||
|
- Wrapper um sqlite3
|
||||||
|
|
||||||
|
3. **Neue Datei:** `database/mysql-adapter.js`
|
||||||
|
|
||||||
|
- MySQL/MariaDB-spezifische Implementierung
|
||||||
|
- Verwendet `mysql2` Package (funktioniert mit MySQL und MariaDB)
|
||||||
|
|
||||||
|
4. **Anpassung:** `database.js`
|
||||||
|
|
||||||
|
- Lädt Konfiguration aus `config/database.json`
|
||||||
|
- Initialisiert entsprechenden Adapter
|
||||||
|
- Behält `initDatabase()` Funktion bei
|
||||||
|
|
||||||
|
### Konfigurationsdatei
|
||||||
|
|
||||||
|
**Neue Datei:** `config/database.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "sqlite",
|
||||||
|
"sqlite": {
|
||||||
|
"path": "./stundenerfassung.db"
|
||||||
|
},
|
||||||
|
"mysql": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 3306,
|
||||||
|
"user": "root",
|
||||||
|
"password": "",
|
||||||
|
"database": "stundenerfassung",
|
||||||
|
"charset": "utf8mb4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin-Interface
|
||||||
|
|
||||||
|
**Anpassung:** `routes/admin.js`
|
||||||
|
|
||||||
|
- Neue Route `/admin/database/config` (GET/PUT)
|
||||||
|
- Zeigt aktuelle Datenbank-Konfiguration
|
||||||
|
- Formular zum Ändern der Datenbank-Einstellungen
|
||||||
|
- Warnung bei Wechsel (Server-Neustart erforderlich)
|
||||||
|
|
||||||
|
**Anpassung:** `views/admin.ejs`
|
||||||
|
|
||||||
|
- Neuer Abschnitt "Datenbank-Konfiguration"
|
||||||
|
- Formular für SQLite/MySQL/MariaDB-Auswahl
|
||||||
|
- Eingabefelder für MySQL/MariaDB-Verbindungsdaten
|
||||||
|
- Test-Verbindung Button
|
||||||
|
- Warnung bei Änderungen
|
||||||
|
- Hinweis: MariaDB funktioniert mit MySQL-Konfiguration
|
||||||
|
|
||||||
|
### SQL-Anpassungen
|
||||||
|
|
||||||
|
**Unterschiede zwischen SQLite und MySQL:**
|
||||||
|
|
||||||
|
- `INTEGER PRIMARY KEY AUTOINCREMENT` → `INT AUTO_INCREMENT PRIMARY KEY`
|
||||||
|
- `TEXT` → `VARCHAR(255)` oder `TEXT`
|
||||||
|
- `REAL` → `DOUBLE` oder `DECIMAL`
|
||||||
|
- `TEXT` (unbegrenzt) → `TEXT` oder `LONGTEXT`
|
||||||
|
- `DATETIME DEFAULT CURRENT_TIMESTAMP` → `DATETIME DEFAULT CURRENT_TIMESTAMP` (gleich)
|
||||||
|
- `INSERT OR IGNORE` → `INSERT IGNORE`
|
||||||
|
- `COLLATE NOCASE` → `COLLATE utf8mb4_general_ci` (für case-insensitive)
|
||||||
|
- Foreign Keys müssen in MySQL explizit aktiviert werden
|
||||||
|
|
||||||
|
**Anpassung:** `database.js` - `initDatabase()`
|
||||||
|
|
||||||
|
- SQL-Generierung basierend auf Datenbanktyp
|
||||||
|
- Separate CREATE TABLE Statements für SQLite und MySQL
|
||||||
|
- Migrationen anpassen (ALTER TABLE Syntax)
|
||||||
|
|
||||||
|
### Package-Abhängigkeiten
|
||||||
|
|
||||||
|
**Anpassung:** `package.json`
|
||||||
|
|
||||||
|
- `mysql2` hinzufügen (bessere Promise-Unterstützung als `mysql`, funktioniert mit MySQL und MariaDB)
|
||||||
|
|
||||||
|
### Services und Routes
|
||||||
|
|
||||||
|
Alle Dateien, die `db` verwenden, müssen angepasst werden:
|
||||||
|
|
||||||
|
- `routes/*.js` (7 Dateien)
|
||||||
|
- `services/*.js` (5 Dateien)
|
||||||
|
- `checkin-server.js`
|
||||||
|
|
||||||
|
**Änderungen:**
|
||||||
|
|
||||||
|
- Promise-basierte Queries verwenden (statt Callbacks)
|
||||||
|
- Oder: Callback-Wrapper in Adapter
|
||||||
|
|
||||||
|
### Initialisierung
|
||||||
|
|
||||||
|
**Anpassung:** `server.js`
|
||||||
|
|
||||||
|
- Prüft ob `config/database.json` existiert
|
||||||
|
- Erstellt Standard-Konfiguration falls nicht vorhanden
|
||||||
|
- Initialisiert Datenbank entsprechend Konfiguration
|
||||||
|
|
||||||
|
## Implementierungsschritte
|
||||||
|
|
||||||
|
1. **Datenbank-Abstraktionsschicht erstellen**
|
||||||
|
|
||||||
|
- `database/db-adapter.js` - Interface
|
||||||
|
- `database/sqlite-adapter.js` - SQLite-Implementierung
|
||||||
|
- `database/mysql-adapter.js` - MySQL-Implementierung
|
||||||
|
|
||||||
|
2. **Konfigurationssystem**
|
||||||
|
|
||||||
|
- `config/database.json` Template
|
||||||
|
- Konfigurations-Loader in `database.js`
|
||||||
|
|
||||||
|
3. **SQL-Anpassungen**
|
||||||
|
|
||||||
|
- `initDatabase()` für beide Datenbanken anpassen
|
||||||
|
- SQL-Dialekt-Unterschiede behandeln
|
||||||
|
|
||||||
|
4. **Admin-Interface**
|
||||||
|
|
||||||
|
- Route für Datenbank-Konfiguration
|
||||||
|
- View mit Konfigurationsformular
|
||||||
|
- Test-Verbindung Funktion
|
||||||
|
|
||||||
|
5. **Alle Queries anpassen**
|
||||||
|
|
||||||
|
- Callbacks zu Promises oder Callback-Wrapper
|
||||||
|
- SQLite-spezifische Syntax entfernen (z.B. `COLLATE NOCASE`)
|
||||||
|
|
||||||
|
6. **Dokumentation**
|
||||||
|
|
||||||
|
- README.md aktualisieren
|
||||||
|
- Konfigurationsanleitung
|
||||||
|
|
||||||
|
## Technische Details
|
||||||
|
|
||||||
|
- **MySQL/MariaDB-Package:** `mysql2` (bessere Promise-Unterstützung, funktioniert mit beiden)
|
||||||
|
- **Kompatibilität:** MariaDB ist vollständig MySQL-kompatibel, verwendet dieselbe Konfiguration
|
||||||
|
- **Fallback:** Bei Fehler in MySQL/MariaDB-Verbindung auf SQLite zurückfallen
|
||||||
|
- **Validierung:** Test-Verbindung vor Speichern der Konfiguration
|
||||||
|
- **Sicherheit:** MySQL/MariaDB-Passwörter verschlüsselt in Config speichern (optional)
|
||||||
|
|
||||||
|
## Dateien die angepasst werden müssen
|
||||||
|
|
||||||
|
- `database.js` → `database/` Ordner mit mehreren Dateien
|
||||||
|
- `routes/admin.js` - Datenbank-Konfiguration
|
||||||
|
- `views/admin.ejs` - Konfigurations-UI
|
||||||
|
- `package.json` - mysql2 Dependency
|
||||||
|
- `server.js` - Konfigurationspr
|
||||||
@@ -18,3 +18,4 @@ Thumbs.db
|
|||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
dev
|
dev
|
||||||
|
backup/
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
#Backups
|
||||||
|
backup/
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|||||||
153
README.md
153
README.md
@@ -1,26 +1,47 @@
|
|||||||
# Stundenerfassungs-System
|
# Stundenerfassungs-System
|
||||||
|
|
||||||
Eine webbasierte Anwendung zur Erfassung von Arbeitszeiten mit Admin-Bereich und PDF-Export.
|
Eine webbasierte Anwendung zur Erfassung von Arbeitszeiten mit Admin-Bereich, PDF-Export, Überstunden-Verwaltung und LDAP-Integration.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Für Mitarbeiter
|
### Für Mitarbeiter
|
||||||
- ✅ Login mit Benutzername und Passwort
|
- ✅ Login mit Benutzername und Passwort oder LDAP
|
||||||
- ✅ Wöchentliche Stundenerfassung (Montag - Sonntag)
|
- ✅ Wöchentliche Stundenerfassung (Montag - Sonntag)
|
||||||
- ✅ Automatisches Speichern der Einträge
|
- ✅ Automatisches Speichern der Einträge
|
||||||
- ✅ Eingabe von Start-/Endzeit, Pausen und Notizen
|
- ✅ Eingabe von Start-/Endzeit, Pausen und Notizen
|
||||||
|
- ✅ Mehrere Tätigkeiten pro Tag mit Projektnummern (bis zu 5)
|
||||||
- ✅ Automatische Berechnung der Gesamtstunden
|
- ✅ Automatische Berechnung der Gesamtstunden
|
||||||
|
- ✅ Überstunden-Verwaltung (Anzeige und Verbrauch)
|
||||||
|
- ✅ Urlaubsverwaltung (ganzer/halber Tag)
|
||||||
|
- ✅ Krankheitstage erfassen
|
||||||
|
- ✅ Feiertage werden automatisch erkannt
|
||||||
|
- ✅ Wochenend-Prozentsätze (konfigurierbar)
|
||||||
- ✅ Wöchentliches Abschicken des Stundenzettels
|
- ✅ Wöchentliches Abschicken des Stundenzettels
|
||||||
|
- ✅ Überstunden-Auswertung pro Woche mit detaillierter Aufschlüsselung
|
||||||
|
- ✅ Anzeige aktueller Überstunden und verbleibender Urlaubstage
|
||||||
|
- ✅ Automatische Zeiterfassung via IP-Ping (optional)
|
||||||
|
- ✅ QR-Code für Check-in/Check-out
|
||||||
|
|
||||||
### Für Administratoren
|
### Für Administratoren
|
||||||
- ✅ Benutzerverwaltung (Anlegen, Löschen)
|
- ✅ Benutzerverwaltung (Anlegen, Löschen, Bearbeiten)
|
||||||
- ✅ Rollenvergabe (Mitarbeiter, Verwaltung, Admin)
|
- ✅ Rollenvergabe (Mitarbeiter, Verwaltung, Admin)
|
||||||
- ✅ Übersicht aller Benutzer
|
- ✅ Mehrere Rollen pro Benutzer möglich
|
||||||
|
- ✅ Übersicht aller Benutzer mit Statistiken
|
||||||
|
- ✅ Personalnummern verwalten
|
||||||
|
- ✅ Wochenstunden und Urlaubstage pro Mitarbeiter konfigurieren
|
||||||
|
- ✅ LDAP-Integration für Benutzer-Synchronisation
|
||||||
|
- ✅ LDAP-Konfiguration und manuelle/automatische Synchronisation
|
||||||
|
- ✅ System-Optionen (Wochenend-Prozentsätze)
|
||||||
|
|
||||||
### Für Verwaltung
|
### Für Verwaltung
|
||||||
- ✅ Postfach mit eingereichten Stundenzetteln
|
- ✅ Postfach mit eingereichten Stundenzetteln
|
||||||
- ✅ PDF-Generierung und Download
|
- ✅ PDF-Generierung und Download
|
||||||
- ✅ Übersichtliche Darstellung aller Einreichungen
|
- ✅ Übersichtliche Darstellung aller Einreichungen
|
||||||
|
- ✅ Wochenansicht mit Statistiken pro Mitarbeiter
|
||||||
|
- ✅ Überstunden-Offset (manuelle Korrekturen)
|
||||||
|
- ✅ Admin-Kommentare zu Stundenzetteln
|
||||||
|
- ✅ Versionsverwaltung bei Änderungen
|
||||||
|
- ✅ Übersicht über Überstunden und Urlaubstage aller Mitarbeiter
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -47,6 +68,14 @@ Für Entwicklung mit automatischem Neustart:
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Docker (optional)
|
||||||
|
|
||||||
|
Das System kann auch mit Docker betrieben werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
## Standard-Zugangsdaten
|
## Standard-Zugangsdaten
|
||||||
|
|
||||||
Nach der Installation sind folgende Benutzer verfügbar:
|
Nach der Installation sind folgende Benutzer verfügbar:
|
||||||
@@ -54,12 +83,12 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
|||||||
### Administrator
|
### Administrator
|
||||||
- **Benutzername:** admin
|
- **Benutzername:** admin
|
||||||
- **Passwort:** admin123
|
- **Passwort:** admin123
|
||||||
- **Funktion:** Kann Benutzer anlegen und verwalten
|
- **Funktion:** Kann Benutzer anlegen und verwalten, LDAP konfigurieren
|
||||||
|
|
||||||
### Verwaltung
|
### Verwaltung
|
||||||
- **Benutzername:** verwaltung
|
- **Benutzername:** verwaltung
|
||||||
- **Passwort:** verwaltung123
|
- **Passwort:** verwaltung123
|
||||||
- **Funktion:** Kann eingereichte Stundenzettel einsehen und PDFs erstellen
|
- **Funktion:** Kann eingereichte Stundenzettel einsehen, PDFs erstellen und Überstunden korrigieren
|
||||||
|
|
||||||
**WICHTIG:** Bitte ändern Sie diese Passwörter nach der ersten Anmeldung!
|
**WICHTIG:** Bitte ändern Sie diese Passwörter nach der ersten Anmeldung!
|
||||||
|
|
||||||
@@ -73,10 +102,15 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
|||||||
- **Start:** Arbeitsbeginn
|
- **Start:** Arbeitsbeginn
|
||||||
- **Ende:** Arbeitsende
|
- **Ende:** Arbeitsende
|
||||||
- **Pause:** Pausenzeit in Minuten
|
- **Pause:** Pausenzeit in Minuten
|
||||||
|
- **Tätigkeiten:** Bis zu 5 Tätigkeiten mit Beschreibung, Stunden und Projektnummer
|
||||||
- **Notizen:** Optional, z.B. Projekt oder Tätigkeit
|
- **Notizen:** Optional, z.B. Projekt oder Tätigkeit
|
||||||
|
- **Überstunden:** Optional, wenn Überstunden verbraucht werden sollen
|
||||||
|
- **Urlaub:** Ganzer oder halber Tag Urlaub
|
||||||
|
- **Krank:** Krankheitstag markieren
|
||||||
4. Die Einträge werden automatisch gespeichert
|
4. Die Einträge werden automatisch gespeichert
|
||||||
5. Am Ende der Woche: Klicken Sie auf **"Woche abschicken"**
|
5. Am Ende der Woche: Klicken Sie auf **"Woche abschicken"**
|
||||||
6. Nach dem Abschicken können keine Änderungen mehr vorgenommen werden
|
6. Nach dem Abschicken können keine Änderungen mehr vorgenommen werden
|
||||||
|
7. **Überstunden-Auswertung:** Klicken Sie auf "Details anzeigen" im Bereich "Aktuelle Überstunden" für eine wöchentliche Aufschlüsselung
|
||||||
|
|
||||||
### Für Administratoren
|
### Für Administratoren
|
||||||
|
|
||||||
@@ -84,11 +118,16 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
|||||||
2. Sie gelangen automatisch zur Benutzerverwaltung
|
2. Sie gelangen automatisch zur Benutzerverwaltung
|
||||||
3. **Neuen Benutzer anlegen:**
|
3. **Neuen Benutzer anlegen:**
|
||||||
- Füllen Sie das Formular aus
|
- Füllen Sie das Formular aus
|
||||||
- Wählen Sie die passende Rolle
|
- Wählen Sie die passende Rolle (mehrere möglich)
|
||||||
|
- Personalnummer, Wochenstunden und Urlaubstage konfigurieren
|
||||||
- Klicken Sie auf "Benutzer anlegen"
|
- Klicken Sie auf "Benutzer anlegen"
|
||||||
4. **Benutzer löschen:**
|
4. **Benutzer löschen:**
|
||||||
- Klicken Sie auf "Löschen" neben dem gewünschten Benutzer
|
- Klicken Sie auf "Löschen" neben dem gewünschten Benutzer
|
||||||
- System-Benutzer (Admin, Verwaltung) können nicht gelöscht werden
|
- System-Benutzer (Admin, Verwaltung) können nicht gelöscht werden
|
||||||
|
5. **LDAP konfigurieren:**
|
||||||
|
- Navigieren Sie zum LDAP-Bereich
|
||||||
|
- Konfigurieren Sie LDAP-Verbindung und Synchronisation
|
||||||
|
- Führen Sie manuelle Synchronisationen durch oder aktivieren Sie automatische Syncs
|
||||||
|
|
||||||
### Für Verwaltung
|
### Für Verwaltung
|
||||||
|
|
||||||
@@ -97,10 +136,16 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
|||||||
3. **PDF erstellen:**
|
3. **PDF erstellen:**
|
||||||
- Klicken Sie auf "PDF herunterladen" neben dem gewünschten Stundenzettel
|
- Klicken Sie auf "PDF herunterladen" neben dem gewünschten Stundenzettel
|
||||||
- Die PDF wird automatisch generiert und heruntergeladen
|
- Die PDF wird automatisch generiert und heruntergeladen
|
||||||
4. Die PDF enthält:
|
4. **Überstunden korrigieren:**
|
||||||
- Mitarbeitername
|
- In der Wochenansicht können Sie manuelle Korrekturen (Offset) für jeden Mitarbeiter vornehmen
|
||||||
|
5. **Kommentare hinzufügen:**
|
||||||
|
- Fügen Sie Admin-Kommentare zu Stundenzetteln hinzu
|
||||||
|
6. Die PDF enthält:
|
||||||
|
- Mitarbeitername und Personalnummer
|
||||||
- Zeitraum
|
- Zeitraum
|
||||||
- Alle Tageseinträge mit Start, Ende, Pause, Stunden und Notizen
|
- Alle Tageseinträge mit Start, Ende, Pause, Stunden, Tätigkeiten und Notizen
|
||||||
|
- Überstunden-Verbrauch
|
||||||
|
- Urlaubstage
|
||||||
- Gesamtstundensumme
|
- Gesamtstundensumme
|
||||||
|
|
||||||
## Technologie-Stack
|
## Technologie-Stack
|
||||||
@@ -110,20 +155,101 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
|||||||
- **Template Engine:** EJS
|
- **Template Engine:** EJS
|
||||||
- **PDF-Generierung:** PDFKit
|
- **PDF-Generierung:** PDFKit
|
||||||
- **Authentifizierung:** bcryptjs + express-session
|
- **Authentifizierung:** bcryptjs + express-session
|
||||||
|
- **LDAP:** ldapjs
|
||||||
|
- **QR-Codes:** qrcode
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
├── services/ # Services
|
||||||
|
│ ├── feiertage-service.js # Feiertags-API
|
||||||
|
│ ├── ldap-service.js # LDAP-Service
|
||||||
|
│ ├── ldap-scheduler.js # LDAP-Synchronisation
|
||||||
|
│ ├── pdf-service.js # PDF-Generierung
|
||||||
|
│ └── ping-service.js # IP-basierte Zeiterfassung
|
||||||
|
├── views/ # EJS-Templates
|
||||||
|
├── public/ # Statische Dateien
|
||||||
|
│ ├── css/
|
||||||
|
│ ├── js/
|
||||||
|
│ └── images/
|
||||||
|
└── helpers/ # Helper-Funktionen
|
||||||
|
```
|
||||||
|
|
||||||
## Datenbankstruktur
|
## Datenbankstruktur
|
||||||
|
|
||||||
### Tabelle: users
|
### Tabelle: users
|
||||||
- Speichert Benutzerinformationen und Zugangsdaten
|
- Speichert Benutzerinformationen und Zugangsdaten
|
||||||
- Passwörter werden verschlüsselt gespeichert
|
- Passwörter werden verschlüsselt gespeichert
|
||||||
|
- Unterstützt mehrere Rollen pro Benutzer
|
||||||
|
- Personalnummer, Wochenstunden, Urlaubstage, Überstunden-Offset
|
||||||
|
|
||||||
### Tabelle: timesheet_entries
|
### Tabelle: timesheet_entries
|
||||||
- Speichert einzelne Tageseinträge
|
- Speichert einzelne Tageseinträge
|
||||||
- Automatische Berechnung der Gesamtstunden
|
- Automatische Berechnung der Gesamtstunden
|
||||||
|
- Unterstützt mehrere Tätigkeiten pro Tag
|
||||||
|
- Überstunden-Verbrauch, Urlaub, Krankheit
|
||||||
|
|
||||||
### Tabelle: weekly_timesheets
|
### Tabelle: weekly_timesheets
|
||||||
- Speichert eingereichte Wochenstundenzettel
|
- Speichert eingereichte Wochenstundenzettel
|
||||||
- Verknüpfung mit Benutzer und Status
|
- Verknüpfung mit Benutzer und Status
|
||||||
|
- Versionsverwaltung und Admin-Kommentare
|
||||||
|
|
||||||
|
### Tabelle: system_options
|
||||||
|
- Systemweite Einstellungen
|
||||||
|
- Wochenend-Prozentsätze (Samstag/Sonntag)
|
||||||
|
|
||||||
|
### Tabelle: public_holidays
|
||||||
|
- Öffentliche Feiertage (Baden-Württemberg)
|
||||||
|
- Automatische Aktualisierung
|
||||||
|
|
||||||
|
### Tabelle: ldap_config
|
||||||
|
- LDAP-Konfiguration
|
||||||
|
- Synchronisationseinstellungen
|
||||||
|
|
||||||
|
### Tabelle: ping_status
|
||||||
|
- IP-basierte Zeiterfassung
|
||||||
|
- Ping-Status pro Benutzer und Tag
|
||||||
|
|
||||||
|
## Features im Detail
|
||||||
|
|
||||||
|
### Überstunden-Verwaltung
|
||||||
|
- Automatische Berechnung basierend auf Sollstunden
|
||||||
|
- Berücksichtigung von Wochenend-Prozentsätzen
|
||||||
|
- Feiertagsstunden werden automatisch hinzugefügt
|
||||||
|
- Urlaubsstunden werden in die Berechnung einbezogen
|
||||||
|
- Manuelle Korrekturen durch Verwaltung möglich
|
||||||
|
- Detaillierte wöchentliche Auswertung für Mitarbeiter
|
||||||
|
|
||||||
|
### Urlaubsverwaltung
|
||||||
|
- Ganzer oder halber Tag Urlaub
|
||||||
|
- Verbleibende Urlaubstage werden angezeigt
|
||||||
|
- Verplante Urlaubstage werden erfasst
|
||||||
|
- Urlaubstage werden in Überstunden-Berechnung berücksichtigt
|
||||||
|
|
||||||
|
### Wochenend-Prozentsätze
|
||||||
|
- Konfigurierbare Prozentsätze für Samstag und Sonntag
|
||||||
|
- Werden automatisch auf gearbeitete Stunden angewendet
|
||||||
|
- Standard: 100% (keine Zuschläge)
|
||||||
|
|
||||||
|
### Feiertage
|
||||||
|
- Automatische Erkennung öffentlicher Feiertage (Baden-Württemberg)
|
||||||
|
- Feiertagsstunden werden automatisch berechnet
|
||||||
|
- Arbeit an Feiertagen zählt als Überstunden
|
||||||
|
|
||||||
|
### LDAP-Integration
|
||||||
|
- Automatische Benutzer-Synchronisation
|
||||||
|
- Konfigurierbare Synchronisationsintervalle
|
||||||
|
- Manuelle Synchronisation möglich
|
||||||
|
- LDAP-Authentifizierung als Alternative zu lokalen Passwörtern
|
||||||
|
|
||||||
## Sicherheit
|
## Sicherheit
|
||||||
|
|
||||||
@@ -131,6 +257,8 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
|||||||
- ✅ Session-basierte Authentifizierung
|
- ✅ Session-basierte Authentifizierung
|
||||||
- ✅ Rollenbasierte Zugriffskontrolle
|
- ✅ Rollenbasierte Zugriffskontrolle
|
||||||
- ✅ CSRF-Schutz durch Sessions
|
- ✅ CSRF-Schutz durch Sessions
|
||||||
|
- ✅ LDAP-Integration für zentrale Authentifizierung
|
||||||
|
- ✅ SQL-Injection-Schutz durch parametrisierte Queries
|
||||||
|
|
||||||
## Anpassungen
|
## Anpassungen
|
||||||
|
|
||||||
@@ -142,6 +270,11 @@ const PORT = 3333; // Ändern Sie hier den Port
|
|||||||
|
|
||||||
### Datenbank-Speicherort
|
### Datenbank-Speicherort
|
||||||
Die Datenbank wird standardmäßig als `stundenerfassung.db` im Projektverzeichnis gespeichert.
|
Die Datenbank wird standardmäßig als `stundenerfassung.db` im Projektverzeichnis gespeichert.
|
||||||
|
Über die Umgebungsvariable `DB_PATH` kann ein anderer Pfad angegeben werden.
|
||||||
|
|
||||||
|
### Wochenend-Prozentsätze
|
||||||
|
Im Admin-Bereich können die Prozentsätze für Samstag und Sonntag konfiguriert werden.
|
||||||
|
Standard: 100% (keine Zuschläge).
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
|
|||||||
10
database.js
10
database.js
@@ -223,7 +223,7 @@ function initDatabase() {
|
|||||||
bind_password TEXT,
|
bind_password TEXT,
|
||||||
base_dn TEXT,
|
base_dn TEXT,
|
||||||
user_search_filter TEXT,
|
user_search_filter TEXT,
|
||||||
username_attribute TEXT DEFAULT 'cn',
|
username_attribute TEXT DEFAULT 'sAMAccountName',
|
||||||
firstname_attribute TEXT DEFAULT 'givenName',
|
firstname_attribute TEXT DEFAULT 'givenName',
|
||||||
lastname_attribute TEXT DEFAULT 'sn',
|
lastname_attribute TEXT DEFAULT 'sn',
|
||||||
sync_interval INTEGER DEFAULT 0,
|
sync_interval INTEGER DEFAULT 0,
|
||||||
@@ -265,6 +265,14 @@ function initDatabase() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Migration: checkin_root_url Spalte hinzufügen
|
||||||
|
db.run(`ALTER TABLE system_options ADD COLUMN checkin_root_url TEXT`, (err) => {
|
||||||
|
// Fehler ignorieren wenn Spalte bereits existiert
|
||||||
|
if (err && !err.message.includes('duplicate column')) {
|
||||||
|
console.warn('Warnung beim Hinzufügen der Spalte checkin_root_url:', err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Migration: Bestehende Rollen zu JSON-Arrays konvertieren
|
// Migration: Bestehende Rollen zu JSON-Arrays konvertieren
|
||||||
// Prüfe ob Rollen noch als einfache Strings gespeichert sind (nicht als JSON-Array)
|
// Prüfe ob Rollen noch als einfache Strings gespeichert sind (nicht als JSON-Array)
|
||||||
db.all('SELECT id, role FROM users', (err, users) => {
|
db.all('SELECT id, role FROM users', (err, users) => {
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
lldap_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
services:
|
|
||||||
lldap:
|
|
||||||
image: lldap/lldap:stable
|
|
||||||
ports:
|
|
||||||
# For LDAP, not recommended to expose, see Usage section.
|
|
||||||
- "3890:3890"
|
|
||||||
# For LDAPS (LDAP Over SSL), enable port if LLDAP_LDAPS_OPTIONS__ENABLED set true, look env below
|
|
||||||
#- "6360:6360"
|
|
||||||
# For the web front-end
|
|
||||||
- "17170:17170"
|
|
||||||
volumes:
|
|
||||||
- "lldap_data:/data"
|
|
||||||
# Alternatively, you can mount a local folder
|
|
||||||
# - "./lldap_data:/data"
|
|
||||||
environment:
|
|
||||||
- UID=1000
|
|
||||||
- GID=1000
|
|
||||||
- TZ=Europe/Berlin
|
|
||||||
- LLDAP_JWT_SECRET=1omV4UDprT1PAJFYXGisVlei/V5b5Uaiqssl9qburwV+T1S0ox8iurI6FJkWPnX5xRUMbHswJwZLG7QzUnzdZw==
|
|
||||||
- LLDAP_KEY_SEED=ffcomviFeT8RByJf7jT3AuzDcFbrgWb+oSuMDp72pql96J4Rdq5gno2Dk1xfWrYPLH5OoS/bpDuOp/oE9T5+sA==
|
|
||||||
- LLDAP_LDAP_BASE_DN=dc=gmbh,dc=de
|
|
||||||
- LLDAP_LDAP_USER_PASS=Delfine1!!! # If the password contains '$', escape it (e.g. Pas$$word sets Pas$word)
|
|
||||||
# If using LDAPS, set enabled true and configure cert and key path
|
|
||||||
# - LLDAP_LDAPS_OPTIONS__ENABLED=true
|
|
||||||
# - LLDAP_LDAPS_OPTIONS__CERT_FILE=/path/to/certfile.crt
|
|
||||||
# - LLDAP_LDAPS_OPTIONS__KEY_FILE=/path/to/keyfile.key
|
|
||||||
# You can also set a different database:
|
|
||||||
# - LLDAP_DATABASE_URL=mysql://mysql-user:password@mysql-server/my-database
|
|
||||||
# - LLDAP_DATABASE_URL=postgres://postgres-user:password@postgres-server/my-database
|
|
||||||
# If using SMTP, set the following variables
|
|
||||||
# - LLDAP_SMTP_OPTIONS__ENABLE_PASSWORD_RESET=true
|
|
||||||
# - LLDAP_SMTP_OPTIONS__SERVER=smtp.example.com
|
|
||||||
# - LLDAP_SMTP_OPTIONS__PORT=465 # Check your smtp provider's documentation for this setting
|
|
||||||
# - LLDAP_SMTP_OPTIONS__SMTP_ENCRYPTION=TLS # How the connection is encrypted, either "NONE" (no encryption, port 25), "TLS" (sometimes called SSL, port 465) or "STARTTLS" (sometimes called TLS, port 587).
|
|
||||||
# - LLDAP_SMTP_OPTIONS__USER=no-reply@example.com # The SMTP user, usually your email address
|
|
||||||
# - LLDAP_SMTP_OPTIONS__PASSWORD=PasswordGoesHere # The SMTP password
|
|
||||||
# - LLDAP_SMTP_OPTIONS__FROM=no-reply <no-reply@example.com> # The header field, optional: how the sender appears in the email. The first is a free-form name, followed by an email between <>.
|
|
||||||
# - LLDAP_SMTP_OPTIONS__TO=admin <admin@example.com> # Same for reply-to, optional.
|
|
||||||
@@ -65,7 +65,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
saturday_percentage: document.getElementById('saturdayPercentage').value,
|
saturday_percentage: document.getElementById('saturdayPercentage').value,
|
||||||
sunday_percentage: document.getElementById('sundayPercentage').value
|
sunday_percentage: document.getElementById('sundayPercentage').value,
|
||||||
|
checkin_root_url: document.getElementById('checkinRootUrl') ? document.getElementById('checkinRootUrl').value : null
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -213,6 +214,9 @@ async function loadOptions() {
|
|||||||
if (document.getElementById('sundayPercentage')) {
|
if (document.getElementById('sundayPercentage')) {
|
||||||
document.getElementById('sundayPercentage').value = config.sunday_percentage || 0;
|
document.getElementById('sundayPercentage').value = config.sunday_percentage || 0;
|
||||||
}
|
}
|
||||||
|
if (document.getElementById('checkinRootUrl')) {
|
||||||
|
document.getElementById('checkinRootUrl').value = config.checkin_root_url || '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Optionen:', error);
|
console.error('Fehler beim Laden der Optionen:', error);
|
||||||
@@ -244,7 +248,7 @@ async function loadLDAPConfig() {
|
|||||||
document.getElementById('ldapSearchFilter').value = config.user_search_filter || '(objectClass=person)';
|
document.getElementById('ldapSearchFilter').value = config.user_search_filter || '(objectClass=person)';
|
||||||
}
|
}
|
||||||
if (document.getElementById('ldapUsernameAttr')) {
|
if (document.getElementById('ldapUsernameAttr')) {
|
||||||
document.getElementById('ldapUsernameAttr').value = config.username_attribute || 'cn';
|
document.getElementById('ldapUsernameAttr').value = config.username_attribute || 'sAMAccountName';
|
||||||
}
|
}
|
||||||
if (document.getElementById('ldapFirstnameAttr')) {
|
if (document.getElementById('ldapFirstnameAttr')) {
|
||||||
document.getElementById('ldapFirstnameAttr').value = config.firstname_attribute || 'givenName';
|
document.getElementById('ldapFirstnameAttr').value = config.firstname_attribute || 'givenName';
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ resetDatabase().catch((error) => {
|
|||||||
bind_password TEXT,
|
bind_password TEXT,
|
||||||
base_dn TEXT,
|
base_dn TEXT,
|
||||||
user_search_filter TEXT,
|
user_search_filter TEXT,
|
||||||
username_attribute TEXT DEFAULT 'cn',
|
username_attribute TEXT DEFAULT 'sAMAccountName',
|
||||||
firstname_attribute TEXT DEFAULT 'givenName',
|
firstname_attribute TEXT DEFAULT 'givenName',
|
||||||
lastname_attribute TEXT DEFAULT 'sn',
|
lastname_attribute TEXT DEFAULT 'sn',
|
||||||
sync_interval INTEGER DEFAULT 0,
|
sync_interval INTEGER DEFAULT 0,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// LDAP Admin Routes
|
// LDAP Admin Routes
|
||||||
|
|
||||||
const { db } = require('../database');
|
const { db } = require('../database');
|
||||||
const LDAPService = require('../ldap-service');
|
const LDAPService = require('../services/ldap-service');
|
||||||
const { requireAdmin } = require('../middleware/auth');
|
const { requireAdmin } = require('../middleware/auth');
|
||||||
|
|
||||||
// Routes registrieren
|
// Routes registrieren
|
||||||
@@ -55,7 +55,7 @@ function registerAdminLDAPRoutes(app) {
|
|||||||
bind_password: bind_password ? bind_password.trim() : null,
|
bind_password: bind_password ? bind_password.trim() : null,
|
||||||
base_dn: base_dn.trim(),
|
base_dn: base_dn.trim(),
|
||||||
user_search_filter: user_search_filter ? user_search_filter.trim() : '(objectClass=person)',
|
user_search_filter: user_search_filter ? user_search_filter.trim() : '(objectClass=person)',
|
||||||
username_attribute: username_attribute ? username_attribute.trim() : 'cn',
|
username_attribute: username_attribute ? username_attribute.trim() : 'sAMAccountName',
|
||||||
firstname_attribute: firstname_attribute ? firstname_attribute.trim() : 'givenName',
|
firstname_attribute: firstname_attribute ? firstname_attribute.trim() : 'givenName',
|
||||||
lastname_attribute: lastname_attribute ? lastname_attribute.trim() : 'sn',
|
lastname_attribute: lastname_attribute ? lastname_attribute.trim() : 'sn',
|
||||||
sync_interval: parseInt(sync_interval) || 0,
|
sync_interval: parseInt(sync_interval) || 0,
|
||||||
|
|||||||
@@ -164,7 +164,8 @@ function registerAdminRoutes(app) {
|
|||||||
return res.json({
|
return res.json({
|
||||||
config: {
|
config: {
|
||||||
saturday_percentage: 100,
|
saturday_percentage: 100,
|
||||||
sunday_percentage: 100
|
sunday_percentage: 100,
|
||||||
|
checkin_root_url: null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -174,7 +175,7 @@ function registerAdminRoutes(app) {
|
|||||||
|
|
||||||
// Optionen speichern
|
// Optionen speichern
|
||||||
app.post('/admin/options', requireAdmin, (req, res) => {
|
app.post('/admin/options', requireAdmin, (req, res) => {
|
||||||
const { saturday_percentage, sunday_percentage } = req.body;
|
const { saturday_percentage, sunday_percentage, checkin_root_url } = req.body;
|
||||||
|
|
||||||
// Validierung
|
// Validierung
|
||||||
const satPercent = parseFloat(saturday_percentage);
|
const satPercent = parseFloat(saturday_percentage);
|
||||||
@@ -188,6 +189,12 @@ function registerAdminRoutes(app) {
|
|||||||
return res.status(400).json({ error: 'Prozentsätze müssen zwischen 100 und 200 liegen' });
|
return res.status(400).json({ error: 'Prozentsätze müssen zwischen 100 und 200 liegen' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validierung der Root URL (optional, kann leer sein)
|
||||||
|
let rootUrl = checkin_root_url ? checkin_root_url.trim() : null;
|
||||||
|
if (rootUrl === '') {
|
||||||
|
rootUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Prüfe ob Eintrag existiert
|
// Prüfe ob Eintrag existiert
|
||||||
db.get('SELECT id FROM system_options WHERE id = 1', (err, existing) => {
|
db.get('SELECT id FROM system_options WHERE id = 1', (err, existing) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -196,8 +203,8 @@ function registerAdminRoutes(app) {
|
|||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// Update
|
// Update
|
||||||
db.run('UPDATE system_options SET saturday_percentage = ?, sunday_percentage = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
|
db.run('UPDATE system_options SET saturday_percentage = ?, sunday_percentage = ?, checkin_root_url = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
|
||||||
[satPercent, sunPercent],
|
[satPercent, sunPercent, rootUrl],
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: 'Fehler beim Speichern der Optionen' });
|
return res.status(500).json({ error: 'Fehler beim Speichern der Optionen' });
|
||||||
@@ -206,8 +213,8 @@ function registerAdminRoutes(app) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Insert
|
// Insert
|
||||||
db.run('INSERT INTO system_options (id, saturday_percentage, sunday_percentage) VALUES (1, ?, ?)',
|
db.run('INSERT INTO system_options (id, saturday_percentage, sunday_percentage, checkin_root_url) VALUES (1, ?, ?, ?)',
|
||||||
[satPercent, sunPercent],
|
[satPercent, sunPercent, rootUrl],
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: 'Fehler beim Speichern der Optionen' });
|
return res.status(500).json({ error: 'Fehler beim Speichern der Optionen' });
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const { db } = require('../database');
|
const { db } = require('../database');
|
||||||
const LDAPService = require('../ldap-service');
|
const LDAPService = require('../services/ldap-service');
|
||||||
const { getDefaultRole } = require('../helpers/utils');
|
const { getDefaultRole } = require('../helpers/utils');
|
||||||
|
|
||||||
// Helper-Funktion für erfolgreiche Anmeldung
|
// Helper-Funktion für erfolgreiche Anmeldung
|
||||||
@@ -76,16 +76,14 @@ function registerAuthRoutes(app) {
|
|||||||
|
|
||||||
// Wenn LDAP aktiviert ist, authentifiziere gegen LDAP
|
// Wenn LDAP aktiviert ist, authentifiziere gegen LDAP
|
||||||
if (isLDAPEnabled) {
|
if (isLDAPEnabled) {
|
||||||
LDAPService.authenticate(username, password, (authErr, authSuccess) => {
|
LDAPService.authenticate(username, password, (authErr, authSuccess, ldapUserInfo) => {
|
||||||
if (authErr || !authSuccess) {
|
if (authErr || !authSuccess) {
|
||||||
// LDAP-Authentifizierung fehlgeschlagen - prüfe lokale Datenbank als Fallback
|
// LDAP-Authentifizierung fehlgeschlagen - prüfe lokale Datenbank als Fallback
|
||||||
// Case-insensitive Suche: COLLATE NOCASE macht den Vergleich case-insensitive
|
|
||||||
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], (err, user) => {
|
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], (err, user) => {
|
||||||
if (err || !user) {
|
if (err || !user) {
|
||||||
return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
|
return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Versuche lokale Authentifizierung
|
|
||||||
if (bcrypt.compareSync(password, user.password)) {
|
if (bcrypt.compareSync(password, user.password)) {
|
||||||
handleSuccessfulLogin(req, res, user, rememberMe);
|
handleSuccessfulLogin(req, res, user, rememberMe);
|
||||||
} else {
|
} else {
|
||||||
@@ -93,9 +91,10 @@ function registerAuthRoutes(app) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// LDAP-Authentifizierung erfolgreich - hole Benutzer aus Datenbank
|
// LDAP-Authentifizierung erfolgreich - Benutzer anhand des kanonischen LDAP-Benutzernamens aus der DB holen
|
||||||
// Case-insensitive Suche: COLLATE NOCASE macht den Vergleich case-insensitive
|
// (Sync speichert den exakten LDAP-Wert, z. B. "geißlerj" oder "GeisslerJ")
|
||||||
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], (err, user) => {
|
const dbLookupUsername = (ldapUserInfo && ldapUserInfo.username) ? ldapUserInfo.username : username;
|
||||||
|
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [dbLookupUsername], (err, user) => {
|
||||||
if (err || !user) {
|
if (err || !user) {
|
||||||
return res.render('login', { error: 'Benutzer nicht in der Datenbank gefunden. Bitte führen Sie eine LDAP-Synchronisation durch.' });
|
return res.render('login', { error: 'Benutzer nicht in der Datenbank gefunden. Bitte führen Sie eine LDAP-Synchronisation durch.' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,36 @@
|
|||||||
const { hasRole } = require('../helpers/utils');
|
const { hasRole } = require('../helpers/utils');
|
||||||
const { requireAuth } = require('../middleware/auth');
|
const { requireAuth } = require('../middleware/auth');
|
||||||
const { generateCheckinCheckoutQRPDF } = require('../services/pdf-service');
|
const { generateCheckinCheckoutQRPDF } = require('../services/pdf-service');
|
||||||
|
const { db } = require('../database');
|
||||||
|
|
||||||
// Routes registrieren
|
// Routes registrieren
|
||||||
function registerDashboardRoutes(app) {
|
function registerDashboardRoutes(app) {
|
||||||
|
// Check-in Root URL abrufen (öffentlich zugänglich für Konfiguration)
|
||||||
|
app.get('/api/checkin-root-url', (req, res) => {
|
||||||
|
db.get('SELECT checkin_root_url FROM system_options WHERE id = 1', (err, options) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Laden der Root URL' });
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
root_url: options && options.checkin_root_url ? options.checkin_root_url : null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
// QR-Code-PDF (Check-in/Check-out) – nur für eingeloggte Nutzer mit Mitarbeiter-Rolle
|
// QR-Code-PDF (Check-in/Check-out) – nur für eingeloggte Nutzer mit Mitarbeiter-Rolle
|
||||||
app.get('/api/dashboard/qr-pdf', requireAuth, (req, res) => {
|
// Interne URLs
|
||||||
|
app.get('/api/dashboard/qr-pdf/internal', requireAuth, (req, res) => {
|
||||||
if (!hasRole(req, 'mitarbeiter')) {
|
if (!hasRole(req, 'mitarbeiter')) {
|
||||||
return res.status(403).send('Zugriff verweigert');
|
return res.status(403).send('Zugriff verweigert');
|
||||||
}
|
}
|
||||||
generateCheckinCheckoutQRPDF(req, res);
|
generateCheckinCheckoutQRPDF(req, res, 'internal');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Externe URLs
|
||||||
|
app.get('/api/dashboard/qr-pdf/external', requireAuth, (req, res) => {
|
||||||
|
if (!hasRole(req, 'mitarbeiter')) {
|
||||||
|
return res.status(403).send('Zugriff verweigert');
|
||||||
|
}
|
||||||
|
generateCheckinCheckoutQRPDF(req, res, 'external');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dashboard für Mitarbeiter
|
// Dashboard für Mitarbeiter
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// LDAP-Scheduler Service
|
// LDAP-Scheduler Service
|
||||||
|
|
||||||
const { db } = require('../database');
|
const { db } = require('../database');
|
||||||
const LDAPService = require('../ldap-service');
|
const LDAPService = require('./ldap-service');
|
||||||
|
|
||||||
// Automatische LDAP-Synchronisation einrichten
|
// Automatische LDAP-Synchronisation einrichten
|
||||||
function setupLDAPScheduler() {
|
function setupLDAPScheduler() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const ldap = require('ldapjs');
|
const ldap = require('ldapjs');
|
||||||
const { db } = require('./database');
|
const { db } = require('../database');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,7 +63,7 @@ class LDAPService {
|
|||||||
filter: searchFilter,
|
filter: searchFilter,
|
||||||
scope: 'sub',
|
scope: 'sub',
|
||||||
attributes: [
|
attributes: [
|
||||||
config.username_attribute || 'cn',
|
config.username_attribute || 'sAMAccountName',
|
||||||
config.firstname_attribute || 'givenName',
|
config.firstname_attribute || 'givenName',
|
||||||
config.lastname_attribute || 'sn'
|
config.lastname_attribute || 'sn'
|
||||||
]
|
]
|
||||||
@@ -78,7 +78,7 @@ class LDAPService {
|
|||||||
|
|
||||||
res.on('searchEntry', (entry) => {
|
res.on('searchEntry', (entry) => {
|
||||||
const user = {
|
const user = {
|
||||||
username: this.getAttributeValue(entry, config.username_attribute || 'cn'),
|
username: this.getAttributeValue(entry, config.username_attribute || 'sAMAccountName'),
|
||||||
firstname: this.getAttributeValue(entry, config.firstname_attribute || 'givenName'),
|
firstname: this.getAttributeValue(entry, config.firstname_attribute || 'givenName'),
|
||||||
lastname: this.getAttributeValue(entry, config.lastname_attribute || 'sn')
|
lastname: this.getAttributeValue(entry, config.lastname_attribute || 'sn')
|
||||||
};
|
};
|
||||||
@@ -247,11 +247,26 @@ class LDAPService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suchvarianten für Benutzername (case-insensitive, ß/ss) für LDAP-Filter.
|
||||||
|
* So findet der Login auch "GeißlerJ", wenn LDAP "geißlerj" oder "GeisslerJ" speichert.
|
||||||
|
*/
|
||||||
|
static getUsernameSearchVariants(usernameStr) {
|
||||||
|
const variants = new Set();
|
||||||
|
variants.add(usernameStr);
|
||||||
|
variants.add(usernameStr.toLowerCase());
|
||||||
|
variants.add(usernameStr.replace(/\u00df/g, 'ss')); // ß -> ss
|
||||||
|
variants.add(usernameStr.replace(/ss/g, '\u00df')); // ss -> ß (z. B. Geissler -> Geißler)
|
||||||
|
return [...variants];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Benutzer gegen LDAP authentifizieren
|
* Benutzer gegen LDAP authentifizieren
|
||||||
*
|
*
|
||||||
* Unterstützt UTF-8-Zeichen wie ß, ä, ö, ü in Usernamen.
|
* Unterstützt UTF-8-Zeichen wie ß, ä, ö, ü in Usernamen.
|
||||||
* Die ldapjs-Bibliothek behandelt UTF-8 automatisch korrekt.
|
* Sucht mit mehreren Varianten (Groß-/Kleinschreibung, ß/ss), damit z. B. "GeißlerJ"
|
||||||
|
* auch gefunden wird, wenn LDAP "geißlerj" oder "GeisslerJ" speichert.
|
||||||
|
* Gibt bei Erfolg den kanonischen LDAP-Benutzernamen zurück (für DB-Lookup).
|
||||||
*/
|
*/
|
||||||
static authenticate(username, password, callback) {
|
static authenticate(username, password, callback) {
|
||||||
// Stelle sicher, dass Username als String behandelt wird (UTF-8 wird korrekt unterstützt)
|
// Stelle sicher, dass Username als String behandelt wird (UTF-8 wird korrekt unterstützt)
|
||||||
@@ -273,12 +288,12 @@ class LDAPService {
|
|||||||
return callback(err, false);
|
return callback(err, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suche nach dem Benutzer in LDAP
|
|
||||||
const baseDN = config.base_dn || '';
|
const baseDN = config.base_dn || '';
|
||||||
const usernameAttr = config.username_attribute || 'cn';
|
const usernameAttr = config.username_attribute || 'sAMAccountName';
|
||||||
// escapeLDAPFilter behandelt UTF-8-Zeichen korrekt (escaped sie nicht)
|
// OR-Filter mit mehreren Varianten (exakt, lowercase, ß/ss), damit Login trotz unterschiedlicher Schreibweise funktioniert
|
||||||
const escapedUsername = this.escapeLDAPFilter(usernameStr);
|
const variants = this.getUsernameSearchVariants(usernameStr);
|
||||||
const searchFilter = `(${usernameAttr}=${escapedUsername})`;
|
const filterParts = variants.map(v => `(${usernameAttr}=${this.escapeLDAPFilter(v)})`);
|
||||||
|
const searchFilter = filterParts.length === 1 ? filterParts[0] : `(|${filterParts.join('')})`;
|
||||||
const searchOptions = {
|
const searchOptions = {
|
||||||
filter: searchFilter,
|
filter: searchFilter,
|
||||||
scope: 'sub',
|
scope: 'sub',
|
||||||
@@ -286,17 +301,19 @@ class LDAPService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let userDN = null;
|
let userDN = null;
|
||||||
|
let canonicalUsername = null;
|
||||||
|
|
||||||
client.search(baseDN, searchOptions, (err, res) => {
|
client.search(baseDN, searchOptions, (err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
client.unbind();
|
client.unbind();
|
||||||
// Verbesserte Fehlermeldung für mögliche Encoding-Probleme
|
|
||||||
const errorMsg = err.message || String(err);
|
const errorMsg = err.message || String(err);
|
||||||
return callback(new Error(`LDAP-Suche fehlgeschlagen: ${errorMsg}. Hinweis: Prüfen Sie, ob der Benutzername UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt enthält.`), false);
|
return callback(new Error(`LDAP-Suche fehlgeschlagen: ${errorMsg}. Hinweis: Prüfen Sie, ob der Benutzername UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt enthält.`), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.on('searchEntry', (entry) => {
|
res.on('searchEntry', (entry) => {
|
||||||
userDN = entry.dn.toString();
|
userDN = entry.dn.toString();
|
||||||
|
// Kanonischen Benutzernamen aus LDAP verwenden (für DB-Lookup nach Sync)
|
||||||
|
canonicalUsername = this.getAttributeValue(entry, usernameAttr) || usernameStr;
|
||||||
});
|
});
|
||||||
|
|
||||||
res.on('error', (err) => {
|
res.on('error', (err) => {
|
||||||
@@ -306,15 +323,12 @@ class LDAPService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.on('end', (result) => {
|
res.on('end', (result) => {
|
||||||
// Service-Account-Verbindung schließen
|
|
||||||
client.unbind();
|
client.unbind();
|
||||||
|
|
||||||
if (!userDN) {
|
if (!userDN) {
|
||||||
// Verbesserte Fehlermeldung: Hinweis auf mögliche Encoding-Probleme
|
|
||||||
return callback(new Error(`Benutzer "${usernameStr}" nicht gefunden. Hinweis: Prüfen Sie, ob der Benutzername korrekt ist und UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt geschrieben sind.`), false);
|
return callback(new Error(`Benutzer "${usernameStr}" nicht gefunden. Hinweis: Prüfen Sie, ob der Benutzername korrekt ist und UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt geschrieben sind.`), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Versuche, sich mit den Benutzer-Credentials zu binden
|
|
||||||
const authClient = ldap.createClient({
|
const authClient = ldap.createClient({
|
||||||
url: config.url,
|
url: config.url,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
@@ -332,7 +346,8 @@ class LDAPService {
|
|||||||
const errorMsg = err.message || String(err);
|
const errorMsg = err.message || String(err);
|
||||||
return callback(new Error(`Ungültiges Passwort oder Authentifizierungsfehler: ${errorMsg}`), false);
|
return callback(new Error(`Ungültiges Passwort oder Authentifizierungsfehler: ${errorMsg}`), false);
|
||||||
}
|
}
|
||||||
callback(null, true);
|
// Erfolg: kanonischen Benutzernamen mitgeben, damit die DB-Lookup mit dem Sync-Benutzernamen funktioniert
|
||||||
|
callback(null, true, { username: canonicalUsername });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -530,18 +530,56 @@ function generatePDFToBuffer(timesheetId, req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check-in/Check-out URL-Basis (wie im Dashboard-Frontend)
|
// Check-in/Check-out URL-Basis (wie im Dashboard-Frontend)
|
||||||
function getCheckinBaseUrl(req) {
|
function getCheckinBaseUrl(req, callback) {
|
||||||
|
// Versuche Root URL aus Datenbank zu laden
|
||||||
|
db.get('SELECT checkin_root_url FROM system_options WHERE id = 1', (err, options) => {
|
||||||
|
if (err) {
|
||||||
|
console.warn('Fehler beim Laden der Root URL, verwende Fallback:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let checkinBaseUrl = null;
|
||||||
|
if (options && options.checkin_root_url && options.checkin_root_url.trim() !== '') {
|
||||||
|
checkinBaseUrl = options.checkin_root_url.trim();
|
||||||
|
// Stelle sicher, dass kein trailing slash vorhanden ist
|
||||||
|
checkinBaseUrl = checkinBaseUrl.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Konstruiere URL aus Request (Port 3334 für Check-in)
|
||||||
|
if (!checkinBaseUrl) {
|
||||||
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
||||||
return baseUrl.replace(/:\d+$/, ':3334');
|
checkinBaseUrl = baseUrl.replace(/:\d+$/, ':3334');
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(checkinBaseUrl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// PDF mit Check-in- und Check-out-QR-Codes (A4)
|
// PDF mit Check-in- und Check-out-QR-Codes (A4)
|
||||||
async function generateCheckinCheckoutQRPDF(req, res) {
|
// urlType: 'internal' oder 'external'
|
||||||
|
async function generateCheckinCheckoutQRPDF(req, res, urlType = 'internal') {
|
||||||
const userId = req.session.userId;
|
const userId = req.session.userId;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return res.status(401).send('Nicht angemeldet');
|
return res.status(401).send('Nicht angemeldet');
|
||||||
}
|
}
|
||||||
const checkinBaseUrl = getCheckinBaseUrl(req);
|
|
||||||
|
let checkinBaseUrl;
|
||||||
|
|
||||||
|
if (urlType === 'external') {
|
||||||
|
// Externe URL: Lade aus Datenbank
|
||||||
|
checkinBaseUrl = await new Promise((resolve) => {
|
||||||
|
getCheckinBaseUrl(req, resolve);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Interne URL: Konstruiere aus Request (Port 3334)
|
||||||
|
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
||||||
|
if (baseUrl.match(/:\d+$/)) {
|
||||||
|
checkinBaseUrl = baseUrl.replace(/:\d+$/, ':3334');
|
||||||
|
} else {
|
||||||
|
const url = new URL(baseUrl);
|
||||||
|
checkinBaseUrl = `${url.protocol}//${url.hostname}:3334`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const checkinUrl = `${checkinBaseUrl}/api/checkin/${userId}`;
|
const checkinUrl = `${checkinBaseUrl}/api/checkin/${userId}`;
|
||||||
const checkoutUrl = `${checkinBaseUrl}/api/checkout/${userId}`;
|
const checkoutUrl = `${checkinBaseUrl}/api/checkout/${userId}`;
|
||||||
|
|
||||||
@@ -554,11 +592,12 @@ async function generateCheckinCheckoutQRPDF(req, res) {
|
|||||||
const firstname = (req.session.firstname || '').replace(/\s+/g, '');
|
const firstname = (req.session.firstname || '').replace(/\s+/g, '');
|
||||||
const lastname = (req.session.lastname || '').replace(/\s+/g, '');
|
const lastname = (req.session.lastname || '').replace(/\s+/g, '');
|
||||||
const namePart = [firstname, lastname].filter(Boolean).join('_') || 'User';
|
const namePart = [firstname, lastname].filter(Boolean).join('_') || 'User';
|
||||||
|
const urlTypeLabel = urlType === 'external' ? 'Extern' : 'Intern';
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const dateStr = today.getFullYear() + '-' +
|
const dateStr = today.getFullYear() + '-' +
|
||||||
String(today.getMonth() + 1).padStart(2, '0') + '-' +
|
String(today.getMonth() + 1).padStart(2, '0') + '-' +
|
||||||
String(today.getDate()).padStart(2, '0');
|
String(today.getDate()).padStart(2, '0');
|
||||||
const filename = `Check-in_Check-out_QR_${namePart}_${dateStr}.pdf`;
|
const filename = `Check-in_Check-out_QR_${urlTypeLabel}_${namePart}_${dateStr}.pdf`;
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/pdf');
|
res.setHeader('Content-Type', 'application/pdf');
|
||||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
@@ -573,7 +612,10 @@ async function generateCheckinCheckoutQRPDF(req, res) {
|
|||||||
const left1 = 50 + (pageWidth / 2 - qrSize - gap / 2);
|
const left1 = 50 + (pageWidth / 2 - qrSize - gap / 2);
|
||||||
const left2 = 50 + (pageWidth / 2 + gap / 2);
|
const left2 = 50 + (pageWidth / 2 + gap / 2);
|
||||||
|
|
||||||
doc.fontSize(18).text('Check-in / Check-out – Zeiterfassung', { align: 'center' });
|
const title = urlType === 'external'
|
||||||
|
? 'Check-in / Check-out – Zeiterfassung (Externe URLs)'
|
||||||
|
: 'Check-in / Check-out – Zeiterfassung (Interne URLs)';
|
||||||
|
doc.fontSize(18).text(title, { align: 'center' });
|
||||||
doc.moveDown(1.5);
|
doc.moveDown(1.5);
|
||||||
|
|
||||||
const topY = doc.y;
|
const topY = doc.y;
|
||||||
|
|||||||
@@ -234,6 +234,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-top: 20px;">
|
||||||
|
<h3 style="margin-bottom: 10px;">Check-in URL-Konfiguration</h3>
|
||||||
|
<p style="margin-bottom: 15px; color: #666;">Definieren Sie die Basis-URL für alle Check-in und Check-out URLs. Diese URL wird vor <code>/api</code> verwendet. Beispiel: https://example.com:3334</p>
|
||||||
|
<label for="checkinRootUrl">Check-in Root URL</label>
|
||||||
|
<input type="text" id="checkinRootUrl" name="checkin_root_url" class="form-control" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" placeholder="https://example.com:3334" value="<%= (typeof options !== 'undefined' && options && options.checkin_root_url) ? options.checkin_root_url : '' %>">
|
||||||
|
<small style="color: #666; display: block; margin-top: 5px;">Lassen Sie dieses Feld leer, um die URL automatisch aus der aktuellen Seite zu generieren.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Optionen speichern</button>
|
<button type="submit" class="btn btn-primary">Optionen speichern</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,7 +297,7 @@
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="ldapUsernameAttr">Username-Attribut</label>
|
<label for="ldapUsernameAttr">Username-Attribut</label>
|
||||||
<input type="text" id="ldapUsernameAttr" name="username_attribute" placeholder="cn" value="cn">
|
<input type="text" id="ldapUsernameAttr" name="username_attribute" placeholder="sAMAccountName" value="sAMAccountName">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -99,28 +99,59 @@
|
|||||||
Automatische Zeiterfassung
|
Automatische Zeiterfassung
|
||||||
</h3>
|
</h3>
|
||||||
<div id="timeCaptureContent" style="display: none; margin-top: 15px;">
|
<div id="timeCaptureContent" style="display: none; margin-top: 15px;">
|
||||||
<!-- URL-Erfassung -->
|
<!-- Interne URLs -->
|
||||||
<div style="margin-bottom: 20px;">
|
<div style="margin-bottom: 20px;">
|
||||||
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555; display: flex; align-items: center; gap: 5px;">
|
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 8px;" onclick="toggleInternalUrls()">
|
||||||
Zeiterfassung per URL
|
<span class="toggle-icon-internal-urls" style="display: inline-block; transition: transform 0.3s; font-size: 10px;">▶</span>
|
||||||
<span class="help-icon" onclick="showHelpModal('url-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
Interne URLs
|
||||||
|
<span class="help-icon" onclick="event.stopPropagation(); showHelpModal('url-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1; margin-left: auto;">?</span>
|
||||||
</h4>
|
</h4>
|
||||||
|
<div id="internalUrlsContent" style="display: none; margin-top: 10px;">
|
||||||
<div class="form-group" style="margin-bottom: 15px;">
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-in URL</label>
|
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-in URL</label>
|
||||||
<div style="display: flex; gap: 5px;">
|
<div style="display: flex; gap: 5px;">
|
||||||
<input type="text" id="checkinUrl" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
<input type="text" id="checkinUrlInternal" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||||
<button onclick="copyToClipboard('checkinUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
<button onclick="copyToClipboard('checkinUrlInternal')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom: 15px;">
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-out URL</label>
|
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-out URL</label>
|
||||||
<div style="display: flex; gap: 5px;">
|
<div style="display: flex; gap: 5px;">
|
||||||
<input type="text" id="checkoutUrl" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
<input type="text" id="checkoutUrlInternal" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||||
<button onclick="copyToClipboard('checkoutUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
<button onclick="copyToClipboard('checkoutUrlInternal')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 12px;">
|
<div style="margin-top: 12px;">
|
||||||
<a href="/api/dashboard/qr-pdf" class="btn btn-sm btn-secondary" style="padding: 8px 12px; text-decoration: none; display: inline-block;" download>QR-Code-PDF herunterladen</a>
|
<a href="/api/dashboard/qr-pdf/internal" class="btn btn-sm btn-secondary" style="padding: 8px 12px; text-decoration: none; display: inline-block;" download>QR-Code-PDF herunterladen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Externe URLs -->
|
||||||
|
<div id="externalUrlsSection" style="margin-bottom: 20px; display: none;">
|
||||||
|
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 8px;" onclick="toggleExternalUrls()">
|
||||||
|
<span class="toggle-icon-external-urls" style="display: inline-block; transition: transform 0.3s; font-size: 10px;">▶</span>
|
||||||
|
Externe URLs
|
||||||
|
<span class="help-icon" onclick="event.stopPropagation(); showHelpModal('url-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1; margin-left: auto;">?</span>
|
||||||
|
</h4>
|
||||||
|
<div id="externalUrlsContent" style="display: none; margin-top: 10px;">
|
||||||
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
|
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-in URL</label>
|
||||||
|
<div style="display: flex; gap: 5px;">
|
||||||
|
<input type="text" id="checkinUrlExternal" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||||
|
<button onclick="copyToClipboard('checkinUrlExternal')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
|
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-out URL</label>
|
||||||
|
<div style="display: flex; gap: 5px;">
|
||||||
|
<input type="text" id="checkoutUrlExternal" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||||
|
<button onclick="copyToClipboard('checkoutUrlExternal')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 12px;">
|
||||||
|
<a href="/api/dashboard/qr-pdf/external" class="btn btn-sm btn-secondary" style="padding: 8px 12px; text-decoration: none; display: inline-block;" download>QR-Code-PDF herunterladen</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -180,6 +211,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interne URLs ein-/ausklappen
|
||||||
|
function toggleInternalUrls() {
|
||||||
|
const content = document.getElementById('internalUrlsContent');
|
||||||
|
const icon = document.querySelector('.toggle-icon-internal-urls');
|
||||||
|
|
||||||
|
if (content && icon) {
|
||||||
|
if (content.style.display === 'none') {
|
||||||
|
content.style.display = 'block';
|
||||||
|
icon.style.transform = 'rotate(90deg)';
|
||||||
|
} else {
|
||||||
|
content.style.display = 'none';
|
||||||
|
icon.style.transform = 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Externe URLs ein-/ausklappen
|
||||||
|
function toggleExternalUrls() {
|
||||||
|
const content = document.getElementById('externalUrlsContent');
|
||||||
|
const icon = document.querySelector('.toggle-icon-external-urls');
|
||||||
|
|
||||||
|
if (content && icon) {
|
||||||
|
if (content.style.display === 'none') {
|
||||||
|
content.style.display = 'block';
|
||||||
|
icon.style.transform = 'rotate(90deg)';
|
||||||
|
} else {
|
||||||
|
content.style.display = 'none';
|
||||||
|
icon.style.transform = 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// URL-Kopier-Funktion
|
// URL-Kopier-Funktion
|
||||||
function copyToClipboard(inputId) {
|
function copyToClipboard(inputId) {
|
||||||
const input = document.getElementById(inputId);
|
const input = document.getElementById(inputId);
|
||||||
@@ -201,24 +264,56 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// URLs mit aktueller Domain aktualisieren (Port 3334 für Check-in)
|
// URLs mit aktueller Domain aktualisieren (Port 3334 für Check-in)
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
const userId = '<%= user.id %>';
|
const userId = '<%= user.id %>';
|
||||||
|
|
||||||
|
// Interne URLs: Konstruiere aus window.location.origin (Port 3334 für Check-in)
|
||||||
const baseUrl = window.location.origin;
|
const baseUrl = window.location.origin;
|
||||||
// Check-in URLs verwenden Port 3334
|
let internalBaseUrl;
|
||||||
// Ersetze Port in URL oder füge Port hinzu falls nicht vorhanden
|
|
||||||
let checkinBaseUrl;
|
|
||||||
if (baseUrl.match(/:\d+$/)) {
|
if (baseUrl.match(/:\d+$/)) {
|
||||||
// Port vorhanden - ersetze ihn
|
// Port vorhanden - ersetze ihn
|
||||||
checkinBaseUrl = baseUrl.replace(/:\d+$/, ':3334');
|
internalBaseUrl = baseUrl.replace(/:\d+$/, ':3334');
|
||||||
} else {
|
} else {
|
||||||
// Kein Port - füge Port hinzu
|
// Kein Port - füge Port hinzu
|
||||||
const url = new URL(baseUrl);
|
const url = new URL(baseUrl);
|
||||||
checkinBaseUrl = `${url.protocol}//${url.hostname}:3334`;
|
internalBaseUrl = `${url.protocol}//${url.hostname}:3334`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setze interne URLs
|
||||||
|
const checkinInputInternal = document.getElementById('checkinUrlInternal');
|
||||||
|
const checkoutInputInternal = document.getElementById('checkoutUrlInternal');
|
||||||
|
if (checkinInputInternal) checkinInputInternal.value = `${internalBaseUrl}/api/checkin/${userId}`;
|
||||||
|
if (checkoutInputInternal) checkoutInputInternal.value = `${internalBaseUrl}/api/checkout/${userId}`;
|
||||||
|
|
||||||
|
// Externe URLs: Versuche Root URL von API zu laden
|
||||||
|
let externalBaseUrl = null;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/checkin-root-url');
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.root_url && result.root_url.trim() !== '') {
|
||||||
|
externalBaseUrl = result.root_url.trim();
|
||||||
|
// Stelle sicher, dass kein trailing slash vorhanden ist
|
||||||
|
externalBaseUrl = externalBaseUrl.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Fehler beim Laden der externen Root URL:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeige externe Sektion nur an, wenn Root URL konfiguriert ist
|
||||||
|
const externalSection = document.getElementById('externalUrlsSection');
|
||||||
|
if (externalBaseUrl) {
|
||||||
|
// Setze externe URLs
|
||||||
|
const checkinInputExternal = document.getElementById('checkinUrlExternal');
|
||||||
|
const checkoutInputExternal = document.getElementById('checkoutUrlExternal');
|
||||||
|
if (checkinInputExternal) checkinInputExternal.value = `${externalBaseUrl}/api/checkin/${userId}`;
|
||||||
|
if (checkoutInputExternal) checkoutInputExternal.value = `${externalBaseUrl}/api/checkout/${userId}`;
|
||||||
|
|
||||||
|
// Zeige externe Sektion an
|
||||||
|
if (externalSection) externalSection.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
// Verstecke externe Sektion
|
||||||
|
if (externalSection) externalSection.style.display = 'none';
|
||||||
}
|
}
|
||||||
const checkinInput = document.getElementById('checkinUrl');
|
|
||||||
const checkoutInput = document.getElementById('checkoutUrl');
|
|
||||||
if (checkinInput) checkinInput.value = `${checkinBaseUrl}/api/checkin/${userId}`;
|
|
||||||
if (checkoutInput) checkoutInput.value = `${checkinBaseUrl}/api/checkout/${userId}`;
|
|
||||||
|
|
||||||
// Rollenwechsel-Handler
|
// Rollenwechsel-Handler
|
||||||
const roleSwitcher = document.getElementById('roleSwitcher');
|
const roleSwitcher = document.getElementById('roleSwitcher');
|
||||||
|
|||||||
Reference in New Issue
Block a user