---
openapi: "3.0.1"
info:
  title: "Timebutler API v2"
  description: "REST-API für die Timebutler HR-SaaS-Plattform. Wird von Integratoren\
    \ für Abwesenheits-, Zeiterfassungs-, Vertretungs- und Benutzerprofilprozesse\
    \ verwendet. Alle Endpunkte außer /oauth2/token erfordern ein Bearer-JWT-Zugriffstoken."
  contact:
    name: "Timebutler Support"
    url: "https://www.timebutler.com"
    email: "support@timebutler.com"
  license:
    name: "Proprietary"
  version: "1.0"
  x-logo:
    url: "/images/logo/logo.png"
    altText: "Timebutler"
servers:
- url: "https://app.timebutler.com/api/v2"
- url: "/api/v2"
tags:
- name: "Abwesenheitsanträge"
  description: "Urlaubs-, Krankheits- und sonstige Abwesenheitsanträge. Mitarbeiter\
    \ stellen und verwalten ihre eigenen Anträge (`/my`, `POST`, `PUT /{id}`, `DELETE\
    \ /{id}`); Manager und Administratoren sehen die offenen Anträge ihres Teams (`/team`,\
    \ `/pending-count`) und können sie genehmigen oder ablehnen (`POST /{id}/approve`,\
    \ `POST /{id}/reject`). Alle Endpunkte erfordern ein Inhaber-Token."
- name: "Abwesenheitsarten"
  description: "Referenzliste der im Unternehmen konfigurierten Abwesenheitsarten\
    \ (Urlaub, Krankheit usw.). Wird zur Befüllung von Auswahlfeldern beim Erstellen\
    \ eines Abwesenheitsantrags verwendet."
- name: "Stornierungsanträge"
  description: "Anträge von Mitarbeitern zur Stornierung (Löschung) einer bereits\
    \ genehmigten Abwesenheit. Vorgesetzte und Administratoren sehen offene Stornierungsanträ\
    ge ihres Teams und können diese genehmigen oder ablehnen."
- name: "Kategorien"
  description: "Referenzliste der Zeiterfassungskategorien des Unternehmens. Wird\
    \ beim Erstellen oder Beenden eines Zeiteintrags verwendet."
- name: "Mitarbeiter"
  description: "Verzeichnis der Mitarbeiter, die der authentifizierte Manager oder\
    \ Administrator sehen kann, einschließlich stabiler Kennungen (interne ID, Personalnummer,\
    \ E-Mail) zur Zuordnung zu anderen Endpunkten."
- name: "Authentifizierung"
  description: "OAuth-2.0-Token-Endpunkt. Stellt JWT-Zugriffstoken und rotierende\
    \ Refresh-Token aus. Dieser Endpunkt ist öffentlich — kein Inhaber-Token erforderlich."
- name: "Projekte"
  description: "Referenzliste der Zeiterfassungsprojekte des Unternehmens. Favoriten\
    \ werden zuerst aufgeführt. Wird beim Erstellen eines Zeiteintrags verwendet."
- name: "Vertretungsmitarbeiter"
  description: "Referenzliste der Mitarbeiter, die beim Einreichen eines Abwesenheitsantrags\
    \ als Vertretung ausgewählt werden können."
- name: "Vertretungsanfragen"
  description: "Anfragen an den authentifizierten Benutzer, während der Abwesenheit\
    \ eines Kollegen als Vertretung zu fungieren. Der Benutzer kann jede Anfrage annehmen\
    \ oder ablehnen."
- name: "Stempeluhr"
  description: "Echtzeit-Stempeluhr. Startet, pausiert, setzt fort, stoppt oder bricht\
    \ die laufende Uhr für den authentifizierten Benutzer ab. Der Dienstgangmodus\
    \ wird ebenfalls hier gesteuert."
- name: "Zeiterfassungsanträge"
  description: "Ausstehende Änderungs- oder Löschanträge für Zeiteinträge, die eine\
    \ Genehmigung durch einen Vorgesetzten oder Administrator erfordern. Vorgesetzte\
    \ und Administratoren können diese Anträge auflisten, genehmigen und ablehnen."
- name: "Zeiteinträge"
  description: "Verwaltung einzelner Zeiterfassungseinträge. Jeder Benutzer kann seine\
    \ eigenen Einträge auflisten (`/my`) und löschen (abhängig von Monatsabschluss-\
    \ und Genehmigungsworkflow-Regeln); Vorgesetzte und Administratoren können sehen,\
    \ wie viele Einträge auf Genehmigung warten."
- name: "Zeiterfassung"
  description: "Tagesübersicht zur Arbeits- und Abwesenheitszeit des authentifizierten\
    \ Benutzers sowie ein Endpunkt zum direkten Einreichen abgeschlossener Zeiteinträ\
    ge (Start-/Endzeitstempel) ohne die Echtzeit-Stempeluhr."
- name: "Benutzerprofil"
  description: "Grundlegende Profilinformationen des authentifizierten Benutzers:\
    \ Name, E-Mail, Rolle, Abteilung und Avatar-URL."
paths:
  /absence-requests/{id}/approve:
    post:
      tags:
      - "Abwesenheitsanträge"
      summary: "Offenen Abwesenheitsantrag genehmigen"
      description: "Vermerkt die Genehmigung des Aufrufers auf einem Antrag im Zustand\
        \ `pending`. Hat der Mitarbeiter mehrere Manager, wechselt der Antrag erst\
        \ dann in `approved`, wenn alle Manager genehmigt haben (oder ein Administrator\
        \ genehmigt). Der Aufrufer muss Administrator oder direkter Vorgesetzter des\
        \ Antragstellers sein."
      operationId: "approve"
      parameters:
      - name: "id"
        in: "path"
        description: "Interne ID des zu genehmigenden Abwesenheitsantrags."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 12345
      responses:
        "200":
          description: "Antrag nach Vermerk der Genehmigung."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AbsenceRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
          content:
            application/json: {}
        "403":
          description: "Der Aufrufer ist weder Manager noch Administrator oder darf\
            \ diesen konkreten Antrag nicht bearbeiten."
          content:
            application/json: {}
        "404":
          description: "Für das Unternehmen des Aufrufers existiert kein Antrag mit\
            \ der angegebenen `id`."
          content:
            application/json: {}
        "409":
          description: "Der Antrag ist nicht im Zustand `pending` (bereits genehmigt,\
            \ abgelehnt oder gelöscht)."
          content:
            application/json: {}
        "500":
          description: "Unerwarteter Serverfehler beim Aktualisieren des Antrags."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests:
    get:
      tags:
      - "Abwesenheitsanträge"
      summary: "Eigene Abwesenheitsanträge des Aufrufers auflisten (Alias)"
      description: "Alias-Endpunkt, der dasselbe Ergebnis wie `GET /my` liefert. In\
        \ neuen Integrationen `/my` bevorzugen — dieser Pfad existiert aus Gründen\
        \ der Abwärtskompatibilität."
      operationId: "getRequests"
      responses:
        "200":
          description: "Liste der Anträge des Aufrufers. Leeres Array, wenn keine\
            \ vorhanden sind."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/AbsenceRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
          content:
            application/json: {}
        "403":
          description: "Das Konto des Aufrufers ist inaktiv."
          content:
            application/json: {}
        "500":
          description: "Unerwarteter Serverfehler."
          content:
            application/json: {}
      security:
      - bearerAuth: []
    post:
      tags:
      - "Abwesenheitsanträge"
      summary: "Neuen Abwesenheitsantrag erstellen"
      description: "Reicht einen neuen Abwesenheits- oder Urlaubsantrag für den Aufrufer\
        \ ein. Der resultierende Status hängt von der Konfiguration des Abwesenheitstyps\
        \ ab: Typen, die eine Genehmigung durch den Manager erfordern, starten mit\
        \ `pending`, andernfalls gehen sie direkt auf `approved`. Datumsangaben sind\
        \ ISO-8601 (`YYYY-MM-DD`). Bei Halbtagsanträgen `halfDay` auf `morning` oder\
        \ `afternoon` setzen und denselben Wert für `startDate` und `endDate` verwenden."
      operationId: "create"
      requestBody:
        description: "Felder des neuen Abwesenheitsantrags."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAbsenceRequest"
        required: true
      responses:
        "200":
          description: "Erstellter Antrag, im vom Server zugewiesenen Zustand."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AbsenceRequestDto"
        "400":
          description: "Fehlende, fehlerhafte oder zueinander inkonsistente Felder\
            \ (ungültige `absenceTypeId`, nicht parsbares Datum, `endDate < startDate`,\
            \ ungültiger `halfDay`-Wert oder unbekannte `substituteEmployeeId`)."
          content:
            application/json: {}
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
          content:
            application/json: {}
        "403":
          description: "Die Rolle des Aufrufers darf diesen Abwesenheitstyp nicht\
            \ eintragen."
          content:
            application/json: {}
        "500":
          description: "Unerwarteter Serverfehler beim Speichern des Antrags."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests/{id}:
    put:
      tags:
      - "Abwesenheitsanträge"
      summary: "Bestehenden Abwesenheitsantrag aktualisieren"
      description: "Aktualisiert einen Antrag, der sich noch im Zustand `pending`\
        \ oder `edit` befindet. Nur die im Request-Body übermittelten Felder werden\
        \ geändert; `null`-Felder bleiben unverändert. Nicht-Administratoren können\
        \ nur ihre eigenen Anträge aktualisieren. Bereits genehmigte oder abgelehnte\
        \ Anträge können nicht mehr aktualisiert werden und liefern `409 Conflict`."
      operationId: "update"
      parameters:
      - name: "id"
        in: "path"
        description: "Interne ID des zu aktualisierenden Abwesenheitsantrags. Entspricht\
          \ dem `id`-Feld aus `GET /my` oder `GET /team`."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 12345
      requestBody:
        description: "Zu aktualisierende Felder. Ein Feld weglassen (oder `null` senden),\
          \ um den bestehenden Wert zu behalten."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateAbsenceRequest"
        required: true
      responses:
        "200":
          description: "Aktualisierter Antrag, wie gespeichert."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AbsenceRequestDto"
        "400":
          description: "Fehlerhafter Body oder ungültige Feldkombination."
          content:
            application/json: {}
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
          content:
            application/json: {}
        "403":
          description: "Der Aufrufer ist weder Eigentümer dieses Antrags noch Administrator,\
            \ oder der Abwesenheitstyp ist für die Rolle des Aufrufers gesperrt."
          content:
            application/json: {}
        "404":
          description: "Für das Unternehmen des Aufrufers existiert kein Antrag mit\
            \ der angegebenen `id`."
          content:
            application/json: {}
        "409":
          description: "Der Antrag befindet sich in einem Zustand, der keine Bearbeitung\
            \ mehr zulässt (bereits genehmigt, abgelehnt oder gelöscht)."
          content:
            application/json: {}
        "500":
          description: "Unerwarteter Serverfehler beim Speichern der Aktualisierung."
          content:
            application/json: {}
      security:
      - bearerAuth: []
    delete:
      tags:
      - "Abwesenheitsanträge"
      summary: "Abwesenheitsantrag löschen oder Stornierungsanfrage senden"
      description: "Das Verhalten hängt vom aktuellen Zustand des Antrags und den\
        \ Rechten des Aufrufers ab: Darf der Aufrufer den Antrag direkt löschen, wird\
        \ er entfernt und das Feld `result` der Antwort ist `\"deleted\"`. Andernfalls\
        \ wird ein Stornierungsantrag erzeugt, den die Manager des Mitarbeiters dann\
        \ genehmigen müssen, und die Antwort `result` ist `\"cancellation_requested\"\
        `. Die Unternehmenskonfiguration kann eine Begründung erforderlich machen,\
        \ wenn nur eine Stornierung möglich ist."
      operationId: "delete"
      parameters:
      - name: "id"
        in: "path"
        description: "Interne ID des zu löschenden oder zu stornierenden Abwesenheitsantrags."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 12345
      requestBody:
        description: "Optionaler Body mit einem freien `reason`-Text. Erforderlich\
          \ für Stornierungsanfragen, wenn die Unternehmenskonfiguration eine Begrü\
          ndung verlangt."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeleteAbsenceRequest"
      responses:
        "200":
          description: "Ergebnis des Lösch-/Stornierungsversuchs. Das Feld `result`\
            \ gibt an, welcher Pfad gewählt wurde."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResultResponse"
        "400":
          description: "Fehlender Stornierungsgrund, obwohl das Unternehmen einen\
            \ verlangt."
          content:
            application/json: {}
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
          content:
            application/json: {}
        "403":
          description: "Der Aufrufer darf diesen Antrag weder löschen noch stornieren\
            \ (falsches Unternehmen, oder die Berechtigungen für den aktuellen Zustand\
            \ lassen keine Aktion zu)."
          content:
            application/json: {}
        "404":
          description: "Für das Unternehmen des Aufrufers existiert kein Antrag mit\
            \ der angegebenen `id`."
          content:
            application/json: {}
        "500":
          description: "Unerwarteter Serverfehler beim Löschen oder Stornieren."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests/my:
    get:
      tags:
      - "Abwesenheitsanträge"
      summary: "Eigene Abwesenheitsanträge des Aufrufers auflisten"
      description: "Liefert jeden Abwesenheits- und Urlaubsantrag des Aufrufers, in\
        \ jedem Zustand (pending, approved, rejected, done). Anträge, die der Aufrufer\
        \ für seine unterstellten Mitarbeiter verwaltet, sind nicht enthalten — dafü\
        r siehe `/team`."
      operationId: "getMyRequests"
      responses:
        "200":
          description: "Liste der Anträge des Aufrufers. Leeres Array, wenn keine\
            \ vorhanden sind."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/AbsenceRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
          content:
            application/json: {}
        "403":
          description: "Das Konto des Aufrufers ist inaktiv."
          content:
            application/json: {}
        "500":
          description: "Unerwarteter Serverfehler."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests/pending-count:
    get:
      tags:
      - "Abwesenheitsanträge"
      summary: "Offene Antragszahlen für das Team des Aufrufers"
      description: "Liefert zwei Zähler für die direkt unterstellten Mitarbeiter des\
        \ Aufrufers: offene Abwesenheitsanträge, die auf seine Genehmigung warten,\
        \ und offene Stornierungsanträge. Wird verwendet, um Benachrichtigungs-Badges\
        \ im Manager-Dashboard anzuzeigen. Liefert Nullen, wenn der Aufrufer keine\
        \ direkt unterstellten Mitarbeiter hat."
      operationId: "getPendingCount"
      responses:
        "200":
          description: "Zähler erfolgreich zurückgegeben."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AbsencePendingCountResponse"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
          content:
            application/json: {}
        "403":
          description: "Der Aufrufer ist weder Manager noch Administrator."
          content:
            application/json: {}
        "500":
          description: "Unerwarteter Serverfehler."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests/team:
    get:
      tags:
      - "Abwesenheitsanträge"
      summary: "Abwesenheitsanträge der unterstellten Mitarbeiter auflisten"
      description: "Liefert jeden Abwesenheitsantrag von Mitarbeitern, die der Aufrufer\
        \ verwaltet (Administratoren sehen alle Unternehmensnutzer), über alle Zustä\
        nde hinweg. Jeder Eintrag enthält den Anzeigenamen und die ID des Mitarbeiters,\
        \ damit das Ergebnis als Teamkalender oder Prüfliste dargestellt werden kann.\
        \ Leeres Array, wenn der Aufrufer keine unterstellten Mitarbeiter hat."
      operationId: "getTeamRequests"
      responses:
        "200":
          description: "Liste der Teamanträge. Leeres Array, falls keine vorhanden\
            \ sind."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/AbsenceRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
          content:
            application/json: {}
        "403":
          description: "Der Aufrufer ist weder Manager noch Administrator."
          content:
            application/json: {}
        "500":
          description: "Unerwarteter Serverfehler."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests/{id}/reject:
    post:
      tags:
      - "Abwesenheitsanträge"
      summary: "Offenen Abwesenheitsantrag ablehnen"
      description: "Markiert einen offenen Abwesenheitsantrag als abgelehnt. Ein optionaler\
        \ Ablehnungsgrund kann im Body übergeben werden — er wird gespeichert und\
        \ dem antragstellenden Mitarbeiter angezeigt. Der Aufrufer muss Administrator\
        \ oder direkter Vorgesetzter des Antragstellers sein."
      operationId: "reject"
      parameters:
      - name: "id"
        in: "path"
        description: "Interne ID des abzulehnenden Abwesenheitsantrags."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 12345
      requestBody:
        description: "Optionaler Body mit einem `reason`-String, der dem Mitarbeiter\
          \ angezeigt wird."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RejectRequest"
      responses:
        "200":
          description: "Antrag nach Vermerk der Ablehnung."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AbsenceRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
          content:
            application/json: {}
        "403":
          description: "Der Aufrufer ist weder Manager noch Administrator oder darf\
            \ diesen konkreten Antrag nicht bearbeiten."
          content:
            application/json: {}
        "404":
          description: "Für das Unternehmen des Aufrufers existiert kein Antrag mit\
            \ der angegebenen `id`."
          content:
            application/json: {}
        "409":
          description: "Der Antrag ist nicht im Zustand `pending`."
          content:
            application/json: {}
        "500":
          description: "Unerwarteter Serverfehler beim Aktualisieren des Antrags."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-types:
    get:
      tags:
      - "Abwesenheitsarten"
      summary: "Abwesenheitsarten auflisten"
      description: "Gibt alle aktiven Abwesenheitsarten zurück, die der authentifizierte\
        \ Benutzer einreichen darf."
      operationId: "getAbsenceTypes"
      responses:
        "200":
          description: "Liste der Abwesenheitsarten."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/AbsenceTypeDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /cancellation-requests/{id}/approve:
    post:
      tags:
      - "Stornierungsanträge"
      summary: "Stornierungsantrag genehmigen"
      description: "Genehmigt den ausstehenden Stornierungsantrag für die angegebene\
        \ Abwesenheit. Die Abwesenheit wird gelöscht und Benachrichtigungs-E-Mails\
        \ werden versendet. Erfordert die Rolle `MANAGER` oder `ADMIN`."
      operationId: "approve_1"
      parameters:
      - name: "id"
        in: "path"
        description: "ID der Abwesenheit."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 42
      responses:
        "200":
          description: "Stornierung genehmigt; gibt den Snapshot der gelöschten Abwesenheit\
            \ zurück."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancellationRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Aufrufer ist kein Vorgesetzter oder Administrator, oder\
            \ ist nicht der Vorgesetzte des Antragsstellers."
        "404":
          description: "Abwesenheit nicht gefunden."
        "409":
          description: "Kein ausstehender Stornierungsantrag für diese Abwesenheit."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /cancellation-requests:
    get:
      tags:
      - "Stornierungsanträge"
      summary: "Offene Stornierungsanträge auflisten"
      description: "Gibt alle offenen Stornierungsanträge zurück, die für den authentifizierten\
        \ Vorgesetzten oder Administrator sichtbar sind. Erfordert die Rolle `MANAGER`\
        \ oder `ADMIN`."
      operationId: "getList"
      responses:
        "200":
          description: "Liste offener Stornierungsanträge."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/CancellationRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Aufrufer ist kein Vorgesetzter oder Administrator."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /cancellation-requests/pending-count:
    get:
      tags:
      - "Stornierungsanträge"
      summary: "Anzahl offener Stornierungsanträge abrufen"
      description: "Gibt die Anzahl der Stornierungsanträge zurück, die auf eine Entscheidung\
        \ eines Vorgesetzten oder Administrators warten. Erfordert die Rolle `MANAGER`\
        \ oder `ADMIN`."
      operationId: "getPendingCount_1"
      responses:
        "200":
          description: "Anzahl offener Anträge."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancellationPendingCountResponse"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Aufrufer ist kein Vorgesetzter oder Administrator."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /cancellation-requests/{id}/reject:
    post:
      tags:
      - "Stornierungsanträge"
      summary: "Stornierungsantrag ablehnen"
      description: "Lehnt den ausstehenden Stornierungsantrag ab. Die Abwesenheit\
        \ bleibt bestehen; der Mitarbeiter wird per E-Mail benachrichtigt. Erfordert\
        \ die Rolle `MANAGER` oder `ADMIN`."
      operationId: "reject_1"
      parameters:
      - name: "id"
        in: "path"
        description: "ID der Abwesenheit."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 42
      responses:
        "200":
          description: "Stornierung abgelehnt; gibt die aktualisierte Abwesenheit\
            \ zurück."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancellationRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Aufrufer ist kein Vorgesetzter oder Administrator, oder\
            \ ist nicht der Vorgesetzte des Antragsstellers."
        "404":
          description: "Abwesenheit nicht gefunden."
        "409":
          description: "Kein ausstehender Stornierungsantrag für diese Abwesenheit."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /categories:
    get:
      tags:
      - "Kategorien"
      summary: "Kategorien auflisten"
      description: "Gibt alle aktiven Kategorien sowie die Standardkategorie des Benutzers\
        \ und die Information zurück, ob eine Kategorie beim Speichern eines Zeiteintrags\
        \ erforderlich ist."
      operationId: "getCategories"
      responses:
        "200":
          description: "Kategorieliste mit Standardwert und Pflichtfeld-Kennzeichnung."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategoriesResponse"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /employees:
    get:
      tags:
      - "Mitarbeiter"
      summary: "Mitarbeiter auflisten"
      description: "Gibt die Mitarbeiter zurück, die der authentifizierte Benutzer\
        \ sehen kann: Administratoren erhalten alle Benutzer des Unternehmens, Manager\
        \ ihre unterstellten Mitarbeiter (einschließlich sich selbst) sowie alle weiteren\
        \ Mitarbeiter, für die ihnen zusätzliche Einsichtsrechte gewährt wurden. Jeder\
        \ Eintrag enthält die interne `id`, die Personalnummer, den Anzeigenamen,\
        \ die E-Mail-Adresse, die Rolle, die Abteilung und die Niederlassung. Die\
        \ `id` entspricht der von `GET /absence-requests/team` zurückgegebenen `employeeId`,\
        \ sodass sich mit diesem Endpunkt ermitteln lässt, zu welchem Mitarbeiter\
        \ ein Abwesenheitsantrag gehört. Gesperrte (deaktivierte) Konten sind mit\
        \ `active` auf `false` enthalten."
      operationId: "getEmployees"
      responses:
        "200":
          description: "Liste der Mitarbeiter. Leeres Array, wenn der Aufrufer keine\
            \ sichtbaren Mitarbeiter hat."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/EmployeeDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
          content:
            application/json: {}
        "403":
          description: "Der Aufrufer ist weder Manager noch Administrator."
          content:
            application/json: {}
        "500":
          description: "Unerwarteter Serverfehler."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /oauth2/token:
    post:
      tags:
      - "Authentifizierung"
      summary: "Token ausstellen oder erneuern"
      description: "Unterstützt fünf Grant-Typen:\n\n**`grant_type=password`** — Authentifizierung\
        \ mit `username` und `password`. Bei Erfolg wird ein Zugriffstoken und ein\
        \ Refresh-Token zurückgegeben. Falls das Konto 2FA aktiviert hat, wird stattdessen\
        \ eine MFA-Anforderung zurückgegeben (`mfa_required=true`, `mfa_token`, `mfa_expires_in`)\
        \ — den Ablauf mit `grant_type=mfa_totp` abschließen.\n\n**`grant_type=refresh_token`**\
        \ — Tauscht ein gültiges `refresh_token` gegen ein neues Zugriffstoken aus\
        \ und rotiert das Refresh-Token (jedes Refresh-Token kann nur einmal verwendet\
        \ werden).\n\n**`grant_type=mfa_totp`** — Zweiter Schritt des 2FA-Ablaufs.\
        \ Den `mfa_token` aus der Passwort-Grant-Anforderung zusammen mit dem aktuellen\
        \ `totp_code` aus der Authenticator-App des Benutzers übermitteln. Bei Erfolg\
        \ werden Zugriffstoken und Refresh-Token zurückgegeben.\n\n**`grant_type=sso_id_token`**\
        \ — Single Sign-On mit einem ID-Token, das die mobile App bereits vom SSO-Anbieter\
        \ erhalten hat. `provider=google` und das vom Anbieter ausgestellte `id_token`\
        \ (ein JWT) übermitteln. Timebutler prüft Signatur, Aussteller und Zielgruppe\
        \ des JWT und gibt für das verknüpfte Timebutler-Konto Zugriffs- und Refresh-Token\
        \ zurück.\n\n**`grant_type=sso_code`** — Single Sign-On mit einem Authorization\
        \ Code, den die mobile App vom Consent-Screen des SSO-Anbieters erhalten hat.\
        \ `provider=microsoft` oder `provider=slack`, den `code` vom Anbieter sowie\
        \ die in der Authorize-Anfrage verwendete `redirect_uri` übermitteln. Bei\
        \ Microsoft-Public-Client-App-Registrierungen zusätzlich den PKCE-`code_verifier`\
        \ senden, dessen SHA-256-Hash als `code_challenge` im Authorize-Schritt verwendet\
        \ wurde. Timebutler tauscht den Code beim Anbieter ein, identifiziert den\
        \ Benutzer und gibt das Token-Paar zurück.\n\nAlle Grant-Typen erfordern auß\
        erdem `client_id`. Verwende `client_id=public` zur Authentifizierung ohne\
        \ Secret, oder übergib `client_id` und `client_secret` für einen registrierten\
        \ vertraulichen Client."
      operationId: "token"
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: "object"
              properties:
                grant_type:
                  type: "string"
                  description: "OAuth-2.0-Grant-Typ. Unterstützte Werte: `password`,\
                    \ `refresh_token`, `mfa_totp`, `sso_id_token`, `sso_code`."
                  example: "password"
                username:
                  type: "string"
                  description: "E-Mail-Adresse des Benutzers. Erforderlich für `grant_type=password`."
                  example: "user@example.com"
                password:
                  type: "string"
                  description: "Passwort des Benutzers. Erforderlich für `grant_type=password`."
                  example: "s3cr3t"
                refresh_token:
                  type: "string"
                  description: "Refresh-Token aus einer vorherigen Token-Antwort.\
                    \ Erforderlich für `grant_type=refresh_token`."
                  example: "rt-a1b2c3d4e5f6..."
                client_id:
                  type: "string"
                  description: "Client-Bezeichner. Verwende `public` für nicht authentifizierte\
                    \ öffentliche Clients."
                  example: "public"
                client_secret:
                  type: "string"
                  description: "Client-Secret. Erforderlich für vertrauliche Clients;\
                    \ weglassen bei `client_id=public`."
                  example: "my-secret"
                mfa_token:
                  type: "string"
                  description: "MFA-Challenge-Token, das bei einem `password`-Grant\
                    \ zurückgegeben wird, wenn 2FA erforderlich ist. Erforderlich\
                    \ für `grant_type=mfa_totp`."
                totp_code:
                  type: "string"
                  description: "Aktueller TOTP-Code aus der Authenticator-App des\
                    \ Benutzers. Erforderlich für `grant_type=mfa_totp`."
                provider:
                  type: "string"
                  description: "SSO-Anbieter-Bezeichner. Erforderlich für `grant_type=sso_id_token`\
                    \ (`google`) und `grant_type=sso_code` (`microsoft`, `slack`)."
                  example: "google"
                id_token:
                  type: "string"
                  description: "Vom Anbieter ausgestelltes SSO-ID-Token (z. B. ein\
                    \ Google-ID-Token-JWT). Erforderlich für `grant_type=sso_id_token`."
                code:
                  type: "string"
                  description: "Vom Authorize-Endpunkt des SSO-Anbieters zurückgegebener\
                    \ Authorization Code. Erforderlich für `grant_type=sso_code`."
                redirect_uri:
                  type: "string"
                  description: "Redirect-URI, die beim Abruf des Authorization Codes\
                    \ verwendet wurde. Muss mit dem beim SSO-Anbieter hinterlegten\
                    \ Wert übereinstimmen. Erforderlich für `grant_type=sso_code`."
                  example: "https://app.example.com/sso/callback"
                code_verifier:
                  type: "string"
                  description: "PKCE-Code-Verifier — der Klartextwert, dessen SHA-256-Hash\
                    \ als `code_challenge` in der Authorization-Anfrage gesendet wurde.\
                    \ Erforderlich für `grant_type=sso_code`, wenn die App-Registrierung\
                    \ des SSO-Anbieters ein öffentlicher Client ist."
            examples:
              password-grant:
                summary: "Passwort-Grant"
                value: "grant_type=password&username=user%40example.com&password=s3cr3t&client_id=public"
              refresh-token-grant:
                summary: "Refresh-Token-Grant"
                value: "grant_type=refresh_token&refresh_token=550e8400-e29b-41d4-a716-446655440000&client_id=public"
              mfa-totp-grant:
                summary: "MFA-TOTP-Grant (2FA zweiter Schritt)"
                value: "grant_type=mfa_totp&mfa_token=mfa-a1b2c3d4e5f6&totp_code=123456&client_id=public"
      responses:
        "200":
          description: "Token erfolgreich ausgestellt, oder MFA-Anforderung zurückgegeben\
            \ (Passwort-Grant mit aktivierter 2FA). Die Antwort ist eine `OauthTokenResponse`\
            \ (Zugriffstoken + Refresh-Token) für `refresh_token`- und `mfa_totp`-Grants,\
            \ oder eine `MfaChallengeResponse` (MFA-Anforderung) für einen `password`-Grant,\
            \ wenn das Konto 2FA aktiviert hat."
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/OauthTokenResponse"
                - $ref: "#/components/schemas/MfaChallengeResponse"
        "400":
          description: "Ungültige Anfrage, nicht unterstützter Grant-Typ oder ungü\
            ltige Anmeldedaten."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OauthErrorResponse"
        "403":
          description: "Zugriff aufgrund einer IP-Adressbeschränkung verweigert."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OauthErrorResponse"
        "429":
          description: "Zu viele Anfragen von dieser IP-Adresse. Versuche es in 1\
            \ Minute erneut."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OauthErrorResponse"
        "500":
          description: "Unerwarteter Serverfehler."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OauthErrorResponse"
  /projects:
    get:
      tags:
      - "Projekte"
      summary: "Projekte auflisten"
      description: "Gibt alle aktiven Projekte zurück. Die Favoriten des Benutzers\
        \ erscheinen zuerst, gefolgt von allen weiteren aktiven Projekten. Enthält\
        \ außerdem das Standardprojekt des Benutzers und die Information, ob ein Projekt\
        \ Pflichtfeld ist."
      operationId: "getProjects"
      responses:
        "200":
          description: "Projektliste mit Standardwert und Pflichtfeld-Kennzeichnung."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectsResponse"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /substitute-employees:
    get:
      tags:
      - "Vertretungsmitarbeiter"
      summary: "Vertretungsmitarbeiter auflisten"
      description: "Gibt alle aktiven Mitarbeiter des Unternehmens zurück, die als\
        \ Vertretung für den authentifizierten Benutzer fungieren können. Der authentifizierte\
        \ Benutzer selbst ist nicht in der Liste enthalten."
      operationId: "getSubstituteEmployees"
      responses:
        "200":
          description: "Liste der Vertretungsmitarbeiter."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/SubstituteEmployeeDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /substitute-requests/{id}/accept:
    post:
      tags:
      - "Vertretungsanfragen"
      summary: "Vertretungsanfrage annehmen"
      description: "Nimmt die Vertretungsanfrage für die angegebene Abwesenheit an.\
        \ Nur die vorgesehene Vertretung darf diesen Endpunkt aufrufen."
      operationId: "accept"
      parameters:
      - name: "id"
        in: "path"
        description: "ID der Abwesenheit."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 42
      responses:
        "200":
          description: "Anfrage angenommen; gibt die aktualisierte Abwesenheit zurü\
            ck."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubstituteRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Aufrufer ist nicht die vorgesehene Vertretung."
        "404":
          description: "Abwesenheit nicht gefunden."
        "409":
          description: "Die Anfrage kann im aktuellen Status nicht angenommen werden\
            \ (bereits angenommen, abgelehnt oder keine Annahme erforderlich)."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /substitute-requests/pending-count:
    get:
      tags:
      - "Vertretungsanfragen"
      summary: "Anzahl offener Vertretungsanfragen abrufen"
      description: "Gibt die Anzahl der Vertretungsanfragen zurück, die noch auf die\
        \ Antwort des authentifizierten Benutzers warten."
      operationId: "getPendingCount_2"
      responses:
        "200":
          description: "Anzahl offener Anfragen."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubstitutePendingCountResponse"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /substitute-requests:
    get:
      tags:
      - "Vertretungsanfragen"
      summary: "Vertretungsanfragen auflisten"
      description: "Gibt alle Vertretungsanfragen zurück, die an den authentifizierten\
        \ Benutzer gerichtet sind, unabhängig von deren Status (ausstehend, angenommen,\
        \ abgelehnt)."
      operationId: "getSubstituteRequests"
      responses:
        "200":
          description: "Liste der Vertretungsanfragen."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/SubstituteRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /substitute-requests/{id}/reject:
    post:
      tags:
      - "Vertretungsanfragen"
      summary: "Vertretungsanfrage ablehnen"
      description: "Lehnt die Vertretungsanfrage für die angegebene Abwesenheit ab.\
        \ Nur die vorgesehene Vertretung darf diesen Endpunkt aufrufen."
      operationId: "reject_2"
      parameters:
      - name: "id"
        in: "path"
        description: "ID der Abwesenheit."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 42
      responses:
        "200":
          description: "Anfrage abgelehnt; gibt die aktualisierte Abwesenheit zurü\
            ck."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubstituteRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Aufrufer ist nicht die vorgesehene Vertretung."
        "404":
          description: "Abwesenheit nicht gefunden."
        "409":
          description: "Die Anfrage kann im aktuellen Status nicht abgelehnt werden."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-clock/business-trip-end:
    post:
      tags:
      - "Stempeluhr"
      summary: "Dienstgang beenden"
      description: "Markiert das Ende eines aktiven Dienstgangs auf der aktuellen\
        \ Stempeluhr. Gibt 409 zurück, wenn kein Dienstgang aktiv ist."
      operationId: "businessTripEnd"
      responses:
        "200":
          description: "Dienstgang beendet; gibt den aktuellen Status zurück."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Dienstgänge nicht aktiviert, Zeiterfassung nicht aktiv oder\
            \ die Stempeluhr ist nicht aktiviert."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "409":
          description: "Kein Dienstgang ist derzeit aktiv."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-clock/business-trip-start:
    post:
      tags:
      - "Stempeluhr"
      summary: "Dienstgang starten"
      description: "Markiert den Beginn eines Dienstgangs auf der aktuell laufenden\
        \ oder pausierten Stempeluhr. Gibt 400 zurück, wenn Dienstgänge für dieses\
        \ Unternehmen nicht aktiviert sind; gibt 409 zurück, wenn die Stempeluhr nicht\
        \ aktiv ist oder ein Dienstgang bereits läuft."
      operationId: "businessTripStart"
      responses:
        "200":
          description: "Dienstgang gestartet; gibt den aktuellen Status zurück."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Dienstgänge nicht aktiviert, Zeiterfassung nicht aktiv oder\
            \ die Stempeluhr ist nicht aktiviert."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "409":
          description: "Stempeluhr läuft nicht/ist nicht pausiert oder ein Dienstgang\
            \ ist bereits aktiv."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-clock/cancel:
    post:
      tags:
      - "Stempeluhr"
      summary: "Stempeluhr abbrechen"
      description: "Verwirft die aktuelle Stempeluhr-Sitzung ohne einen Zeiteintrag\
        \ zu speichern. Gibt 409 zurück, wenn die Stempeluhr bereits `idle` ist."
      operationId: "cancel"
      responses:
        "200":
          description: "Stempeluhr abgebrochen; gibt den Status `idle` zurück."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Zeiterfassung für dieses Unternehmen nicht aktiv oder die\
            \ Stempeluhr ist nicht aktiviert."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "409":
          description: "Die Stempeluhr ist bereits `idle`."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-clock/status:
    get:
      tags:
      - "Stempeluhr"
      summary: "Stempeluhr-Status abrufen"
      description: "Gibt den aktuellen Status der Stempeluhr zurück: `idle`, `running`,\
        \ `paused` oder `waiting`. Enthält außerdem die verstrichene Zeit, die Pausenzeit\
        \ und den Dienstgang-Status."
      operationId: "getStatus"
      responses:
        "200":
          description: "Aktueller Stempeluhr-Status."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Zeiterfassung für dieses Unternehmen nicht aktiv."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-clock/pause:
    post:
      tags:
      - "Stempeluhr"
      summary: "Stempeluhr pausieren"
      description: "Pausiert eine laufende Stempeluhr. Gibt 409 zurück, wenn die Stempeluhr\
        \ nicht im Status `running` ist."
      operationId: "pause"
      responses:
        "200":
          description: "Stempeluhr pausiert; gibt den aktuellen Status zurück."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Zeiterfassung für dieses Unternehmen nicht aktiv oder die\
            \ Stempeluhr ist nicht aktiviert."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Das Pausieren ist für diesen Benutzer auf das Zeiterfassungsterminal\
            \ beschränkt."
        "409":
          description: "Die Stempeluhr läuft nicht."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-clock/resume:
    post:
      tags:
      - "Stempeluhr"
      summary: "Stempeluhr fortsetzen"
      description: "Setzt eine pausierte Stempeluhr fort. Gibt 409 zurück, wenn die\
        \ Stempeluhr nicht im Status `paused` ist."
      operationId: "resume"
      responses:
        "200":
          description: "Stempeluhr fortgesetzt; gibt den aktuellen Status zurück."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Zeiterfassung für dieses Unternehmen nicht aktiv oder die\
            \ Stempeluhr ist nicht aktiviert."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Das Fortsetzen ist für diesen Benutzer auf das Zeiterfassungsterminal\
            \ beschränkt."
        "409":
          description: "Die Stempeluhr ist nicht pausiert."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-clock/start:
    post:
      tags:
      - "Stempeluhr"
      summary: "Stempeluhr starten"
      description: "Startet die Stempeluhr für den authentifizierten Benutzer. Gibt\
        \ 409 zurück, wenn die Stempeluhr bereits läuft. Die Uhr kann den Status `waiting`\
        \ annehmen, wenn ein Mindest-Startintervall konfiguriert ist."
      operationId: "start"
      responses:
        "200":
          description: "Stempeluhr gestartet; gibt den aktuellen Status zurück."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Zeiterfassung für dieses Unternehmen nicht aktiv oder die\
            \ Stempeluhr ist nicht aktiviert."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Benutzer ist nicht berechtigt, Zeiteinträge zu erstellen,\
            \ oder das Starten ist für diesen Benutzer auf das Zeiterfassungsterminal\
            \ beschränkt."
        "409":
          description: "Die Stempeluhr läuft bereits oder die Startbedingungen sind\
            \ nicht erfüllt."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-clock/stop:
    post:
      tags:
      - "Stempeluhr"
      summary: "Stempeluhr stoppen und speichern"
      description: "Stoppt die laufende oder pausierte Stempeluhr und speichert den\
        \ resultierenden Zeiteintrag. Optional können eine Projekt-ID und Anmerkungen\
        \ angegeben werden. Befindet sich die Uhr im Status `waiting`, wird sie verworfen\
        \ und 204 zurückgegeben. Gibt 409 zurück, wenn die Gesamtdauer zu kurz ist\
        \ (< 1 Minute) oder ein doppelter Eintrag erkannt wird.\n\nVerletzt die Sitzung\
        \ die Pausenregelung des Unternehmens und wurde keine `breakRegulation`-Entscheidung\
        \ gesendet, wird der Eintrag **nicht** gespeichert: Die Antwort enthält `outcome:\
        \ confirmationRequired` und ein `breakRegulation`-Objekt mit der erforderlichen\
        \ Änderung. Den Stopp mit `breakRegulation: apply` (Korrektur übernehmen)\
        \ oder `breakRegulation: ignore` (unverändert speichern) erneut senden. Clients\
        \ sollten `ignore` nur bei nicht verpflichtender Regelung anbieten (`breakRegulation.obligatory`\
        \ in der Antwort); der Server erzwingt dies derzeit nicht. Ist Auto-Übernahme\
        \ konfiguriert, wird die Korrektur bereits beim ersten Stopp ohne Entscheidung\
        \ angewendet."
      operationId: "stop"
      requestBody:
        description: "Optionale Projekt-ID, Kategorie-ID und Anmerkungen für den gespeicherten\
          \ Eintrag."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimeClockStopRequest"
      responses:
        "200":
          description: "Entweder die gespeicherten Eintragsdetails (`outcome: saved`)\
            \ oder eine Bestätigungsanforderung zur Pausenregelung (`outcome: confirmationRequired`)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStopResponse"
        "204":
          description: "Stempeluhr war im Status `waiting` und wurde verworfen, ohne\
            \ zu speichern."
        "400":
          description: "Zeiterfassung für dieses Unternehmen nicht aktiv oder die\
            \ Stempeluhr ist nicht aktiviert."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Das Stoppen ist für diesen Benutzer auf das Zeiterfassungsterminal\
            \ beschränkt."
        "409":
          description: "Stempeluhr ist nicht aktiv, Dauer zu kurz oder doppelter Eintrag\
            \ erkannt."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-entry-requests/{id}/approve:
    post:
      tags:
      - "Zeiterfassungsanträge"
      summary: "Zeiterfassungsantrag genehmigen"
      description: "Genehmigt den ausstehenden Änderungs- oder Löschantrag für den\
        \ angegebenen Zeiteintrag. Bei Löschanträgen wird der Eintrag entfernt; bei\
        \ Änderungsanträgen wird der Eintrag auf die vorgeschlagenen Werte aktualisiert.\
        \ Erfordert die Rolle `MANAGER` oder `ADMIN`."
      operationId: "approve_2"
      parameters:
      - name: "id"
        in: "path"
        description: "ID des Zeiteintrags."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 123
      responses:
        "200":
          description: "Antrag genehmigt; gibt den aktualisierten Zeiteintrag zurü\
            ck."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntryRequestDto"
        "400":
          description: "Zeiterfassung für dieses Unternehmen nicht aktiv."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Aufrufer ist kein Vorgesetzter oder Administrator, oder\
            \ nur Administratoren dürfen in diesem Unternehmen genehmigen."
        "404":
          description: "Zeiteintrag nicht gefunden."
        "409":
          description: "Kein ausstehender Antrag oder der vorgeschlagene Änderung\
            \ hat einen Terminkonflikt."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-entry-requests:
    get:
      tags:
      - "Zeiterfassungsanträge"
      summary: "Ausstehende Zeiterfassungsanträge auflisten"
      description: "Gibt alle ausstehenden Zeiterfassungsanträge im Team des Vorgesetzten\
        \ zurück, die auf Genehmigung warten. Erfordert die Rolle `MANAGER` oder `ADMIN`.\
        \ Gibt eine leere Liste zurück, wenn die Zeiterfassung nicht aktiv ist oder\
        \ der Aufrufer ein Vorgesetzter in einem Unternehmen mit ausschließlicher\
        \ Admin-Genehmigung ist."
      operationId: "getList_1"
      responses:
        "200":
          description: "Liste ausstehender Zeiterfassungsanträge."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/TimeEntryRequestDto"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Aufrufer ist kein Vorgesetzter oder Administrator."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-entry-requests/{id}/reject:
    post:
      tags:
      - "Zeiterfassungsanträge"
      summary: "Zeiterfassungsantrag ablehnen"
      description: "Lehnt den ausstehenden Änderungs- oder Löschantrag für den angegebenen\
        \ Zeiteintrag ab. Ein optionaler Ablehnungsgrund kann im Anfragetext angegeben\
        \ werden. Der Mitarbeiter wird per E-Mail benachrichtigt. Erfordert die Rolle\
        \ `MANAGER` oder `ADMIN`."
      operationId: "reject_3"
      parameters:
      - name: "id"
        in: "path"
        description: "ID des Zeiteintrags."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 123
      requestBody:
        description: "Optionaler Ablehnungsgrund."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RejectRequest"
      responses:
        "200":
          description: "Antrag abgelehnt; gibt den Zeiteintrag im aktuellen Status\
            \ zurück."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntryRequestDto"
        "400":
          description: "Zeiterfassung für dieses Unternehmen nicht aktiv."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Aufrufer ist kein Vorgesetzter oder Administrator, oder\
            \ nur Administratoren dürfen in diesem Unternehmen ablehnen."
        "404":
          description: "Zeiteintrag nicht gefunden."
        "409":
          description: "Kein ausstehender Antrag für diesen Eintrag."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-entries/{id}:
    delete:
      tags:
      - "Zeiteinträge"
      summary: "Zeiteintrag löschen"
      description: "Löscht den angegebenen Zeiteintrag. Wenn die Zeiterfassungseinstellungen\
        \ des Unternehmens eine Genehmigung durch den Vorgesetzten für Löschungen\
        \ erfordern, wird stattdessen ein Löschantrag gestellt. Das Antwortfeld `result`\
        \ ist entweder `deleted` oder `deletion_requested`."
      operationId: "delete_1"
      parameters:
      - name: "id"
        in: "path"
        description: "ID des Zeiteintrags."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 123
      responses:
        "200":
          description: "Eintrag gelöscht oder Löschantrag gestellt."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResultResponse"
        "400":
          description: "Ungültige ID oder Zeiterfassung ist für dieses Unternehmen\
            \ nicht aktiv."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Aufrufer darf diesen Eintrag nicht löschen."
        "404":
          description: "Zeiteintrag nicht gefunden."
        "409":
          description: "Der Eintrag ist durch den Monatsabschluss gesperrt."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-entries/my:
    get:
      tags:
      - "Zeiteinträge"
      summary: "Eigene Zeiteinträge des Aufrufers auflisten"
      description: "Liefert die erfassten Zeiteinträge des Aufrufers, passend zur\
        \ Listenansicht der Zeiterfassung in der Web-Oberfläche. Ohne Parameter wird\
        \ der aktuelle Monat zurückgegeben. Mit `year` und `month` kann ein anderer\
        \ Monat gewählt werden, mit `year` allein ein ganzes Jahr. Einträge können\
        \ zusätzlich nach Projekt, Kategorie und Status gefiltert werden. Die Einträ\
        ge sind nach Datum und Startzeit sortiert, neueste zuerst."
      operationId: "getMyEntries"
      parameters:
      - name: "year"
        in: "query"
        description: "Jahr, für das Einträge aufgelistet werden. Zulässiger Bereich:\
          \ 1970-2100. Standard ist das aktuelle Jahr."
        schema:
          type: "integer"
          format: "int32"
        example: 2025
      - name: "month"
        in: "query"
        description: "Monat, für den Einträge aufgelistet werden (1-12). Standard\
          \ ist der aktuelle Monat, wenn auch `year` fehlt; weglassen und nur `year`\
          \ übergeben, um das ganze Jahr zu erhalten."
        schema:
          type: "integer"
          format: "int32"
        example: 7
      - name: "projectId"
        in: "query"
        description: "Nur Einträge zurückgeben, die diesem Projekt zugeordnet sind.\
          \ Entspricht `id` aus `GET /projects`."
        schema:
          type: "integer"
          format: "int32"
        example: 17
      - name: "categoryId"
        in: "query"
        description: "Nur Einträge zurückgeben, die dieser Kategorie zugeordnet sind.\
          \ Entspricht `id` aus `GET /categories`."
        schema:
          type: "integer"
          format: "int32"
        example: 4
      - name: "status"
        in: "query"
        description: "Nur Einträge mit diesem Status zurückgeben. Einer von `done`,\
          \ `pending`, `approved`, `rejected`, `editing`."
        schema:
          type: "string"
        example: "done"
      responses:
        "200":
          description: "Liste der Zeiteinträge des Aufrufers. Leeres Array, wenn keine\
            \ vorhanden sind oder die Zeiterfassung nicht aktiv ist."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/TimeEntryDto"
        "400":
          description: "Ungültiger Wert für `year`, `month`, `projectId`, `categoryId`\
            \ oder `status`."
          content:
            application/json: {}
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
          content:
            application/json: {}
        "403":
          description: "Das Konto des Aufrufers ist inaktiv."
          content:
            application/json: {}
        "500":
          description: "Unerwarteter Serverfehler."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /time-entries/pending-count:
    get:
      tags:
      - "Zeiteinträge"
      summary: "Anzahl ausstehender Zeiteinträge abrufen"
      description: "Gibt die Anzahl der Zeiteinträge im Team des Vorgesetzten zurü\
        ck, die auf Genehmigung warten. Erfordert die Rolle `MANAGER` oder `ADMIN`."
      operationId: "getPendingCount_3"
      responses:
        "200":
          description: "Anzahl ausstehender Einträge."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntryPendingCountResponse"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Aufrufer ist kein Vorgesetzter oder Administrator."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-tracking/clock-entry:
    post:
      tags:
      - "Zeiterfassung"
      summary: "Abgeschlossenen Zeiteintrag einreichen"
      description: "Erstellt einen Zeiteintrag aus expliziten Start- und End-Unix-Zeitstempeln\
        \ (Millisekunden). Start und Ende müssen am selben Kalendertag liegen. Wenn\
        \ das Unternehmen eine Genehmigung durch den Vorgesetzten für neue Einträ\
        ge erfordert, wird der Eintrag mit dem Status `pending` erstellt; andernfalls\
        \ ist er sofort `done`."
      operationId: "createClockEntry"
      requestBody:
        description: "Zeiteintrag mit Start-/Endzeitstempeln sowie optionalem Projekt\
          \ und Anmerkungen."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ClockEntryRequest"
        required: true
      responses:
        "200":
          description: "Eintrag erstellt; gibt die Eintrags-ID sowie die geleisteten\
            \ und Pausenminuten zurück."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClockEntryResponse"
        "400":
          description: "Fehlender oder ungültiger Anfragetext, ungültige Zeitstempel\
            \ oder Zeiterfassung nicht aktiv."
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "403":
          description: "Der Benutzer darf keine Zeiteinträge erstellen."
        "422":
          description: "Die Anmerkungen überschreiten die maximale Länge."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-tracking/time-off-summary:
    get:
      tags:
      - "Zeiterfassung"
      summary: "Abwesenheitszusammenfassung abrufen"
      description: "Gibt die verbleibenden und die Gesamtanzahl der Urlaubstage des\
        \ authentifizierten Benutzers für das laufende Jahr sowie die Anzahl der genommenen\
        \ Krankheitstage zurück."
      operationId: "getTimeOffSummary"
      responses:
        "200":
          description: "Abwesenheitszusammenfassung."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeOffSummaryResponse"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /time-tracking/work-summary:
    get:
      tags:
      - "Zeiterfassung"
      summary: "Heutige Arbeitszusammenfassung abrufen"
      description: "Gibt die Sollarbeitszeit, die geleisteten Minuten, die verbleibenden\
        \ Minuten, den Fertigstellungsgrad und die Pausenminuten des authentifizierten\
        \ Benutzers für den heutigen Tag zurück."
      operationId: "getWorkSummary"
      responses:
        "200":
          description: "Heutige Arbeitszusammenfassung."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkSummaryResponse"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
  /user/profile:
    get:
      tags:
      - "Benutzerprofil"
      summary: "Profil abrufen"
      description: "Gibt Anzeigename, E-Mail-Adresse, Rolle (`USER`, `MANAGER`, `ADMIN`),\
        \ Abteilung, Niederlassung und Avatar-URL des authentifizierten Benutzers\
        \ zurück."
      operationId: "getProfile"
      responses:
        "200":
          description: "Profildaten."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserProfileResponse"
        "401":
          description: "Das Inhaber-Token fehlt oder ist ungültig."
        "500":
          description: "Unerwarteter Serverfehler."
      security:
      - bearerAuth: []
components:
  schemas:
    AbsenceRequestDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne ID des Abwesenheitsantrags."
          example: "12345"
        type:
          type: "string"
          description: "Abwesenheitstyp-Code (3 Zeichen), z. B. `\"URL\"` für Urlaub."
          example: "URL"
        startDate:
          type: "string"
          description: "Erster Tag der Abwesenheit im ISO-8601-Format (`YYYY-MM-DD`)."
          example: "2025-08-01"
        endDate:
          type: "string"
          description: "Letzter Tag der Abwesenheit im ISO-8601-Format (`YYYY-MM-DD`)."
          example: "2025-08-05"
        days:
          type: "number"
          description: "Anzahl der Werktage, die die Abwesenheit umfasst, berechnet\
            \ gemäß dem Arbeitszeitplan des Mitarbeiters."
          format: "double"
          example: 4.0
        status:
          type: "string"
          description: "Aktueller Status des Antrags. Einer von `pending` (wartet\
            \ auf Genehmigung), `approved` oder `rejected`."
          example: "pending"
          enum:
          - "pending"
          - "approved"
          - "rejected"
        halfDay:
          type: "string"
          description: "Halbtags-Indikator. `\"morning\"` oder `\"afternoon\"` fü\
            r Halbtagsanträge; `null` für ganztägige Abwesenheiten."
          nullable: true
          example: "morning"
          enum:
          - "morning"
          - "afternoon"
        substitutePending:
          type: "boolean"
          description: "Ob die Bestätigung des Vertreters noch aussteht. `null`, wenn\
            \ kein Vertreter gesetzt wurde."
          nullable: true
          example: false
        medicalCertificateProvided:
          type: "boolean"
          description: "Ob ein Attest für diese Abwesenheit hochgeladen wurde. Nur\
            \ vorhanden bei Abwesenheitstypen, die ein Attest erfordern; sonst `null`."
          nullable: true
          example: false
        employeeName:
          type: "string"
          description: "Anzeigename des Mitarbeiters, der den Antrag gestellt hat.\
            \ Nur in Antworten von `GET /team` vorhanden; `null` in `GET /my`-Antworten."
          nullable: true
          example: "Maria Müller"
        employeeId:
          type: "string"
          description: "Interne ID des Mitarbeiters, der den Antrag gestellt hat.\
            \ Nur in Antworten von `GET /team` vorhanden; `null` in `GET /my`-Antworten."
          nullable: true
          example: "42"
    CreateAbsenceRequest:
      required:
      - "absenceTypeId"
      - "endDate"
      - "startDate"
      type: "object"
      properties:
        absenceTypeId:
          type: "string"
          description: "Abwesenheitstyp-Code (3 Zeichen), z. B. `\"URL\"` für Urlaub.\
            \ Muss einem aktiven Typ aus `GET /absence-types` entsprechen."
          example: "URL"
        startDate:
          type: "string"
          description: "Erster Tag der Abwesenheit im ISO-8601-Format (`YYYY-MM-DD`)."
          example: "2025-08-01"
        endDate:
          type: "string"
          description: "Letzter Tag der Abwesenheit im ISO-8601-Format (`YYYY-MM-DD`).\
            \ Muss gleich oder nach `startDate` liegen. Bei Halbtagsanträgen muss\
            \ es gleich `startDate` sein."
          example: "2025-08-05"
        halfDay:
          type: "string"
          description: "Auf `\"morning\"` oder `\"afternoon\"` setzen, um einen einzelnen\
            \ Halbtag zu beantragen. Wenn gesetzt, müssen `startDate` und `endDate`\
            \ denselben Tag haben. Weglassen oder `null` senden für eine ganztägige\
            \ Abwesenheit."
          example: "morning"
          enum:
          - "morning"
          - "afternoon"
        comments:
          type: "string"
          description: "Optionaler Freitextkommentar, der den Managern des Mitarbeiters\
            \ angezeigt wird."
          example: "Holiday trip"
        substituteEmployeeId:
          type: "string"
          description: "Mitarbeiter-ID des designierten Vertreters. Muss zum selben\
            \ Unternehmen gehören. Weglassen oder `null` senden, wenn kein Vertreter\
            \ benötigt wird."
          example: "42"
    DeleteAbsenceRequest:
      type: "object"
      properties:
        reason:
          type: "string"
          description: "Optionaler Freitextgrund für die Stornierung. Erforderlich,\
            \ wenn die Unternehmenskonfiguration einen Grund für Stornierungsanträ\
            ge vorschreibt."
          example: "Plans changed"
    DeleteResultResponse:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne ID des betroffenen Eintrags."
          example: "12345"
        result:
          type: "string"
          description: "Ergebnis der Operation. `\"deleted\"`, wenn der Eintrag direkt\
            \ gelöscht wurde; `\"cancellation_requested\"`, wenn stattdessen ein Stornierungsantrag\
            \ erstellt wurde; `\"deletion_requested\"`, wenn ein Löschantrag erstellt\
            \ wurde (für Zeiteinträge, die genehmigt werden müssen)."
          example: "deleted"
          enum:
          - "deleted"
          - "cancellation_requested"
          - "deletion_requested"
    AbsencePendingCountResponse:
      type: "object"
      properties:
        numberOfAbsenceRequests:
          type: "integer"
          description: "Anzahl der Abwesenheitsanträge im Team des Aufrufers, die\
            \ auf die Genehmigung des Aufrufers warten."
          format: "int32"
          example: 3
        numberOfAbsenceRequestCancellations:
          type: "integer"
          description: "Anzahl der Stornierungsanträge im Team des Aufrufers, die\
            \ auf eine Genehmigung warten."
          format: "int32"
          example: 1
    RejectRequest:
      type: "object"
      properties:
        reason:
          type: "string"
          description: "Optionaler Freitextgrund für die Ablehnung. Wird am Antrag\
            \ gespeichert und dem Mitarbeiter angezeigt."
          example: "Insufficient staffing during that period"
    UpdateAbsenceRequest:
      type: "object"
      properties:
        absenceTypeId:
          type: "string"
          description: "Neuer Abwesenheitstyp-Code (3 Zeichen). `null` senden, um\
            \ den bestehenden Typ zu behalten."
          example: "URL"
        startDate:
          type: "string"
          description: "Neues Startdatum im ISO-8601-Format (`YYYY-MM-DD`). `null`\
            \ senden, um den bestehenden Wert zu behalten."
          example: "2025-08-01"
        endDate:
          type: "string"
          description: "Neues Enddatum im ISO-8601-Format (`YYYY-MM-DD`). Muss gleich\
            \ oder nach `startDate` liegen. `null` senden, um den bestehenden Wert\
            \ zu behalten."
          example: "2025-08-05"
        halfDay:
          type: "string"
          description: "Neue Halbtags-Einstellung. `\"morning\"` oder `\"afternoon\"\
            ` um auf einen Halbtag zu wechseln; leerer String `\"\"` um den Halbtag\
            \ zu entfernen und auf ganztägig zu stellen; `null` um den bestehenden\
            \ Wert zu behalten."
          example: "morning"
          enum:
          - "morning"
          - "afternoon"
          - ""
        comments:
          type: "string"
          description: "Neuer Freitextkommentar. `null` senden, um den bestehenden\
            \ Kommentar zu behalten; leeren String senden, um ihn zu löschen."
          example: "Rescheduled trip"
        substituteEmployeeId:
          type: "string"
          description: "Neue Vertreter-Mitarbeiter-ID. `null` senden, um den bestehenden\
            \ Vertreter zu behalten; `\"0\"` oder einen leeren String senden, um den\
            \ Vertreter zu entfernen."
          example: "42"
    AbsenceTypeDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Abwesenheitstyp-Code (3 Zeichen), der beim Erstellen oder\
            \ Aktualisieren eines Abwesenheitsantrags verwendet wird."
          example: "URL"
        name:
          type: "string"
          description: "Lesbarer Name der Abwesenheitsart in der Sprache des Benutzers."
          example: "Urlaub"
        needsApproval:
          type: "boolean"
          description: "Ob Anträge dieses Typs eine Genehmigung durch den Manager\
            \ erfordern, bevor sie als genehmigt gelten. Bei `false` wird der Antrag\
            \ sofort bei der Erstellung genehmigt."
          example: true
        requiresMedicalCertificate:
          type: "boolean"
          description: "Ob für Abwesenheiten dieses Typs ein Attest hochgeladen werden\
            \ muss."
          example: false
    CancellationRequestDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne ID des zu stornierenden Abwesenheitsantrags."
          example: "12345"
        employeeId:
          type: "string"
          description: "Interne ID des Mitarbeiters, der den ursprünglichen Abwesenheitsantrag\
            \ gestellt hat."
          example: "42"
        employeeName:
          type: "string"
          description: "Anzeigename des Mitarbeiters, der den ursprünglichen Abwesenheitsantrag\
            \ gestellt hat."
          example: "Maria Müller"
        absenceType:
          type: "string"
          description: "Abwesenheitstyp-Code (3 Zeichen) des zu stornierenden Antrags."
          example: "URL"
        startDate:
          type: "string"
          description: "Startdatum der zu stornierenden Abwesenheit im ISO-8601-Format\
            \ (`YYYY-MM-DD`)."
          example: "2025-08-01"
        endDate:
          type: "string"
          description: "Enddatum der zu stornierenden Abwesenheit im ISO-8601-Format\
            \ (`YYYY-MM-DD`)."
          example: "2025-08-05"
        days:
          type: "number"
          description: "Anzahl der Werktage, die die Abwesenheit umfasst."
          format: "double"
          example: 4.0
        status:
          type: "string"
          description: "Aktueller Status des Stornierungsantrags. Einer von `pending`\
            \ (wartet auf Genehmigung) oder `approved`."
          example: "pending"
          enum:
          - "pending"
          - "approved"
        cancelComments:
          type: "string"
          description: "Optionaler Freitextgrund, den der Mitarbeiter beim Stellen\
            \ des Stornierungsantrags angegeben hat."
          nullable: true
          example: "Plans changed"
    CancellationPendingCountResponse:
      type: "object"
      properties:
        numberOfCancellationRequests:
          type: "integer"
          description: "Anzahl der Stornierungsanträge im Team des Aufrufers, die\
            \ auf eine Genehmigung warten."
          format: "int32"
          example: 2
    CategoriesResponse:
      type: "object"
      properties:
        categories:
          type: "array"
          description: "Liste der Kostenstellen/Kategorien, die für das Unternehmen\
            \ des authentifizierten Benutzers verfügbar sind."
          items:
            $ref: "#/components/schemas/CategoryDto"
        defaultCategoryId:
          type: "string"
          description: "ID der Kategorie, die beim Erstellen eines Zeiteintrags standardmä\
            ßig vorausgewählt ist. `null`, wenn kein Standard konfiguriert ist."
          nullable: true
          example: "7"
        isCategoryMandatory:
          type: "boolean"
          description: "Ob der Mitarbeiter beim Erstellen eines Zeiteintrags eine\
            \ Kategorie auswählen muss."
          example: false
    CategoryDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne ID der Kategorie, die beim Einreichen eines Zeiteintrags\
            \ verwendet wird."
          example: "7"
        name:
          type: "string"
          description: "Lesbarer Name der Kategorie."
          example: "Internal project"
      description: "Liste der Kostenstellen/Kategorien, die für das Unternehmen des\
        \ authentifizierten Benutzers verfügbar sind."
    EmployeeDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne Mitarbeiter-ID (v1-CSV-Spalte `userid`). Dies ist\
            \ derselbe Wert, der von `GET /absence-requests/team` als `employeeId`\
            \ zurückgegeben wird, und kann daher verwendet werden, um zu ermitteln,\
            \ zu welchem Mitarbeiter ein Abwesenheitsantrag gehört."
          example: "42"
        employeeNumber:
          type: "string"
          description: "Vom Unternehmen vergebene Personalnummer (v1-CSV-Spalte `employeeNo`).\
            \ `null`, wenn für den Mitarbeiter keine Personalnummer hinterlegt ist."
          nullable: true
          example: "MA-1024"
        firstName:
          type: "string"
          description: "Vorname des Mitarbeiters (v1-CSV-Spalte `firstname`)."
          nullable: true
          example: "Maria"
        lastName:
          type: "string"
          description: "Nachname des Mitarbeiters (v1-CSV-Spalte `lastname`)."
          nullable: true
          example: "Müller"
        name:
          type: "string"
          description: "Vollständiger Anzeigename des Mitarbeiters (Vor- und Nachname\
            \ zusammengefügt)."
          example: "Maria Müller"
        email:
          type: "string"
          description: "E-Mail-Adresse des Mitarbeiters (v1-CSV-Spalte `email`). `null`,\
            \ wenn keine E-Mail-Adresse hinterlegt ist."
          nullable: true
          example: "maria.mueller@example.com"
        phone:
          type: "string"
          description: "Festnetznummer (v1-CSV-Spalte `phone`). `null`, wenn nicht\
            \ gesetzt."
          nullable: true
          example: "+49 30 1234567"
        mobile:
          type: "string"
          description: "Mobilnummer (v1-CSV-Spalte `mobile`). `null`, wenn nicht gesetzt."
          nullable: true
          example: "+49 170 1234567"
        role:
          type: "string"
          description: "Rolle des Mitarbeiters im Unternehmen. Einer von `USER`, `MANAGER`\
            \ oder `ADMIN`. Dies ist das stabile, sprachunabhängige Gegenstück zur\
            \ lokalisierten v1-CSV-Spalte `userType`."
          example: "USER"
          enum:
          - "USER"
          - "MANAGER"
          - "ADMIN"
        department:
          type: "string"
          description: "Name der Abteilung, der der Mitarbeiter angehört (v1-CSV-Spalte\
            \ `unit`). `null`, wenn das Unternehmen keine Abteilungen verwendet."
          nullable: true
          example: "Engineering"
        costCenter:
          type: "string"
          description: "Kostenstelle, der der Mitarbeiter zugeordnet ist (v1-CSV-Spalte\
            \ `kst`). `null`, wenn nicht gesetzt."
          nullable: true
          example: "CC-100"
        branchOffice:
          type: "string"
          description: "Name der Niederlassung, der der Mitarbeiter zugeordnet ist\
            \ (v1-CSV-Spalte `niederlassung`). `null`, wenn das Unternehmen keine\
            \ Niederlassungen verwendet."
          nullable: true
          example: "Berlin"
        language:
          type: "string"
          description: "Bevorzugte Oberflächensprache des Mitarbeiters als Locale-Zeichenkette\
            \ (v1-CSV-Spalte `lang`). `null`, wenn nicht gesetzt."
          nullable: true
          example: "de_DE"
        managerIds:
          type: "array"
          description: "Interne IDs der zugeordneten Manager des Mitarbeiters (v1-CSV-Spalte\
            \ `userIdMNG`). Leeres Array, wenn dem Mitarbeiter kein Manager zugeordnet\
            \ ist."
          items:
            type: "string"
            example: "17"
        additionalInfo:
          type: "string"
          description: "Zusätzliche Freitext-Information zum Mitarbeiter (v1-CSV-Spalte\
            \ `addInfo`). `null`, wenn nicht gesetzt."
          nullable: true
          example: "Works part-time on Fridays"
        employmentStartDate:
          type: "string"
          description: "Eintrittsdatum im ISO-8601-Format (`YYYY-MM-DD`) (v1-CSV-Spalte\
            \ `jobStartDateAPI`). `null`, wenn nicht gesetzt."
          nullable: true
          example: "2020-03-15"
        employmentEndDate:
          type: "string"
          description: "Austrittsdatum im ISO-8601-Format (`YYYY-MM-DD`) (v1-CSV-Spalte\
            \ `jobEndDateAPI`). `null`, wenn der Mitarbeiter noch beschäftigt ist\
            \ bzw. kein Austrittsdatum gesetzt ist."
          nullable: true
          example: "2024-12-31"
        dateOfBirth:
          type: "string"
          description: "Geburtsdatum im ISO-8601-Format (`YYYY-MM-DD`) (v1-CSV-Spalte\
            \ `birthdayAPI`). `null`, wenn nicht gesetzt."
          nullable: true
          example: "1988-07-24"
        active:
          type: "boolean"
          description: "Ob das Konto des Mitarbeiters aktiv ist. Dies ist die Umkehrung\
            \ der v1-CSV-Spalte `isAccountLocked`: `false` bei gesperrten (deaktivierten)\
            \ Konten, die weiterhin aufgeführt werden, damit historische `employeeId`-Verweise\
            \ aufgelöst werden können."
          example: true
    OauthTokenResponse:
      type: "object"
      properties:
        access_token:
          type: "string"
          description: "JWT-Zugriffstoken. Bei nachfolgenden Anfragen als `Authorization:\
            \ Bearer <token>` übergeben."
          example: "eyJhbGciOiJSUzI1NiJ9..."
        refresh_token:
          type: "string"
          description: "Undurchsichtiger Refresh-Token. Gegen ein neues Zugriffstoken\
            \ über `grant_type=refresh_token` eintauschen. Jeder Refresh-Token kann\
            \ nur einmal verwendet werden."
          example: "rt-a1b2c3d4e5f6..."
        token_type:
          type: "string"
          description: "Token-Typ. Immer `Bearer`."
          example: "Bearer"
        expires_in:
          type: "integer"
          description: "Gültigkeitsdauer des Zugriffstokens in Sekunden."
          format: "int32"
          example: 3600
        scope:
          type: "string"
          description: "Gewährter Geltungsbereich."
          example: "read"
    MfaChallengeResponse:
      type: "object"
      properties:
        mfa_required:
          type: "boolean"
          description: "Immer `true` — zeigt an, dass ein 2FA-Schritt erforderlich\
            \ ist."
          example: true
        mfa_token:
          type: "string"
          description: "Kurzlebiger Token, der diese MFA-Anforderung identifiziert.\
            \ Als `mfa_token` mit `grant_type=mfa_totp` übergeben."
          example: "mfa-a1b2c3d4e5f6..."
        mfa_expires_in:
          type: "integer"
          description: "Gültigkeitsdauer des MFA-Tokens in Sekunden."
          format: "int32"
          example: 300
      description: "Wird von `grant_type=password` zurückgegeben, wenn das Konto 2FA\
        \ aktiviert hat. Den `mfa_token` gegen ein vollständiges Token-Paar eintauschen,\
        \ indem dieser Endpunkt erneut mit `grant_type=mfa_totp` aufgerufen wird."
    OauthErrorResponse:
      type: "object"
      properties:
        error:
          type: "string"
          description: "OAuth-2.0-Fehlercode (z. B. `invalid_grant`, `invalid_client`)."
          example: "invalid_grant"
        error_description:
          type: "string"
          description: "Menschenlesbare Fehlerbeschreibung."
          example: "Invalid username or password"
    ProjectDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne ID des Projekts, die beim Einreichen eines Zeiteintrags\
            \ verwendet wird."
          example: "5"
        name:
          type: "string"
          description: "Lesbarer Name des Projekts."
          example: "Website Relaunch"
        isFavorite:
          type: "boolean"
          description: "Ob dieses Projekt vom authentifizierten Benutzer als Favorit\
            \ markiert wurde."
          example: true
      description: "Liste der Projekte, die für das Unternehmen des authentifizierten\
        \ Benutzers verfügbar sind."
    ProjectsResponse:
      type: "object"
      properties:
        projects:
          type: "array"
          description: "Liste der Projekte, die für das Unternehmen des authentifizierten\
            \ Benutzers verfügbar sind."
          items:
            $ref: "#/components/schemas/ProjectDto"
        defaultProjectId:
          type: "string"
          description: "ID des Projekts, das beim Erstellen eines Zeiteintrags standardmä\
            ßig vorausgewählt ist. `null`, wenn kein Standard konfiguriert ist."
          nullable: true
          example: "5"
        isProjectMandatory:
          type: "boolean"
          description: "Ob der Mitarbeiter beim Erstellen eines Zeiteintrags ein Projekt\
            \ auswählen muss."
          example: true
    SubstituteEmployeeDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne Mitarbeiter-ID, die als `substituteEmployeeId` beim\
            \ Erstellen eines Abwesenheitsantrags verwendet wird."
          example: "42"
        name:
          type: "string"
          description: "Anzeigename des Mitarbeiters."
          example: "Thomas Schmidt"
        department:
          type: "string"
          description: "Abteilung, der der Mitarbeiter angehört. `null`, wenn das\
            \ Unternehmen keine Abteilungen verwendet."
          nullable: true
          example: "Engineering"
    SubstituteRequestDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne ID des Abwesenheitsantrags, für den die Vertretung\
            \ angefragt wurde."
          example: "12345"
        employeeId:
          type: "string"
          description: "Interne ID des abwesenden Mitarbeiters, der eine Vertretung\
            \ benötigt."
          example: "7"
        employeeName:
          type: "string"
          description: "Anzeigename des abwesenden Mitarbeiters."
          example: "Anna Bauer"
        absenceType:
          type: "string"
          description: "Abwesenheitstyp-Code (3 Zeichen) der Abwesenheit, für die\
            \ diese Vertretung angefragt wurde."
          example: "URL"
        startDate:
          type: "string"
          description: "Startdatum der Abwesenheit im ISO-8601-Format (`YYYY-MM-DD`)."
          example: "2025-08-01"
        endDate:
          type: "string"
          description: "Enddatum der Abwesenheit im ISO-8601-Format (`YYYY-MM-DD`)."
          example: "2025-08-05"
        days:
          type: "number"
          description: "Anzahl der Werktage, die die Abwesenheit umfasst."
          format: "double"
          example: 4.0
        status:
          type: "string"
          description: "Aktueller Status der Vertretungsbestätigung. Einer von `pending`\
            \ (wartet auf Bestätigung des Vertreters) oder `confirmed`."
          example: "pending"
          enum:
          - "pending"
          - "confirmed"
    SubstitutePendingCountResponse:
      type: "object"
      properties:
        numberOfSubstituteRequests:
          type: "integer"
          description: "Anzahl der Vertretungsanfragen, die auf die Bestätigung des\
            \ Aufrufers warten."
          format: "int32"
          example: 1
    TimeClockStatusDto:
      type: "object"
      properties:
        status:
          type: "string"
          description: "Aktueller Zustand der Zeiterfassung. Einer von `idle` (nicht\
            \ aktiv), `running` (eingestempelt), `paused` oder `waiting` (gestartet,\
            \ aber bis zum Ablauf eines Mindestintervalls gehalten)."
          example: "running"
          enum:
          - "idle"
          - "running"
          - "paused"
          - "waiting"
        startTimestamp:
          type: "integer"
          description: "Unix-Zeitstempel in Millisekunden, wann die aktuelle Einstempelung\
            \ begonnen hat. `null`, wenn `status` `idle` ist."
          format: "int64"
          nullable: true
          example: 1753700400000
        pauseTimestamp:
          type: "integer"
          description: "Unix-Zeitstempel in Millisekunden, wann die aktuelle Pause\
            \ begonnen hat. `null`, wenn die Uhr nicht pausiert ist."
          format: "int64"
          nullable: true
          example: 1753714800000
        workTimeElapsedSeconds:
          type: "integer"
          description: "Gesamte bisher vergangene Arbeitszeit in der aktuellen Sitzung,\
            \ in Sekunden (ohne Pausen)."
          format: "int32"
          example: 14400
        breakElapsedSeconds:
          type: "integer"
          description: "Dauer der aktuell laufenden Pause, in Sekunden. `0`, wenn\
            \ die Uhr nicht pausiert ist."
          format: "int32"
          example: 0
        accumulatedBreakSeconds:
          type: "integer"
          description: "Gesamte aufgelaufene Pausenzeit in der aktuellen Sitzung,\
            \ in Sekunden (einschließlich abgeschlossener Pausen)."
          format: "int32"
          example: 1800
        waitSeconds:
          type: "integer"
          description: "Mindestanzahl verbleibender Sekunden, bevor der Mitarbeiter\
            \ gemäß Unternehmensrichtlinie ausstempeln darf. `null`, wenn kein Minimum\
            \ gilt."
          format: "int32"
          nullable: true
          example: 0
        isBusinessTripActive:
          type: "boolean"
          description: "Ob für diese Sitzung aktuell eine Dienstreise aktiv ist. `false`,\
            \ wenn keine Dienstreise aktiv ist oder die Funktion nicht aktiviert ist."
          example: false
        startOnlyOnTerminal:
          type: "boolean"
          description: "Ob das Unternehmen das Starten der Uhr auf das Zeiterfassungsterminal\
            \ beschränkt, sodass dieser Benutzer sie nicht über die Stempeluhr in\
            \ seinem Konto starten darf. Bereits für den authentifizierten Benutzer\
            \ aufgelöst (eine Ausnahme-Benutzergruppe wird berücksichtigt)."
          example: false
        pauseOnlyOnTerminal:
          type: "boolean"
          description: "Ob das Unternehmen das Pausieren/Fortsetzen der Uhr auf das\
            \ Zeiterfassungsterminal beschränkt. Bereits für den authentifizierten\
            \ Benutzer aufgelöst (eine Ausnahme-Benutzergruppe wird berücksichtigt)."
          example: false
        stopOnlyOnTerminal:
          type: "boolean"
          description: "Ob das Unternehmen das Stoppen der Uhr auf das Zeiterfassungsterminal\
            \ beschränkt. Bereits für den authentifizierten Benutzer aufgelöst (eine\
            \ Ausnahme-Benutzergruppe wird berücksichtigt)."
          example: false
    TimeClockBreakRegulationInfo:
      type: "object"
      properties:
        hasNoRemedy:
          type: "boolean"
          description: "`true`, wenn die Verletzung nicht automatisch behoben werden\
            \ kann (z. B. die maximale tägliche Arbeitszeit ist bereits überschritten).\
            \ Die Sitzung kann dann nur verworfen oder, sofern die Regelung nicht\
            \ verpflichtend ist, mit `ignore` unverändert gespeichert werden."
          example: false
        obligatory:
          type: "boolean"
          description: "`true`, wenn die Pausenregelung für dieses Unternehmen verpflichtend\
            \ ist. Ist sie verpflichtend, darf die Sitzung nicht unverändert gespeichert\
            \ werden; der Client blendet die Option `ignore` aus."
          example: false
        correctionWorkMinutes:
          type: "integer"
          description: "Minuten, die bei Übernahme der Korrektur von der Arbeitszeit\
            \ abgezogen würden."
          format: "int32"
          example: 30
        correctionBreakMinutes:
          type: "integer"
          description: "Minuten, die bei Übernahme der Korrektur zur Pausenzeit hinzugefü\
            gt würden."
          format: "int32"
          example: 30
        resultingWorkMinutes:
          type: "integer"
          description: "Resultierende Nettoarbeitszeit in Minuten nach Übernahme der\
            \ Korrektur."
          format: "int32"
          example: 450
        resultingBreakMinutes:
          type: "integer"
          description: "Resultierende Gesamtpausenzeit in Minuten nach Übernahme der\
            \ Korrektur."
          format: "int32"
          example: 30
        detailText:
          type: "string"
          description: "Menschenlesbare, lokalisierte Zusammenfassung der erforderlichen\
            \ Änderung zur Anzeige."
          example: "Required change: working time -0:30, break +0:30"
      description: "Details zur Verletzung der Pausenregelung. Nur vorhanden, wenn\
        \ `outcome` `confirmationRequired` ist; andernfalls null."
    TimeClockStopResponse:
      type: "object"
      properties:
        outcome:
          type: "string"
          description: "`saved`, wenn der Eintrag gespeichert wurde, oder `confirmationRequired`,\
            \ wenn vor dem Speichern eine Entscheidung zur Pausenregelung erforderlich\
            \ ist."
          example: "saved"
          enum:
          - "saved"
          - "confirmationRequired"
        id:
          type: "string"
          description: "Interne ID des Zeiteintrags, der beim Ausstempeln erstellt\
            \ wurde. Null, wenn `outcome` `confirmationRequired` ist."
          example: "98765"
        workedMinutes:
          type: "integer"
          description: "Erfasste Nettoarbeitszeit der Sitzung, in Minuten (Gesamtdauer\
            \ minus Pausen). Null, wenn `outcome` `confirmationRequired` ist."
          format: "int32"
          example: 450
        breakMinutes:
          type: "integer"
          description: "Erfasste Gesamtpausenzeit der Sitzung, in Minuten. Null, wenn\
            \ `outcome` `confirmationRequired` ist."
          format: "int32"
          example: 30
        breakRegulationApplied:
          type: "boolean"
          description: "`true`, wenn beim gespeicherten Eintrag eine Pausenregelungskorrektur\
            \ angewendet wurde (Arbeitszeit gekürzt, Pause erhöht)."
          example: true
        breakRegulationIgnored:
          type: "boolean"
          description: "`true`, wenn der Eintrag trotz Verletzung der Pausenregelung\
            \ unverändert gespeichert wurde (der Benutzer hat sie ignoriert)."
          example: false
        breakRegulation:
          $ref: "#/components/schemas/TimeClockBreakRegulationInfo"
    TimeClockStopRequest:
      type: "object"
      properties:
        projectId:
          type: "integer"
          description: "ID des Projekts, dem die abgeschlossene Sitzung zugeordnet\
            \ werden soll. `0` senden, wenn keine Projektzuordnung benötigt wird."
          format: "int32"
          example: 5
        categoryId:
          type: "integer"
          description: "ID der Kategorie, der die abgeschlossene Sitzung zugeordnet\
            \ werden soll. `0` senden, wenn keine Kategoriezuordnung benötigt wird."
          format: "int32"
          example: 7
        remarks:
          type: "string"
          description: "Optionaler Freitextkommentar für den abgeschlossenen Zeiteintrag."
          example: "Code review"
        breakRegulation:
          type: "string"
          description: "Entscheidung, wie eine Verletzung der Pausenregelung behandelt\
            \ wird. Beim ersten Stopp weglassen: Verletzt die Sitzung die Pausenregelung,\
            \ speichert der Server nicht, sondern liefert `outcome: confirmationRequired`\
            \ mit der erforderlichen Änderung. Den Stopp mit `apply` erneut senden,\
            \ um die Korrektur zu übernehmen (Arbeitszeit kürzen und fehlende Pause\
            \ ergänzen), oder mit `ignore`, um die Sitzung unverändert zu speichern.\
            \ Clients sollten `ignore` nur bei nicht verpflichtender Regelung anbieten\
            \ (`obligatory` in der Bestätigungsantwort); der Server erzwingt dies\
            \ derzeit nicht."
          example: "apply"
          enum:
          - "apply"
          - "ignore"
    TimeEntryRequestDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne ID des Zeiteintrags."
          example: "98765"
        employeeId:
          type: "string"
          description: "Interne ID des Mitarbeiters, der den Zeiteintrag eingereicht\
            \ hat. Nur in Team-/Manageransichten vorhanden."
          nullable: true
          example: "42"
        employeeName:
          type: "string"
          description: "Anzeigename des Mitarbeiters, der den Zeiteintrag eingereicht\
            \ hat. Nur in Team-/Manageransichten vorhanden."
          nullable: true
          example: "Maria Müller"
        date:
          type: "string"
          description: "Datum des Zeiteintrags im ISO-8601-Format (`YYYY-MM-DD`)."
          example: "2025-07-28"
        startTime:
          type: "string"
          description: "Einstempelzeit im Format `HH:mm`."
          example: "08:00"
        endTime:
          type: "string"
          description: "Ausstempelzeit im Format `HH:mm`."
          example: "17:00"
        workedMinutes:
          type: "integer"
          description: "Nettoarbeitszeit für diesen Eintrag, in Minuten (Gesamtdauer\
            \ minus Pausen)."
          format: "int32"
          example: 480
        breakMinutes:
          type: "integer"
          description: "Gesamte Pausenzeit für diesen Eintrag, in Minuten."
          format: "int32"
          example: 60
        project:
          type: "string"
          description: "Name des Projekts, das diesem Eintrag zugeordnet ist. `null`,\
            \ wenn kein Projekt zugeordnet wurde."
          nullable: true
          example: "Website Relaunch"
        category:
          type: "string"
          description: "Name der Kategorie, die diesem Eintrag zugeordnet ist. `null`,\
            \ wenn keine Kategorie zugeordnet wurde."
          nullable: true
          example: "Internal project"
        remarks:
          type: "string"
          description: "Freitextkommentar, der diesem Eintrag beigefügt ist. `null`,\
            \ wenn kein Kommentar eingegeben wurde."
          nullable: true
          example: "Sprint planning"
        requestType:
          type: "string"
          description: "Typ der Anfrage, die diesen Eintrag erzeugt hat. Einer von\
            \ `manual` (manuell eingegeben) oder `clock` (durch Ausstempeln erstellt)."
          example: "manual"
          enum:
          - "manual"
          - "clock"
        status:
          type: "string"
          description: "Genehmigungsstatus dieses Zeiteintrags. Einer von `pending`\
            \ (wartet auf Genehmigung) oder `approved`."
          example: "pending"
          enum:
          - "pending"
          - "approved"
    TimeEntryDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne ID des Zeiteintrags."
          example: "98765"
        date:
          type: "string"
          description: "Datum des Zeiteintrags im ISO-8601-Format (`YYYY-MM-DD`)."
          example: "2025-07-28"
        startTime:
          type: "string"
          description: "Startzeit im 24-Stunden-Format `HH:mm`. `null`, wenn der Eintrag\
            \ als Dauer ohne Start-/Endzeit erfasst wurde."
          nullable: true
          example: "08:00"
        endTime:
          type: "string"
          description: "Endzeit im 24-Stunden-Format `HH:mm`. `null`, wenn der Eintrag\
            \ als Dauer ohne Start-/Endzeit erfasst wurde."
          nullable: true
          example: "17:00"
        workedMinutes:
          type: "integer"
          description: "Nettoarbeitszeit für diesen Eintrag, in Minuten (Gesamtdauer\
            \ minus Pausen)."
          format: "int32"
          example: 480
        breakMinutes:
          type: "integer"
          description: "Gesamte Pausenzeit für diesen Eintrag, in Minuten."
          format: "int32"
          example: 60
        projectId:
          type: "string"
          description: "Interne ID des Projekts, das diesem Eintrag zugeordnet ist.\
            \ Entspricht `id` aus `GET /projects`. `null`, wenn kein Projekt zugeordnet\
            \ wurde."
          nullable: true
          example: "17"
        project:
          type: "string"
          description: "Name des Projekts, das diesem Eintrag zugeordnet ist. `null`,\
            \ wenn kein Projekt zugeordnet wurde."
          nullable: true
          example: "Website Relaunch"
        categoryId:
          type: "string"
          description: "Interne ID der Kategorie, die diesem Eintrag zugeordnet ist.\
            \ Entspricht `id` aus `GET /categories`. `null`, wenn keine Kategorie\
            \ zugeordnet wurde."
          nullable: true
          example: "4"
        category:
          type: "string"
          description: "Name der Kategorie, die diesem Eintrag zugeordnet ist. `null`,\
            \ wenn keine Kategorie zugeordnet wurde."
          nullable: true
          example: "Internal project"
        status:
          type: "string"
          description: "Status dieses Zeiteintrags. Einer von `done` (erfasst, keine\
            \ Genehmigung erforderlich), `pending` (wartet auf Genehmigung), `approved`,\
            \ `rejected` oder `editing`."
          example: "done"
          enum:
          - "done"
          - "pending"
          - "approved"
          - "rejected"
          - "editing"
        autoStopped:
          type: "boolean"
          description: "`true`, wenn dieser Eintrag automatisch vom System beendet\
            \ wurde (Auto-Stopp) und nicht vom Benutzer."
          example: false
        remarks:
          type: "string"
          description: "Freitextkommentar, der diesem Eintrag beigefügt ist. `null`,\
            \ wenn kein Kommentar eingegeben wurde."
          nullable: true
          example: "Sprint planning"
    TimeEntryPendingCountResponse:
      type: "object"
      properties:
        numberOfTimeEntries:
          type: "integer"
          description: "Anzahl der Zeiteinträge im Team des Aufrufers, die auf eine\
            \ Genehmigung warten."
          format: "int32"
          example: 5
    ClockEntryResponse:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne ID des erstellten Zeiteintrags."
          example: "98765"
        workedMinutes:
          type: "integer"
          description: "Erfasste Nettoarbeitszeit für diesen Eintrag, in Minuten (Gesamtdauer\
            \ minus Pausen)."
          format: "int32"
          example: 480
        breakMinutes:
          type: "integer"
          description: "Erfasste Gesamtpausenzeit für diesen Eintrag, in Minuten."
          format: "int32"
          example: 30
    ClockEntryRequest:
      required:
      - "endTimestamp"
      - "startTimestamp"
      type: "object"
      properties:
        startTimestamp:
          type: "integer"
          description: "Beginn des Arbeitszeitraums als Unix-Zeitstempel in Millisekunden."
          format: "int64"
          example: 1753700400000
        endTimestamp:
          type: "integer"
          description: "Ende des Arbeitszeitraums als Unix-Zeitstempel in Millisekunden."
          format: "int64"
          example: 1753729200000
        pauseSeconds:
          type: "integer"
          description: "Gesamte Pausendauer innerhalb des Arbeitszeitraums, in Sekunden."
          format: "int32"
          example: 1800
        projectId:
          type: "integer"
          description: "ID des Projekts, dem dieser Eintrag zugeordnet werden soll.\
            \ `0` senden, wenn keine Projektzuordnung benötigt wird."
          format: "int32"
          example: 5
        remarks:
          type: "string"
          description: "Optionaler Freitextkommentar für diesen Zeiteintrag."
          example: "Sprint planning"
    TimeOffSummaryResponse:
      type: "object"
      properties:
        vacationDaysRemaining:
          type: "number"
          description: "Anzahl der Urlaubstage, die dem Mitarbeiter im laufenden Jahr\
            \ noch zur Verfügung stehen, nach Berücksichtigung genehmigter und ausstehender\
            \ Anträge."
          format: "double"
          example: 12.5
        vacationDaysTotal:
          type: "number"
          description: "Gesamter Urlaubsanspruch des Mitarbeiters für das laufende\
            \ Jahr."
          format: "double"
          example: 30.0
        illnessDaysTaken:
          type: "number"
          description: "Anzahl der in diesem Jahr genommenen Krankheitstage."
          format: "double"
          example: 3.0
    WorkSummaryResponse:
      type: "object"
      properties:
        commitmentMinutes:
          type: "integer"
          description: "Gesamte Arbeitszeit, die der Mitarbeiter im abgefragten Zeitraum\
            \ vertraglich leisten muss, in Minuten."
          format: "int32"
          example: 2400
        workedMinutes:
          type: "integer"
          description: "Gesamte Arbeitszeit, die der Mitarbeiter im abgefragten Zeitraum\
            \ tatsächlich geleistet hat, in Minuten."
          format: "int32"
          example: 2280
        remainingMinutes:
          type: "integer"
          description: "Verbleibende Arbeitszeit, die im abgefragten Zeitraum noch\
            \ geleistet werden muss, in Minuten. Negative Werte bedeuten Überstunden."
          format: "int32"
          example: 120
        percentDone:
          type: "integer"
          description: "Prozentsatz der vertraglichen Arbeitszeit, der bisher geleistet\
            \ wurde, auf die nächste ganze Zahl gerundet."
          format: "int32"
          example: 95
        breakMinutes:
          type: "integer"
          description: "Gesamte im abgefragten Zeitraum erfasste Pausenzeit, in Minuten."
          format: "int32"
          example: 300
    UserProfileResponse:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Interne ID des authentifizierten Benutzers."
          example: "42"
        name:
          type: "string"
          description: "Vollständiger Anzeigename des Benutzers."
          example: "Maria Müller"
        email:
          type: "string"
          description: "E-Mail-Adresse des Benutzers."
          example: "maria.mueller@example.com"
        role:
          type: "string"
          description: "Rolle des Benutzers im Unternehmen. Einer von `USER`, `MANAGER`\
            \ oder `ADMIN`."
          example: "MANAGER"
          enum:
          - "USER"
          - "MANAGER"
          - "ADMIN"
        department:
          type: "string"
          description: "Name der Abteilung, der der Benutzer angehört. `null`, wenn\
            \ das Unternehmen keine Abteilungen verwendet."
          nullable: true
          example: "Engineering"
        branchOffice:
          type: "string"
          description: "Name der Niederlassung, der der Benutzer zugeordnet ist. `null`,\
            \ wenn das Unternehmen keine Niederlassungen verwendet."
          nullable: true
          example: "Berlin"
        country:
          type: "object"
          description: "Land des Benutzers, wie in seinem Profil konfiguriert. Die\
            \ Struktur hängt von der Unternehmenskonfiguration ab."
          nullable: true
        avatar:
          type: "string"
          description: "URL des Profilbilds des Benutzers. `null`, wenn kein Profilbild\
            \ hochgeladen wurde."
          nullable: true
          example: "/images/avatar/42/abc123.jpg?v=5"
        isBusinessTripEnabled:
          type: "boolean"
          description: "Ob die Dienstreisefunktion für diesen Benutzer aktiviert ist.\
            \ `null`, wenn die Funktion im Unternehmensplan nicht verfügbar ist."
          nullable: true
          example: true
        isTimeTrackingEnabled:
          type: "boolean"
          description: "Ob die Zeiterfassung für das Unternehmen des Benutzers aktiv\
            \ ist."
          example: true
  securitySchemes:
    bearerAuth:
      type: "http"
      scheme: "bearer"
      bearerFormat: "JWT"
