ada1b51f33
- docs/api.md: komplette API-Referenz (1375 Zeilen, alle Endpunkte) - docs/architecture.md: Tech-Stack, DB-Schema, RLS-Architektur, Auth-Flow - docs/deployment.md: Setup, nginx, systemd, update.sh, Backup/Rollback - docs/development.md: Dev-Setup, Test-Workflow, Code-Konventionen, Fallstricke Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1376 lines
25 KiB
Markdown
1376 lines
25 KiB
Markdown
# TimeMaster – API-Referenz
|
||
|
||
Alle Endpunkte sind unter dem Präfix `/api/v1` erreichbar.
|
||
Authentifizierung: `Authorization: Bearer <access_token>` (JWT, 30 min gültig).
|
||
In der Entwicklungsumgebung ist die interaktive Dokumentation unter `/docs` (Swagger UI) und `/redoc` verfügbar.
|
||
|
||
---
|
||
|
||
## Inhaltsverzeichnis
|
||
|
||
1. [System](#system)
|
||
2. [Authentifizierung & 2FA](#authentifizierung--2fa)
|
||
3. [Benutzer](#benutzer)
|
||
4. [Firmen & Abteilungen](#firmen--abteilungen)
|
||
5. [Zeiterfassung](#zeiterfassung)
|
||
6. [Abwesenheiten](#abwesenheiten)
|
||
7. [Dashboard & Reports](#dashboard--reports)
|
||
8. [Audit-Log](#audit-log)
|
||
9. [Kiosk-Geräte](#kiosk-geräte)
|
||
10. [LDAP-Integration](#ldap-integration)
|
||
11. [SMTP-Konfiguration](#smtp-konfiguration)
|
||
12. [CalDAV-Integration](#caldav-integration)
|
||
13. [Busylight-Integration](#busylight-integration)
|
||
14. [Kimai-Import](#kimai-import)
|
||
|
||
---
|
||
|
||
## Rollen-Hierarchie
|
||
|
||
| Rolle | Beschreibung |
|
||
|-------|-------------|
|
||
| `SUPER_ADMIN` | Systemadministrator, sieht alle Firmen, umgeht RLS |
|
||
| `COMPANY_ADMIN` | Firmenadministrator, verwaltet alles in seiner Firma |
|
||
| `HR` | Personalverwaltung, genehmigt Abwesenheiten, sieht alle Mitarbeiter |
|
||
| `MANAGER` | Teamleiter, genehmigt Einträge/Abwesenheiten seiner Mitarbeiter |
|
||
| `EMPLOYEE` | Normaler Mitarbeiter, sieht nur eigene Daten |
|
||
|
||
---
|
||
|
||
## System
|
||
|
||
### `GET /health`
|
||
|
||
Gesundheitsprüfung ohne Authentifizierung.
|
||
|
||
**Response:**
|
||
```json
|
||
{ "status": "ok", "app": "TimeMaster", "env": "production" }
|
||
```
|
||
|
||
---
|
||
|
||
## Authentifizierung & 2FA
|
||
|
||
Router-Präfix: `/auth`
|
||
|
||
### `POST /auth/register`
|
||
|
||
Neue Firma + Admin-Konto anlegen.
|
||
|
||
- **Rate-Limit:** 3/Stunde
|
||
- **Erforderliche Rolle:** keine (öffentlich)
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"company_name": "Acme GmbH",
|
||
"email": "admin@acme.de",
|
||
"password": "Sicher123",
|
||
"first_name": "Max",
|
||
"last_name": "Mustermann"
|
||
}
|
||
```
|
||
|
||
**Response `201`:** `TokenResponse` (siehe unten)
|
||
|
||
**Fehler:**
|
||
- `400` – E-Mail bereits registriert
|
||
- `429` – Rate-Limit überschritten
|
||
|
||
---
|
||
|
||
### `POST /auth/login`
|
||
|
||
Einloggen mit E-Mail + Passwort. Gibt bei aktivem TOTP einen `partial_token` zurück.
|
||
|
||
- **Rate-Limit:** 10/Minute
|
||
- **Erforderliche Rolle:** keine
|
||
|
||
**Request:**
|
||
```json
|
||
{ "email": "user@example.de", "password": "Passwort123" }
|
||
```
|
||
|
||
**Response:** `TokenResponse`
|
||
|
||
```json
|
||
{
|
||
"access_token": "<jwt>",
|
||
"refresh_token": "<opaque>",
|
||
"token_type": "bearer",
|
||
"requires_totp": false,
|
||
"partial_token": null
|
||
}
|
||
```
|
||
|
||
Wenn `requires_totp: true`, wird `partial_token` zurückgegeben und `access_token`/`refresh_token` sind leer. Anschließend `POST /auth/totp/login` aufrufen.
|
||
|
||
**Fehler:**
|
||
- `401` – Ungültige Anmeldedaten
|
||
- `403` – Konto deaktiviert
|
||
|
||
---
|
||
|
||
### `POST /auth/refresh`
|
||
|
||
Access Token erneuern (Token Rotation – der alte Refresh Token wird dabei ungültig).
|
||
|
||
**Request:** `{ "refresh_token": "..." }`
|
||
**Response:** `TokenResponse`
|
||
|
||
**Fehler:** `401` – Ungültiger oder abgelaufener Token
|
||
|
||
---
|
||
|
||
### `POST /auth/logout`
|
||
|
||
Sitzung beenden (Refresh Token invalidieren).
|
||
|
||
**Request:** `{ "refresh_token": "..." }`
|
||
**Response:** `{ "message": "Logged out successfully" }`
|
||
|
||
---
|
||
|
||
### `POST /auth/password-reset`
|
||
|
||
Passwort-Reset anfordern (Link per E-Mail).
|
||
|
||
- **Rate-Limit:** 3/Stunde
|
||
- **Erforderliche Rolle:** keine
|
||
|
||
**Request:** `{ "email": "user@example.de" }`
|
||
|
||
**Fehler:** `400` – Konto wird über LDAP verwaltet (Passwort-Reset nicht möglich)
|
||
|
||
---
|
||
|
||
### `POST /auth/password-reset/confirm`
|
||
|
||
Neues Passwort setzen mit Reset-Token aus der E-Mail.
|
||
|
||
- **Rate-Limit:** 5/Stunde
|
||
|
||
**Request:**
|
||
```json
|
||
{ "token": "<reset-token>", "new_password": "NeuesPasswort123" }
|
||
```
|
||
|
||
**Fehler:** `400` – Token ungültig/abgelaufen, Passwort zu schwach
|
||
|
||
---
|
||
|
||
### `POST /auth/invite/accept`
|
||
|
||
Einladungstoken einlösen und Konto aktivieren.
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"token": "<invite-token>",
|
||
"password": "MeinPasswort1",
|
||
"first_name": "Anna",
|
||
"last_name": "Beispiel"
|
||
}
|
||
```
|
||
|
||
**Response:** `UserOut`
|
||
|
||
---
|
||
|
||
### `GET /auth/me`
|
||
|
||
Eigenes Profil abrufen.
|
||
|
||
- **Erforderliche Rolle:** beliebig (eingeloggt)
|
||
|
||
**Response:** `UserOut`
|
||
|
||
---
|
||
|
||
### `POST /auth/change-password`
|
||
|
||
Passwort ändern (benötigt aktuelles Passwort).
|
||
|
||
- **Erforderliche Rolle:** beliebig
|
||
|
||
**Request:**
|
||
```json
|
||
{ "current_password": "Alt123", "new_password": "Neu456" }
|
||
```
|
||
|
||
**Validierung:** min. 8 Zeichen, 1 Großbuchstabe, 1 Ziffer
|
||
|
||
---
|
||
|
||
### TOTP / Zwei-Faktor-Authentifizierung
|
||
|
||
#### `POST /auth/totp/setup`
|
||
|
||
TOTP-Secret generieren und `otpauth://` URI für den Authenticator zurückgeben. Noch nicht aktiviert.
|
||
|
||
**Response:** `{ "secret": "BASE32...", "otpauth_uri": "otpauth://..." }`
|
||
|
||
---
|
||
|
||
#### `POST /auth/totp/setup/save`
|
||
|
||
Generiertes Secret in DB persistieren (ohne Aktivierung).
|
||
|
||
---
|
||
|
||
#### `POST /auth/totp/confirm`
|
||
|
||
Ersten Code bestätigen und 2FA aktivieren.
|
||
|
||
**Request:** `{ "code": "123456" }`
|
||
|
||
**Fehler:** `400` – Kein Secret vorhanden / Ungültiger Code
|
||
|
||
---
|
||
|
||
#### `POST /auth/totp/disable`
|
||
|
||
2FA deaktivieren. Benötigt aktuelles Passwort + gültigen TOTP-Code.
|
||
|
||
**Request:**
|
||
```json
|
||
{ "password": "MeinPasswort", "code": "123456" }
|
||
```
|
||
|
||
---
|
||
|
||
#### `POST /auth/totp/login`
|
||
|
||
Zweiter Login-Schritt bei aktivem TOTP: `partial_token` + TOTP-Code → vollständige Tokens.
|
||
|
||
- **Rate-Limit:** 10/Minute
|
||
|
||
**Request:**
|
||
```json
|
||
{ "partial_token": "<partial-jwt>", "code": "123456" }
|
||
```
|
||
|
||
**Response:** `TokenResponse`
|
||
|
||
**Fehler:** `401` – Ungültiger partial_token | `400` – Ungültiger Code
|
||
|
||
---
|
||
|
||
## Benutzer
|
||
|
||
Router-Präfix: `/users`
|
||
|
||
### `GET /users/`
|
||
|
||
Alle Benutzer der eigenen Firma auflisten.
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
- **Query-Parameter:**
|
||
- `skip` (int, default 0)
|
||
- `limit` (int, default 50, max 500)
|
||
- `active_only` (bool, default true)
|
||
- `search` (str, max 100) – sucht in Name, E-Mail und Personalnummer
|
||
|
||
**Response:**
|
||
```json
|
||
{ "total": 42, "items": [ /* UserOut */ ] }
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /users/invite`
|
||
|
||
Neuen Benutzer einladen (E-Mail mit Einladungslink).
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"email": "mitarbeiter@example.de",
|
||
"first_name": "Maria",
|
||
"last_name": "Muster",
|
||
"role": "EMPLOYEE",
|
||
"department_id": null,
|
||
"personnel_number": "0042"
|
||
}
|
||
```
|
||
|
||
**Response `201`:** `UserOut`
|
||
|
||
**Fehler:** `400` – E-Mail bereits vergeben | `422` – Personalnummer bereits belegt
|
||
|
||
---
|
||
|
||
### `GET /users/me`
|
||
|
||
Eigenes Profil (identisch mit `GET /auth/me`).
|
||
|
||
---
|
||
|
||
### `GET /users/next-personnel-number`
|
||
|
||
Nächste freie Personalnummer vorschlagen (Counter wird nicht erhöht).
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Response:** `{ "next": "0043" }`
|
||
|
||
---
|
||
|
||
### `GET /users/by-personnel/{number}`
|
||
|
||
Benutzer per Personalnummer suchen (Lookup für externe Integrationen).
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Fehler:** `404` – Nicht gefunden
|
||
|
||
---
|
||
|
||
### `GET /users/import-template.csv`
|
||
|
||
CSV-Vorlage für Massen-Import herunterladen.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Response:** CSV-Datei
|
||
|
||
---
|
||
|
||
### `POST /users/import/preview`
|
||
|
||
CSV-Import vorschauen (keine DB-Änderungen).
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
- **Content-Type:** `multipart/form-data`, Feld `file`
|
||
|
||
**Response:** `UserImportResult` mit Zeilen-Status (create/reactivate/error)
|
||
|
||
---
|
||
|
||
### `POST /users/import/apply`
|
||
|
||
CSV-Import durchführen.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Verhalten:**
|
||
- E-Mail existiert aktiv → Fehler
|
||
- E-Mail existiert, aber deaktiviert → Reaktivieren mit neuen Daten
|
||
- Leere Personalnummer → Auto-Vergabe
|
||
|
||
---
|
||
|
||
### `GET /users/{user_id}`
|
||
|
||
Einzelnen Benutzer abrufen.
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
### `PATCH /users/{user_id}`
|
||
|
||
Benutzer bearbeiten.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Editierbare Felder:** `first_name`, `last_name`, `role`, `department_id`, `work_schedule_id`, `kuerzel`, `personnel_number`, `can_manual_time_entry`
|
||
|
||
---
|
||
|
||
### `POST /users/{user_id}/deactivate`
|
||
|
||
Benutzer deaktivieren (kein Hard-Delete, `is_active = false`, Personalnummer bleibt reserviert).
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
### `POST /users/{user_id}/reactivate`
|
||
|
||
Deaktivierten Benutzer reaktivieren.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
### `POST /users/{user_id}/kiosk-pin`
|
||
|
||
Kiosk-PIN setzen. Eigene PIN: jeder Benutzer. Fremde PIN: COMPANY_ADMIN/SUPER_ADMIN.
|
||
|
||
**Request:** `{ "pin": "1234" }`
|
||
|
||
---
|
||
|
||
## Firmen & Abteilungen
|
||
|
||
Router-Präfix: `/companies`
|
||
|
||
### `GET /companies/me`
|
||
|
||
Eigene Firmen-Daten abrufen.
|
||
|
||
- **Erforderliche Rolle:** beliebig
|
||
|
||
**Response:** `CompanyOut`
|
||
|
||
---
|
||
|
||
### `PATCH /companies/me`
|
||
|
||
Firma bearbeiten.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Editierbare Felder:** `name`, `country`, `state`, `logo_url`, `settings`, `personnel_number_required`, `personnel_number_mode`, `sick_note_required_after_days`
|
||
|
||
---
|
||
|
||
### `GET /companies/me/departments`
|
||
|
||
Alle Abteilungen auflisten.
|
||
|
||
- **Erforderliche Rolle:** beliebig
|
||
|
||
---
|
||
|
||
### `POST /companies/me/departments`
|
||
|
||
Neue Abteilung anlegen.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Request:** `{ "name": "Entwicklung", "description": null }`
|
||
|
||
---
|
||
|
||
### `PATCH /companies/me/departments/{dept_id}`
|
||
|
||
Abteilung bearbeiten.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
### `DELETE /companies/me/departments/{dept_id}`
|
||
|
||
Abteilung löschen.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Fehler:** `404` – Nicht gefunden
|
||
|
||
---
|
||
|
||
## Zeiterfassung
|
||
|
||
Router-Präfix: `/time`
|
||
|
||
### Stempeluhr
|
||
|
||
#### `POST /time/stamp-in`
|
||
|
||
Einstempeln – startet einen neuen Zeiterfassungseintrag.
|
||
|
||
- **Erforderliche Rolle:** beliebig
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"source": "web",
|
||
"project_id": null,
|
||
"note": "Projekt X"
|
||
}
|
||
```
|
||
|
||
**Response:** `TimeEntryWithWarnings`
|
||
```json
|
||
{
|
||
"entry": { /* TimeEntryOut */ },
|
||
"warnings": ["Bei mehr als 6h Anwesenheit sind mind. 30 min Pause vorgeschrieben – ArbZG §4"]
|
||
}
|
||
```
|
||
|
||
**Fehler:** `409` – Bereits eingestempelt
|
||
|
||
---
|
||
|
||
#### `POST /time/stamp-out`
|
||
|
||
Ausstempeln – schließt den offenen Eintrag. Enthält ArbZG-Warnungen falls zutreffend.
|
||
|
||
**Request:** `{ "note": null }`
|
||
|
||
**Fehler:** `404` – Kein offener Eintrag
|
||
|
||
---
|
||
|
||
#### `POST /time/break-start`
|
||
|
||
Pause beginnen.
|
||
|
||
**Fehler:** `404` – Kein offener Eintrag | `409` – Bereits in Pause
|
||
|
||
---
|
||
|
||
#### `POST /time/break-end`
|
||
|
||
Pause beenden.
|
||
|
||
---
|
||
|
||
### Einträge
|
||
|
||
#### `GET /time/today`
|
||
|
||
Alle Einträge des heutigen Tages des eingeloggten Benutzers.
|
||
|
||
**Response:** `list[TimeEntryOut]`
|
||
|
||
---
|
||
|
||
#### `GET /time/entries`
|
||
|
||
Zeiterfassungseinträge auflisten.
|
||
|
||
- **Query-Parameter:**
|
||
- `user_id` (UUID, optional) – EMPLOYEE sieht nur eigene
|
||
- `date_from` / `date_to` (date)
|
||
- `status` (`pending` | `approved` | `rejected`)
|
||
- `skip` / `limit` (int)
|
||
|
||
**Response:** `{ "total": int, "items": [TimeEntryOut] }`
|
||
|
||
---
|
||
|
||
#### `POST /time/entries`
|
||
|
||
Manuellen Eintrag anlegen.
|
||
|
||
- Mitarbeiter nur mit Berechtigung `can_manual_time_entry`
|
||
- MANAGER/HR/ADMIN können `user_id` setzen
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"date": "2026-05-23",
|
||
"start_time": "08:00",
|
||
"end_time": "17:00",
|
||
"break_minutes": 30,
|
||
"note": "Nacherfassung"
|
||
}
|
||
```
|
||
|
||
**Response:** `TimeEntryWithWarnings`
|
||
|
||
---
|
||
|
||
#### `PATCH /time/entries/{entry_id}`
|
||
|
||
Eintrag korrigieren. Mitarbeiter: nur eigene ausstehende. Manager: alle der Company.
|
||
|
||
**Request:** `{ "start_time": "08:30", "correction_note": "..." }`
|
||
|
||
---
|
||
|
||
#### `POST /time/entries/{entry_id}/approve`
|
||
|
||
Eintrag genehmigen. Self-Approval-Schutz: Eigene Einträge können nicht selbst genehmigt werden.
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
#### `POST /time/entries/{entry_id}/reject`
|
||
|
||
Eintrag ablehnen.
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Request:** `{ "rejection_note": "Bitte korrigieren" }`
|
||
|
||
---
|
||
|
||
#### `DELETE /time/entries/{entry_id}`
|
||
|
||
Eintrag löschen. Mitarbeiter: nur eigene offene/ausstehende. Manager: alle außer genehmigte (außer HR/Admin).
|
||
|
||
**Response:** `204 No Content`
|
||
|
||
---
|
||
|
||
### Überstundenkonto
|
||
|
||
#### `GET /time/balance/me`
|
||
|
||
Eigenes Überstundenkonto.
|
||
|
||
- **Query-Parameter:** `period_start` / `period_end` (date, optional)
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"user_id": "uuid",
|
||
"period_start": "2026-05-01",
|
||
"period_end": "2026-05-23",
|
||
"total_hours_worked": 92.5,
|
||
"expected_hours": 88.0,
|
||
"overtime_hours": 4.5,
|
||
"approved_entries": 18,
|
||
"pending_entries": 2
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### `GET /time/balance/{user_id}`
|
||
|
||
Überstundenkonto für einen Benutzer.
|
||
|
||
- **Erforderliche Rolle:** EMPLOYEE sieht nur sich selbst, sonst MANAGER+
|
||
|
||
---
|
||
|
||
### Arbeitspläne
|
||
|
||
#### `GET /time/schedules`
|
||
|
||
Alle Arbeitspläne der Firma.
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
#### `POST /time/schedules`
|
||
|
||
Neuen Arbeitsplan anlegen.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"name": "Vollzeit",
|
||
"mon_h": 8.0, "tue_h": 8.0, "wed_h": 8.0, "thu_h": 8.0, "fri_h": 8.0,
|
||
"sat_h": 0.0, "sun_h": 0.0,
|
||
"valid_from": "2026-01-01"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### `PATCH /time/schedules/{schedule_id}`
|
||
|
||
Arbeitsplan bearbeiten.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
#### `DELETE /time/schedules/{schedule_id}`
|
||
|
||
Arbeitsplan löschen.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
## Abwesenheiten
|
||
|
||
Kein Router-Präfix (direkt unter `/api/v1/`).
|
||
|
||
### Abwesenheitstypen
|
||
|
||
#### `GET /absence-types/`
|
||
|
||
Alle Abwesenheitstypen der Firma.
|
||
|
||
- **Erforderliche Rolle:** beliebig
|
||
|
||
---
|
||
|
||
#### `POST /absence-types/`
|
||
|
||
Neuen Abwesenheitstyp anlegen.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"name": "Urlaub",
|
||
"color": "#3B82F6",
|
||
"category": "vacation",
|
||
"requires_approval": true,
|
||
"deducts_vacation": true,
|
||
"affects_overtime_balance": false,
|
||
"requires_certificate": false,
|
||
"certificate_after_days": 3,
|
||
"is_paid": true,
|
||
"max_days_per_year": 30
|
||
}
|
||
```
|
||
|
||
**Kategorien:** `vacation` | `sick` | `overtime_comp` | `training` | `business_trip` | `other`
|
||
|
||
---
|
||
|
||
#### `PATCH /absence-types/{type_id}`
|
||
|
||
Abwesenheitstyp bearbeiten.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
### Feiertage
|
||
|
||
#### `GET /public-holidays/`
|
||
|
||
Feiertage auflisten.
|
||
|
||
- **Query-Parameter:** `year` (int, Pflicht), `country` (default "DE"), `state` (optional, z.B. "BY")
|
||
|
||
---
|
||
|
||
#### `POST /public-holidays/`
|
||
|
||
Feiertag manuell anlegen.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
### Abwesenheiten
|
||
|
||
#### `GET /absences/calendar`
|
||
|
||
Team-Kalender: alle Abwesenheiten im Zeitraum.
|
||
|
||
- **Query-Parameter:** `year` (int, Pflicht), `month` (1–12, optional)
|
||
|
||
---
|
||
|
||
#### `GET /absences/balance`
|
||
|
||
Eigenes Urlaubskonto (Jahr erforderlich).
|
||
|
||
- **Query-Parameter:** `year` (int, Pflicht)
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"user_id": "uuid",
|
||
"year": 2026,
|
||
"base_days": 30,
|
||
"special_days": 0,
|
||
"carried_over_days": 5,
|
||
"carried_over_expires_at": "2026-03-31",
|
||
"carried_over_expired": false,
|
||
"used_days": 12.0,
|
||
"pending_days": 3.0,
|
||
"remaining_days": 20.0
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### `GET /absences/balance/{user_id}`
|
||
|
||
Urlaubskonto eines Mitarbeiters.
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
#### `POST /absences/quick-sick`
|
||
|
||
Sofort-Krankmeldung. Automatisch genehmigt, nutzt den ersten aktiven SICK-Abwesenheitstyp.
|
||
|
||
**Request:**
|
||
```json
|
||
{ "start_date": "2026-05-23", "end_date": "2026-05-23" }
|
||
```
|
||
|
||
---
|
||
|
||
#### `GET /absences/sick-stats`
|
||
|
||
Krankheitsstatistik (rolling 12 Monate) inkl. Bradford-Faktor.
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
- **Query-Parameter:** `user_id` (optional), `ref_date` (optional, default heute)
|
||
|
||
**Response:** Liste von `SickStatsOut`:
|
||
```json
|
||
[{
|
||
"user_id": "uuid",
|
||
"bradford_factor": 7.2,
|
||
"episodes": 3,
|
||
"total_days": 9.0,
|
||
"certificate_overdue": false
|
||
}]
|
||
```
|
||
|
||
---
|
||
|
||
#### `GET /absences/overtime-balance`
|
||
|
||
Eigenes Überstunden-Ausgleichskonto.
|
||
|
||
---
|
||
|
||
#### `GET /absences/`
|
||
|
||
Abwesenheiten auflisten.
|
||
|
||
- **Query-Parameter:** `user_id`, `type_id`, `status`, `year` (alle optional)
|
||
- EMPLOYEE sieht nur eigene Abwesenheiten
|
||
|
||
---
|
||
|
||
#### `POST /absences/`
|
||
|
||
Abwesenheitsantrag stellen. Mit `for_user_id` können MANAGER+ für andere beantragen.
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"type_id": "uuid",
|
||
"start_date": "2026-06-01",
|
||
"end_date": "2026-06-05",
|
||
"half_day_start": false,
|
||
"half_day_end": false,
|
||
"substitute_id": null,
|
||
"note": "Jahresurlaub"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### `GET /absences/{absence_id}`
|
||
|
||
Einzelne Abwesenheit abrufen.
|
||
|
||
---
|
||
|
||
#### `PATCH /absences/{absence_id}`
|
||
|
||
Ausstehenden Antrag bearbeiten. Nur solange Status `pending`.
|
||
|
||
---
|
||
|
||
#### `DELETE /absences/{absence_id}`
|
||
|
||
Eigenen ausstehenden Antrag stornieren.
|
||
|
||
**Response:** `204 No Content`
|
||
|
||
---
|
||
|
||
#### `POST /absences/{absence_id}/approve`
|
||
|
||
Abwesenheit genehmigen. Self-Approval-Schutz aktiv.
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
#### `POST /absences/{absence_id}/reject`
|
||
|
||
Abwesenheit ablehnen.
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Request:** `{ "rejection_reason": "Zu viele gleichzeitige Abwesenheiten" }`
|
||
|
||
---
|
||
|
||
#### `PATCH /absences/{absence_id}/certificate`
|
||
|
||
AU-Bescheinigung als eingegangen markieren.
|
||
|
||
- **Erforderliche Rolle:** HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Request:** `{ "received_at": "2026-05-23" }`
|
||
|
||
---
|
||
|
||
#### `PATCH /absences/balance/{user_id}`
|
||
|
||
Urlaubskonto eines Mitarbeiters anpassen (Grundurlaub, Sondertage, Resturlaub).
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
- **Query-Parameter:** `year` (int, Pflicht)
|
||
|
||
---
|
||
|
||
## Dashboard & Reports
|
||
|
||
Kein Router-Präfix.
|
||
|
||
### Dashboard
|
||
|
||
#### `GET /dashboard/me`
|
||
|
||
Mitarbeiter-Dashboard: eigene Stunden, Urlaubsstand, Status heute.
|
||
|
||
**Response:** `EmployeeDashboard`
|
||
|
||
---
|
||
|
||
#### `GET /dashboard/team`
|
||
|
||
Team-Dashboard: Anwesenheit heute, ausstehende Genehmigungen.
|
||
|
||
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
#### `GET /dashboard/company`
|
||
|
||
Unternehmens-Dashboard: Gesamtübersicht, Überstunden, kommende Abwesenheiten.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
### Reports (JSON)
|
||
|
||
#### `GET /reports/time`
|
||
|
||
Zeiterfassungsbericht. EMPLOYEE sieht nur eigene Einträge.
|
||
|
||
- **Query-Parameter:** `date_from`, `date_to` (default: aktueller Monat), `user_id` (optional)
|
||
|
||
---
|
||
|
||
#### `GET /reports/absences`
|
||
|
||
Abwesenheitsbericht. EMPLOYEE sieht nur eigene Abwesenheiten.
|
||
|
||
---
|
||
|
||
#### `GET /reports/overtime`
|
||
|
||
Überstundenbericht.
|
||
|
||
---
|
||
|
||
#### `GET /reports/overtime/detail`
|
||
|
||
Erweiterter Überstundenbericht mit Wochen- und Tagesaufschlüsselung.
|
||
|
||
---
|
||
|
||
### Export
|
||
|
||
#### `GET /reports/time/export`
|
||
|
||
Zeiterfassungsbericht herunterladen.
|
||
|
||
- **Query-Parameter:** `format` (`csv` | `xlsx` | `pdf`, default `csv`)
|
||
|
||
---
|
||
|
||
#### `GET /reports/absences/export`
|
||
|
||
Abwesenheitsbericht herunterladen.
|
||
|
||
- **Query-Parameter:** `format` (`csv` | `xlsx` | `pdf`)
|
||
|
||
---
|
||
|
||
#### `GET /reports/overtime/export`
|
||
|
||
Überstundenbericht herunterladen (Detailansicht).
|
||
|
||
- **Query-Parameter:** `format` (`csv` | `xlsx` | `pdf`)
|
||
|
||
---
|
||
|
||
## Audit-Log
|
||
|
||
Kein Router-Präfix.
|
||
|
||
### `GET /audit-logs`
|
||
|
||
Audit-Log-Einträge auflisten.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
- SUPER_ADMIN sieht alle Firmen, COMPANY_ADMIN nur die eigene
|
||
- **Query-Parameter:**
|
||
- `user_id` (UUID, optional)
|
||
- `action` (string, Teilstring-Suche)
|
||
- `entity_type` (string, exakt)
|
||
- `date_from` / `date_to` (datetime)
|
||
- `limit` (default 50, max 500)
|
||
- `offset` (int)
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"total": 1234,
|
||
"items": [{
|
||
"id": "uuid",
|
||
"user_id": "uuid",
|
||
"user_name": "Max Mustermann",
|
||
"action": "absence_approved",
|
||
"entity_type": "absence",
|
||
"entity_id": "uuid",
|
||
"old_value": null,
|
||
"new_value": { "status": "approved" },
|
||
"ip_address": "192.168.1.1",
|
||
"created_at": "2026-05-23T10:00:00Z"
|
||
}]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `GET /audit-logs/actions`
|
||
|
||
Alle vorhandenen Action-Werte für Filter-Dropdown.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Response:** `["absence_approved", "busylight_token_rotated", ...]`
|
||
|
||
---
|
||
|
||
### `GET /audit-logs/entity-types`
|
||
|
||
Alle vorhandenen Entity-Typen für Filter-Dropdown.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
## Kiosk-Geräte
|
||
|
||
Router-Präfix: `/kiosk`
|
||
|
||
### `GET /kiosk/devices`
|
||
|
||
Alle registrierten Kiosk-Geräte der Firma.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
### `POST /kiosk/devices`
|
||
|
||
Neues Kiosk-Gerät registrieren. Token wird nur einmalig zurückgegeben.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"name": "Eingang Berlin",
|
||
"location": "Hauptgebäude"
|
||
}
|
||
```
|
||
|
||
**Response `201`:** `KioskDeviceCreated` (enthält einmalig das Klartext-Token)
|
||
|
||
---
|
||
|
||
### `GET /kiosk/devices/{device_id}`
|
||
|
||
Einzelnes Gerät abrufen.
|
||
|
||
---
|
||
|
||
### `PATCH /kiosk/devices/{device_id}`
|
||
|
||
Gerät bearbeiten (Name, Standort).
|
||
|
||
---
|
||
|
||
### `POST /kiosk/devices/{device_id}/rotate-token`
|
||
|
||
Token rotieren – das alte Token wird sofort ungültig.
|
||
|
||
**Response:** `KioskDeviceCreated` (neues Token)
|
||
|
||
---
|
||
|
||
### `DELETE /kiosk/devices/{device_id}`
|
||
|
||
Gerät löschen.
|
||
|
||
---
|
||
|
||
### `GET /kiosk/me`
|
||
|
||
Kiosk-Gerät authentifiziert sich und aktualisiert `last_seen_at`.
|
||
|
||
- **Authentifizierung:** Header `X-Kiosk-Token: <token>` (kein JWT)
|
||
|
||
---
|
||
|
||
## LDAP-Integration
|
||
|
||
Router-Präfix: `/ldap`
|
||
Alle Endpunkte erfordern: **COMPANY_ADMIN** oder **SUPER_ADMIN**
|
||
|
||
### `GET /ldap/config`
|
||
|
||
Aktuelle LDAP-Konfiguration abrufen.
|
||
|
||
---
|
||
|
||
### `POST /ldap/config`
|
||
|
||
LDAP-Konfiguration erstellen oder überschreiben.
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"enabled": true,
|
||
"host": "ldap.example.de",
|
||
"port": 389,
|
||
"use_ssl": false,
|
||
"use_tls": true,
|
||
"bind_dn": "cn=admin,dc=example,dc=de",
|
||
"bind_password": "secret",
|
||
"base_dn": "dc=example,dc=de",
|
||
"user_search_filter": "(objectClass=person)",
|
||
"attr_email": "mail",
|
||
"attr_firstname": "givenName",
|
||
"attr_lastname": "sn",
|
||
"attr_username": "uid",
|
||
"attr_department": "department"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `PATCH /ldap/config`
|
||
|
||
LDAP-Konfiguration teilweise bearbeiten.
|
||
|
||
---
|
||
|
||
### `POST /ldap/test`
|
||
|
||
LDAP-Verbindung testen.
|
||
|
||
**Response:** `{ "success": true, "message": "..." }`
|
||
|
||
---
|
||
|
||
### `GET /ldap/preview`
|
||
|
||
Ersten 50 LDAP-Benutzer zur Vorschau vor dem Sync.
|
||
|
||
---
|
||
|
||
### `POST /ldap/sync`
|
||
|
||
LDAP-Benutzer synchronisieren.
|
||
|
||
**Request:** `{ "default_role": "EMPLOYEE" }`
|
||
|
||
**Response:** `{ "created": 5, "updated": 12, "deactivated": 1, "errors": [] }`
|
||
|
||
---
|
||
|
||
## SMTP-Konfiguration
|
||
|
||
Router-Präfix: `/smtp`
|
||
Alle Endpunkte erfordern: **COMPANY_ADMIN** oder **SUPER_ADMIN**
|
||
|
||
### `GET /smtp/config`
|
||
|
||
SMTP-Konfiguration abrufen.
|
||
|
||
---
|
||
|
||
### `POST /smtp/config`
|
||
|
||
SMTP-Konfiguration erstellen oder überschreiben.
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"host": "smtp.example.de",
|
||
"port": 587,
|
||
"use_tls": false,
|
||
"use_starttls": true,
|
||
"username": "noreply@example.de",
|
||
"from_email": "noreply@example.de",
|
||
"from_name": "TimeMaster",
|
||
"password": "secret",
|
||
"is_enabled": true
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /smtp/test`
|
||
|
||
Test-E-Mail senden.
|
||
|
||
**Request:** `{ "to": "test@example.de" }`
|
||
|
||
**Fehler:** `502` – SMTP-Fehler
|
||
|
||
---
|
||
|
||
## CalDAV-Integration
|
||
|
||
Router-Präfix: `/caldav`
|
||
|
||
### Firmenkalender
|
||
|
||
#### `GET /caldav/company/config`
|
||
|
||
Firmen-CalDAV-Konfiguration abrufen.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
---
|
||
|
||
#### `POST /caldav/company/config`
|
||
|
||
Firmen-CalDAV-Konfiguration speichern.
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"enabled": true,
|
||
"principal_url": "https://cal.example.de/remote.php/dav",
|
||
"calendar_url": "https://cal.example.de/remote.php/dav/calendars/user/timemaster/",
|
||
"username": "user",
|
||
"password": "secret",
|
||
"calendar_display_name": "TimeMaster",
|
||
"verify_ssl": true
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### `POST /caldav/company/test`
|
||
|
||
Firmen-CalDAV-Verbindung testen.
|
||
|
||
---
|
||
|
||
#### `POST /caldav/company/resync`
|
||
|
||
Alle genehmigten Abwesenheiten neu in den Firmenkalender synchronisieren.
|
||
|
||
**Response:** `{ "synced": 42, "errors": 0 }`
|
||
|
||
---
|
||
|
||
### Persönlicher Kalender
|
||
|
||
#### `GET /caldav/user/config`
|
||
|
||
Persönliche CalDAV-Konfiguration (eigener Benutzer).
|
||
|
||
---
|
||
|
||
#### `POST /caldav/user/config`
|
||
|
||
Persönliche CalDAV-Konfiguration speichern.
|
||
|
||
---
|
||
|
||
#### `POST /caldav/user/test`
|
||
|
||
Persönliche CalDAV-Verbindung testen.
|
||
|
||
---
|
||
|
||
## Busylight-Integration
|
||
|
||
Kein Router-Präfix.
|
||
|
||
### Token-Verwaltung
|
||
|
||
#### `GET /companies/me/busylight-token`
|
||
|
||
Status des Busylight-Pull-Tokens.
|
||
|
||
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
|
||
|
||
**Response:** `{ "configured": true, "created_at": "2026-05-07T..." }`
|
||
|
||
---
|
||
|
||
#### `POST /companies/me/busylight-token/rotate`
|
||
|
||
Neues Busylight-Token generieren (altes wird ungültig). Token wird einmalig im Klartext zurückgegeben.
|
||
|
||
**Response:** `{ "token": "abc...", "created_at": "..." }`
|
||
|
||
---
|
||
|
||
#### `DELETE /companies/me/busylight-token`
|
||
|
||
Busylight-Token widerrufen.
|
||
|
||
---
|
||
|
||
### Pull-Endpunkt
|
||
|
||
#### `GET /busylight/users`
|
||
|
||
Anwesenheitsstatus aller Mitarbeiter mit Personalnummer.
|
||
|
||
- **Authentifizierung:** `Authorization: Bearer <busylight-token>` (kein JWT, firmenspezifisches Token)
|
||
- **Rate-Limit:** 60/Minute
|
||
- Nur Mitarbeiter mit gesetzter Personalnummer werden zurückgegeben
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"date": "2026-05-23",
|
||
"users": [{
|
||
"personnel_number": "0042",
|
||
"full_name": "Maria Muster",
|
||
"absences_today": [{ "type": "Urlaub", "category": "vacation" }]
|
||
}]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Kimai-Import
|
||
|
||
Router-Präfix: `/import`
|
||
Erforderliche Rolle: **HR**, **COMPANY_ADMIN**, **SUPER_ADMIN**
|
||
|
||
### `POST /import/kimai/preview`
|
||
|
||
Kimai-CSV-Export vorschauen (keine DB-Änderungen).
|
||
|
||
- **Content-Type:** `multipart/form-data`
|
||
- **Felder:** `user_id` (UUID als String), `file` (CSV-Datei)
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"preview": [{ "date": "...", "start": "...", "end": "...", "type": "time" }],
|
||
"time_count": 150,
|
||
"absence_count": 12,
|
||
"skip_count": 3,
|
||
"errors": []
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### `POST /import/kimai/apply`
|
||
|
||
Kimai-CSV-Import durchführen.
|
||
|
||
**Response:** `{ "time_imported": 150, "absence_imported": 12, "skipped": 3, "errors": [] }`
|