openapi: 3.0.3
info:
  title: calwebapp API v2 (Beta — calServer 6.0)
  description: |
    REST-API v2 fuer das calwebapp-Kalibriermanagementsystem (calServer 6.0).

    > **Achtung:** API v2 befindet sich in aktiver Entwicklung und ist noch nicht produktionsreif.
    > Endpunkte, Antwortformate und Authentifizierung koennen sich noch aendern.

    Basiert auf Laravel 11 mit JSON:API-kompatiblem Antwortformat und Bearer-Token-Authentifizierung.

    Die produktive API v1 (Yii-basiert, Header-Auth) ist als eigenstaendige
    Spezifikation unter [openapi.yaml](openapi.yaml) verfuegbar.

    ## Uebersicht
    - **Base URL:** `https://<domain>/api/v2`
    - **Content Type (Responses):** `application/json`
    - **Content Type (Requests):** `application/json`
    - **Encoding:** UTF-8
    - **Authentifizierung:** Bearer Token (Sanctum)

    ## Antwortformat (JSON:API)
    Alle erfolgreichen Antworten folgen diesem Schema:
    ```json
    {
      "data": { ... },
      "meta": { "total": 42, "page": 1, "per_page": 25, "last_page": 2 },
      "links": { "self": "...", "next": "...", "prev": null }
    }
    ```
    Fehlerantworten:
    ```json
    {
      "error": {
        "status": 422,
        "title": "Validation Error",
        "detail": "The name field is required."
      }
    }
    ```

    ## Pagination
    - `page[size]` — Datensaetze pro Seite (Standard 25, max 100)
    - `page[number]` — Seitennummer (Standard 1)

    ## Sortierung
    - `sort` — Feldname. Praefix `-` fuer absteigende Sortierung.
    - Beispiel: `sort=-asset_number`

    ## Filterung
    - `filter[field]=value` — Exakter Filter
    - `filter[field]=like:value` — Teilstring-Suche
    - `filter[field]=gt:value` / `lt:value` — Groesser/kleiner als
    - `filter[field]=in:a,b,c` — Mehrfachwerte
    - `filter[field]=between:a,b` — Bereichsfilter

    ## Berechtigungen
    Alle Endpunkte erfordern einen gueltigen Bearer-Token.
    Zusaetzliche Berechtigungen pro Ressource:
    - **Inventar:** `inventory_view`, `inventory_edit`, `inventory_delete`
    - **Kategorien (lesen):** Jeder authentifizierte Benutzer
    - **Kategorien (schreiben):** `inventory_edit`
    - **Felder:** Jeder authentifizierte Benutzer
    - **Additional Fields (schreiben):** `inventory_edit`

    ## Feldnamen (API v2 vs. v1)
    API v2 verwendet sprechende Feldnamen statt codierter Spaltennamen:

    | API v2 | DB-Spalte | Beschreibung |
    |--------|-----------|--------------|
    | `asset_number` | I4201 | Inventarnummer |
    | `serial_number` | I4202 | Seriennummer |
    | `description` | I4203 | Geraetebeschreibung |
    | `manufacturer` | I4206 | Hersteller |
    | `model` | I4207 | Modell / Typ |
    | `location` | I4210 | Standort |
    | `status` | I4211 | Statuscode |
    | `calibration_interval` | I4220 | Kalibrierintervall |

    Die vollstaendige Feldzuordnung ist ueber `GET /api/v2/fields/{table}` abrufbar.
  version: 2.0.0
  contact:
    name: calwebapp Support
  license:
    name: Proprietary

servers:
  - url: https://{domain}
    description: Production server
    variables:
      domain:
        default: your-domain.com
        description: Your calwebapp domain
  - url: http://localhost
    description: Local development

security:
  - BearerAuth: []

tags:
  - name: auth
    description: Authentifizierung und Token-Verwaltung
  - name: health
    description: System-Health-Checks
  - name: inventory
    description: Inventar- und Geraeteverwaltung
  - name: category
    description: Kategorien fuer Inventar, Kalibrierungen, Reparaturen und Buchungen
  - name: field
    description: Feldkonfiguration und Metadaten (dynamische Feldverwaltung)
  - name: additional-field
    description: Kategorie- und statusabhaengige Feld-Overrides

paths:
  # ──────────────────────────────────────────────
  # AUTH
  # ──────────────────────────────────────────────
  /api/v2/auth/token:
    post:
      tags: [auth]
      summary: Bearer-Token erzeugen
      description: |
        Erzeugt einen Bearer-Token fuer die API-Authentifizierung.
        Der Token wird fuer alle weiteren Anfragen im `Authorization`-Header verwendet.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                  description: E-Mail-Adresse des Benutzers
                  example: api@example.com
                password:
                  type: string
                  format: password
                  description: Passwort des Benutzers
                device_name:
                  type: string
                  description: Geraetename fuer Token-Identifikation
                  example: api-client
            example:
              email: api@example.com
              password: "GeheimesPasswort123"
              device_name: api-client
      responses:
        '200':
          description: Token erfolgreich erzeugt
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                    description: Bearer-Token fuer die API-Authentifizierung
                    example: "1|a1b2c3d4e5f6g7h8i9j0..."
              example:
                token: "1|a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v2/auth/login:
    post:
      tags: [auth]
      summary: Session-Login
      description: |
        Session-basierte Anmeldung (fuer Browser-Clients).
        Kein Token erforderlich — stattdessen wird ein Session-Cookie gesetzt.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                  example: user@example.com
                password:
                  type: string
                  format: password
      responses:
        '200':
          description: Anmeldung erfolgreich
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Authenticated"
                  user:
                    type: object
                    properties:
                      id:
                        type: string
                      email:
                        type: string
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v2/auth/logout:
    post:
      tags: [auth]
      summary: Abmelden
      description: Aktuellen Token widerrufen.
      responses:
        '200':
          description: Abmeldung erfolgreich
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Logged out"

  /api/v2/auth/me:
    get:
      tags: [auth]
      summary: Aktuellen Benutzer abrufen
      description: Gibt die Daten des authentifizierten Benutzers zurueck.
      responses:
        '200':
          description: Benutzerdaten
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      email:
                        type: string
                      firstname:
                        type: string
                      lastname:
                        type: string
                      role:
                        type: string

  # ──────────────────────────────────────────────
  # HEALTH
  # ──────────────────────────────────────────────
  /api/v2/health:
    get:
      tags: [health]
      summary: System-Health-Check
      description: Gibt den Systemstatus zurueck. Keine Authentifizierung erforderlich.
      security: []
      responses:
        '200':
          description: System ist gesund
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  # ──────────────────────────────────────────────
  # INVENTORY
  # ──────────────────────────────────────────────
  /api/v2/inventories:
    get:
      tags: [inventory]
      summary: Inventarliste abrufen
      description: |
        Paginierte Liste aller Inventarobjekte inkl. zugeordneter Kategorien.
        Erfordert Permission `inventory_view`.

        **Feldnamen:** Verwendet sprechende API-Namen (z.B. `asset_number` statt `I4201`).
        Die vollstaendige Zuordnung ist ueber `GET /api/v2/fields/inventory` abrufbar.
      parameters:
        - $ref: '#/components/parameters/pageSize'
        - $ref: '#/components/parameters/pageNumber'
        - name: "filter[asset_number]"
          in: query
          description: "Filter nach Inventarnummer (unterstuetzt `like:` Operator)"
          schema:
            type: string
          example: "like:FLUKE"
        - name: "filter[manufacturer]"
          in: query
          description: Filter nach Hersteller
          schema:
            type: string
        - name: sort
          in: query
          description: |
            Sortierfeld. Praefix `-` fuer absteigende Sortierung.
            Beispiel: `-asset_number`
          schema:
            type: string
      responses:
        '200':
          description: Inventarliste
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InventoryCollection'
              example:
                data:
                  - id: "ea79b775-8b4b-8e17-98c9-b33e7065ff6f"
                    type: inventory
                    attributes:
                      asset_number: "DMM-001"
                      serial_number: "SN-1234567890"
                      description: "Digital Multimeter"
                      manufacturer: "FLUKE"
                      model: "179"
                      location: "Labor A"
                      status: 1
                      calibration_interval: 12
                      customer_id: "45f09720-082f-5c74-9cfa-a605e2863904"
                    relationships:
                      customer:
                        data:
                          id: "45f09720-082f-5c74-9cfa-a605e2863904"
                          type: customer
                      categories:
                        - id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                          type: category
                          attributes:
                            name: "Messgeraet"
                            short_name: "MG"
                            type: inventory
                            parent_id: null
                            sort_num: 1
                            color: "#4CAF50"
                            text_color: "#FFFFFF"
                            rentable: 1
                meta:
                  total: 150
                  page: 1
                  per_page: 25
                  last_page: 6
                links:
                  self: "/api/v2/inventories?page[number]=1&page[size]=25"
                  next: "/api/v2/inventories?page[number]=2&page[size]=25"
                  prev: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    post:
      tags: [inventory]
      summary: Inventar anlegen
      description: |
        Neues Inventarobjekt erstellen. Erfordert Permission `inventory_edit`.
        MTAG (UUID) wird automatisch generiert, wenn nicht angegeben.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InventoryInput'
            example:
              asset_number: "DMM-002"
              serial_number: "SN-9876543210"
              description: "Oszilloskop"
              manufacturer: "Keysight"
              model: "DSOX1204G"
              location: "Labor B"
              calibration_interval: 24
              customer_id: "45f09720-082f-5c74-9cfa-a605e2863904"
      responses:
        '201':
          description: Inventar erfolgreich erstellt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/InventoryResource'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'

  /api/v2/inventories/{mtag}:
    get:
      tags: [inventory]
      summary: Einzelnes Inventarobjekt abrufen
      description: |
        Gibt ein Inventarobjekt mit Kunden- und Kategoriezuordnung zurueck.
        Erfordert Permission `inventory_view`.
      parameters:
        - $ref: '#/components/parameters/mtag'
      responses:
        '200':
          description: Inventarobjekt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/InventoryResource'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags: [inventory]
      summary: Inventar aktualisieren
      description: |
        Inventarobjekt aktualisieren (Partial Update).
        Nur uebergebene Felder werden geaendert. Erfordert Permission `inventory_edit`.
      parameters:
        - $ref: '#/components/parameters/mtag'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InventoryInput'
            example:
              location: "Labor C"
              calibration_interval: 6
      responses:
        '200':
          description: Inventar aktualisiert
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/InventoryResource'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

    delete:
      tags: [inventory]
      summary: Inventar loeschen
      description: |
        Inventarobjekt loeschen. Erfordert Permission `inventory_delete`.
        Gibt 409 Conflict zurueck, wenn zugehoerige Kalibrierungen existieren.
      parameters:
        - $ref: '#/components/parameters/mtag'
      responses:
        '204':
          description: Erfolgreich geloescht
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Konflikt — Inventar hat zugehoerige Kalibrierungen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  status: 409
                  title: Conflict
                  detail: "Inventory has related calibrations and cannot be deleted."

  /api/v2/inventories/export:
    get:
      tags: [inventory]
      summary: Inventar exportieren
      description: |
        Export der Inventarliste als CSV oder JSON.
        Erfordert Permission `inventory_view`.
      parameters:
        - name: format
          in: query
          description: Exportformat
          schema:
            type: string
            enum: [csv, json]
            default: csv
        - name: columns
          in: query
          description: |
            Kommaseparierte Liste der zu exportierenden Spalten.
            Standard: asset_number, serial_number, description, manufacturer, model, location, status, calibration_interval
          schema:
            type: string
          example: "asset_number,serial_number,manufacturer,model"
      responses:
        '200':
          description: Exportdaten
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # ──────────────────────────────────────────────
  # CATEGORIES
  # ──────────────────────────────────────────────
  /api/v2/categories:
    get:
      tags: [category]
      summary: Alle Kategorien abrufen
      description: |
        Gibt alle Kategorien zurueck, sortiert nach `sort_num`.

        Kategorien koennen hierarchisch verschachtelt sein (`parent_id`).
        Jede Kategorie hat einen Typ: `inventory`, `booking`, `repair` oder `calibration`.

        **Lesen:** Jeder authentifizierte Benutzer.
      parameters:
        - name: "filter[type]"
          in: query
          description: |
            Nach Kategorietyp filtern.
            Moegliche Werte: `inventory`, `booking`, `repair`, `calibration`.
          schema:
            type: string
            enum: [inventory, booking, repair, calibration]
          example: inventory
        - name: "filter[parent_id]"
          in: query
          description: Nur Unterkategorien dieser Eltern-UUID zurueckgeben
          schema:
            type: string
            format: uuid
        - name: "filter[root_only]"
          in: query
          description: Nur Hauptkategorien (ohne Eltern) zurueckgeben
          schema:
            type: boolean
      responses:
        '200':
          description: Liste aller Kategorien
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CategoryCollection'
              example:
                data:
                  - id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                    type: category
                    attributes:
                      name: "Messgeraet"
                      short_name: "MG"
                      type: inventory
                      parent_id: null
                      sort_num: 1
                      color: "#4CAF50"
                      text_color: "#FFFFFF"
                      rentable: 1
                  - id: "7fb91c22-3a44-4e89-b0fc-8d5e6f7a8b9c"
                    type: category
                    attributes:
                      name: "Multimeter"
                      short_name: "MM"
                      type: inventory
                      parent_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                      sort_num: 1
                      color: "#2196F3"
                      text_color: "#FFFFFF"
                      rentable: 1
                  - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    type: category
                    attributes:
                      name: "Pruefmittel"
                      short_name: "PM"
                      type: calibration
                      parent_id: null
                      sort_num: 2
                      color: "#FF9800"
                      text_color: "#000000"
                      rentable: 0
                meta:
                  total: 3
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [category]
      summary: Kategorie anlegen
      description: |
        Neue Kategorie erstellen. Erfordert Permission `inventory_edit`.

        Unterkategorien koennen durch Angabe von `parent_id` erstellt werden.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryInput'
            examples:
              hauptkategorie:
                summary: Hauptkategorie erstellen
                value:
                  name: "Messgeraet"
                  short_name: "MG"
                  type: inventory
                  color: "#4CAF50"
                  text_color: "#FFFFFF"
                  rentable: 1
              unterkategorie:
                summary: Unterkategorie erstellen
                value:
                  name: "Multimeter"
                  short_name: "MM"
                  type: inventory
                  parent_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  sort_num: 1
                  color: "#2196F3"
      responses:
        '201':
          description: Kategorie erfolgreich erstellt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CategoryResource'
              example:
                data:
                  id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  type: category
                  attributes:
                    name: "Messgeraet"
                    short_name: "MG"
                    type: inventory
                    parent_id: null
                    sort_num: 0
                    color: "#4CAF50"
                    text_color: "#FFFFFF"
                    rentable: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'

  /api/v2/categories/{id}:
    get:
      tags: [category]
      summary: Einzelne Kategorie abrufen
      description: Gibt eine Kategorie anhand ihrer UUID zurueck.
      parameters:
        - $ref: '#/components/parameters/categoryId'
      responses:
        '200':
          description: Kategorie
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CategoryResource'
              example:
                data:
                  id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  type: category
                  attributes:
                    name: "Messgeraet"
                    short_name: "MG"
                    type: inventory
                    parent_id: null
                    sort_num: 1
                    color: "#4CAF50"
                    text_color: "#FFFFFF"
                    rentable: 1
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags: [category]
      summary: Kategorie aktualisieren
      description: |
        Kategorie aktualisieren (Partial Update).
        Nur uebergebene Felder werden geaendert. `type` kann nicht geaendert werden.
        Erfordert Permission `inventory_edit`.
      parameters:
        - $ref: '#/components/parameters/categoryId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryUpdate'
            example:
              name: "Messgeraet (aktualisiert)"
              color: "#388E3C"
              sort_num: 2
      responses:
        '200':
          description: Kategorie aktualisiert
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CategoryResource'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

    delete:
      tags: [category]
      summary: Kategorie loeschen
      description: |
        Kategorie loeschen. Erfordert Permission `inventory_edit`.
        Gibt 409 Conflict zurueck, wenn die Kategorie Unterkategorien hat.
      parameters:
        - $ref: '#/components/parameters/categoryId'
      responses:
        '204':
          description: Erfolgreich geloescht
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Konflikt — Kategorie hat Unterkategorien
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  status: 409
                  title: Conflict
                  detail: "Category has child categories and cannot be deleted."

  # ──────────────────────────────────────────────
  # FIELDS (Feldkonfiguration)
  # ──────────────────────────────────────────────
  /api/v2/fields:
    get:
      tags: [field]
      summary: Verfuegbare Tabellen mit Felduebersicht abrufen
      description: |
        Gibt eine Uebersicht aller Tabellen zurueck, fuer die Feldkonfigurationen existieren.
        Pro Tabelle wird die Gesamtanzahl und die Anzahl aktiver Felder angezeigt.

        Aktive Felder sind solche, bei denen mindestens eines der folgenden Flags gesetzt ist:
        `edit_mandatory`, `edit_visible` oder `view_visible`.
      responses:
        '200':
          description: Tabellenuebersicht mit Feldzaehlung
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/FieldTableSummary'
              example:
                data:
                  - table: inventory
                    field_count: 65
                    active_field_count: 32
                  - table: calibration
                    field_count: 98
                    active_field_count: 45
                  - table: customer
                    field_count: 38
                    active_field_count: 20
                  - table: location
                    field_count: 15
                    active_field_count: 8
                  - table: repair
                    field_count: 20
                    active_field_count: 12
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v2/fields/{table}:
    get:
      tags: [field]
      summary: Feldkonfigurationen fuer eine Tabelle abrufen
      description: |
        Gibt die Feldkonfigurationen fuer eine bestimmte Tabelle zurueck.
        Nur Felder mit mindestens einem aktiven Flag (`edit_mandatory`, `edit_visible`
        oder `view_visible`) werden zurueckgegeben.

        **Gueltige Tabellen:** `inventory`, `calibration`, `customer`, `location`, `repair`

        **Kategorie-/Status-Overrides:** Wenn `category_uID` oder `status_uID` angegeben wird,
        werden zusaetzliche Feld-Overrides aus der `additional_field`-Tabelle eingemischt.
        Status-Overrides haben Vorrang vor Kategorie-Overrides.

        Dies ist der Nachfolger des Legacy-Endpunkts `GET /api/{resource}/rules`.
      parameters:
        - name: table
          in: path
          required: true
          description: |
            Name der Tabelle. Gueltige Werte:
            `inventory`, `calibration`, `customer`, `location`, `repair`
          schema:
            type: string
            enum: [inventory, calibration, customer, location, repair]
          example: inventory
        - name: category_uID
          in: query
          description: |
            UUID einer Kategorie. Wenn angegeben, werden Feld-Overrides
            fuer diese Kategorie eingemischt (z.B. zusaetzliche Pflichtfelder).
          schema:
            type: string
            format: uuid
          example: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        - name: status_uID
          in: query
          description: |
            UUID eines Status. Wenn angegeben, werden Feld-Overrides
            fuer diesen Status eingemischt. Hat Vorrang vor Kategorie-Overrides.
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Feldkonfigurationen der Tabelle
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/FieldConfigurationResource'
              example:
                data:
                  - id: "I4201:inventory"
                    type: field-configuration
                    attributes:
                      api_name: asset_number
                      column_name: I4201
                      table_name: inventory
                      label: "Inventarnummer"
                      field_type: text
                      field_type_raw: varchar
                      default_value: null
                      filterable: true
                      sortable: true
                      editable: true
                      visible: true
                      grid_default: true
                      mandatory: true
                      display_order: 1
                      display_order_grid: 1
                      width: null
                      options: null
                      field_category: null
                      is_user_defined: false
                      audit: true
                      allow_copy: true
                      security_message: false
                      small_view_visible: true
                      link_table_note: null
                  - id: "I4202:inventory"
                    type: field-configuration
                    attributes:
                      api_name: serial_number
                      column_name: I4202
                      table_name: inventory
                      label: "Seriennummer"
                      field_type: text
                      field_type_raw: varchar
                      default_value: null
                      filterable: true
                      sortable: true
                      editable: true
                      visible: true
                      grid_default: true
                      mandatory: false
                      display_order: 2
                      display_order_grid: 2
                      width: null
                      options: null
                      field_category: null
                      is_user_defined: false
                      audit: true
                      allow_copy: false
                      security_message: false
                      small_view_visible: true
                      link_table_note: null
                  - id: "I4209:inventory"
                    type: field-configuration
                    attributes:
                      api_name: status_code
                      column_name: I4209
                      table_name: inventory
                      label: "Status"
                      field_type: select
                      field_type_raw: varchar
                      default_value: null
                      filterable: true
                      sortable: true
                      editable: true
                      visible: true
                      grid_default: true
                      mandatory: false
                      display_order: 9
                      display_order_grid: 5
                      width: null
                      options:
                        - value: "uuid-status-active"
                          label: "Aktiv"
                        - value: "uuid-status-inactive"
                          label: "Inaktiv"
                        - value: "uuid-status-repair"
                          label: "In Reparatur"
                      field_category: StatusDatabase
                      is_user_defined: false
                      audit: true
                      allow_copy: false
                      security_message: false
                      small_view_visible: true
                      link_table_note: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Tabelle nicht gefunden
          content:
            application/json:
              schema:
                type: object
                properties:
                  type:
                    type: string
                  title:
                    type: string
                  status:
                    type: integer
                  detail:
                    type: string
              example:
                type: "https://httpstatuses.com/404"
                title: "Not Found"
                status: 404
                detail: "No field configuration found for table 'unknown'."

  # ──────────────────────────────────────────────
  # ADDITIONAL FIELDS
  # ──────────────────────────────────────────────
  /api/v2/additional-fields:
    get:
      tags: [additional-field]
      summary: Alle Feld-Overrides abrufen
      description: |
        Gibt alle zusaetzlichen Feld-Overrides zurueck, die pro Kategorie oder Status
        definiert sind. Diese Overrides aendern die Sichtbarkeit und Pflichtangaben
        von Feldern in Abhaengigkeit der zugeordneten Kategorie oder des Status.
      parameters:
        - name: "filter[object_uID]"
          in: query
          description: Nach Kategorie- oder Status-UUID filtern
          schema:
            type: string
            format: uuid
        - name: "filter[type]"
          in: query
          description: "Nach Override-Typ filtern: `category` oder `status`"
          schema:
            type: string
            enum: [category, status]
      responses:
        '200':
          description: Liste der Feld-Overrides
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AdditionalFieldResource'
              example:
                data:
                  - id: "af-uuid-001"
                    type: additional-field
                    attributes:
                      object_uID: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                      override_type: category
                      column_name: I4206
                      default_value: null
                      edit_visible: 1
                      edit_mandatory: 1
                      view_visible: 1
                      display_order: 5
                      empty_now: 0
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [additional-field]
      summary: Feld-Override erstellen
      description: |
        Neuen Feld-Override erstellen. Erfordert Permission `inventory_edit`.
        Ermoeglicht es, Felder fuer eine bestimmte Kategorie oder einen Status
        als sichtbar, pflichtgebunden oder mit Standardwert zu konfigurieren.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdditionalFieldInput'
            example:
              object_uID: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              type: category
              column_name: I4206
              edit_visible: 1
              edit_mandatory: 1
              view_visible: 1
              default_value: "FLUKE"
      responses:
        '201':
          description: Feld-Override erstellt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AdditionalFieldResource'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'

  /api/v2/additional-fields/{id}:
    get:
      tags: [additional-field]
      summary: Einzelnen Feld-Override abrufen
      parameters:
        - name: id
          in: path
          required: true
          description: UUID des Feld-Overrides
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Feld-Override
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AdditionalFieldResource'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags: [additional-field]
      summary: Feld-Override aktualisieren
      description: Erfordert Permission `inventory_edit`.
      parameters:
        - name: id
          in: path
          required: true
          description: UUID des Feld-Overrides
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdditionalFieldUpdate'
            example:
              edit_mandatory: 0
              default_value: null
      responses:
        '200':
          description: Feld-Override aktualisiert
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AdditionalFieldResource'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [additional-field]
      summary: Feld-Override loeschen
      description: Erfordert Permission `inventory_edit`.
      parameters:
        - name: id
          in: path
          required: true
          description: UUID des Feld-Overrides
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Erfolgreich geloescht
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

components:
  # ──────────────────────────────────────────────
  # SECURITY SCHEMES
  # ──────────────────────────────────────────────
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Sanctum API Token — erzeugen via `POST /api/v2/auth/token`.

        **Verwendung:** `Authorization: Bearer <token>`

  # ──────────────────────────────────────────────
  # PARAMETERS
  # ──────────────────────────────────────────────
  parameters:
    pageSize:
      name: "page[size]"
      in: query
      description: Datensaetze pro Seite (Standard 25, max 100)
      schema:
        type: integer
        default: 25
        maximum: 100
    pageNumber:
      name: "page[number]"
      in: query
      description: Seitennummer (Standard 1)
      schema:
        type: integer
        default: 1
    mtag:
      name: mtag
      in: path
      required: true
      description: MTAG (UUID) des Inventarobjekts
      schema:
        type: string
        format: uuid
      example: "ea79b775-8b4b-8e17-98c9-b33e7065ff6f"
    categoryId:
      name: id
      in: path
      required: true
      description: UUID der Kategorie
      schema:
        type: string
        format: uuid
      example: "3fa85f64-5717-4562-b3fc-2c963f66afa6"

  # ──────────────────────────────────────────────
  # RESPONSES
  # ──────────────────────────────────────────────
  responses:
    Unauthorized:
      description: Fehlende oder ungueltige Authentifizierung
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              status: 401
              title: Unauthorized
              detail: "Unauthenticated."
    Forbidden:
      description: Fehlende Berechtigungen
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              status: 403
              title: Forbidden
              detail: "Missing required permission: inventory_edit"
    NotFound:
      description: Ressource nicht gefunden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              status: 404
              title: Not Found
              detail: "No query results for model."
    ValidationError:
      description: Validierungsfehler
      content:
        application/json:
          schema:
            type: object
            properties:
              message:
                type: string
              errors:
                type: object
                additionalProperties:
                  type: array
                  items:
                    type: string
          example:
            message: "The name field is required."
            errors:
              name:
                - "The name field is required."

  # ──────────────────────────────────────────────
  # SCHEMAS
  # ──────────────────────────────────────────────
  schemas:
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            status:
              type: integer
            title:
              type: string
            detail:
              type: string

    # ── Category ──────────────────────────────────
    CategoryAttributes:
      type: object
      properties:
        name:
          type: string
          description: Kategoriename
          example: "Messgeraet"
        short_name:
          type: string
          nullable: true
          description: Kurzname
          example: "MG"
        type:
          type: string
          enum: [inventory, booking, repair, calibration]
          description: Kategorietyp
          example: inventory
        parent_id:
          type: string
          format: uuid
          nullable: true
          description: UUID der uebergeordneten Kategorie (null = Hauptkategorie)
        sort_num:
          type: integer
          nullable: true
          description: Sortierreihenfolge
          example: 1
        color:
          type: string
          nullable: true
          description: Hintergrundfarbe (Hex)
          example: "#4CAF50"
        text_color:
          type: string
          nullable: true
          description: Textfarbe (Hex)
          example: "#FFFFFF"
        rentable:
          type: integer
          nullable: true
          description: "Ausleihbar (0 = nein, 1 = ja)"
          example: 1

    CategoryResource:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: UUID der Kategorie
        type:
          type: string
          enum: [category]
        attributes:
          $ref: '#/components/schemas/CategoryAttributes'

    CategoryCollection:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/CategoryResource'
        meta:
          type: object
          properties:
            total:
              type: integer

    CategoryInput:
      type: object
      required: [name, type]
      properties:
        name:
          type: string
          maxLength: 255
          description: Kategoriename (Pflichtfeld)
          example: "Messgeraet"
        short_name:
          type: string
          maxLength: 50
          description: Kurzname
          example: "MG"
        type:
          type: string
          enum: [inventory, booking, repair, calibration]
          description: Kategorietyp (Pflichtfeld)
          example: inventory
        parent_id:
          type: string
          format: uuid
          nullable: true
          description: UUID der Elternkategorie
        sort_num:
          type: integer
          description: Sortierreihenfolge
          example: 1
        color:
          type: string
          maxLength: 50
          description: Hintergrundfarbe (Hex)
          example: "#4CAF50"
        text_color:
          type: string
          maxLength: 50
          description: Textfarbe (Hex)
          example: "#FFFFFF"
        rentable:
          type: integer
          enum: [0, 1]
          description: "Ausleihbar (0 = nein, 1 = ja)"
          example: 1

    CategoryUpdate:
      type: object
      description: Partial-Update — nur uebergebene Felder werden geaendert
      properties:
        name:
          type: string
          maxLength: 255
        short_name:
          type: string
          maxLength: 50
        parent_id:
          type: string
          format: uuid
          nullable: true
        sort_num:
          type: integer
        color:
          type: string
          maxLength: 50
        text_color:
          type: string
          maxLength: 50
        rentable:
          type: integer
          enum: [0, 1]

    # ── Inventory ─────────────────────────────────
    InventoryAttributes:
      type: object
      description: |
        Inventar-Attribute mit sprechenden Feldnamen.
        Die Zuordnung zu Datenbank-Spalten (I4201, I4202, ...) ist dynamisch
        und kann ueber `GET /api/v2/fields/inventory` abgefragt werden.
      properties:
        asset_number:
          type: string
          description: Inventarnummer (I4201)
          example: "DMM-001"
        serial_number:
          type: string
          nullable: true
          description: Seriennummer (I4202)
          example: "SN-1234567890"
        description:
          type: string
          nullable: true
          description: Geraetebeschreibung (I4203)
          example: "Digital Multimeter"
        manufacturer:
          type: string
          nullable: true
          description: Hersteller (I4206)
          example: "FLUKE"
        model:
          type: string
          nullable: true
          description: Modell/Typ (I4207)
          example: "179"
        location:
          type: string
          nullable: true
          description: Standort (I4210)
          example: "Labor A"
        status:
          type: integer
          nullable: true
          description: Statuscode (I4211)
          example: 1
        calibration_interval:
          type: integer
          nullable: true
          description: Kalibrierintervall in Monaten (I4220)
          example: 12
        customer_id:
          type: string
          nullable: true
          description: Kunden-UUID (FK zu customer.KTAG)
          example: "45f09720-082f-5c74-9cfa-a605e2863904"
      additionalProperties: true

    InventoryRelationships:
      type: object
      properties:
        customer:
          type: object
          properties:
            data:
              type: object
              nullable: true
              properties:
                id:
                  type: string
                type:
                  type: string
                  enum: [customer]
        categories:
          type: array
          description: "Zugeordnete Kategorien (type=inventory)"
          items:
            $ref: '#/components/schemas/CategoryResource'

    InventoryResource:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: MTAG (UUID) des Inventarobjekts
        type:
          type: string
          enum: [inventory]
        attributes:
          $ref: '#/components/schemas/InventoryAttributes'
        relationships:
          $ref: '#/components/schemas/InventoryRelationships'

    InventoryCollection:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/InventoryResource'
        meta:
          type: object
          properties:
            total:
              type: integer
            page:
              type: integer
            per_page:
              type: integer
            last_page:
              type: integer
        links:
          type: object
          properties:
            self:
              type: string
            next:
              type: string
              nullable: true
            prev:
              type: string
              nullable: true

    InventoryInput:
      type: object
      properties:
        asset_number:
          type: string
          maxLength: 255
          description: Inventarnummer (Pflichtfeld bei Erstellung)
          example: "DMM-001"
        serial_number:
          type: string
          maxLength: 255
          example: "SN-1234567890"
        description:
          type: string
          maxLength: 255
          example: "Digital Multimeter"
        type_code:
          type: string
          maxLength: 255
        manufacturer:
          type: string
          maxLength: 255
          example: "FLUKE"
        model:
          type: string
          maxLength: 255
          example: "179"
        location:
          type: string
          maxLength: 255
          example: "Labor A"
        status:
          type: integer
          example: 1
        calibration_interval:
          type: integer
          description: Kalibrierintervall in Monaten
          example: 12
        customer_id:
          type: string
          maxLength: 36
          description: Kunden-UUID
      required:
        - asset_number

    # ── Field Configuration ───────────────────────
    FieldTableSummary:
      type: object
      description: Uebersicht einer Tabelle mit Feldkonfigurationen
      properties:
        table:
          type: string
          description: Tabellenname
          example: inventory
        field_count:
          type: integer
          description: Gesamtanzahl der Felder
          example: 65
        active_field_count:
          type: integer
          description: Anzahl aktiver Felder (edit_visible, edit_mandatory oder view_visible)
          example: 32

    FieldConfigurationResource:
      type: object
      properties:
        id:
          type: string
          description: "Zusammengesetzter Schluessel: `column_name:table_name`"
          example: "I4201:inventory"
        type:
          type: string
          enum: [field-configuration]
        attributes:
          $ref: '#/components/schemas/FieldConfigurationAttributes'

    FieldConfigurationAttributes:
      type: object
      properties:
        api_name:
          type: string
          description: Sprechender API-Feldname (z.B. `asset_number`)
          example: asset_number
        column_name:
          type: string
          description: Datenbank-Spaltenname (z.B. `I4201`)
          example: I4201
        table_name:
          type: string
          description: Tabellenname
          example: inventory
        label:
          type: string
          description: Menschenlesbares Label
          example: "Inventarnummer"
        field_type:
          type: string
          enum: [text, number, date, select, boolean]
          description: |
            Abgeleiteter Feldtyp fuer Grid/Formulare:
            - `text` — Textfelder (varchar, textarea, ...)
            - `number` — Zahlenfelder (int, float, decimal, ...)
            - `date` — Datumsfelder
            - `select` — Auswahlfelder (Dropdown, StatusDatabase, PickList, ...)
            - `boolean` — Ja/Nein-Felder
          example: text
        field_type_raw:
          type: string
          description: Originaler Feldtyp aus der Datenbank
          example: varchar
        default_value:
          type: string
          nullable: true
          description: Standardwert fuer neue Datensaetze
        filterable:
          type: boolean
          description: Im Grid filterbar
          example: true
        sortable:
          type: boolean
          description: Im Grid sortierbar
          example: true
        editable:
          type: boolean
          description: In Bearbeitungsformularen sichtbar
          example: true
        visible:
          type: boolean
          description: In der Detailansicht sichtbar
          example: true
        grid_default:
          type: boolean
          description: Standardmaessig im Grid angezeigt
          example: true
        mandatory:
          type: boolean
          description: Pflichtfeld in Formularen
          example: true
        display_order:
          type: integer
          description: Anzeigereihenfolge in Formularen
          example: 1
        display_order_grid:
          type: integer
          description: Anzeigereihenfolge im Grid
          example: 1
        width:
          type: integer
          nullable: true
          description: Spaltenbreite (null = automatisch)
        options:
          type: array
          nullable: true
          description: Auswahloptionen fuer Select-Felder
          items:
            type: object
            properties:
              value:
                type: string
              label:
                type: string
        field_category:
          type: string
          nullable: true
          description: |
            Feldkategorie (steuert Optionsquelle):
            `StatusDatabase`, `RequiredList`, `PickList`, `RequiredDatabase`, `PickDatabase`
        is_user_defined:
          type: boolean
          description: Benutzerdefiniertes Feld
        audit:
          type: boolean
          description: Im Audit-Trail erfasst
        allow_copy:
          type: boolean
          description: Beim Kopieren uebernommen
        security_message:
          type: boolean
          description: Sicherheitsmeldung anzeigen
        small_view_visible:
          type: boolean
          description: In der kompakten Ansicht sichtbar
        link_table_note:
          type: string
          nullable: true
          description: Verknuepfungstabelle fuer Notiz-Felder

    # ── Additional Field ──────────────────────────
    AdditionalFieldResource:
      type: object
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [additional-field]
        attributes:
          type: object
          properties:
            object_uID:
              type: string
              format: uuid
              description: UUID der Kategorie oder des Status
            override_type:
              type: string
              enum: [category, status]
              description: Typ des Overrides
            column_name:
              type: string
              description: Spaltenname des ueberschriebenen Feldes
              example: I4206
            default_value:
              type: string
              nullable: true
              description: Standardwert
            edit_visible:
              type: integer
              enum: [0, 1]
              description: Im Bearbeitungsformular sichtbar
            edit_mandatory:
              type: integer
              enum: [0, 1]
              description: Pflichtfeld im Bearbeitungsformular
            view_visible:
              type: integer
              enum: [0, 1]
              description: In der Detailansicht sichtbar
            display_order:
              type: integer
              description: Anzeigereihenfolge
            empty_now:
              type: integer
              enum: [0, 1]
              description: Feld bei bestehenden Datensaetzen leeren

    AdditionalFieldInput:
      type: object
      required: [object_uID, type, column_name]
      properties:
        object_uID:
          type: string
          format: uuid
          description: UUID der Kategorie oder des Status
        type:
          type: string
          enum: [category, status]
          description: Override-Typ
        column_name:
          type: string
          description: Spaltenname des zu ueberschreibenden Feldes
          example: I4206
        default_value:
          type: string
          nullable: true
        edit_visible:
          type: integer
          enum: [0, 1]
        edit_mandatory:
          type: integer
          enum: [0, 1]
        view_visible:
          type: integer
          enum: [0, 1]
        display_order:
          type: integer
        empty_now:
          type: integer
          enum: [0, 1]

    AdditionalFieldUpdate:
      type: object
      description: Partial-Update — nur uebergebene Felder werden geaendert
      properties:
        default_value:
          type: string
          nullable: true
        edit_visible:
          type: integer
          enum: [0, 1]
        edit_mandatory:
          type: integer
          enum: [0, 1]
        view_visible:
          type: integer
          enum: [0, 1]
        display_order:
          type: integer
        empty_now:
          type: integer
          enum: [0, 1]
