openapi: 3.1.0
info:
  title: ShiftScan Public REST-API
  version: "1.0.0"
  description: |
    Öffentliche REST-API von ShiftScan für Enterprise-Kunden. Mit einem
    persönlichen API-Key liest du Stammdaten (Mitarbeiter, Objekte, Projekte,
    Kunden), Dienstpläne, Zeitbuchungen und Abwesenheiten und legst Mitarbeiter
    an — z. B. zur Anbindung von Lohnbuchhaltung, BI oder eigenen Tools.

    ## Authentifizierung
    Jeder Request benötigt einen API-Key im `Authorization`-Header:

    ```
    Authorization: Bearer ssk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ```

    Alternativ wird der Header `X-API-Key: ssk_…` akzeptiert. Keys erstellst und
    widerrufst du unter **Einstellungen → API-Zugang**. Der Klartext-Key wird
    **nur einmal** bei der Erstellung angezeigt — danach ist nur noch das Prefix
    sichtbar. Ein Key ist immer an **genau einen Mandanten** gebunden; alle
    Antworten sind automatisch auf diesen Mandanten beschränkt.

    Der API-Zugang setzt einen aktiven **Enterprise-Plan** voraus (Trial
    eingeschlossen). Nach einem Downgrade werden bestehende Keys wirkungslos
    (`403 FEATURE_LOCKED`).

    ## Scopes
    Jeder Key hat einen oder beide Scopes:
    - `read` — lesende Endpunkte (GET).
    - `write` — schreibende Endpunkte (POST/PUT/PATCH). Fehlt der Scope → `403`.

    ## Antwortformat
    Erfolg:
    ```json
    { "success": true, "data": { … } }
    ```
    Fehler:
    ```json
    { "success": false, "error": { "code": "NOT_FOUND", "message": "…" } }
    ```

    ## Pagination
    Listen-Endpunkte unterstützen `limit` (1–1000, Default 100) und `offset`
    (Default 0) und liefern ein `pagination`-Objekt zurück:
    ```json
    { "success": true, "data": { "employees": [ … ],
      "pagination": { "limit": 100, "offset": 0, "total": 248 } } }
    ```
    Über `total` siehst du, ob weitere Seiten existieren. Hole Folgeseiten mit
    `offset = offset + limit`.

    ## Inkrementeller Sync (Delta)
    Jede Listen-Zeile enthält ein `updated_at`. Mit `?updated_since=<ISO-8601>`
    erhältst du nur Datensätze, die seit diesem Zeitpunkt geändert wurden. Merke
    dir den größten `updated_at` der Antwort und verwende ihn beim nächsten Abruf
    als neues `updated_since`.

    ## Rate-Limit
    **60 Anfragen pro Minute und Key.** Jede Antwort trägt die Header
    `X-RateLimit-Limit`, `X-RateLimit-Remaining` und `X-RateLimit-Reset`
    (Unix-Sekunden). Bei Überschreitung → `429` mit `Retry-After` (Sekunden).
    Mehrere Keys erhöhen den Gesamtdurchsatz (Limit gilt pro Key).

servers:
  - url: https://{projectRef}.supabase.co/functions/v1/api
    description: Produktiv-Endpunkt (Supabase Edge Function)
    variables:
      projectRef:
        default: ukoxeppdsvqlvoybkzsy
        description: Deine Supabase-Projekt-Referenz (Teil der SUPABASE_URL).

security:
  - bearerAuth: []

tags:
  - name: Allgemein
  - name: Mitarbeiter
  - name: Objekte
  - name: Projekte & Kunden
  - name: Dienstplan
  - name: Zeit & Abwesenheit

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: ssk_<64 hex>
      description: "API-Key als Bearer-Token (`Authorization: Bearer ssk_…`)."

  parameters:
    limit:
      name: limit
      in: query
      description: Maximale Anzahl Datensätze (1–1000).
      schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
    offset:
      name: offset
      in: query
      description: Anzahl zu überspringender Datensätze (Pagination).
      schema: { type: integer, minimum: 0, default: 0 }
    updatedSince:
      name: updated_since
      in: query
      description: Nur Datensätze mit `updated_at` größer als dieser ISO-8601-Zeitstempel.
      schema: { type: string, format: date-time }
      example: "2026-06-01T00:00:00Z"

  headers:
    X-RateLimit-Limit:
      description: Maximale Anfragen pro Minute und Key.
      schema: { type: integer, example: 60 }
    X-RateLimit-Remaining:
      description: Verbleibende Anfragen im aktuellen Fenster.
      schema: { type: integer, example: 59 }
    X-RateLimit-Reset:
      description: Unix-Zeitstempel (Sekunden), zu dem das Fenster zurückgesetzt wird.
      schema: { type: integer }
    Retry-After:
      description: Sekunden bis zum nächsten erlaubten Request (nur bei 429).
      schema: { type: integer, example: 60 }

  schemas:
    Error:
      type: object
      properties:
        success: { type: boolean, const: false }
        error:
          type: object
          properties:
            code: { type: string, example: NOT_FOUND }
            message: { type: string }
          required: [code, message]
      required: [success, error]
    Pagination:
      type: object
      properties:
        limit:  { type: integer, example: 100 }
        offset: { type: integer, example: 0 }
        total:  { type: integer, example: 248, description: Gesamtzahl passender Datensätze (vor Pagination). }
      required: [limit, offset, total]
    Employee:
      type: object
      properties:
        id: { type: string, format: uuid }
        display_name: { type: string }
        vorname: { type: string }
        nachname: { type: string }
        email: { type: [string, "null"], format: email }
        rolle: { type: string, enum: [admin, manager, user] }
        ist_aktiv: { type: boolean }
        personalnummer: { type: [string, "null"] }
        updated_at: { type: string, format: date-time }
    EmployeeDetail:
      allOf:
        - $ref: '#/components/schemas/Employee'
        - type: object
          properties:
            telefon: { type: [string, "null"] }
            eintrittsdatum: { type: [string, "null"], format: date }
            soll_stunden_woche: { type: [number, "null"] }
            stundensatz: { type: [number, "null"] }
            arbeitszeitmodell: { type: [string, "null"] }
    Objekt:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        typ: { type: [string, "null"] }
        ist_aktiv: { type: boolean }
        strasse: { type: [string, "null"] }
        plz: { type: [string, "null"] }
        stadt: { type: [string, "null"] }
        bundesland: { type: [string, "null"] }
        updated_at: { type: string, format: date-time }
    Projekt:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        status: { type: [string, "null"] }
        kunde_id: { type: [string, "null"], format: uuid }
        budget_stunden: { type: [number, "null"] }
        budget_betrag: { type: [number, "null"] }
        stundensatz: { type: [number, "null"] }
        start_datum: { type: [string, "null"], format: date }
        end_datum: { type: [string, "null"], format: date }
        updated_at: { type: string, format: date-time }
    Kunde:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        ansprechpartner: { type: [string, "null"] }
        email: { type: [string, "null"], format: email }
        telefon: { type: [string, "null"] }
        ist_aktiv: { type: boolean }
        standardsatz: { type: [number, "null"] }
        updated_at: { type: string, format: date-time }
    Shift:
      type: object
      properties:
        id: { type: string, format: uuid }
        employee_id: { type: [string, "null"], format: uuid }
        objekt_id: { type: [string, "null"], format: uuid }
        projekt_id: { type: [string, "null"], format: uuid }
        kunde_id: { type: [string, "null"], format: uuid }
        datum: { type: string, format: date }
        beginn_zeit: { type: [string, "null"] }
        ende_zeit: { type: [string, "null"] }
        status: { type: string }
        updated_at: { type: string, format: date-time }
    TimeEntry:
      type: object
      properties:
        id: { type: string, format: uuid }
        employee_id: { type: [string, "null"], format: uuid }
        employee_name: { type: [string, "null"] }
        objekt_id: { type: [string, "null"], format: uuid }
        clocked_in_at: { type: string, format: date-time }
        clocked_out_at: { type: [string, "null"], format: date-time }
        stunden: { type: [number, "null"] }
        pause_minuten: { type: [integer, "null"] }
        updated_at: { type: string, format: date-time }
    Absence:
      type: object
      properties:
        id: { type: string, format: uuid }
        employee_id: { type: string, format: uuid }
        typ: { type: string }
        von_datum: { type: string, format: date }
        bis_datum: { type: string, format: date }
        status: { type: string }
        arbeitstage_count: { type: [number, "null"] }
        updated_at: { type: string, format: date-time }

  responses:
    Unauthorized:
      description: API-Key fehlt, ist ungültig oder widerrufen.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Kein Enterprise-Plan oder fehlender Scope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Endpunkt oder Datensatz nicht gefunden.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ValidationError:
      description: Ungültige Eingabe oder ungültiger Query-Parameter.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Rate-Limit überschritten (60/min pro Key).
      headers:
        Retry-After: { $ref: '#/components/headers/Retry-After' }
        X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

paths:
  /v1/me:
    get:
      tags: [Allgemein]
      summary: Kontext des API-Keys
      description: Liefert Mandant, Plan und Scopes des Keys — praktisch zum Testen der Authentifizierung.
      responses:
        '200':
          description: Key-Kontext.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      tenant_id: { type: string, format: uuid }
                      tenant_name: { type: [string, "null"] }
                      plan: { type: [string, "null"] }
                      scopes: { type: array, items: { type: string } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/employees:
    get:
      tags: [Mitarbeiter]
      summary: Mitarbeiter auflisten
      parameters:
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/offset' }
        - { $ref: '#/components/parameters/updatedSince' }
      responses:
        '200':
          description: Liste der Mitarbeiter.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      employees: { type: array, items: { $ref: '#/components/schemas/Employee' } }
                      pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Mitarbeiter]
      summary: Mitarbeiter anlegen
      description: >-
        Erstellt einen Mitarbeiter. Erfordert den `write`-Scope. Achtung: pro
        aktivem Mitarbeiter wird ggf. ein Seat abgerechnet (per-seat Billing).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [vorname, nachname]
              properties:
                vorname: { type: string, minLength: 1 }
                nachname: { type: string, minLength: 1 }
                email: { type: string, format: email }
                rolle: { type: string, enum: [admin, manager, user], default: user }
      responses:
        '201':
          description: Mitarbeiter erstellt.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      employee:
                        type: object
                        properties:
                          id: { type: string, format: uuid }
                          display_name: { type: string }
                          email: { type: [string, "null"] }
                          rolle: { type: string }
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/employees/{id}:
    get:
      tags: [Mitarbeiter]
      summary: Einen Mitarbeiter abrufen
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Mitarbeiter-Details.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      employee: { $ref: '#/components/schemas/EmployeeDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/objekte:
    get:
      tags: [Objekte]
      summary: Objekte auflisten
      parameters:
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/offset' }
        - { $ref: '#/components/parameters/updatedSince' }
      responses:
        '200':
          description: Liste der Objekte.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      objekte: { type: array, items: { $ref: '#/components/schemas/Objekt' } }
                      pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/objekte/{id}:
    get:
      tags: [Objekte]
      summary: Ein Objekt abrufen
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Objekt-Details (inkl. Koordinaten, kunde_id, stundensatz).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      objekt: { $ref: '#/components/schemas/Objekt' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/projekte:
    get:
      tags: [Projekte & Kunden]
      summary: Projekte auflisten
      parameters:
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/offset' }
        - { $ref: '#/components/parameters/updatedSince' }
      responses:
        '200':
          description: Liste der Projekte.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      projekte: { type: array, items: { $ref: '#/components/schemas/Projekt' } }
                      pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/kunden:
    get:
      tags: [Projekte & Kunden]
      summary: Kunden auflisten
      parameters:
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/offset' }
        - { $ref: '#/components/parameters/updatedSince' }
      responses:
        '200':
          description: Liste der Kunden.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      kunden: { type: array, items: { $ref: '#/components/schemas/Kunde' } }
                      pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/shifts:
    get:
      tags: [Dienstplan]
      summary: Dienstplan-Zuweisungen auflisten
      description: Abgesagte Schichten werden ausgeblendet. Zeitraum standardmäßig heute bis +14 Tage.
      parameters:
        - { name: from, in: query, description: Startdatum (YYYY-MM-DD, Default heute), schema: { type: string, format: date } }
        - { name: to, in: query, description: Enddatum (YYYY-MM-DD, Default heute+14 Tage), schema: { type: string, format: date } }
        - { name: objekt_id, in: query, description: Auf ein Objekt einschränken., schema: { type: string, format: uuid } }
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/offset' }
        - { $ref: '#/components/parameters/updatedSince' }
      responses:
        '200':
          description: Liste der Schicht-Zuweisungen.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      shifts: { type: array, items: { $ref: '#/components/schemas/Shift' } }
                      pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/time-entries:
    get:
      tags: [Zeit & Abwesenheit]
      summary: Zeitbuchungen auflisten
      description: Zeitraum standardmäßig letzte 30 Tage bis heute (nach Einstempel-Zeitpunkt).
      parameters:
        - { name: from, in: query, description: Startdatum (YYYY-MM-DD, Default heute-30 Tage), schema: { type: string, format: date } }
        - { name: to, in: query, description: Enddatum (YYYY-MM-DD, Default heute), schema: { type: string, format: date } }
        - { name: employee_id, in: query, description: Auf einen Mitarbeiter einschränken., schema: { type: string, format: uuid } }
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/offset' }
        - { $ref: '#/components/parameters/updatedSince' }
      responses:
        '200':
          description: Liste der Zeitbuchungen.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      timeEntries: { type: array, items: { $ref: '#/components/schemas/TimeEntry' } }
                      pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/absences:
    get:
      tags: [Zeit & Abwesenheit]
      summary: Abwesenheiten auflisten
      parameters:
        - { name: status, in: query, description: 'Auf einen Status filtern (z. B. genehmigt, beantragt).', schema: { type: string } }
        - { name: year, in: query, description: Auf ein Kalenderjahr filtern (über von_datum)., schema: { type: integer, example: 2026 } }
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/offset' }
        - { $ref: '#/components/parameters/updatedSince' }
      responses:
        '200':
          description: Liste der Abwesenheiten.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, const: true }
                  data:
                    type: object
                    properties:
                      absences: { type: array, items: { $ref: '#/components/schemas/Absence' } }
                      pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
