openapi: 3.0.3
info:
  title: OkosMunkalap REST API
  version: "2026-06-15"
  description: |
    Az OkosMunkalap nyilvános REST API-ja ügyfelek és munkalapok programozott
    kezeléséhez. Részletes leírás: https://docs.okosmunkalap.hu/fejlesztoi-dokumentacio/

    ## Hitelesítés
    Minden hívás API kulccsal hitelesített, az `Authorization: Bearer <kulcs>`
    fejlécben. A kulcs `omk_live_` előtaggal kezdődik. A kulcs kizárólag
    szerver-szerver használatra való.

    ## Válaszformátum
    Siker: `{ "data": ... }` (listánál `pagination` is). Hiba:
    `{ "error": { "code", "message", "status", "requestId" } }`.

    ## Dátumok
    A kérésekben a dátumok ISO 8601 stringek (teljes UTC ajánlott, pl.
    `2026-06-16T09:00:00Z`). A válaszokban Firestore időbélyeg objektumként
    érkeznek (`_seconds` / `_nanoseconds`).
servers:
  - url: https://api.okosmunkalap.hu
    description: Éles
tags:
  - name: Rendszer
    description: Kapcsolat és kulcs ellenőrzés
  - name: Ügyfelek
    description: Ügyfelek létrehozása, lekérdezése, módosítása
  - name: Munkalapok
    description: Munkalapok létrehozása, listázása, státuszváltás, kollégához rendelés
  - name: Csapat
    description: A fiók csapattagjai (a hozzárendelésekhez)
  - name: Egyedi mezők
    description: A fiók egyedi mező definíciói
  - name: Készlet
    description: A tételtörzs (anyagok, munkadíjak) kezelése
  - name: Árajánlatok
    description: Árajánlatok létrehozása, listázása, lekérdezése
security:
  - bearerAuth: []
paths:
  /v1/ping:
    parameters:
      - $ref: "#/components/parameters/ApiVersion"
    get:
      tags: [Rendszer]
      summary: Kapcsolat és kulcs ellenőrzése
      description: Visszaadja a kulcs alapadatait. Nem igényel külön jogosultságot.
      responses:
        "200":
          description: A kulcs érvényes
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      pong: { type: boolean, example: true }
                      keyPrefix: { type: string, example: omk_live_a1b2c3d }
                      permissions:
                        type: array
                        items: { type: string }
                        example: [customers:read, worksheets:create]
                      apiVersion: { type: string, example: "2026-06-15" }
                      dataScope:
                        type: string
                        enum: [external_system, all_account]
                      requestId: { type: string, example: req_a1b2c3d4e5f6a7b8 }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/customers:
    parameters:
      - $ref: "#/components/parameters/ApiVersion"
    post:
      tags: [Ügyfelek]
      summary: Új ügyfél létrehozása
      description: |
        Jogosultság: `customers:create`. `external_system` hatókörű kulcsnál
        az `externalId` kötelező. Több névteres kulcsnál a cél-névteret az
        `externalSystem` mezővel add meg (enélkül az elsődleges névtér;
        elsődleges nélkül több névtérnél 400 `EXTERNAL_SYSTEM_AMBIGUOUS`).
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateCustomer" }
      responses:
        "201":
          description: Az ügyfél létrejött
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Customer" }
                  warnings:
                    type: array
                    items: { $ref: "#/components/schemas/Warning" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    get:
      tags: [Ügyfelek]
      summary: Ügyfél keresése külső azonosítóval vagy telefonnal
      description: |
        Jogosultság: `customers:read`. Add meg az egyik paramétert. Ha mindkettőt
        megadod, az `externalId` élvez elsőbbséget.
      parameters:
        - name: externalId
          in: query
          schema: { type: string }
          description: A kulcs névterében tárolt külső azonosító (pontos egyezés)
        - name: phone
          in: query
          schema: { type: string }
          description: Telefonszám; a rendszer E.164 formátumra normalizálja
        - $ref: "#/components/parameters/ExternalSystemQuery"
      responses:
        "200":
          description: A megtalált ügyfél
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Customer" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/customers/{id}:
    parameters:
      - $ref: "#/components/parameters/CustomerId"
      - $ref: "#/components/parameters/ApiVersion"
    get:
      tags: [Ügyfelek]
      summary: Ügyfél lekérdezése azonosító alapján
      description: "Jogosultság: `customers:read`."
      responses:
        "200":
          description: Az ügyfél
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Customer" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    patch:
      tags: [Ügyfelek]
      summary: Ügyfél részleges módosítása
      description: |
        Jogosultság: `customers:write`. Csak a megadott mezők módosulnak; az
        opcionális szöveges mezők üres stringgel (`""`) törölhetők (a `name`
        kötelező, nem üríthető). Az `externalId` nem módosítható ezen a
        végponton (ha elküldöd, figyelmen kívül marad).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateCustomer" }
      responses:
        "200":
          description: A módosított ügyfél
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Customer" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/customers/{id}/addresses:
    parameters:
      - $ref: "#/components/parameters/CustomerId"
      - $ref: "#/components/parameters/ApiVersion"
    post:
      tags: [Ügyfelek]
      summary: Cím hozzáadása ügyfélhez
      description: |
        Jogosultság: `customers:write`. Új helyszín vagy számlázási cím hozzáadása. A
        csomag-specifikus cím-limit túllépése 403 `ADDRESS_LIMIT_EXCEEDED`. A meglévő
        munkalapok/ajánlatok denormalizált címét NEM módosítja.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomerAddressCreateBody" }
      responses:
        "201":
          description: A frissített ügyfél (az új címmel)
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Customer" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/customers/{id}/addresses/{addressId}:
    parameters:
      - $ref: "#/components/parameters/CustomerId"
      - name: addressId
        in: path
        required: true
        schema: { type: string }
        description: A cím azonosítója (a Customer.addresses[].id mezőből)
      - $ref: "#/components/parameters/ApiVersion"
    patch:
      tags: [Ügyfelek]
      summary: Ügyfél címének módosítása
      description: |
        Jogosultság: `customers:write`. Csak a megadott mezők változnak; üres értékkel az
        opcionális mező törölhető. Az elsődlegesség a `.../primary` végponton állítható.
        Ismeretlen `addressId` → 404 `ADDRESS_NOT_FOUND`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomerAddressInput" }
      responses:
        "200":
          description: A frissített ügyfél
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Customer" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    delete:
      tags: [Ügyfelek]
      summary: Ügyfél címének törlése
      description: |
        Jogosultság: `customers:write`. Az elsődleges cím nem törölhető (409
        `ADDRESS_PRIMARY_IMMUTABLE`) — előbb állíts be másikat elsődlegesnek. A sikeres
        törlés utáni ismételt hívás 404 `ADDRESS_NOT_FOUND` (a törlés nem ismételhető).
      responses:
        "200":
          description: A frissített ügyfél (a cím nélkül)
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Customer" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/customers/{id}/addresses/{addressId}/primary:
    parameters:
      - $ref: "#/components/parameters/CustomerId"
      - name: addressId
        in: path
        required: true
        schema: { type: string }
        description: Az elsődlegessé teendő cím azonosítója
      - $ref: "#/components/parameters/ApiVersion"
    post:
      tags: [Ügyfelek]
      summary: Elsődleges cím beállítása
      description: |
        Jogosultság: `customers:write`. A megadott cím lesz az elsődleges (a korábbi
        elsődleges megszűnik az lenni); pontosan egy elsődleges cím lesz. Ismeretlen
        `addressId` → 404 `ADDRESS_NOT_FOUND`.
      responses:
        "200":
          description: A frissített ügyfél
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Customer" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/worksheets:
    parameters:
      - $ref: "#/components/parameters/ApiVersion"
    post:
      tags: [Munkalapok]
      summary: Új munkalap létrehozása (match-or-create)
      description: |
        Jogosultság: `worksheets:create` (a `customer.createIfMissing` ág esetén
        emellett `customers:create` is). Több névteres kulcsnál a kérés
        top-level `externalSystem` mezőjével adható meg a cél-névtér (enélkül az
        elsődleges; elsődleges nélkül több névtérnél 400 `EXTERNAL_SYSTEM_AMBIGUOUS`).
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateWorksheet" }
      responses:
        "201":
          description: A munkalap létrejött
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Worksheet" }
                  warnings:
                    type: array
                    description: A `createIfMissing` ág ügyfél-figyelmeztetései (pl. `ADDRESS_UNSTRUCTURED`); csak akkor szerepel, ha van figyelmeztetés
                    items: { $ref: "#/components/schemas/Warning" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    get:
      tags: [Munkalapok]
      summary: Munkalapok szűrt listája
      description: "Jogosultság: `worksheets:read`. Kurzor alapú lapozás."
      parameters:
        - name: status
          in: query
          schema: { $ref: "#/components/schemas/WorksheetStatusValue" }
        - name: customerId
          in: query
          schema: { type: string }
        - name: customerExternalId
          in: query
          schema: { type: string }
        - $ref: "#/components/parameters/ExternalSystemQuery"
        - name: scheduledDateFrom
          in: query
          description: "ISO 8601 dátum (ÉÉÉÉ-HH-NN) vagy teljes dátum-idő (pl. 2026-06-15T08:00:00Z)."
          schema: { type: string, example: "2026-06-15" }
        - name: scheduledDateTo
          in: query
          description: "Felső határ, bezárólag. Csak dátumot (ÉÉÉÉ-HH-NN) megadva a teljes nap beleszámít; idő-komponenssel (pl. 2026-06-30T16:00:00Z) a megadott pillanatig."
          schema: { type: string, example: "2026-06-30" }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: starting_after
          in: query
          description: Az előző oldal nextCursor értéke
          schema: { type: string }
      responses:
        "200":
          description: A munkalapok listája
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Worksheet" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/worksheets/{id}:
    parameters:
      - $ref: "#/components/parameters/WorksheetId"
      - $ref: "#/components/parameters/ApiVersion"
    get:
      tags: [Munkalapok]
      summary: Munkalap lekérdezése azonosító alapján
      description: "Jogosultság: `worksheets:read`."
      responses:
        "200":
          description: A munkalap
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Worksheet" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/worksheets/{id}/status:
    parameters:
      - $ref: "#/components/parameters/WorksheetId"
      - $ref: "#/components/parameters/ApiVersion"
    patch:
      tags: [Munkalapok]
      summary: Munkalap státuszának módosítása
      description: |
        Jogosultság: `worksheets:write`. AI célú kulcsnál a végállapotú váltás
        `confirmTerminalStatus: true` megerősítést igényel.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorksheetStatusUpdate" }
      responses:
        "200":
          description: A frissített munkalap
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Worksheet" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/worksheets/{id}/assign:
    parameters:
      - $ref: "#/components/parameters/WorksheetId"
      - $ref: "#/components/parameters/ApiVersion"
    post:
      tags: [Munkalapok]
      summary: Munkalap hozzárendelése kollégákhoz
      description: |
        Jogosultság: `worksheets:write`. A `userId` értékeket a
        `GET /v1/team-members` végponttal oldd fel (ahhoz `team:read` kell).

        A body a KÍVÁNT VÉGÁLLAPOTOT írja le, nem hozzáad: a küldött lista lecseréli a
        munkalap addigi hozzárendeléseit, az üres tömb pedig mindet törli. A hívás emiatt
        idempotens. A megjelenített nevet a szerver a csapattag adataiból tölti ki, a
        hívó csak azonosítót ad. A hozzárendelt kolléga a szokásos értesítést kapja.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorksheetAssignmentUpdate" }
      responses:
        "200":
          description: A frissített munkalap
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Worksheet" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/team-members:
    parameters:
      - $ref: "#/components/parameters/ApiVersion"
    get:
      tags: [Csapat]
      summary: A fiók aktív csapattagjai
      description: |
        Jogosultság: `team:read`. A hozzárendelésekhez szükséges `userId` feloldására való:
        a munkalap kollégához rendelése azonosítót vár, a partner-rendszer pedig általában
        nevet ismer.

        Csak az AKTÍV tagok jelennek meg, névsorban. Csapat nélküli fióknál üres lista jön
        vissza, nem hiba. A válasz szándékosan szűk: a tagok belső adatai (bérköltség,
        aláírás-kép, raktár- és modul-jogosultságok) nem kerülnek ki az API-ra.
      responses:
        "200":
          description: A csapattagok listája
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/TeamMember" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/service-locations:
    parameters:
      - $ref: "#/components/parameters/ApiVersion"
    get:
      tags: [Munkalapok]
      summary: A fiók aktív telephelyei (javítási helyszínek)
      description: |
        Jogosultság: `worksheets:read`. A szerviz munkalap `serviceLocation` mezőjéhez való:
        innen származik a telephely azonosítója.

        Csak az AKTÍV, telephely típusú helyszínek jelennek meg, névsorban: a raktár, a jármű
        és a virtuális helyszín nem javítási helyszín. Telephely nélküli fióknál üres lista
        jön vissza, nem hiba (ilyenkor a szerviz munkalapra a cég címe kerül).
      responses:
        "200":
          description: A telephelyek listája
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ServiceLocation" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/materials:
    parameters:
      - $ref: "#/components/parameters/ApiVersion"
    post:
      tags: [Készlet]
      summary: Tétel felvétele a törzsbe
      description: |
        Jogosultság: `materials:create`. A készlet modul **Pro csomagtól** érhető el; alacsonyabb
        csomagon a válasz 403 `MODULE_NOT_AVAILABLE`.

        **Match-or-create.** A hívás előbb meglévő tételt keres, és csak akkor hoz létre újat, ha
        nincs találat. Az illesztés két szabálya (a felület CSV-importjával azonos):

        1. azonos **cikkszám** (`sku`),
        2. cikkszám nélkül: azonos **név + egység + típus**.

        Találat esetén a válasz **200** és a MEGLÉVŐ tétel jön vissza (`matched: true`), új tételnél
        **201** (`matched: false`). Így egy megismételt hívás nem duplikálja a törzset. A megbízható
        kulcs a cikkszám: a név-egyezés kis- és nagybetű-érzékeny, tehát cikkszám nélkül az eltérő
        írásmód külön tételt hoz létre.

        **Árazás.** Az `unitPrice` (bruttó eladási ár) elhagyható: ilyenkor a rendszer a fiók
        felár-beállításából számolja a `purchasePrice`-ból. Ha a felár ki van kapcsolva, a beszerzési
        ár lesz az eladási is. Alanyi adómentes fióknál az effektív ÁFA-kulcs 0.

        **Készlet.** A végpont SOHA nem állít nyitókészletet, és nem hoz létre készlet-mozgást: az
        árlistából nincs valós készlet-információ. A bevételezés a felületen történik. A raktár
        hozzárendelése automatikus.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateMaterial" }
      responses:
        "201":
          description: A tétel létrejött
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Material" }
        "200":
          description: Már létező tételre illesztettünk, nem jött létre új rekord
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Material" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    get:
      tags: [Készlet]
      summary: Tételtörzs szűrt listája
      description: |
        Jogosultság: `materials:read`. A készlet modul **Pro csomagtól** érhető el; alacsonyabb
        csomagon a válasz 403 `MODULE_NOT_AVAILABLE`. Kurzor alapú lapozás, legújabb elöl.

        A szűrők tudatosan szűkek: **cikkszám** és **típus**. A cikkszám a partner-integrációk
        lookup-kulcsa, a típus a leggyakoribb leszűkítés. Szabad szöveges keresésre és kategória
        szerinti szűrésre a végpont nem való: azt az AI-összekötő `search_materials` eszköze adja,
        ami a keresőindexen fut.

        A tételek **készletét a lista nem tartalmazza** (a törzs-rekord nem hordoz készletet): azt
        a `GET /v1/materials/{id}/stock` adja meg raktáranként.
      parameters:
        - name: sku
          in: query
          description: Pontos cikkszám-egyezés
          schema: { type: string, maxLength: 100 }
        - name: type
          in: query
          schema:
            type: string
            enum: [material, labor, travel, other]
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: starting_after
          in: query
          description: Az előző oldal nextCursor értéke
          schema: { type: string }
      responses:
        "200":
          description: A tételek listája
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Material" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/materials/{id}:
    parameters:
      - $ref: "#/components/parameters/ApiVersion"
      - name: id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Készlet]
      summary: Egy tétel lekérése
      description: |
        Jogosultság: `materials:read`. A készlet modul **Pro csomagtól** érhető el; alacsonyabb
        csomagon a válasz 403 `MODULE_NOT_AVAILABLE`. Ismeretlen azonosítóra 404
        `MATERIAL_NOT_FOUND`.
      responses:
        "200":
          description: A tétel
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Material" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/materials/{id}/stock:
    parameters:
      - $ref: "#/components/parameters/ApiVersion"
      - name: id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Készlet]
      summary: Egy tétel készlete raktáranként
      description: |
        Jogosultság: `materials:read`. A készlet modul **Pro csomagtól** érhető el; alacsonyabb
        csomagon a válasz 403 `MODULE_NOT_AVAILABLE`. Ismeretlen azonosítóra 404
        `MATERIAL_NOT_FOUND`.

        A `totalStock` a raktárankénti sorok ÖSSZEGE, tehát a végösszeg és a bontás mindig
        ugyanabból a forrásból jön. Készlet nélküli tételnél (nem raktározott munkadíj, vagy még be
        nem vételezett anyag) a válasz nulla és üres `locations` lista, nem hiba.

        A válasz nincs lapozva: a raktárak száma csomag-szinten korlátos.
      responses:
        "200":
          description: A tétel készlete
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/MaterialStock" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/quotes:
    parameters:
      - $ref: "#/components/parameters/ApiVersion"
    post:
      tags: [Árajánlatok]
      summary: Új árajánlat létrehozása
      description: |
        Jogosultság: `quotes:create`. Az ajánlat MEGLÉVŐ ügyfélre készül (`customerId`); új
        ügyfélhez előbb a `POST /v1/customers` végpont. Az ajánlat mindig **draft** státuszban jön
        létre, és a végpont NEM küldi ki az ügyfélnek: a kiküldés, az elfogadás és az elutasítás a
        felületen (illetve az ügyfél publikus oldalán) történik. Megosztási link sem keletkezik.

        **Az összegeket a szerver számolja** a tételsorokból, a felülettel azonos kalkulátorral: a
        kérésben nem küldhető végösszeg. Mivel a tételek `unitPrice` mezője bruttó egységár, a
        `subtotal` (a tételek bruttó összege) és a `total` (bruttó végösszeg) kedvezmény nélkül
        megegyezik; a `vatAmount` a bruttóban foglalt ÁFA.

        **Havi keret.** Az árajánlat minden csomagban elérhető, de a havi darabszám csomagfüggő
        (Ingyenes: 3, Alap: 20, Profi és Üzleti: korlátlan). A keret betelte után a válasz 403
        `QUOTE_LIMIT_EXCEEDED`, a `details.limit` és a `details.current` mezőkkel. A keret a
        naptári hónap fordulóján nullázódik.

        **Adat hatókör.** `external_system` hatókörű kulcsnál a hivatkozott ügyfélnek a kulcs
        névterében kell lennie, különben a válasz 404 `CUSTOMER_NOT_FOUND`.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateQuote" }
      responses:
        "201":
          description: Az árajánlat létrejött
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Quote" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    get:
      tags: [Árajánlatok]
      summary: Árajánlatok szűrt listája
      description: |
        Jogosultság: `quotes:read`. Kurzor alapú lapozás, legújabb elöl.

        `external_system` hatókörű kulcsnál a `customerId` szűrő KÖTELEZŐ (az ajánlat nem hordoz
        külső rendszer szerinti besorolást, ezért a hatókör csak ügyfélenként tartható); enélkül a
        válasz 400 `MISSING_REQUIRED_FIELD`.
      parameters:
        - name: status
          in: query
          description: A tárolt státusz-érték (az egyedi, `custom_` prefixű státuszok is)
          schema: { $ref: "#/components/schemas/QuoteStatusValue" }
        - name: customerId
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: starting_after
          in: query
          description: Az előző oldal nextCursor értéke
          schema: { type: string }
      responses:
        "200":
          description: Az árajánlatok listája
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Quote" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/quotes/{id}:
    parameters:
      - $ref: "#/components/parameters/QuoteId"
      - $ref: "#/components/parameters/ApiVersion"
    get:
      tags: [Árajánlatok]
      summary: Árajánlat lekérdezése azonosító alapján
      description: "Jogosultság: `quotes:read`."
      responses:
        "200":
          description: Az árajánlat
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Quote" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/custom-fields:
    parameters:
      - $ref: "#/components/parameters/ApiVersion"
    get:
      tags: [Egyedi mezők]
      summary: A fiók egyedi mező definícióinak lekérdezése
      description: "Jogosultság: `customers:read` vagy `worksheets:read`."
      responses:
        "200":
          description: Az egyedi mező definíciók
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/CustomFieldDefinition" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /v1/worksheet-statuses:
    parameters:
      - $ref: "#/components/parameters/ApiVersion"
    get:
      tags: [Munkalapok]
      summary: A fiók munkalap-státusz definícióinak lekérdezése
      description: |
        Jogosultság: `worksheets:read`. A munkalap `status` mezőjében kapott értékekhez
        (beépített és `custom_` prefixű egyedi státuszok) adja meg a megjelenített nevet és a
        definíciót, a felületi sorrendben. A definíciók fiókonként eltérhetnek — futásidőben
        kérdezd le, ne égesd be.
      responses:
        "200":
          description: A státusz-definíciók
          headers:
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
            X-API-Version-Used: { $ref: "#/components/headers/ApiVersionUsed" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WorksheetStatusDefinition" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        API kulcs Bearer tokenként. Formátum: `omk_live_...`. A kulcsot a
        Beállítások → Integrációk → Kulcsok oldalon hozhatod létre.

        Egy `external_system` hatókörű kulcs egy vagy több külső-rendszer névteret
        kaphat (max. 10) egy elsődlegessel. Egynél több névtér esetén a külső-azonosító
        flow-knál add meg az `externalSystem` mezőt: íráskor (POST) a cél-névtér
        kiválasztásához (enélkül az elsődleges; elsődleges nélkül 400
        `EXTERNAL_SYSTEM_AMBIGUOUS`), olvasáskor (GET) a keresés egy névtérre szűkítéséhez
        (enélkül minden névtérben keres, ütközésnél 409 `EXTERNAL_ID_AMBIGUOUS`).
  parameters:
    ApiVersion:
      name: X-API-Version
      in: header
      required: false
      description: >-
        A kulcs alapértelmezett API verziójának felülírása erre a kérésre. Jelenleg csak a
        "2026-06-15" verzió támogatott; bármely más érték 400 (INVALID_REQUEST_BODY) hibát ad.
      schema: { type: string, enum: ["2026-06-15"], example: "2026-06-15" }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Biztonságos újrapróbáláshoz POST kérésnél (max. 255 karakter)
      schema: { type: string, maxLength: 255 }
    ExternalSystemQuery:
      name: externalSystem
      in: query
      required: false
      description: >-
        Opcionális célzott külső-rendszer névtér. Több névteres kulcsnál íráskor ezzel
        adható meg a cél-névtér (enélkül az elsődleges); olvasáskor erre szűkíti a keresést
        (enélkül minden névtérben keres). Ha a megadott névtér nincs a kulcs névtér-listájában:
        403 `EXTERNAL_NAMESPACE_FORBIDDEN`.
      schema: { type: string, minLength: 1, maxLength: 64 }
    CustomerId:
      name: id
      in: path
      required: true
      description: Az ügyfél belső azonosítója
      schema: { type: string }
    WorksheetId:
      name: id
      in: path
      required: true
      description: A munkalap belső azonosítója
      schema: { type: string }
    QuoteId:
      name: id
      in: path
      required: true
      description: Az árajánlat belső azonosítója
      schema: { type: string }
  headers:
    RequestId:
      description: A kérés egyedi azonosítója. Minden válaszban jelen van.
      schema: { type: string, example: req_a1b2c3d4e5f6a7b8 }
    ApiVersionUsed:
      description: A ténylegesen alkalmazott API verzió. Sikeres hitelesítés után.
      schema: { type: string, example: "2026-06-15" }
    RateLimitLimit:
      description: A percenkénti kéréskorlát. Sikeres hitelesítés után.
      schema: { type: integer }
    RateLimitRemaining:
      description: Hátralévő kérések az aktuális percben. Sikeres hitelesítés után.
      schema: { type: integer }
    RateLimitReset:
      description: A percablak nullázódása Unix epoch másodpercben. Sikeres hitelesítés után.
      schema: { type: integer }
  responses:
    BadRequest:
      description: >-
        A kérés érvénytelen. Külső-azonosító flow-knál többek közt: `EXTERNAL_SYSTEM_AMBIGUOUS`
        (több névtér, nincs elsődleges és nincs explicit `externalSystem`),
        `EXTERNAL_ID_FIELD_MISMATCH` (az `externalId` és a hozzá rendelt egyedi mező értéke eltér),
        `EXTERNAL_ID_NOT_SINGLE_TOKEN` ("több azonosító egy mezőben" módú névtérnél a keresés vagy
        az `externalId` elválasztó karaktert tartalmaz; keress egyetlen azonosítóra).
      headers:
        X-Request-Id: { $ref: "#/components/headers/RequestId" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Érvénytelen vagy hiányzó API kulcs
      headers:
        X-Request-Id: { $ref: "#/components/headers/RequestId" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: A kulcsnak nincs jogosultsága a művelethez (MISSING_PERMISSION), a kulcs lejárt (KEY_EXPIRED), vagy a megadott externalSystem nincs a kulcs névtér-listájában (EXTERNAL_NAMESPACE_FORBIDDEN)
      headers:
        X-Request-Id: { $ref: "#/components/headers/RequestId" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: >-
        Az erőforrás nem található (vagy a hatókörön kívül esik). Munkalap-létrehozásnál ide
        tartozik a `SERVICE_LOCATION_NOT_FOUND` is: a megadott telephely-azonosító vagy -név nem
        illeszkedik aktív telephelyre (az üzenet felsorolja a választhatókat).
      headers:
        X-Request-Id: { $ref: "#/components/headers/RequestId" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: >-
        Ütközés (pl. idempotencia, duplikált külső azonosító, megerősítés szükséges, a lookup több
        ügyfélre talált — EXTERNAL_ID_AMBIGUOUS, vagy a megadott telephely-név több telephelyre
        illeszkedik — SERVICE_LOCATION_AMBIGUOUS)
      headers:
        X-Request-Id: { $ref: "#/components/headers/RequestId" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    TooManyRequests:
      description: Kéréskorlát vagy havi keret túllépve
      headers:
        X-Request-Id: { $ref: "#/components/headers/RequestId" }
        Retry-After:
          description: Hány másodperc múlva próbáld újra
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Address:
      type: object
      description: >-
        Cím strukturáltan vagy szabad szövegként. Minden mező opcionális. Ha csak `fullText`
        érkezik (strukturált mező nélkül, magyar címmel), a rendszer megkísérli a szöveg
        strukturálását: siker esetén a zip/city/street kitöltődik és az addressParseStatus
        `parsed_from_fulltext` lesz; egyébként a cím szabad szövegként tárolódik
        (`unstructured`) és a válasz `ADDRESS_UNSTRUCTURED` figyelmeztetést ad.
      properties:
        zip: { type: string, maxLength: 20 }
        city: { type: string, maxLength: 100 }
        street: { type: string, maxLength: 200 }
        country: { type: string, maxLength: 100 }
        fullText: { type: string, maxLength: 300, description: Szabad szöveges cím }
    ExternalSystemField:
      type: string
      minLength: 1
      maxLength: 64
      description: >-
        Opcionális célzott külső-rendszer névtér. Több névteres kulcsnál íráskor ezzel
        adható meg a cél-névtér (enélkül az elsődleges); olvasáskor erre szűkíti a keresést
        (enélkül minden névtérben keres). Ha a megadott névtér nincs a kulcs névtér-listájában:
        403 `EXTERNAL_NAMESPACE_FORBIDDEN`.
    CustomerCreateData:
      type: object
      required: [name]
      description: Ügyfél létrehozási adatok externalId nélkül (a worksheet createIfMissing is ezt használja).
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        phone: { type: string, maxLength: 30 }
        email: { type: string, format: email }
        company: { type: string, maxLength: 100 }
        taxNumber:
          type: string
          pattern: '^\d{8}-\d-\d{2}$'
          example: 12345678-1-42
        taxCategory:
          type: string
          enum: [domestic, eu_reverse_charge, non_eu_export]
        address: { $ref: "#/components/schemas/Address" }
        customFields: { $ref: "#/components/schemas/CustomFields" }
        siteContactName:
          type: string
          maxLength: 100
          writeOnly: true
          description: Csak írható; a válaszban nem jelenik meg
        siteContactPhone:
          type: string
          maxLength: 30
          writeOnly: true
          description: Csak írható; a válaszban nem jelenik meg
        siteContactEmail:
          type: string
          format: email
          writeOnly: true
          description: Csak írható; a válaszban nem jelenik meg
        notes:
          type: string
          maxLength: 500
          writeOnly: true
          description: Csak írható; a válaszban nem jelenik meg
        isVip: { type: boolean }
    CreateCustomer:
      description: Ügyfél létrehozás a POST /v1/customers végponthoz (externalId opcionális).
      allOf:
        - $ref: "#/components/schemas/CustomerCreateData"
        - type: object
          properties:
            externalId:
              type: string
              minLength: 1
              maxLength: 255
              description: A kulcs külső rendszer névterébe kerül
            externalSystem:
              $ref: "#/components/schemas/ExternalSystemField"
            addresses:
              type: array
              maxItems: 20
              description: |
                Több helyszín / számlázási cím létrehozáskor (alternatíva az `address`
                mezőhöz; egyszerre csak az egyiket add meg, különben 400). A csomag-cím-limit
                túllépése 403 `ADDRESS_LIMIT_EXCEEDED`. Az elsődleges cím az `isPrimary`-jelölt
                vagy az első elem.
              items: { $ref: "#/components/schemas/CustomerAddressCreate" }
    UpdateCustomer:
      type: object
      description: Részleges módosítás; minden mező opcionális; az opcionális szöveges mezők üres stringgel ("") törölhetők. Az externalId nem módosítható.
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        phone: { type: string, maxLength: 30 }
        email: { type: string, format: email }
        company: { type: string, maxLength: 100 }
        taxNumber: { type: string, pattern: '^\d{8}-\d-\d{2}$' }
        taxCategory:
          type: string
          enum: [domestic, eu_reverse_charge, non_eu_export]
        address: { $ref: "#/components/schemas/Address" }
        customFields: { $ref: "#/components/schemas/CustomFields" }
        siteContactName:
          type: string
          maxLength: 100
          writeOnly: true
          description: Csak írható; a válaszban nem jelenik meg
        siteContactPhone:
          type: string
          maxLength: 30
          writeOnly: true
          description: Csak írható; a válaszban nem jelenik meg
        siteContactEmail:
          type: string
          format: email
          writeOnly: true
          description: Csak írható; a válaszban nem jelenik meg
        notes:
          type: string
          maxLength: 500
          writeOnly: true
          description: Csak írható; a válaszban nem jelenik meg
        isVip: { type: boolean }
    Customer:
      type: object
      description: Ügyfél API reprezentáció. A nem kitöltött mezők kimaradnak.
      properties:
        id: { type: string }
        name: { type: string }
        phone: { type: string }
        email: { type: string }
        company: { type: string }
        taxNumber: { type: string }
        taxCategory:
          type: string
          enum: [domestic, eu_reverse_charge, non_eu_export]
        address: { $ref: "#/components/schemas/Address" }
        addresses:
          type: array
          description: "Több helyszín / számlázási cím (id-vel). A cím-műveletek (POST/PATCH/DELETE /addresses) ezekre hivatkoznak."
          items: { $ref: "#/components/schemas/CustomerAddress" }
        addressParseStatus:
          type: string
          enum: [structured, parsed_from_fulltext, unstructured]
          description: >-
            structured = strukturáltan megadott cím; parsed_from_fulltext = szabad szövegből
            automatikusan kinyert strukturált cím (a fullText referenciaként megmarad);
            unstructured = csak szabad szöveg / hiányos cím.
        externalIds:
          type: object
          description: >-
            Külső rendszer névtér → a mező nyers értéke (amit a felhasználó a mezőben lát). external_system
            hatókörnél csak a kulcs saját névtere jelenik meg. "Több azonosító egy mezőben" módú névtérnél
            ez a teljes felsorolás (pl. "3158/3159/3160"), ami tükör, nem kereshetőségi ígéret; a
            ténylegesen kereshető azonosítókat az externalIdValues adja.
          additionalProperties: { type: string }
          example: { monitoring: "CUST-9001" }
        externalIdValues:
          type: object
          description: >-
            Külső rendszer névtér → a ténylegesen kereshető azonosítók listája. Egyértékű névtérnél egyelemű
            lista; "Több azonosító egy mezőben" módú névtérnél a mező szétbontott, egyedileg kereshető
            azonosítói. Üres lista = a mezőben van érték, de egyetlen azonosító sem kereshető (pl. mindegyik
            ütközik). Kereséshez, összevetéshez ezt használd. Lásd "Több azonosító egy mezőben".
          additionalProperties:
            type: array
            items: { type: string }
          example: { monitoring: ["3158", "3159", "3160"] }
        customFields: { $ref: "#/components/schemas/CustomFields" }
        isVip: { type: boolean }
        url: { type: string, format: uri }
    CustomerAddress:
      type: object
      description: Ügyfél-cím (helyszín vagy számlázási cím) a válaszban. A nem kitöltött mezők kimaradnak.
      properties:
        id: { type: string }
        label: { type: string, maxLength: 60 }
        type: { type: string, enum: [site, billing] }
        zip: { type: string, maxLength: 20 }
        city: { type: string, maxLength: 100 }
        street: { type: string, maxLength: 200 }
        country: { type: string, maxLength: 100 }
        isPrimary: { type: boolean }
        name: { type: string, maxLength: 100, description: "Magánszemély neve (jellemzően számlázási cím)" }
        company: { type: string, maxLength: 100 }
        taxNumber: { type: string, pattern: '^\d{8}-\d-\d{2}$' }
        email: { type: string, format: email }
        phone: { type: string, maxLength: 30 }
        contactName: { type: string, maxLength: 100, description: "Helyszíni kapcsolattartó neve (site cím)" }
        contactPhone: { type: string, maxLength: 30 }
        note: { type: string, maxLength: 300 }
    CustomerAddressInput:
      type: object
      description: |
        Cím-input. Létrehozáskor (POST) kötelező: `label`, `city`, `street`, és `name` VAGY
        `company`. Magyar irányítószám legalább 4 jegy. Módosításkor (PATCH) minden mező
        opcionális, és üres értékkel az opcionális mező törölhető; a merge után a teljes cím
        ugyanezen invariánsoknak meg kell feleljen.
      properties:
        label: { type: string, minLength: 1, maxLength: 60 }
        type: { type: string, enum: [site, billing], description: "Default: site" }
        name: { type: string, maxLength: 100 }
        company: { type: string, maxLength: 100 }
        taxNumber: { type: string, pattern: '^\d{8}-\d-\d{2}$' }
        zip: { type: string, maxLength: 20 }
        city: { type: string, minLength: 1, maxLength: 100 }
        street: { type: string, minLength: 1, maxLength: 200 }
        country: { type: string, maxLength: 100, description: "Default: Magyarország" }
        email: { type: string, format: email }
        phone: { type: string, maxLength: 30 }
        contactName: { type: string, maxLength: 100 }
        contactPhone: { type: string, maxLength: 30 }
        note: { type: string, maxLength: 300 }
    CustomerAddressCreateBody:
      description: Cím-input a POST /addresses végponthoz — a kötelező mezők (label, city, street) jelölve.
      allOf:
        - $ref: "#/components/schemas/CustomerAddressInput"
        - type: object
          required: [label, city, street]
    CustomerAddressCreate:
      description: Cím a createCustomer bulk úthoz (a CustomerAddressInput + isPrimary, kötelező mezőkkel).
      allOf:
        - $ref: "#/components/schemas/CustomerAddressInput"
        - type: object
          required: [label, city, street]
          properties:
            isPrimary: { type: boolean, description: "Ez legyen az elsődleges cím (pontosan egy lehet az)" }
    WorksheetCustomerRef:
      description: |
        Ügyfél referencia háromféleképpen: meglévő azonosítóval, külső azonosítós
        kereséssel, vagy match-or-create módon (createIfMissing). Adj meg `id`-t VAGY
        `lookupExternalId`-t. A szerver elnéző: ha mindkettő jelen van, az `id` ág nyer
        (ezért anyOf, nem oneOf).
      anyOf:
        - type: object
          required: [id]
          properties:
            id: { type: string, minLength: 1 }
        - type: object
          required: [lookupExternalId]
          properties:
            lookupExternalId: { type: string, minLength: 1, maxLength: 255 }
            createIfMissing:
              allOf:
                - $ref: "#/components/schemas/CustomerCreateData"
              description: Ügyfél adatok externalId nélkül; ha nincs találat, ebből jön létre az ügyfél
    WorksheetItemInput:
      type: object
      required: [description]
      properties:
        description: { type: string, minLength: 1, maxLength: 500 }
        type:
          type: string
          enum: [material, labor, travel, other, section]
          default: other
        quantity: { type: number, minimum: 0, default: 1 }
        unit: { type: string, maxLength: 20, default: db }
        unitPrice:
          type: number
          minimum: 0
          description: Bruttó egységár; a szerver számolja a totalPrice-t
        vatRate:
          type: integer
          enum: [0, 5, 18, 27]
          default: 27
        note: { type: string, maxLength: 1000 }
        isWarranty:
          type: boolean
          description: Garanciális tétel-e; a garancia mezők csak akkor érvényesek, ha ez true
        warrantyDuration:
          type: integer
          minimum: 1
          description: Garancia időtartama, egész szám (pl. 12)
        warrantyUnit:
          type: string
          enum: [month, year]
          description: month (hónap) vagy year (év)
        warrantyNote:
          type: string
          maxLength: 2000
          description: "Garancia megjegyzés: mire szól a garancia"
    WorksheetItem:
      type: object
      description: Munkalap tétel a válaszban
      properties:
        id: { type: string }
        type:
          type: string
          enum: [material, labor, travel, other, section]
        description: { type: string }
        quantity: { type: number }
        unit: { type: string }
        unitPrice: { type: number, description: Bruttó egységár }
        totalPrice: { type: number, description: Bruttó sorösszeg (egész Ft) }
        vatRate: { type: integer, enum: [0, 5, 18, 27] }
        note: { type: string }
        deviceLocalId:
          type: string
          description: >-
            Több eszközös szerviz munkalapon annak az átvett eszköznek az azonosítója, amelyhez a
            tétel tartozik (a `devices` tömb elemének `id` mezője). Eszközhöz nem kötött tételen
            kimarad.
        isWarranty:
          type: boolean
          description: Garanciális tétel-e; a garancia mezők csak akkor szerepelnek a válaszban, ha ez true
        warrantyDuration:
          type: integer
          description: Garancia időtartama, egész szám
        warrantyUnit:
          type: string
          enum: [month, year]
          description: month (hónap) vagy year (év)
        warrantyNote:
          type: string
          description: Garancia megjegyzés
    CreateWorksheet:
      type: object
      required: [customer]
      properties:
        customer: { $ref: "#/components/schemas/WorksheetCustomerRef" }
        externalSystem:
          $ref: "#/components/schemas/ExternalSystemField"
        title: { type: string, maxLength: 200 }
        notes:
          type: string
          maxLength: 2000
          writeOnly: true
          description: Publikus megjegyzés. Csak írható; a válaszban nem jelenik meg.
        internalNotes:
          type: string
          maxLength: 2000
          writeOnly: true
          description: Csak írható; a válaszban nem jelenik meg
        category: { type: string, maxLength: 100 }
        siteContactName:
          type: string
          maxLength: 100
          description: Bejelentő / helyszíni kapcsolattartó; nem módosítja az ügyféltörzset
        siteContactPhone: { type: string, maxLength: 30 }
        customFields: { $ref: "#/components/schemas/CustomFields" }
        status:
          type: string
          enum: [draft, in_progress]
          default: draft
        scheduledDate:
          type: string
          description: "ISO 8601 dátum (ÉÉÉÉ-HH-NN) vagy teljes dátum-idő. Időzóna nélkül az UTC nem garantált — a teljes UTC alak ajánlott."
          example: "2026-06-16T09:00:00Z"
        workAddress:
          allOf:
            - $ref: "#/components/schemas/Address"
          writeOnly: true
          description: Munkavégzési cím. Csak írható; a válaszban nem jelenik meg. Kizárólag a munkalap munkavégzési helyszínét adja — a munkalap számlázási címe automatikusan az ügyfél törzsadataiból áll össze.
        items:
          type: array
          items: { $ref: "#/components/schemas/WorksheetItemInput" }
        assignments:
          type: array
          maxItems: 20
          description: >-
            Kollégához rendelés már létrehozáskor. A `worksheets:create` mellett `team:read`
            jogosultságot is igényel, mert a szerver a csapattag nevét is visszaadja.
            Ugyanaz a tartalom utólag a `POST /v1/worksheets/{id}/assign` végponttal állítható.
          items: { $ref: "#/components/schemas/WorksheetAssignmentInput" }
        worksheetMode: { $ref: "#/components/schemas/WorksheetMode" }
        isSurvey:
          type: boolean
          default: false
          description: >-
            Felmérés-e. A `worksheetMode`-dal együtt adja a bizonylat típusát és a sorszám-prefixet:
            field+munkalap = ML, service+munkalap = SZ, field+felmérés = FE, service+felmérés = SZF.
        devices:
          type: array
          maxItems: 20
          description: >-
            A javításra átvett eszközök. CSAK `service` módban adható meg (field módban 400).
            Az eszközök a fiók eszköz-nyilvántartásába is bekerülnek.
          items: { $ref: "#/components/schemas/WorksheetDeviceInput" }
        intakeCondition:
          allOf:
            - $ref: "#/components/schemas/WorksheetIntakeCondition"
          description: >-
            Átvételi dokumentáció. CSAK `service` módban. Legalább egy rögzített eszközzel együtt
            ez a feltétele az átvételi jegyzőkönyv generálásának.
        serviceLocation:
          allOf:
            - $ref: "#/components/schemas/ServiceLocationRef"
          description: >-
            A javítás műhelye. CSAK `service` módban. Megadás nélkül a fiók alapértelmezett
            műhelye kerül a munkalapra (utoljára használt, egyébként az első aktív telephely;
            telephely nélküli fióknál a cég címe). Nem adható meg a `serviceLocationType:
            customerAddress` vagy a `workAddress` mellett.
        serviceLocationType: { $ref: "#/components/schemas/ServiceLocationType" }
    Worksheet:
      type: object
      description: Munkalap API reprezentáció. A nem kitöltött mezők kimaradnak.
      properties:
        id: { type: string }
        worksheetNumber: { type: string, example: ML-2026-0042 }
        status: { $ref: "#/components/schemas/WorksheetStatusValue" }
        customerId: { type: string }
        customerName: { type: string }
        title: { type: string }
        category: { type: string }
        siteContactName: { type: string }
        siteContactPhone: { type: string }
        scheduledDate: { $ref: "#/components/schemas/Timestamp" }
        completedAt: { $ref: "#/components/schemas/Timestamp" }
        subtotal: { type: number, description: Tételek bruttó összege }
        vatAmount: { type: number, description: A bruttóban foglalt ÁFA }
        total: { type: number, description: Bruttó végösszeg }
        items:
          type: array
          items: { $ref: "#/components/schemas/WorksheetItem" }
        customFields: { $ref: "#/components/schemas/CustomFields" }
        assignments:
          type: array
          description: A munkalapra rendelt kollégák; hozzárendelés nélküli munkalapon kimarad
          items: { $ref: "#/components/schemas/WorksheetAssignment" }
        customerCreated:
          type: boolean
          description: Csak a létrehozás válaszában; jelzi, ha új ügyfél is létrejött
        worksheetMode: { $ref: "#/components/schemas/WorksheetMode" }
        isSurvey: { type: boolean }
        devices:
          type: array
          description: A javításra átvett eszközök; csak szerviz munkalapon
          items: { $ref: "#/components/schemas/WorksheetDevice" }
        serviceLocationType:
          allOf:
            - $ref: "#/components/schemas/ServiceLocationType"
          description: >-
            A javítás helyszínének típusa. Csak szerviz munkalapon jelenik meg, ott mindig
            (a mező nélküli, korábbi dokumentumokon is `site`).
        serviceLocation:
          type: object
          description: A javítás műhelye; csak műhelyes (`site`) szerviz munkalapon
          properties:
            id: { type: string }
            name: { type: string }
            code: { type: string }
        createdAt: { $ref: "#/components/schemas/Timestamp" }
        url: { type: string, format: uri }
    WorksheetMode:
      type: string
      enum: [field, service]
      default: field
      description: >-
        A munkavégzés módja: `field` = kiszállásos, az ügyfélnél végzett munka,
        `service` = szerviz, az eszközt műhelybe veszik át.
    WorksheetDeviceInput:
      type: object
      required: [type]
      description: A javításra átvett eszköz. A belső azonosítót a szerver generálja.
      properties:
        type: { type: string, maxLength: 100, description: Az eszköz típusa, pl. „Automata kávégép" }
        brand: { type: string, maxLength: 100 }
        model: { type: string, maxLength: 100 }
        serialNumber: { type: string, maxLength: 100 }
        accessories: { type: string, maxLength: 500, description: Az eszközzel átvett tartozékok }
        storageLocation:
          type: string
          maxLength: 100
          description: >-
            Fizikai tárolóhely a műhelyen belül, pl. „A-23 polc". Ez NEM a telephely:
            a javítás helyszínét a `serviceLocation` adja.
    WorksheetDevice:
      allOf:
        - $ref: "#/components/schemas/WorksheetDeviceInput"
        - type: object
          properties:
            id: { type: string, description: A munkalapon belüli eszköz-azonosító (szerver generálja) }
    WorksheetIntakeCondition:
      type: object
      required: [customerReportedIssue]
      description: Átvételi dokumentáció a szerviz munkalapon.
      properties:
        customerReportedIssue:
          type: string
          maxLength: 2000
          description: Az ügyfél által jelzett hiba. Ha a blokkot megadod, kötelező.
        notes: { type: string, maxLength: 2000, description: Megjegyzés az eszköz átvételkori állapotáról }
    ServiceLocationType:
      type: string
      enum: [site, customerAddress]
      description: >-
        A javítás helyszínének típusa: `site` = saját műhely (alapértelmezés),
        `customerAddress` = az ügyfélnél, kiszállással. Csak szerviz munkalapon értelmezett.
    ServiceLocationRef:
      type: object
      description: >-
        Telephely-hivatkozás: vagy az azonosítójával, vagy a nevével. A név feloldása
        kis-nagybetűtől és ékezettől független, magyar toldalékot elvisel; nem egyértelmű
        névnél 409 `SERVICE_LOCATION_AMBIGUOUS`, ismeretlennél 404 `SERVICE_LOCATION_NOT_FOUND`.
      oneOf:
        - type: object
          required: [id]
          properties:
            id: { type: string, maxLength: 128, description: A telephely azonosítója a `GET /v1/service-locations` válaszából }
        - type: object
          required: [name]
          properties:
            name: { type: string, maxLength: 200, example: Sopron Műhely }
    ServiceLocation:
      type: object
      description: Aktív telephely, amely a szerviz munkalap javítási helyszíne lehet.
      properties:
        id: { type: string }
        name: { type: string, example: Sopron Műhely }
        code: { type: string, description: Rövid kód, ha van megadva }
        address: { $ref: "#/components/schemas/Address" }
        isDefaultForRepairs:
          type: boolean
          description: >-
            Erre a telephelyre kerül a szerviz munkalap, ha nem adsz meg helyszínt. A fiók
            legutóbb használt műhelyét követi, ezért idővel változhat.
    CreateMaterial:
      type: object
      required: [name]
      properties:
        name: { type: string, maxLength: 200, example: Sarokszelep 1/2" }
        sku:
          type: string
          maxLength: 100
          description: Cikkszám. Ez az elsődleges illesztési kulcs, érdemes mindig megadni.
          example: SZ-001
        type:
          type: string
          enum: [material, labor, travel, other]
          default: material
          description: A tétel típusa. A `labor` és a `travel` nem készletezhető.
        unit: { type: string, maxLength: 20, default: db, example: db }
        internalName: { type: string, maxLength: 200, description: Belső elnevezés, az ügyfél nem látja }
        category: { type: string, maxLength: 100 }
        manufacturer: { type: string, maxLength: 100 }
        notes: { type: string, maxLength: 1000 }
        purchasePrice:
          type: number
          minimum: 0
          description: Bruttó beszerzési egységár forintban. Az eladási ár felár-számításának alapja.
          example: 1270
        unitPrice:
          type: number
          minimum: 0
          description: >-
            Bruttó eladási egységár forintban. Elhagyható: ilyenkor a fiók felár-beállításából
            számolódik a beszerzési árból.
          example: 1588
        vatRate:
          type: number
          enum: [0, 5, 18, 27]
          description: ÁFA-kulcs. Alanyi adómentes fióknál a rendszer 0-ra oldja fel.
        minimumStock:
          type: number
          minimum: 0
          description: Készlet-riasztási küszöb. NEM nyitókészlet.
    Material:
      type: object
      description: Készlet-tétel a törzsből.
      properties:
        id: { type: string }
        name: { type: string, example: Sarokszelep 1/2" }
        sku: { type: string, example: SZ-001 }
        type: { type: string, enum: [material, labor, travel, other] }
        unit: { type: string, example: db }
        internalName: { type: string }
        category: { type: string }
        manufacturer: { type: string }
        notes: { type: string }
        unitPrice: { type: number, description: Bruttó eladási egységár }
        netUnitPrice: { type: number, description: Nettó eladási egységár }
        purchasePrice: { type: number, description: Bruttó beszerzési egységár }
        netPurchasePrice: { type: number, description: Nettó beszerzési egységár }
        vatRate: { type: number, enum: [0, 5, 18, 27] }
        minimumStock: { type: number }
        matched:
          type: boolean
          description: >-
            `true`, ha a hívás meglévő tételre illesztett, tehát ebben a hívásban nem jött létre új
            rekord. Ilyenkor a válasz státusza 200, egyébként 201.
        createdAt: { $ref: "#/components/schemas/Timestamp" }
        url: { type: string, description: A tétel megnyitása az alkalmazásban }
    MaterialStock:
      type: object
      description: >-
        Egy tétel készlete raktáranként. A `totalStock` a `locations` sorok összege, tehát a
        végösszeg és a bontás mindig ugyanabból a forrásból jön.
      properties:
        materialId: { type: string }
        name: { type: string, example: Sarokszelep 1/2" }
        sku: { type: string, example: SZ-001 }
        unit: { type: string, example: db }
        totalStock: { type: number, description: A raktárankénti készletek összege }
        minimumStock: { type: number, description: A tételre beállított készlet-riasztási küszöb }
        locations:
          type: array
          description: Raktárankénti bontás, raktárnév szerint rendezve. Készlet nélküli tételnél üres.
          items:
            type: object
            properties:
              locationId: { type: string }
              locationName: { type: string, example: Központi raktár }
              currentStock: { type: number }
              reservedStock: { type: number, description: Foglalt mennyiség, ha van }
              minimumStock: { type: number, description: A raktárra beállított küszöb, ha van }
              stockLevel:
                type: string
                enum: [out_of_stock, low_stock, ok, overstock]
                description: A készlet-állapot a felülettel azonos küszöbökkel
              binLocation:
                type: string
                example: A-03-02
                description: "Tárolóhely (zóna-állvány-polc), ha van"
        url: { type: string, description: A tétel megnyitása az alkalmazásban }
    CreateQuote:
      type: object
      required: [customerId, title, items]
      properties:
        customerId:
          type: string
          maxLength: 255
          description: >-
            A MEGLÉVŐ ügyfél azonosítója. Új ügyfélhez előbb a `POST /v1/customers`; ajánlat
            ügyfél-rekord nélküli érdeklődőre nem hozható létre.
        title: { type: string, minLength: 1, maxLength: 200, example: Fürdőszoba felújítás }
        items:
          type: array
          minItems: 1
          maxItems: 200
          description: A tételsorok. Az összegeket ezekből a szerver számolja.
          items: { $ref: "#/components/schemas/WorksheetItemInput" }
        notes:
          type: string
          maxLength: 5000
          description: Ügyfélnek látható megjegyzés. Csak írható, a válaszban nem jelenik meg.
        internalNotes:
          type: string
          maxLength: 5000
          description: Belső megjegyzés, az ügyfél sosem látja. Csak írható, a válaszban nem jelenik meg.
        validUntil:
          type: string
          description: >-
            Az érvényesség vége ISO 8601 dátumként. Elhagyva a létrehozástól számított 30 nap,
            ahogy a felületen is.
          example: "2026-09-15"
    Quote:
      type: object
      description: Árajánlat API reprezentáció. A nem kitöltött mezők kimaradnak.
      properties:
        id: { type: string }
        quoteNumber: { type: string, example: AJ-2026-0001 }
        status: { $ref: "#/components/schemas/QuoteStatusValue" }
        customerId: { type: string }
        customerName: { type: string }
        customerCompany: { type: string }
        title: { type: string }
        items:
          type: array
          items: { $ref: "#/components/schemas/WorksheetItem" }
        subtotal: { type: number, description: A tételek bruttó összege (kedvezmény előtt) }
        discount:
          type: number
          description: >-
            A dokumentum-szintű kedvezmény összege. Csak akkor jelenik meg, ha van: kedvezményt a
            felületen lehet adni, az API nem állít be ilyet.
        vatAmount: { type: number, description: A bruttóban foglalt ÁFA }
        total: { type: number, description: Bruttó végösszeg (kedvezmény után) }
        validUntil: { $ref: "#/components/schemas/Timestamp" }
        createdAt: { $ref: "#/components/schemas/Timestamp" }
        url: { type: string, format: uri, description: Az árajánlat megnyitása az alkalmazásban }
    QuoteStatusValue:
      description: |
        Beépített árajánlat státusz vagy `custom_` előtagú egyedi státusz. Az egyedi státusz teljes
        mintája: `^custom_[\p{L}\p{N}_-]{1,80}$` (Unicode betű/szám).
      oneOf:
        - type: string
          title: Beépített státusz
          enum:
            - draft
            - sent
            - viewed
            - commented
            - accepted
            - rejected
            - expired
            - worksheet_generated
            - survey_generated
        - type: string
          title: Egyedi státusz
          # A tényleges szerver-validáció Unicode-mintája: ^custom_[\p{L}\p{N}_-]{1,80}$
          # (lásd a description-t). Itt szándékosan tool-biztos minta áll, mert a Unicode
          # property escape-eket (\p{L}) több OpenAPI-generátor hibásan kezeli.
          pattern: '^custom_.{1,80}$'
      example: draft
    WorksheetRole:
      type: string
      description: |
        Szerepkör a munkalapon. `lead` = fő felelős, `surveyor` = felmérő,
        `quote_maker` = árajánlatkészítő, `coordinator` = munkaszervező,
        `installer` = kivitelező, `contact` = kapcsolattartó. Egy munkalapon a `surveyor`
        és az `installer` több főt is kaphat, a többi szerepkör egyet.
      enum: [lead, surveyor, quote_maker, coordinator, installer, contact]
      default: lead
    WorksheetAssignmentInput:
      type: object
      required: [userId]
      description: >-
        A hozzárendelés bemenete: csak a kit (`userId`) és a mit (`role`). A megjelenített nevet
        a szerver tölti ki a csapattag adataiból.
      properties:
        userId:
          type: string
          maxLength: 128
          description: A csapattag azonosítója a `GET /v1/team-members` válaszából
        role: { $ref: "#/components/schemas/WorksheetRole" }
    WorksheetAssignment:
      type: object
      description: Egy hozzárendelés a munkalap válaszában.
      properties:
        role: { $ref: "#/components/schemas/WorksheetRole" }
        userId: { type: string }
        userName: { type: string }
    WorksheetAssignmentUpdate:
      type: object
      required: [assignments]
      properties:
        assignments:
          type: array
          maxItems: 20
          description: A munkalap kívánt hozzárendelés-listája; az üres tömb mindet törli
          items: { $ref: "#/components/schemas/WorksheetAssignmentInput" }
    TeamMember:
      type: object
      description: Egy aktív csapattag. A nem kitöltött mezők kimaradnak.
      properties:
        userId:
          type: string
          description: Ezt add meg a hozzárendelésekben
        displayName: { type: string, example: "Kovács Péter" }
        email: { type: string, format: email }
        role:
          type: string
          description: Csapat-szerepkör (nem a munkalap szerepköre)
          enum: [owner, admin, member]
        phone:
          type: string
          description: Csapaton belüli telefonszám, ha meg van adva
        photoUrl: { type: string, format: uri }
    WorksheetStatusUpdate:
      type: object
      required: [status]
      properties:
        status: { $ref: "#/components/schemas/WorksheetStatusValue" }
        completedAt:
          type: string
          description: "ISO 8601 dátum (ÉÉÉÉ-HH-NN) vagy teljes dátum-idő. Időzóna nélkül az UTC nem garantált — a teljes UTC alak ajánlott."
          example: "2026-06-16T16:30:00Z"
        confirmTerminalStatus:
          type: boolean
          description: AI célú kulcsnál a végállapotú váltás megerősítése
    WorksheetStatusValue:
      description: |
        Beépített munkalap státusz vagy `custom_` előtagú egyedi státusz. Az egyedi
        státusz teljes mintája: `^custom_[\p{L}\p{N}_-]{1,80}$` (Unicode betű/szám).
      oneOf:
        - type: string
          title: Beépített státusz
          enum:
            - draft
            - quote_generated
            - worksheet_generated
            - survey_done
            - in_progress
            - completed
            - sent
            - invoiced
            - partially_invoiced
            - cancelled
        - type: string
          title: Egyedi státusz
          # A tényleges szerver-validáció Unicode-mintája: ^custom_[\p{L}\p{N}_-]{1,80}$
          # (lásd a description-t). Itt szándékosan tool-biztos minta áll, mert a Unicode
          # property escape-eket (\p{L}) több OpenAPI-generátor hibásan kezeli.
          pattern: '^custom_.{1,80}$'
      example: in_progress
    CustomFieldDefinition:
      type: object
      properties:
        id: { type: string }
        label: { type: string }
        type: { type: string, enum: [text, number] }
        appliesTo: { type: string, enum: [customer, worksheet] }
        required: { type: boolean }
        externalSystemNamespace:
          type: string
          description: >-
            Csak ha jelen van: a mező egy külső rendszer azonosítóhoz van kötve, ez a névtér neve. Az ilyen
            mezőt az externalId + externalSystem párral állítsd (nem a customFields-en); a válaszban/webhookban
            az externalIds alatt jön vissza. Eltérő érték mindkét helyen → 400 EXTERNAL_ID_FIELD_MISMATCH.
    CustomFields:
      type: object
      description: "Egyedi mezők: kulcs a mező azonosítója, érték string"
      additionalProperties: { type: string }
      example: { fld_x1: "12345" }
    WorksheetStatusDefinition:
      type: object
      properties:
        value:
          type: string
          description: A státusz értéke — ezt kapod a munkalap `status` mezőjében és ezt küldheted a PATCH /v1/worksheets/{id}/status hívásban
          example: custom_alkatreszre_var
        label: { type: string, example: "Alkatrészre vár" }
        mode: { type: string, enum: [field, service], description: "field = kiszállásos, service = szerviz munkalap" }
        color: { type: string, description: "App-paletta token (pl. emerald) vagy hex kód" }
        builtIn: { type: boolean }
        isCompleted:
          type: boolean
          description: "Csak ha true: a státusz teljesített (kész) munkának számít a fiókban"
        systemManaged:
          type: boolean
          description: "Csak ha true: a státuszt rendszer-folyamat kezeli, API-ból ne állítsd"
    Pagination:
      type: object
      properties:
        hasMore: { type: boolean }
        limit: { type: integer }
        nextCursor:
          type: string
          description: A következő oldal starting_after értéke (csak ha hasMore igaz)
    Timestamp:
      type: object
      description: Firestore időbélyeg. A `_seconds` a Unix epoch másodperc.
      properties:
        _seconds: { type: integer, format: int64, example: 1781600400 }
        _nanoseconds: { type: integer, example: 0 }
    Warning:
      type: object
      description: Nem blokkoló figyelmeztetés a válasz warnings tömbjében
      properties:
        code: { type: string, example: ADDRESS_UNSTRUCTURED }
        message: { type: string }
    Error:
      type: object
      properties:
        error:
          type: object
          required: [code, message, status, requestId]
          properties:
            code: { type: string, example: INVALID_API_KEY }
            message: { type: string }
            status: { type: integer, example: 401 }
            requestId: { type: string, example: req_a1b2c3d4e5f6a7b8 }
            details:
              type: object
              description: Kódfüggő kiegészítő adat (pl. a validációs hibák listája)
