Zum Inhalt

Time Tracking Modul

Zweck

Das Time Tracking Modul bietet eine vollständige Zeiterfassungslösung für Mitarbeiter. Es ermöglicht Clock In/Out, manuelle Zeiteinträge, Pausen-Management, Abwesenheitsverwaltung, Zeitkonto-Verwaltung und Genehmigungs-Workflows.

Verantwortlichkeiten

Was gehört zum Modul

  • Zeiterfassung: Clock In/Out, manuelle Einträge, Live-Tracking
  • Pausen-Management: Start/Ende von Pausen, ArbZG-konforme Pausenerfassung
  • Abwesenheitsverwaltung: Urlaub, Krankheit, Sonderurlaub mit Kontingent-Verwaltung
  • Zeitkonto: Zielstunden, Saldo-Berechnung, manuelle Anpassungen
  • Genehmigungen: Überstunden-Genehmigungen, projektbezogene Zeiterfassung
  • Kalender-Integration: Zeiteinträge werden als Events im Kalender angezeigt

Was gehört nicht zum Modul

Implementierung

Backend- und Frontend-API sind modular aufgebaut; Details siehe interne Entwickler-Dokumentation.

Konfiguration

Entitlement

Das Modul erfordert das Entitlement module.time_tracking. Dieses wird pro Tenant aktiviert/deaktiviert.

Entitlement-Key: module.time_tracking

Environment Variables

Keine modulspezifischen ENV-Variablen erforderlich.

Defaults

  • Zielstunden pro Monat: 160 Stunden (40h/Woche × 4 Wochen)
  • Arbeitszeit-Typ: Vollzeit (FULL_TIME)
  • Wöchentliche Stunden: 40 Stunden

Abhängigkeiten

Optionale Abhängigkeiten

  • work-time-compliance: Für automatische Compliance-Prüfungen (Ruhezeiten, Schichtdauer, Pausen)
  • projects: Für projektbezogene Zeiterfassung (erfordert immer Genehmigung)

Core-Abhängigkeiten

  • calendar-core: Zeiteinträge werden als Events im Kalender angezeigt
  • members: Für Rollenprüfung (Admin/Manager für erweiterte Funktionen)
  • notifications: Für Benachrichtigungen bei Genehmigungen

API-Endpoints

Authentifizierung

Alle Endpoints erfordern Authentifizierung. Zwei Methoden werden unterstützt:

  1. Firebase ID Token (Standard für Web-App)
  2. API-Token (für externe Nutzung)

Header: Authorization: Bearer <token>

User-Endpoints

Status

GET /api/time-tracking/status

Gibt den aktuellen Status zurück (laufender Entry, heute-Stats).

Response:

{
  "isRunning": true,
  "runningEntry": {
    "id": "entry123",
    "clockIn": "2024-01-15T08:00:00Z",
    "status": "running",
    "entryType": "work"
  },
  "today": {
    "totalMinutes": 240,
    "entriesCount": 2
  }
}

Clock In

POST /api/time-tracking/clock-in

Startet die Zeiterfassung.

Request Body:

{
  "note": "Optional: Notiz",
  "projectId": "Optional: Projekt-ID"
}

Response:

{
  "message": "Clocked in successfully",
  "entry": {
    "id": "entry123",
    "clockIn": "2024-01-15T08:00:00Z",
    "status": "running",
    "entryType": "work",
    "projectId": "project123"
  }
}

Hinweis: Wenn projectId angegeben wird, wird beim Clock-Out der Status automatisch auf PENDING_APPROVAL gesetzt.

Fehler: - 409 ALREADY_CLOCKED_IN: Bereits eingestempelt

Clock Out

POST /api/time-tracking/clock-out

Beendet die Zeiterfassung.

Request Body:

{
  "note": "Optional: Notiz"
}

Response:

{
  "message": "Clocked out successfully",
  "entry": {
    "id": "entry123",
    "clockIn": "2024-01-15T08:00:00Z",
    "clockOut": "2024-01-15T17:00:00Z",
    "status": "completed",
    "durationMinutes": 540
  }
}

Status-Verhalten: - Ohne projectId: Status wird auf COMPLETED gesetzt - Mit projectId: Status wird auf PENDING_APPROVAL gesetzt (erfordert Genehmigung)

Fehler: - 409 NOT_CLOCKED_IN: Nicht eingestempelt

Time Entries auflisten

GET /api/time-tracking/entries?limit=50

Listet TimeEntries des aktuellen Users.

Query-Parameter: - limit (optional): Maximale Anzahl (Standard: 50, Maximum: 100)

Response:

{
  "entries": [
    {
      "id": "entry123",
      "clockIn": "2024-01-15T08:00:00Z",
      "clockOut": "2024-01-15T17:00:00Z",
      "status": "completed",
      "durationMinutes": 540,
      "entryType": "work",
      "note": "Arbeitszeit"
    }
  ],
  "count": 1
}

Laufenden Entry abrufen

GET /api/time-tracking/running

Gibt den aktuell laufenden Entry zurück (falls vorhanden).

Response:

{
  "isRunning": true,
  "entry": {
    "id": "entry123",
    "clockIn": "2024-01-15T08:00:00Z",
    "clockOut": null,
    "status": "running",
    "entryType": "work"
  }
}

Hinweis: Wenn kein laufender Entry vorhanden ist, wird isRunning: false und entry: null zurückgegeben.

Time Entry abrufen

GET /api/time-tracking/entries/:entryId

Lädt einen einzelnen TimeEntry.

Response:

{
  "entry": {
    "id": "entry123",
    "clockIn": "2024-01-15T08:00:00Z",
    "clockOut": "2024-01-15T17:00:00Z",
    "status": "completed",
    "durationMinutes": 540
  }
}

Fehler: - 404 NOT_FOUND: Eintrag nicht gefunden

Manuellen Time Entry erstellen

POST /api/time-tracking/entries

Erstellt einen manuellen TimeEntry.

Request Body:

{
  "clockIn": "2024-01-15T08:00:00Z",
  "clockOut": "2024-01-15T17:00:00Z",
  "entryType": "work",
  "projectId": "Optional: Projekt-ID",
  "note": "Optional: Notiz"
}

Validierung: - clockIn muss vor clockOut liegen - Maximale Dauer: 24 Stunden - Automatische Pausenabrechnung basierend auf ArbZG

Response:

{
  "message": "Eintrag erstellt",
  "entry": {
    "id": "entry123",
    "clockIn": "2024-01-15T08:00:00Z",
    "clockOut": "2024-01-15T17:00:00Z",
    "status": "completed",
    "durationMinutes": 495
  }
}

Status-Verhalten: - Ohne projectId: Status wird auf COMPLETED gesetzt - Mit projectId: Status wird auf PENDING_APPROVAL gesetzt (erfordert Genehmigung)

Fehler: - 400 VALIDATION_ERROR: Ungültige Zeiten oder Dauer

Time Entry aktualisieren

PUT /api/time-tracking/entries/:entryId

Aktualisiert einen TimeEntry.

Request Body:

{
  "clockIn": "2024-01-15T08:00:00Z",
  "clockOut": "2024-01-15T17:00:00Z",
  "entryType": "work",
  "note": "Aktualisierte Notiz"
}

Einschränkungen: - Nur eigene Einträge können bearbeitet werden - Laufende Einträge können nicht bearbeitet werden

Fehler: - 403 FORBIDDEN: Keine Berechtigung - 400 VALIDATION_ERROR: Laufende Einträge können nicht bearbeitet werden

Time Entry löschen

DELETE /api/time-tracking/entries/:entryId

Löscht einen TimeEntry.

Einschränkungen: - Nur eigene Einträge können gelöscht werden - Laufende Einträge können nicht gelöscht werden

Fehler: - 403 FORBIDDEN: Keine Berechtigung - 400 VALIDATION_ERROR: Laufende Einträge können nicht gelöscht werden

Pausen-Management

Pausen-Status abrufen

GET /api/time-tracking/break/status

Gibt den aktuellen Pausen-Status zurück.

Response:

{
  "isOnBreak": true,
  "breakEntry": {
    "id": "break123",
    "clockIn": "2024-01-15T12:00:00Z",
    "status": "running",
    "entryType": "break"
  },
  "workEntryBeforeBreak": {
    "id": "work123",
    "clockIn": "2024-01-15T08:00:00Z",
    "clockOut": "2024-01-15T12:00:00Z",
    "status": "completed",
    "entryType": "work"
  }
}

Pause starten

POST /api/time-tracking/break/start

Startet eine Pause (stoppt laufenden WORK-Entry, startet BREAK-Entry).

Request Body:

{
  "note": "Optional: Notiz"
}

Response:

{
  "message": "Pause gestartet",
  "workEntry": {
    "id": "work123",
    "clockOut": "2024-01-15T12:00:00Z",
    "status": "completed"
  },
  "breakEntry": {
    "id": "break123",
    "clockIn": "2024-01-15T12:00:00Z",
    "status": "running",
    "entryType": "break"
  }
}

Fehler: - 409 BREAK_ALREADY_RUNNING: Pause läuft bereits - 409 NO_WORK_ENTRY: Kein laufender Arbeitseintrag

Pause beenden

POST /api/time-tracking/break/end

Beendet eine Pause (stoppt BREAK-Entry, startet neuen WORK-Entry).

Request Body:

{
  "note": "Optional: Notiz"
}

Response:

{
  "message": "Pause beendet",
  "breakEntry": {
    "id": "break123",
    "clockOut": "2024-01-15T12:30:00Z",
    "status": "completed",
    "durationMinutes": 30
  },
  "workEntry": {
    "id": "work124",
    "clockIn": "2024-01-15T12:30:00Z",
    "status": "running",
    "entryType": "work"
  }
}

Fehler: - 409 NO_BREAK_RUNNING: Keine laufende Pause gefunden

Pausen-Vorschlag

GET /api/time-tracking/break-suggestion?date=2024-01-15

Berechnet einen Pausen-Vorschlag basierend auf ArbZG.

Query-Parameter: - date (optional): Datum für Berechnung (ISO 8601, Standard: heute)

Response:

{
  "suggestion": {
    "requiredMinutes": 30,
    "reason": "Bei mehr als 6 Stunden Arbeitszeit sind mindestens 30 Minuten Pause erforderlich (ArbZG). Bereits erfasst: 0 Minuten."
  }
}

ArbZG-Regeln: - > 6 Stunden: 30 Minuten Pause erforderlich - > 9 Stunden: 45 Minuten Pause erforderlich

Hinweis: Admin- und Manager-Endpoints sind verfügbar, erfordern jedoch entsprechende Rollenberechtigungen. Detaillierte Dokumentation ist nur für autorisierte Administratoren verfügbar.

Zeitkonto-Endpoints

Zeitkonto abrufen

GET /api/time-tracking/time-account/:year/:month

Lädt das Zeitkonto für einen Monat.

Path-Parameter: - year: Jahr (z.B. 2024) - month: Monat (1-12)

Response:

{
  "account": {
    "userId": "user123",
    "year": 2024,
    "month": 1,
    "targetHours": 160,
    "actualHours": 165.5,
    "timeTrackingHours": 165.5,
    "shiftHours": 0,
    "balanceHours": 5.5,
    "manualAdjustments": [],
    "complianceAdjustments": [],
    "createdAt": "2024-01-01T00:00:00Z",
    "updatedAt": "2024-01-31T23:59:59Z"
  }
}

Zeitkonto-Historie

GET /api/time-tracking/time-account/history?limit=12

Lädt die Historie der Zeitkonten.

Query-Parameter: - limit (optional): Anzahl Monate (Standard: 12, Maximum: 24)

Zielstunden abrufen (Admin)

GET /api/time-tracking/time-account/target/:userId

Lädt die Zielstunden für einen User (nur Admin/Manager).

Response:

{
  "target": {
    "userId": "user123",
    "monthlyTargetHours": 160,
    "employmentType": "full_time",
    "weeklyHours": 40,
    "updatedAt": "2024-01-01T00:00:00Z",
    "updatedBy": "admin123"
  }
}

Zielstunden setzen (Admin)

PUT /api/time-tracking/time-account/target

Setzt die Zielstunden für einen User (nur Admin/Manager).

Request Body:

{
  "userId": "user123",
  "monthlyTargetHours": 160,
  "employmentType": "full_time",
  "weeklyHours": 40
}

Manuelle Anpassung hinzufügen (Admin)

POST /api/time-tracking/time-account/:year/:month/adjust

Fügt eine manuelle Anpassung hinzu (nur Admin/Manager).

Request Body:

{
  "userId": "user123",
  "amountHours": 5.5,
  "reason": "Überstunden-Ausgleich"
}

Zeitkonto exportieren

GET /api/time-tracking/time-account/export?format=json&startYear=2024&startMonth=1&endYear=2024&endMonth=12

Exportiert Zeitkonto-Daten für DSGVO (Art. 15).

Query-Parameter: - format (optional): json oder csv (Standard: json) - startYear, startMonth (optional): Start-Zeitraum - endYear, endMonth (optional): End-Zeitraum

Response (JSON):

{
  "accounts": [...],
  "count": 12
}

Response (CSV): CSV-Datei mit UTF-8 BOM für Excel-Kompatibilität

Abwesenheits-Endpoints

Abwesenheit erstellen

POST /api/time-tracking/absences

Erstellt eine Abwesenheit.

Request Body:

{
  "type": "vacation",
  "startDate": "2024-07-01",
  "endDate": "2024-07-14",
  "note": "Optional: Notiz",
  "attestDocumentId": "Optional; bei type=sick Pflicht: ID des hochgeladenen Mitglieder-Dokuments (Attest)",
  "attestFileName": "Optional: Dateiname des Attests für Anzeige"
}

Typen: vacation, sick, holiday, special

Hinweis: Bei type=sick ist attestDocumentId erforderlich (Arbeitsunfähigkeitsbescheinigung muss zuvor als Mitglieder-Dokument hochgeladen und die ID hier angegeben werden).

Response:

{
  "message": "Abwesenheit beantragt",
  "absence": {
    "id": "absence123",
    "type": "vacation",
    "startDate": "2024-07-01",
    "endDate": "2024-07-14",
    "days": 10,
    "status": "pending"
  }
}

Abwesenheiten auflisten

GET /api/time-tracking/absences?limit=100&startDate=2024-01-01&endDate=2024-12-31&status=pending&type=vacation

Query-Parameter: - limit (optional): Maximale Anzahl (Standard: 100, Maximum: 200) - startDate (optional): Start-Datum (ISO 8601) - endDate (optional): End-Datum (ISO 8601) - status (optional): pending, approved, rejected - type (optional): vacation, sick, holiday, special

Abwesenheit abrufen

GET /api/time-tracking/absences/:absenceId

  • Eigene Abwesenheiten immer; Admin/Manager können beliebige Abwesenheiten laden (z. B. für Genehmigungsansicht).
  • Response kann optionale Felder attestDocumentId und attestFileName enthalten (bei Krankmeldung mit Attest).

Attest herunterladen

GET /api/time-tracking/absences/:absenceId/attest/download

Liefert eine signierte Download-URL für das Attest (Arbeitsunfähigkeitsbescheinigung) einer Abwesenheit.

  • Berechtigung: Antragsteller (eigene Abwesenheit) oder Admin/Manager.
  • Response: { "downloadUrl": "...", "expiresAt": "..." } (URL ca. 1 Stunde gültig).
  • 404: Wenn keine Abwesenheit gefunden wird oder kein Attest zugeordnet ist.

Abwesenheit aktualisieren

PUT /api/time-tracking/absences/:absenceId

Einschränkungen: - Nur eigene Abwesenheiten können bearbeitet werden - Ausstehende Abwesenheiten können nicht bearbeitet werden

Abwesenheit löschen

DELETE /api/time-tracking/absences/:absenceId

Einschränkungen: - Nur eigene Abwesenheiten können gelöscht werden - Ausstehende Abwesenheiten können nicht gelöscht werden

Kontingent abrufen

GET /api/time-tracking/absences/quota/:year

Lädt Kontingente für ein Jahr.

Response:

{
  "quota": {
    "uid": "user123",
    "year": 2024,
    "vacationDays": 25,
    "sickDays": 0,
    "specialDays": 0,
    "usedVacationDays": 10,
    "usedSickDays": 0,
    "usedSpecialDays": 0
  }
}

Kontingent setzen (Admin)

PUT /api/time-tracking/absences/quota/:year

Setzt Kontingente für ein Jahr (nur Admin/Manager).

Request Body:

{
  "userId": "user123",
  "vacationDays": 25,
  "sickDays": 0,
  "specialDays": 0
}

Kontingent-Verfügbarkeit prüfen

GET /api/time-tracking/absences/quota/:year/check?type=vacation&days=10

Prüft ob Kontingent verfügbar ist.

Query-Parameter: - type (required): vacation, sick, special - days (required): Anzahl Tage

Response:

{
  "available": 15,
  "sufficient": true
}

Genehmigungs-Endpoints

Hinweis: Diese Endpoints erfordern Admin- oder Manager-Rolle (außer GET /approvals/entity/:type/:entityId).

Genehmigungen auflisten

GET /api/time-tracking/approvals?limit=100&status=pending&type=time_entry&requestedBy=user123

Query-Parameter: - limit (optional): Maximale Anzahl (Standard: 100, Maximum: 200) - status (optional): pending, approved, rejected - type (optional): time_entry, absence, overtime, allowance - requestedBy (optional): User-ID

Genehmigung abrufen

GET /api/time-tracking/approvals/:approvalId

Genehmigung für Objekt abrufen

GET /api/time-tracking/approvals/entity/:type/:entityId

Lädt Genehmigung für ein Objekt (z.B. TimeEntry).

Path-Parameter: - type: time_entry, project_time_entry, etc. - entityId: ID des Objekts

Genehmigung genehmigen

POST /api/time-tracking/approvals/:approvalId/approve

Genehmigt einen Antrag (nur Admin/Manager).

Request Body:

{
  "note": "Optional: Notiz"
}

Verhalten bei projektbezogenen Zeiterfassungen: - Wenn die Genehmigung für einen project_time_entry oder time_entry mit projectId genehmigt wird, wird der Status des TimeEntry automatisch von PENDING_APPROVAL auf COMPLETED gesetzt.

Response:

{
  "message": "Genehmigung genehmigt",
  "approval": {
    "id": "approval123",
    "type": "project_time_entry",
    "entityId": "entry123",
    "status": "approved",
    "approvedBy": "admin123",
    "approvedAt": "2024-01-15T10:00:00Z"
  }
}

Genehmigung ablehnen

POST /api/time-tracking/approvals/:approvalId/reject

Lehnt einen Antrag ab (nur Admin/Manager).

Request Body:

{
  "reason": "Ablehnungsgrund (erforderlich)"
}

Verhalten bei projektbezogenen Zeiterfassungen: - Wenn die Genehmigung für einen project_time_entry oder time_entry mit projectId abgelehnt wird, wird der Status des TimeEntry automatisch von PENDING_APPROVAL auf REJECTED gesetzt.

Response:

{
  "message": "Genehmigung abgelehnt",
  "approval": {
    "id": "approval123",
    "type": "project_time_entry",
    "entityId": "entry123",
    "status": "rejected",
    "rejectedBy": "admin123",
    "rejectedAt": "2024-01-15T10:00:00Z",
    "rejectionReason": "Ungültige Zeiten"
  }
}

Externe API-Nutzung

API-Token erstellen

  1. Öffnen Sie die Einstellungen in der Web-App
  2. Gehen Sie zum Tab Sicherheit
  3. Klicken Sie auf Neuer Token unter "API-Tokens"
  4. Geben Sie einen Namen ein (z.B. "Production API", "Script Integration")
  5. Klicken Sie auf Token erstellen
  6. WICHTIG: Kopieren Sie den Token sofort – er wird nicht erneut angezeigt!

API-Token verwenden

Alle API-Endpoints können mit einem API-Token verwendet werden:

Header: Authorization: Bearer <api-token>

WICHTIG - Sicherheitshinweise: - API-Tokens sind wie Passwörter zu behandeln - Tokens niemals in Code committen oder in öffentlichen Repositories speichern - Tokens in Environment Variables oder Secret Managers speichern - Regelmäßige Token-Rotation empfohlen - Bei Kompromittierung sofort Token widerrufen und neu erstellen

Beispiele

Minimal: Clock In/Out

# API-Token aus Environment Variable laden
API_TOKEN="${TIMEAM_API_TOKEN}"

# Clock In
curl -X POST https://api.example.com/api/time-tracking/clock-in \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"note": "Arbeitsbeginn"}'

# Clock Out
curl -X POST https://api.example.com/api/time-tracking/clock-out \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"note": "Arbeitsende"}'

Status abrufen

curl -X GET https://api.example.com/api/time-tracking/status \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Manuellen Eintrag erstellen

API_TOKEN="${TIMEAM_API_TOKEN}"
curl -X POST https://api.example.com/api/time-tracking/entries \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "clockIn": "2024-01-15T08:00:00Z",
    "clockOut": "2024-01-15T17:00:00Z",
    "entryType": "work",
    "note": "Arbeitszeit"
  }'

Pause starten/beenden

API_TOKEN="${TIMEAM_API_TOKEN}"

# Pause starten
curl -X POST https://api.example.com/api/time-tracking/break/start \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"note": "Mittagspause"}'

# Pause beenden
curl -X POST https://api.example.com/api/time-tracking/break/end \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"note": "Pause beendet"}'

Abwesenheit beantragen

API_TOKEN="${TIMEAM_API_TOKEN}"
curl -X POST https://api.example.com/api/time-tracking/absences \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "vacation",
    "startDate": "2024-07-01",
    "endDate": "2024-07-14",
    "note": "Sommerurlaub"
  }'

Zeitkonto abrufen

API_TOKEN="${TIMEAM_API_TOKEN}"
curl -X GET https://api.example.com/api/time-tracking/time-account/2024/1 \
  -H "Authorization: Bearer ${API_TOKEN}"

Zeitkonto exportieren (CSV)

API_TOKEN="${TIMEAM_API_TOKEN}"
curl -X GET "https://api.example.com/api/time-tracking/time-account/export?format=csv&startYear=2024&startMonth=1&endYear=2024&endMonth=12" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -o zeitkonto-export.csv

Typischer Use-Case: Automatische Zeiterfassung

#!/bin/bash

# API-Token aus Environment Variable oder Secret Manager laden
# NIEMALS Token direkt im Code speichern!
API_TOKEN="${TIMEAM_API_TOKEN}"
API_BASE="https://api.example.com"

# Clock In um 8:00 Uhr
curl -X POST "$API_BASE/api/time-tracking/clock-in" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"note": "Automatisch eingestempelt"}'

# Pause um 12:00 Uhr
sleep 14400  # 4 Stunden warten
curl -X POST "$API_BASE/api/time-tracking/break/start" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json"

# Pause beenden um 12:30 Uhr
sleep 1800  # 30 Minuten warten
curl -X POST "$API_BASE/api/time-tracking/break/end" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json"

# Clock Out um 17:00 Uhr
sleep 16200  # 4,5 Stunden warten
curl -X POST "$API_BASE/api/time-tracking/clock-out" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"note": "Automatisch ausgestempelt"}'

Firestore Collections

Time Entries

Pfad: /tenants/{tenantId}/timeEntries/{entryId}

{
  uid: string;                    // Firebase Auth UID
  email: string;                  // E-Mail (für Anzeige)
  clockIn: Timestamp;             // Clock-In Zeitpunkt
  clockOut: Timestamp | null;     // Clock-Out Zeitpunkt (null wenn running)
  status: 'running' | 'completed' | 'pending_approval' | 'rejected';
  durationMinutes: number | null; // Dauer in Minuten
  entryType?: 'work' | 'break';   // Typ (Standard: 'work')
  projectId?: string;             // Optional: Projekt-ID
  note?: string;                  // Optional: Notiz
  source?: 'clock' | 'manual' | 'shift'; // Quelle
  createdAt: Timestamp;
  updatedAt: Timestamp;
}

Status-Werte: - running: Eintrag läuft noch (Clock-In ohne Clock-Out) - completed: Eintrag abgeschlossen und genehmigt (ohne Projekt oder nach Genehmigung) - pending_approval: Eintrag wartet auf Genehmigung (nur bei projektbezogenen Einträgen) - rejected: Eintrag wurde abgelehnt (nur bei projektbezogenen Einträgen)

Time Accounts

Pfad: /tenants/{tenantId}/timeAccounts/{accountId}

Dokument-ID: {userId}_{year}_{month}

{
  userId: string;
  year: number;
  month: number;
  targetHours: number;             // Zielstunden
  actualHours: number;             // Ist-Stunden (gesamt)
  timeTrackingHours: number;       // Stunden aus Zeiterfassung
  shiftHours: number;              // Stunden aus Schichten
  balanceHours: number;           // Saldo (Ist - Ziel)
  manualAdjustments: Array<{...}>; // Manuelle Anpassungen
  complianceAdjustments: Array<{...}>; // Compliance-Anpassungen
  createdAt: Timestamp;
  updatedAt: Timestamp;
}

Time Account Targets

Pfad: /tenants/{tenantId}/timeAccountTargets/{userId}

{
  userId: string;
  monthlyTargetHours: number;     // Zielstunden pro Monat
  employmentType?: 'full_time' | 'part_time';
  weeklyHours?: number;           // Wöchentliche Stunden
  updatedAt: Timestamp;
  updatedBy: string;               // UID des Admins
}

Absences

Pfad: /tenants/{tenantId}/absences/{absenceId}

{
  uid: string;
  email: string;
  type: 'vacation' | 'sick' | 'holiday' | 'special';
  startDate: string;              // ISO 8601 Datum
  endDate: string;                 // ISO 8601 Datum
  days: number;                    // Anzahl Tage
  status: 'pending' | 'approved' | 'rejected';
  requestedBy: string;             // UID
  approvedBy?: string;              // UID (optional)
  approvedAt?: Timestamp;          // Optional
  rejectionReason?: string;        // Optional
  note?: string;                   // Optional
  createdAt: Timestamp;
  updatedAt: Timestamp;
}

Approvals

Pfad: /tenants/{tenantId}/approvals/{approvalId}

{
  type: 'time_entry' | 'absence' | 'overtime' | 'allowance' | 'project_time_entry';
  entityId: string;                // ID des Objekts
  entityType: string;              // Typ des Objekts
  requestedBy: string;             // UID
  requestedAt: Timestamp;
  approvedBy?: string;              // UID (optional)
  approvedAt?: Timestamp;          // Optional
  rejectedBy?: string;              // UID (optional)
  rejectedAt?: Timestamp;           // Optional
  rejectionReason?: string;        // Optional
  status: 'pending' | 'approved' | 'rejected';
  metadata?: Record<string, unknown>; // Zusätzliche Daten
  createdAt: Timestamp;
  updatedAt: Timestamp;
}

Fehlerfälle

HTTP-Status-Codes

  • 200 OK: Erfolgreich
  • 201 Created: Ressource erstellt
  • 400 Bad Request: Validierungsfehler
  • 401 Unauthorized: Nicht authentifiziert
  • 403 Forbidden: Keine Berechtigung
  • 404 Not Found: Ressource nicht gefunden
  • 409 Conflict: Konflikt (z.B. bereits eingestempelt)
  • 500 Internal Server Error: Server-Fehler

Fehlercodes

  • ALREADY_CLOCKED_IN: Bereits eingestempelt
  • NOT_CLOCKED_IN: Nicht eingestempelt
  • BREAK_ALREADY_RUNNING: Pause läuft bereits
  • NO_BREAK_RUNNING: Keine laufende Pause
  • NO_WORK_ENTRY: Kein laufender Arbeitseintrag
  • VALIDATION_ERROR: Validierungsfehler
  • NOT_FOUND: Ressource nicht gefunden
  • FORBIDDEN: Keine Berechtigung

Logging

Fehler werden protokolliert und können über ein Monitoring-System überwacht werden.

Tests

Status: Nicht im Repo gefunden

TODO: - Unit-Tests für Service-Funktionen - Integration-Tests für API-Endpoints - E2E-Tests für Frontend-Komponenten

Empfohlene Test-Struktur: Unit-Tests für Service, Integration-Tests für API, E2E-Tests für Frontend (Details siehe interne Entwickler-Dokumentation).

FAQ / Troubleshooting

Was passiert bei doppeltem Clock-In?

Wenn ein User bereits eingestempelt ist und erneut Clock-In aufruft, erhält er einen 409 ALREADY_CLOCKED_IN Fehler. Der laufende Entry bleibt unverändert.

Lösung: Zuerst Clock-Out aufrufen, dann erneut Clock-In.

Wie werden Pausen automatisch erfasst?

Bei Clock-Out oder manuellen Einträgen wird automatisch geprüft, ob eine Pause erforderlich ist (basierend auf ArbZG): - > 6 Stunden: 30 Minuten Pause - > 9 Stunden: 45 Minuten Pause

Falls erforderlich, wird automatisch ein BREAK-Entry erstellt.

Hinweis: Automatische Pausen werden nur erstellt, wenn noch keine automatische Pause für den Tag existiert.

Wie funktioniert das Zeitkonto?

Das Zeitkonto wird automatisch aktualisiert, wenn: - Ein TimeEntry erstellt/aktualisiert/gelöscht wird - Eine Schicht abgeschlossen wird (wenn shift-pool Modul aktiv)

Berechnung: - actualHours = timeTrackingHours + shiftHours - balanceHours = actualHours - targetHours

Manuelle Anpassungen: Admins können manuelle Anpassungen hinzufügen (z.B. Überstunden-Ausgleich).

Wie nutze ich die API extern mit Token?

  1. Token erstellen: In den Einstellungen → Sicherheit → API-Tokens → Neuer Token
  2. Token kopieren: Beim Erstellen sofort kopieren (wird nicht erneut angezeigt)
  3. Token verwenden: In jedem Request als Authorization: Bearer <token> Header

Beispiel:

API_TOKEN="${TIMEAM_API_TOKEN}"
curl -X GET https://api.example.com/api/time-tracking/status \
  -H "Authorization: Bearer ${API_TOKEN}"

Kann ich mehrere API-Tokens haben?

Ja, Sie können beliebig viele API-Tokens erstellen. Jeder Token hat einen eigenen Namen und kann unabhängig gelöscht werden.

Empfehlung: Erstellen Sie separate Tokens für verschiedene Anwendungen (z.B. "Production API", "Development Scripts").

Was passiert, wenn ich einen Token verliere?

Wenn Sie einen Token verlieren, können Sie ihn nicht wiederherstellen. Sie müssen: 1. Den alten Token löschen (falls Sie ihn noch sehen können) 2. Einen neuen Token erstellen

Sicherheit: Gelöschte Tokens funktionieren sofort nicht mehr.

Wie werden projektbezogene Zeiterfassungen genehmigt?

Projektbezogene Zeiterfassungen (mit projectId) erfordern immer eine Genehmigung, unabhängig von der Arbeitszeit.

Workflow: 1. User erstellt TimeEntry mit projectId (Clock-In/Out oder manuell) 2. Status wird automatisch auf PENDING_APPROVAL gesetzt (nicht COMPLETED) 3. System erstellt automatisch Approval-Request 4. Admin/Manager genehmigt oder lehnt ab 5. Bei Genehmigung: Status wird auf COMPLETED gesetzt 6. Bei Ablehnung: Status wird auf REJECTED gesetzt 7. User wird benachrichtigt

Wichtig: Einträge ohne projectId werden weiterhin direkt auf COMPLETED gesetzt und benötigen keine Genehmigung.

Wie funktioniert die Überstunden-Genehmigung?

Überstunden-Genehmigungen werden automatisch erstellt, wenn: - Die tägliche Arbeitszeit (nur WORK-Einträge) über 8 Stunden liegt - Der TimeEntry abgeschlossen wird (Clock-Out oder manueller Eintrag)

Workflow: 1. User arbeitet > 8 Stunden 2. System erstellt automatisch Approval-Request mit Überstunden 3. Admin/Manager genehmigt oder lehnt ab 4. User wird benachrichtigt

Wie werden Abwesenheiten genehmigt?

Abwesenheiten werden automatisch als pending erstellt. Admins/Manager können sie über die Genehmigungs-Endpoints genehmigen oder ablehnen.

Kontingent-Prüfung: Beim Erstellen wird automatisch geprüft, ob genügend Kontingent verfügbar ist.