openapi: 3.1.0

info:
  title: Vyplňto.cz REST API
  version: 1.0.0
  description: |
    REST API služby **Vyplňto.cz** pro programové vkládání dotazníků, sledování
    jejich průběhu a export vyplněných odpovědí. Je určené zákazníkům s placenou
    licencí (mimo Vyplňto STUDENT) a jejich nástrojům (včetně AI agentů).

    ## Životní cyklus dotazníku
    Dotazník prochází stavy **koncept → sběr → vyhodnocený**:

    1. **Ověříte se** API tokenem (viz *Autentizace*).
    2. **Vytvoříte koncept** – `POST /questionnaires`. Vznikne dotazník ve stavu
       `draft`, který zatím nikdo nevyplňuje. Získáte jeho `slug`.
    3. **Doladíte a vyzkoušíte** – celý koncept lze přepsat přes
       `PUT /questionnaires/{slug}` a otestovat přes dočasný náhled
       `POST /questionnaires/{slug}/preview` (proklikání včetně větvení, bez
       ukládání odpovědí).
    4. **Publikujete** – `POST /questionnaires/{slug}/publish` s datem ukončení.
       Před spuštěním se dotazník zvaliduje; od té chvíle sbírá odpovědi na
       adrese `https://<slug>.vyplnto.cz/`.
    5. **Sledujete průběh** – `GET /questionnaires` (seznam) nebo
       `GET /questionnaires/{slug}` (detail se statistikami).
    6. **Prodloužíte / obnovíte / ukončíte sběr** – opět
       `POST /questionnaires/{slug}/publish` s novým datem (datum na aktuální
       čas sběr ukončí).
    7. **Exportujete odpovědi** – `GET /questionnaires/{slug}/responses`.

    Dále lze dotazník vrátit do konceptu (`/unpublish`), vynulovat odpovědi
    (`DELETE …/responses`) nebo smazat (`DELETE …/{slug}`).

    ## Autentizace
    Každý požadavek musí obsahovat váš osobní API token v hlavičce:

    ```
    Authorization: Bearer VAS_TOKEN
    ```

    Token je vázaný na váš účet – všechny vytvořené dotazníky patří vám a přes
    API vidíte a spravujete jen vlastní dotazníky. Token nikdy nesdílejte a
    nevkládejte do veřejného kódu.

    Neplatný nebo chybějící token vrací `401`. Je-li token platný, ale k účtu
    není aktivní licence opravňující k API (Vyplňto PREMIUM či vyšší), vrací
    **kterékoli volání** `403` (`license_expired`).

    ## Identifikátor dotazníku (`slug`)
    Dotazník se v celém API adresuje pomocí `slug` – je to část jeho veřejné
    adresy `https://<slug>.vyplnto.cz/`. Slug si můžete zvolit při vkládání,
    nebo ho systém odvodí z názvu dotazníku.

    ## Formáty
    - **Datum a čas** se uvádí jako text `YYYY-MM-DD HH:MM:SS` v čase platném
      v ČR (např. `2027-12-31 12:00:00`).
    - **Jazyk** se uvádí kódem ISO 639-1 (např. `cs`, `en`).

    ## Limity
    Platí dvě nezávislé vrstvy omezení; obě při překročení vrací `429`:

    - **Počet vložených dotazníků** (denní a měsíční limit podle licence) –
      aktuální hodnoty zjistíte přes `GET /account`.
    - **Rychlost volání API** (rate limit) – nejvýše **60 požadavků za minutu**
      (a 1 000 za hodinu) na token; přísnější limity platí pro zápisové
      operace, export a náhled.

    Každá odpověď obsahuje informativní hlavičky, podle kterých můžete tempo
    řídit:
    ```
    X-RateLimit-Limit: 60
    X-RateLimit-Remaining: 57
    X-RateLimit-Reset: 1737550800     (unix čas resetu okna)
    ```
    Odpověď `429` navíc obsahuje hlavičku `Retry-After` (počet sekund, než to
    zkusíte znovu).

  contact:
    name: Vyplňto.cz
    url: https://www.vyplnto.cz

servers:
  - url: /api/v1

tags:
  - name: Account
    description: Informace o vašem účtu a licenci
  - name: Questionnaires
    description: Vkládání a správa dotazníků
  - name: Responses
    description: Export vyplněných dotazníků

security:
  - bearerAuth: []

paths:

  /account:
    get:
      tags: [Account]
      summary: Informace o účtu
      description: |
        Vrátí základní informace o vašem účtu – typ licence, do kdy je
        předplacená a jaké platí limity pro vkládání dotazníků přes API.
      operationId: getAccount
      responses:
        '200':
          description: Informace o účtu
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Account'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'

  /questionnaires:
    get:
      tags: [Questionnaires]
      summary: Seznam vašich dotazníků
      description: |
        Vrátí přehled vašich dotazníků se základními údaji a stavem. Detail
        konkrétního dotazníku (včetně otázek) získáte přes
        `GET /questionnaires/{slug}`.
      operationId: listQuestionnaires
      responses:
        '200':
          description: Seznam dotazníků
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/QuestionnaireSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'
    post:
      tags: [Questionnaires]
      summary: Vytvoření konceptu dotazníku
      description: |
        Vytvoří nový dotazník včetně všech otázek a nabízených odpovědí ve stavu
        **koncept** (`draft`). Koncept zatím nikdo nevyplňuje – sběr spustíte až
        přes `POST /questionnaires/{slug}/publish`.

        Data se ověřují už při vkládání: nekonzistentní dotazník (chybějící texty
        otázek, nesmyslné větvení, „visící" otázka apod.) API odmítne. Buď se
        uloží celý dotazník, nebo (při chybě) nic.

        **Dočasné omezení:** účet smí mít v jednu chvíli jen **jeden koncept**
        (`draft`). Máte-li rozpracovaný koncept, API vrátí `409` se slugem tohoto
        konceptu – nejdřív ho publikujte, nebo smažte. (Omezení plyne ze stávající
        aplikace a bude do budoucna uvolněno.)
      operationId: createQuestionnaire
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Questionnaire'
      responses:
        '201':
          description: Koncept dotazníku byl vytvořen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Questionnaire'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'
        '409':
          description: |
            Účet už má rozpracovaný koncept (`draft`). Tělo `Error` obsahuje slug
            existujícího konceptu; nejdřív ho publikujte, nebo smažte.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: conflict
                message: 'Není možné mít více dotazníků ve stavu draft. Nejprve dotazník se slugem "muj-koncept" publikujte nebo smažte.'
        '429':
          $ref: '#/components/responses/RateLimited'

  /questionnaires/{slug}:
    parameters:
      - name: slug
        in: path
        required: true
        description: Identifikátor dotazníku (část adresy `https://<slug>.vyplnto.cz/`).
        schema:
          type: string
          pattern: '^[a-z0-9-]+$'
          example: spokojenost-2027
    get:
      tags: [Questionnaires]
      summary: Detail dotazníku
      description: Vrátí kompletní strukturu dotazníku včetně otázek a aktuálních statistik.
      operationId: getQuestionnaire
      responses:
        '200':
          description: Detail dotazníku
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Questionnaire'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Questionnaires]
      summary: Přepsání konceptu dotazníku
      description: |
        Nahradí celý obsah dotazníku novými daty (kompletní aktualizace –
        pošlete celý dotazník, ne jen změněná pole).

        **Doporučený postup (neveřejný pilot → veřejný sběr):** vytvořte dotazník
        jako **neveřejný** (`public: false`), odladěte ho na malém vzorku
        (kolegové, vedoucí – sdílíte s nimi běžnou adresu dotazníku, žádný
        časově omezený náhled) a teprve pak ho přes `PUT` přepněte na
        **veřejný** (`public: true`). Veřejná kvóta (`publicPerDay`/`Month`) se
        čerpá až při **prvním** přepnutí na veřejný; je-li vyčerpaná, přepnutí
        se odmítne (`429 quota_exceeded`). Opakované přepínání veřejný/neveřejný
        už kvótu znovu nečerpá.

        **Podmínky:**
        - Dotazník musí být ve stavu **koncept** (`draft`). Běžící ani
          vyhodnocený dotazník takto měnit nelze – nejdřív ho vraťte do konceptu
          přes `POST /questionnaires/{slug}/unpublish`.
        - Dotazník **nesmí mít žádné uložené odpovědi**. Změna struktury by je
          znehodnotila; pokud odpovědi má, nejprve je vynulujte přes
          `DELETE /questionnaires/{slug}/responses`.
        - **Veřejný dotazník se schválenou bezplatnou propagací**
          (`info.promotionApproved = approved`) **nelze přepnout na neveřejný**
          (`public = false`). Výměnou za pomoc se sháněním respondentů se
          výsledky zveřejňují, proto je po schválení propagace už nelze skrýt.
          Pokus vrátí `409`.

        Data se validují stejně přísně jako při vytvoření.
      operationId: updateQuestionnaire
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Questionnaire'
      responses:
        '200':
          description: Koncept byl přepsán
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Questionnaire'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
    delete:
      tags: [Questionnaires]
      summary: Smazání dotazníku
      description: |
        Smaže dotazník. **Přes API nelze smazat:**
        - uzamčený dotazník (`info.locked = true`),
        - veřejný dotazník se 100 a více odpověďmi (jeho výsledky jsou součástí
          veřejného archivu).

        V takovém případě API vrátí `409`; smazání takového dotazníku je možné
        řešit přes podpora@vyplnto.cz.
      operationId: deleteQuestionnaire
      responses:
        '204':
          description: Dotazník byl smazán
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /questionnaires/{slug}/publish:
    parameters:
      - name: slug
        in: path
        required: true
        description: Identifikátor dotazníku.
        schema:
          type: string
          pattern: '^[a-z0-9-]+$'
          example: spokojenost-2027
    post:
      tags: [Questionnaires]
      summary: Spuštění, prodloužení, obnovení nebo ukončení sběru
      description: |
        Uvede dotazník do sběru dat do zadaného data (`endDate`). Jedna metoda
        pokrývá všechny situace kolem běhu dotazníku:

        - **Publikace konceptu** (`draft` → `active`) – dotazník se před
          spuštěním **zvaliduje**; je-li nekonzistentní (např. po úpravách na
          webu), publikace selže a vrátí seznam chyb.
        - **Obnovení vyhodnoceného** (`finished` → `active`) – sběr se znovu
          spustí do nového data.
        - **Prodloužení / zkrácení** běžícího dotazníku – nastaví nové `endDate`.
        - **Ukončení sběru** – zadejte `endDate` na aktuální čas (nebo do
          minulosti); dotazník se přepne do vyhodnocení.

        `endDate` nesmí přesáhnout platnost vaší licence.
      operationId: publishQuestionnaire
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublishRequest'
      responses:
        '200':
          description: Sběr byl nastaven podle požadavku
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Questionnaire'
        '400':
          description: |
            Dotazník neprošel validací a nebyl publikován, nebo je `endDate`
            neplatné (mimo platnost licence).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: validation_error
                message: Dotazník nelze publikovat.
                details:
                  - 'Otázka č. 5 nemá vyplněný text.'
                  - 'Na otázku č. 7 se nelze při vyplňování dostat.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'
        '404':
          $ref: '#/components/responses/NotFound'

  /questionnaires/{slug}/unpublish:
    parameters:
      - name: slug
        in: path
        required: true
        description: Identifikátor dotazníku.
        schema:
          type: string
          pattern: '^[a-z0-9-]+$'
          example: spokojenost-2027
    post:
      tags: [Questionnaires]
      summary: Vrácení dotazníku do konceptu
      description: |
        Pozastaví sběr a vrátí dotazník do stavu **koncept** (`draft`). Během
        konceptu dotazník nikdo nevyplňuje. Uložené odpovědi zůstanou zachovány –
        chcete-li poté měnit strukturu (`PUT`), musíte je nejprve vynulovat.

        **Dočasné omezení:** účet smí mít jen jeden koncept, takže vrátit dotazník
        do konceptu nelze, pokud už jiný koncept (`draft`) existuje – API vrátí
        `409`. Uzamčený dotazník (`info.locked = true`) vrátit do konceptu nelze
        rovněž.
      operationId: unpublishQuestionnaire
      responses:
        '200':
          description: Dotazník byl vrácen do konceptu
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Questionnaire'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /questionnaires/{slug}/preview:
    parameters:
      - name: slug
        in: path
        required: true
        description: Identifikátor dotazníku.
        schema:
          type: string
          pattern: '^[a-z0-9-]+$'
          example: spokojenost-2027
    post:
      tags: [Questionnaires]
      summary: Dočasný náhled konceptu
      description: |
        Vytvoří dočasný odkaz, na kterém si můžete dotazník **prohlédnout a
        zkušebně vyplnit** – včetně vyzkoušení větvení. Koncept jinak na webu
        vyplnit nelze (návštěvník vidí jen informaci, že se dotazník upravuje).

        Funguje **pouze pro dotazník ve stavu koncept** (`draft`); u
        publikovaného dotazníku náhled nedává smysl (vrátí `409`).

        Odkaz je platný **15 minut** od vytvoření. Vyplnění v náhledu jsou pouze
        testovací – **nikam se neukládají** a nezapočítávají se do statistik.
      operationId: createPreview
      responses:
        '201':
          description: Náhledový odkaz byl vytvořen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preview'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /questionnaires/{slug}/responses:
    parameters:
      - name: slug
        in: path
        required: true
        description: Identifikátor dotazníku.
        schema:
          type: string
          pattern: '^[a-z0-9-]+$'
          example: spokojenost-2027
    get:
      tags: [Responses]
      summary: Export vyplněných dotazníků
      description: |
        Stáhne dokončená vyplnění dotazníku ve formátu **JSON Lines**
        (`application/x-ndjson`) – každý řádek je samostatný JSON.

        - **První řádek** je hlavička se strukturou dotazníku (číslo, text a typ
          každé otázky) – viz schéma `ResponsesHeader`.
        - **Každý další řádek** je jedno vyplnění – viz schéma `ResponseRecord`.

        Exportují se **všechna** uložená vyplnění; příznak `valid` u každého
        rozlišuje potvrzená od nepotvrzených/duplicitních. Odpovědi jsou uvedené
        textem a **přeskočené/nevyplněné otázky se neuvádějí** (viz `Answer`).
        Nahrané soubory se exportují jen jako metadata + odkaz ke stažení. IP
        adresy respondentů se neexportují.

        Data lze exportovat u dotazníků v libovolném stavu (koncept, probíhající
        sběr i ukončený).

        **Inkrementální stahování:** pomocí `afterId` a `limit` lze stahovat po
        částech – uložte si poslední stažené `responseId` a příště ho pošlete
        v `afterId`, čímž dostanete jen nová vyplnění. Vyplnění se vrací seřazená
        vzestupně podle `responseId`.
      operationId: exportResponses
      parameters:
        - name: afterId
          in: query
          required: false
          description: |
            Vrátí jen vyplnění s `responseId` **větším** než zadaná hodnota
            (`0` = všechna). Slouží k inkrementálnímu stahování jen dosud
            nestažených vyplnění.
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          required: false
          description: Maximální počet vrácených vyplnění (`0` = bez omezení).
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: |
            Proud vyplnění ve formátu JSON Lines. První řádek `ResponsesHeader`,
            následují řádky `ResponseRecord`.
          content:
            application/x-ndjson:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ResponsesHeader'
                  - $ref: '#/components/schemas/ResponseRecord'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Responses]
      summary: Vynulování odpovědí
      description: |
        Smaže všechna uložená vyplnění dotazníku. Používá se před přepsáním
        struktury dotazníku (`PUT`), když už nějaké odpovědi existují.

        **Přes API nelze provést** u uzamčeného dotazníku nebo u veřejného
        dotazníku se 100 a více odpověďmi (`409`); v takovém případě kontaktujte
        podpora@vyplnto.cz.
      operationId: deleteResponses
      responses:
        '204':
          description: Odpovědi byly vynulovány
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /questionnaires/{slug}/responses/{responseId}/files/{questionOrder}:
    parameters:
      - name: slug
        in: path
        required: true
        description: Identifikátor dotazníku.
        schema:
          type: string
          pattern: '^[a-z0-9-]+$'
          example: spokojenost-2027
      - name: responseId
        in: path
        required: true
        description: Číslo vyplnění (`ResponseRecord.responseId`).
        schema:
          type: integer
        example: 214
      - name: questionOrder
        in: path
        required: true
        description: Pořadí otázky typu `file_upload` (`questionOrder` z exportu).
        schema:
          type: integer
        example: 7
    get:
      tags: [Responses]
      summary: Stažení nahraného souboru
      description: |
        Stáhne soubor nahraný respondentem u otázky typu `file_upload`. Odkaz
        získáte z exportu (`Answer.file.downloadUrl`). Vyžaduje stejný API token
        jako zbytek API (soubor patří vašemu dotazníku).

        Soubor se vždy vrací jako **příloha** (`Content-Disposition: attachment`,
        `application/octet-stream`) – obsah nahrál respondent a Vyplňto.cz ho
        nijak nekontroluje, proto se nikdy nezobrazuje inline.
      operationId: downloadResponseFile
      responses:
        '200':
          description: Binární obsah souboru (jako příloha ke stažení).
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/LicenseExpired'
        '404':
          $ref: '#/components/responses/NotFound'

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'Osobní API token v hlavičce `Authorization: Bearer <token>`.'

  responses:
    Unauthorized:
      description: Chybějící nebo neplatný API token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: unauthorized
            message: Neplatný API token.
    LicenseExpired:
      description: |
        Token je platný, ale k účtu není aktivní licence opravňující k používání
        API (vyžadováno Vyplňto PREMIUM či vyšší). Celé API je nedostupné do
        obnovení licence.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: license_expired
            message: Platnost licence vypršela. Obnovte předplatné pro další používání API.
    NotFound:
      description: Dotazník nebyl nalezen (nebo k němu váš token nemá přístup).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: not_found
            message: Dotazník nenalezen.
    ValidationError:
      description: Vstupní data nejsou v pořádku.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: validation_error
            message: Dotazník obsahuje chyby.
            details:
              - 'Otázka č. 3: chybí text otázky.'
              - 'Veřejný dotazník musí mít vyplněné věkové cílení.'
    RateLimited:
      description: |
        Překročen limit. Rozlišují se dva případy podle pole `code`:
        - `rate_limited` – příliš mnoho požadavků za krátké okno (rate limit);
          platí pro kterékoli volání API,
        - `quota_exceeded` – vyčerpaná denní/měsíční kvóta na vytváření dotazníků
          (jen `POST /questionnaires`).

        V obou případech je uvedena hlavička `Retry-After`.
      headers:
        Retry-After:
          description: Počet sekund, po kterých lze požadavek zopakovat.
          schema:
            type: integer
          example: 30
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            rate_limited:
              value:
                code: rate_limited
                message: Byl překročen limit požadavků, zkuste to za chvíli znovu.
            quota_exceeded:
              value:
                code: quota_exceeded
                message: Byl vyčerpán denní limit dotazníků celkem přes API (20).
    Conflict:
      description: |
        Operaci nelze v aktuálním stavu dotazníku provést (např. přepsání
        dotazníku, který není konceptem nebo má odpovědi; smazání uzamčeného
        nebo veřejného dotazníku se 100+ odpověďmi). Co nelze vyřešit přes API,
        pomůže vyřešit podpora@vyplnto.cz.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: conflict
            message: Dotazník má uložené odpovědi – nejdřív je vynulujte.

  schemas:

    Error:
      type: object
      required: [code, message]
      description: Jednotný formát chybové odpovědi.
      properties:
        code:
          type: string
          description: Strojově čitelný kód chyby.
          examples: [unauthorized, not_found, validation_error, rate_limited]
        message:
          type: string
          description: Srozumitelný popis chyby.
        details:
          type: array
          description: Volitelný seznam konkrétních problémů (typicky u validace).
          items:
            type: string

    # =========================================================================
    #  ÚČET
    # =========================================================================
    Account:
      type: object
      description: Informace o účtu a licenci.
      properties:
        license:
          type: string
          description: Název aktuální licence.
          example: Vyplňto PREMIUM
        subscribedUntil:
          type: string
          description: Datum a čas, do kdy je licence předplacená.
          example: '2027-01-01 12:00:00'
        limits:
          $ref: '#/components/schemas/AccountLimits'

    LimitUsage:
      type: object
      description: |
        Limit s aktuálním čerpáním. `limit` je horní mez (`null` = neomezeno),
        `used` je počet už spotřebovaný v aktuálním okně.
      properties:
        limit:
          type: [integer, 'null']
          description: Horní mez (null = neomezeno).
          example: 20
        used:
          type: integer
          description: Aktuální čerpání v tomto okně.
          example: 3

    AccountLimits:
      type: object
      description: |
        Limity účtu pro API i s aktuálním čerpáním. Konkrétní hodnoty závisí na
        typu licence a případném individuálním navýšení.

        - **Rate limity** (`ratePerMinute`, `ratePerHour`) omezují počet
          požadavků za krátké okno; překročení vrací `429` (`rate_limited`).
        - **Kvóty na vytváření** (`publicPerDay`/`publicPerMonth`,
          `totalPerDay`/`totalPerMonth`) omezují počet dotazníků vložených přes
          API za den/měsíc; při vyčerpání `POST /questionnaires` vrátí `429`
          (`quota_exceeded`) s hlavičkou `Retry-After`.
        - **Strukturální limity** (`maxPayloadBytes`, `maxQuestions`,
          `maxAnswersPerQuestion`) jsou pevné stropy jednoho požadavku
          (bez „čerpání").
      properties:
        ratePerMinute:
          $ref: '#/components/schemas/LimitUsage'
        ratePerHour:
          $ref: '#/components/schemas/LimitUsage'
        publicPerDay:
          allOf: [$ref: '#/components/schemas/LimitUsage']
          description: Veřejné dotazníky vložené přes API za jeden den.
        publicPerMonth:
          allOf: [$ref: '#/components/schemas/LimitUsage']
          description: Veřejné dotazníky vložené přes API za jeden měsíc.
        totalPerDay:
          allOf: [$ref: '#/components/schemas/LimitUsage']
          description: Dotazníky celkem (veřejné i neveřejné) za jeden den.
        totalPerMonth:
          allOf: [$ref: '#/components/schemas/LimitUsage']
          description: Dotazníky celkem za jeden měsíc.
        maxPayloadBytes:
          type: integer
          description: Maximální velikost těla požadavku v bajtech.
          example: 20971520
        maxQuestions:
          type: integer
          description: Maximální počet otázek v jednom dotazníku.
          example: 500
        maxAnswersPerQuestion:
          type: integer
          description: Maximální počet odpovědí/řádků/sloupců na jednu otázku.
          example: 200

    # =========================================================================
    #  DOTAZNÍK – přehled v seznamu
    # =========================================================================
    QuestionnaireSummary:
      type: object
      description: Základní údaje o dotazníku pro výpis v seznamu.
      properties:
        slug:
          type: string
          example: spokojenost-2027
        title:
          type: string
          example: Spokojenost zákazníků 2027
        detail:
          type: string
          description: Odkaz na detail tohoto dotazníku v API (nikoli adresa pro vyplňování).
          example: https://www.vyplnto.cz/api/v1/questionnaires/spokojenost-2027
        status:
          $ref: '#/components/schemas/QuestionnaireStatus'
        startedAt:
          type: [string, 'null']
          description: Datum a čas začátku sběru dat.
          example: '2027-03-01 09:00:00'
        endDate:
          type: [string, 'null']
          description: Datum a čas ukončení sběru dat.
          example: '2027-03-31 23:59:59'
        responsesTotal:
          type: integer
          description: Celkový počet vyplnění (všechna uložená vyplnění bez ohledu na potvrzení).
          example: 214
        completionRate:
          type: [number, 'null']
          description: Úspěšnost dokončení dotazníku v procentech.
          example: 78.5

    # =========================================================================
    #  DOTAZNÍK – detail / vkládání
    # =========================================================================
    Questionnaire:
      type: object
      required: [title, questions]
      description: |
        Kompletní dotazník. Stejná struktura se používá při vkládání
        (`POST`) i ve čtení (`GET`); pole ve skupině `info` a `url` jsou pouze
        informativní a při vkládání se ignorují.
      # Veřejný dotazník musí mít vyplněné věkové cílení.
      if:
        properties:
          public:
            const: true
        required: [public]
      then:
        properties:
          targeting:
            required: [ageFrom, ageTo]
        required: [targeting]
      properties:
        slug:
          type: string
          pattern: '^[a-z0-9-]+$'
          description: |
            Identifikátor dotazníku a část jeho adresy
            `https://<slug>.vyplnto.cz/`. Při vkládání **nepovinné**. Uvedete-li
            ho, jde jen o **negarantované přání** – je-li obsazené (nebo jinak
            nevhodné), systém přidělí jiný slug. Když ho neuvedete, odvodí se
            z názvu dotazníku. Skutečný slug vždy najdete v odpovědi. Po vytvoření
            se už nemění.
          example: spokojenost-2027
        url:
          type: string
          readOnly: true
          description: Veřejná adresa dotazníku.
          example: https://spokojenost-2027.vyplnto.cz/
        title:
          type: string
          maxLength: 100
          description: Název dotazníku (bez formátování). Zobrazuje se respondentům i v přehledech.
          example: Spokojenost zákazníků 2027
        language:
          $ref: '#/components/schemas/Language'
        category:
          $ref: '#/components/schemas/Category'
        public:
          type: boolean
          default: false
          description: |
            Určuje viditelnost dotazníku a jeho výsledků:
            - `true` – dotazník je zařazen do veřejného katalogu a jeho
              **výsledky se zveřejní** v archivu pod licencí CC-BY 3.0. Veřejný
              dotazník musí mít vyplněné věkové cílení (`targeting.ageFrom`,
              `targeting.ageTo`).
            - `false` – dotazník je dostupný pouze na své adrese
              `https://<slug>.vyplnto.cz/` a výsledky vidíte jen vy. Cílení se
              u neveřejných dotazníků nepoužije.
        filterDuplicateIp:
          type: boolean
          default: false
          description: |
            Zapne odstranění opakovaných vyplnění ze stejné IP adresy – při
            vyhodnocení zůstane pouze první vyplnění z každé adresy. IP adresy
            slouží výhradně k tomuto účelu, nikde se nezobrazují ani neexportují.
        redirectUrl:
          type: [string, 'null']
          description: |
            Adresa (http/https), na kterou bude respondent přesměrován po
            odeslání dotazníku. Je-li vyplněná, **nezobrazí se děkovný text**
            (`texts.thankYou`) na Vyplňto.cz. Ponechte prázdné, pokud
            přesměrování nechcete.
          example: https://www.example.com/dekujeme
        endDate:
          type: [string, 'null']
          description: |
            Datum a čas ukončení sběru dat. Poté se dotazník automaticky uzavře
            a vyhodnotí. Nesmí přesáhnout platnost vaší licence. Sběr začíná
            ihned po vložení, začátek se proto neuvádí.
          example: '2027-03-31 23:59:59'

        texts:
          $ref: '#/components/schemas/QuestionnaireTexts'
        display:
          $ref: '#/components/schemas/QuestionnaireDisplay'
        targeting:
          $ref: '#/components/schemas/Targeting'
        notifications:
          $ref: '#/components/schemas/Notifications'
        branding:
          $ref: '#/components/schemas/Branding'

        questions:
          type: array
          minItems: 1
          description: Otázky dotazníku v pořadí zadaném polem `order`.
          items:
            $ref: '#/components/schemas/Question'

        info:
          $ref: '#/components/schemas/QuestionnaireInfo'

    PublishRequest:
      type: object
      required: [endDate]
      description: Parametry spuštění / změny sběru dat.
      properties:
        endDate:
          type: string
          description: |
            Datum a čas ukončení sběru dat (`YYYY-MM-DD HH:MM:SS`, čas v ČR).
            Pozdější datum sběr spustí nebo prodlouží (do mezí licence), datum na
            aktuální čas či do minulosti sběr ukončí. Nesmí přesáhnout platnost
            licence.
          example: '2027-04-30 23:59:59'

    Preview:
      type: object
      description: |
        Dočasný náhledový odkaz na koncept. Odkaz **není jednorázový** – během
        platnosti (15 minut) ho lze otevřít vícekrát a poslat i více lidem
        (kolegům, vedoucímu). Vyplnění v náhledu se nikam neukládají.
      properties:
        url:
          type: string
          description: Odkaz pro prohlédnutí a zkušební vyplnění konceptu.
          example: https://www.vyplnto.cz/databaze-dotazniku/spokojenost-2027/?apiPreview=9f3c1a…
        expiresAt:
          type: string
          description: Datum a čas, kdy odkaz přestane platit (15 minut od vytvoření).
          example: '2027-02-10 14:25:00'

    QuestionnaireTexts:
      type: object
      description: |
        Texty zobrazené respondentům kolem dotazníku. Mohou obsahovat prostý
        text i HTML (např. odkazy či zvýraznění). Obrázky vkládejte odkazem.
      properties:
        introduction:
          type: [string, 'null']
          description: Úvodní text na začátku dotazníku (přivítání, vysvětlení účelu).
        thankYou:
          type: [string, 'null']
          description: Text zobrazený po odeslání dotazníku (poděkování).
        help:
          type: [string, 'null']
          description: Nápověda k vyplňování dotazníku – zobrazí se v dotazníku nad první otázkou.
        header:
          type: [string, 'null']
          description: Záhlaví zobrazené nad dotazníkem na každé stránce.
        footer:
          type: [string, 'null']
          description: Zápatí zobrazené pod dotazníkem na každé stránce.

    QuestionnaireDisplay:
      type: object
      description: Nastavení vzhledu a chování dotazníku při vyplňování.
      properties:
        mode:
          type: string
          enum: [single_question, whole_form]
          default: whole_form
          description: |
            Způsob zobrazení otázek:
            - `whole_form` – celý dotazník najednou na jedné stránce (**výchozí**),
            - `single_question` – respondentovi se ukazuje jedna otázka po druhé.

            **Doporučujeme ponechat `whole_form`** pro pohodlnější vyplňování –
            u `single_question` musí respondent každou odpověď potvrdit, aby se
            dostal na další otázku.
        allowReturnToPrevious:
          type: boolean
          default: false
          description: |
            Umožnit respondentovi vrátit se k předchozí otázce. Uplatní se pouze
            při zobrazení `single_question`.
        allowDiscussion:
          type: boolean
          default: true
          description: Povolit veřejnou diskusi u dotazníku i u jeho výsledků.
        customCss:
          type: [string, 'null']
          description: |
            Vzhled dotazníku. Lze uvést odkaz na vlastní externí styl, nebo
            některý z předpřipravených stylů dostupných přímo na Vyplňto.cz:
            `/profi.css`, `/profi-bez.css`, `/moderni.css`,
            `/moderni-cislovany.css`, `/moderni-hnedy.css`, `/moderni-modry.css`,
            `/moderni-modry-wide.css`, `/moderni-ruzovy.css`,
            `/moderni-tmavemodry.css`, `/moderni-tmavemodry-cislovany.css`,
            `/moderni-tmavemodry-cislovany-2.css`, `/moderni-tmavemodry-nocaps.css`,
            `/moderni-tucne.css`.

    Targeting:
      type: object
      description: |
        Cílová skupina dotazníku – komu je dotazník určen. U **veřejných**
        dotazníků je tento objekt povinný a musí obsahovat alespoň věkové
        rozmezí (`ageFrom`, `ageTo`); rozsah 13–68 znamená fakticky „bez
        omezení", ale musí být zvolen. U neveřejných dotazníků se cílení
        nepoužije.
      properties:
        location:
          $ref: '#/components/schemas/TargetLocation'
        gender:
          type: string
          enum: [any, female, male]
          default: any
          description: 'Pohlaví: `any` bez omezení, `female` pouze ženy, `male` pouze muži.'
        ageFrom:
          type: [integer, 'null']
          minimum: 13
          maximum: 68
          description: 'Dolní hranice věku (13–68; `13` znamená „13 a méně").'
        ageTo:
          type: [integer, 'null']
          minimum: 13
          maximum: 68
          description: 'Horní hranice věku (13–68; `68` znamená „68 a více"). Musí být ≥ `ageFrom`.'
        group:
          type: string
          enum: [any, parents, students, employees, entrepreneurs]
          default: any
          description: |
            Skupina respondentů: `any` bez omezení, `parents` rodiče (alespoň
            jedno dítě), `students` studenti, `employees` zaměstnanci,
            `entrepreneurs` podnikatelé / OSVČ.
        note:
          type: [string, 'null']
          maxLength: 255
          description: Krátké doplňující upřesnění cílové skupiny, např. „jen pro matky".

    Notifications:
      type: object
      description: |
        E-mailové adresy, na které se mají zasílat pravidelné přehledy s exportem
        vyplnění. Ponechte prázdné, pokud přehledy nechcete.
      properties:
        dailyReportEmail:
          type: [string, 'null']
          format: email
          description: Adresa pro denní přehled vyplnění.
        weeklyReportEmail:
          type: [string, 'null']
          format: email
          description: Adresa pro týdenní přehled vyplnění.

    Branding:
      type: object
      description: Možnosti dostupné v placených licencích.
      properties:
        showAds:
          type: boolean
          default: true
          description: Zobrazovat u dotazníku upoutávky na jiné dotazníky.
        chartShowSource:
          type: boolean
          default: true
          description: Uvádět u grafů výsledků, že byly vytvořeny na Vyplňto.cz.

    QuestionnaireInfo:
      type: object
      readOnly: true
      description: Aktuální stav a statistiky dotazníku (pouze pro čtení).
      properties:
        status:
          $ref: '#/components/schemas/QuestionnaireStatus'
        responsesTotal:
          type: integer
          description: Počet vyplnění celkem (může obsahovat i duplicity, které se při vyhodnocení odstraní).
        responsesValid:
          type: integer
          description: Počet platných vyplnění po odstranění duplicit (k dispozici zejména po vyhodnocení).
        completionRate:
          type: [number, 'null']
          description: Úspěšnost dokončení dotazníku v procentech.
        promotionApproved:
          type: string
          enum: [approved, rejected, undecided]
          description: |
            Stav schválení bezplatné propagace dotazníku administrátorem Vyplňto.cz:
            `approved` schváleno, `rejected` neschváleno, `undecided` zatím neposouzeno.
        locked:
          type: boolean
          description: Dotazník je uzamčen proti úpravám (např. u schválené placené propagace).

    QuestionnaireStatus:
      type: string
      enum: [draft, active, finished]
      description: |
        Stav dotazníku: `draft` koncept (nikdo ho zatím nevyplňuje),
        `active` sbírá odpovědi, `finished` ukončený a vyhodnocený.
        Smazané dotazníky API nevrací (jejich detail skončí chybou `404`).

    # =========================================================================
    #  ČÍSELNÍKY DOTAZNÍKU
    # =========================================================================
    Language:
      type: string
      default: cs
      description: |
        Jazyk rozhraní dotazníku (kód ISO 639-1). Podporované jazyky:
        `cs` česky, `sk` slovensky, `en` anglicky, `ru` rusky, `de` německy,
        `pl` polsky, `vi` vietnamsky, `ja` japonsky, `bg` bulharsky,
        `uk` ukrajinsky, `hu` maďarsky, `es` španělsky, `zh` čínsky,
        `ko` korejsky, `lt` litevsky, `sv` švédsky, `fr` francouzsky, `sl` slovinsky.
      enum:
        [cs, sk, en, ru, de, pl, vi, ja, bg, uk, hu, es, zh, ko, lt, sv, fr, sl]

    Category:
      type: string
      default: uncategorized
      description: |
        Tematická kategorie dotazníku:
        `uncategorized` nezařazeno, `business_economy` business & ekonomie,
        `transport` doprava, `philosophy_religion` filozofie & náboženství,
        `food_drink` jídlo & pití, `culture_events` kultura & akce,
        `trade_marketing` obchod & marketing,
        `politics_public_sector` politika & veřejný sektor,
        `industry_agriculture` průmysl & zemědělství,
        `psychology_sociology` psychologie & sociologie, `services` služby,
        `technology_it` technologie & IT, `relationships_sex` vztahy & sex,
        `entertainment_leisure` zábava & volný čas,
        `health_lifestyle` zdraví & životní styl.
      enum:
        - uncategorized
        - business_economy
        - transport
        - philosophy_religion
        - food_drink
        - culture_events
        - trade_marketing
        - politics_public_sector
        - industry_agriculture
        - psychology_sociology
        - services
        - technology_it
        - relationships_sex
        - entertainment_leisure
        - health_lifestyle

    TargetLocation:
      type: string
      default: any
      description: |
        Cílová lokalita respondentů:
        `any` bez omezení, `cz` ČR, `sk` SR, dále města `praha`, `brno`,
        `ostrava`, `plzen`, `liberec`, `olomouc` a kraje `jihocesky`,
        `jihomoravsky`, `karlovarsky`, `vysocina`, `liberecky`,
        `moravskoslezsky`, `olomoucky`, `pardubicky`, `plzensky`, `stredocesky`,
        `ustecky`, `zlinsky`, `kralovehradecky`.
      enum:
        - any
        - cz
        - sk
        - praha
        - brno
        - ostrava
        - plzen
        - liberec
        - olomouc
        - jihocesky
        - jihomoravsky
        - karlovarsky
        - vysocina
        - liberecky
        - moravskoslezsky
        - olomoucky
        - pardubicky
        - plzensky
        - stredocesky
        - ustecky
        - zlinsky
        - kralovehradecky

    # =========================================================================
    #  OTÁZKA
    # =========================================================================
    Question:
      type: object
      required: [type, text, order]
      description: |
        Jedna otázka dotazníku. Podle typu otázky (`type`) se doplňují další
        pole – u výběrových otázek `choices`, u maticových `rows` a `columns`,
        u sémantického diferenciálu `semanticDifferential`.
      properties:
        order:
          type: integer
          minimum: 1
          description: |
            Pořadí otázky v dotazníku, jedinečné v rámci dotazníku. Zároveň
            slouží jako odkaz při větvení (`nextQuestionOrder`).
        type:
          $ref: '#/components/schemas/QuestionType'
        text:
          type: string
          maxLength: 1000
          description: Znění otázky (prostý text bez formátování).
          example: Jak jste spokojeni s našimi službami?
        required:
          type: boolean
          default: false
          description: Zda musí respondent otázku zodpovědět.
        nextQuestionOrder:
          type: integer
          minimum: 0
          description: |
            Kam pokračovat po zodpovězení této otázky (větvení):
            - **vynecháno** – pokračuje se následující otázkou v pořadí,
            - **číslo** – přeskočí se na otázku s tímto pořadím (`order`),
            - **`0`** – dotazník zde končí.

            U rozdělujících otázek (`single_choice_branching`) se pokračování
            určuje u jednotlivých odpovědí (`choices[].nextQuestionOrder`) a toto
            pole se neuplatní.
        texts:
          $ref: '#/components/schemas/QuestionTexts'
        settings:
          $ref: '#/components/schemas/QuestionSettings'
        choices:
          type: array
          description: |
            Nabízené odpovědi u výběrových otázek (typy `single_*`, `multiple_*`,
            `ranking`). Pořadí je dané pořadím v seznamu.
          items:
            $ref: '#/components/schemas/Choice'
        rows:
          type: array
          description: |
            Řádky (podotázky) u maticových otázek. Pořadí je dané pořadím
            v seznamu. U `matrix_numbers` respondent ke každému řádku vyplní
            celočíselnou hodnotu.
          items:
            $ref: '#/components/schemas/MatrixRow'
        columns:
          type: array
          description: |
            Sloupce matice – lze definovat **pouze u typu `matrix_free`** (vlastní
            sloupce). Např. řádky „káva", „čaj", „rum" a sloupce „nepiju",
            „občas si dám", „piju denně". Ostatní maticové typy mají sloupce dané
            svým typem. Pořadí je dané pořadím v seznamu.
          items:
            $ref: '#/components/schemas/MatrixColumn'
        semanticDifferential:
          type: array
          description: |
            Řádky sémantického diferenciálu (typ `semantic_differential`). Každý
            řádek má vlastní dvojici protikladných pólů a vlastní škálu; škály se
            mezi řádky mohou lišit. Pořadí je dané pořadím v seznamu.
          items:
            $ref: '#/components/schemas/SemanticDifferential'

    QuestionTexts:
      type: object
      description: |
        Doprovodné texty kolem otázky (prostý text i HTML). Zobrazují se
        v tomto pořadí: `heading` (nad rámečkem otázky, může uvozovat i skupinu
        otázek) → `before` (nad zněním otázky) → znění otázky → `after` (pod
        zněním, před polem odpovědi) → pole odpovědi → `footer` (v zápatí otázky).
      properties:
        heading:
          type: [string, 'null']
          description: Nadpis nebo uvození nad otázkou (mimo rámeček otázky).
        before:
          type: [string, 'null']
          description: Text nad zněním otázky.
        after:
          type: [string, 'null']
          description: Text pod zněním otázky, před polem pro odpověď.
        footer:
          type: [string, 'null']
          description: Text v zápatí otázky.

    QuestionSettings:
      type: object
      description: Doplňková nastavení otázky. Většina se týká jen některých typů otázek.
      properties:
        customNumber:
          type: [string, 'null']
          maxLength: 10
          description: |
            Vlastní označení otázky místo automatického pořadového čísla, např.
            „A)". Použije se pouze v dotazníku zobrazeném respondentům; v exportu
            dat je otázka vždy pod svým pořadovým číslem. Ponechte prázdné pro
            automatické číslování.
        private:
          type: boolean
          default: false
          description: |
            Neveřejná otázka – u veřejného dotazníku se její odpovědi nezveřejní
            ve výsledcích (vhodné pro citlivé údaje, např. e-mail).
        shuffleOptions:
          type: boolean
          default: false
          description: |
            Zobrazit nabízené odpovědi (u maticových otázek řádky) v náhodném
            pořadí – omezuje vliv pořadí na odpovědi respondentů.
        autoRandomAnswer:
          type: boolean
          default: false
          description: |
            Pouze pro rozdělující otázky (`single_choice_branching`). Otázka se
            respondentovi nezobrazí a zodpoví se automaticky náhodně – slouží
            k náhodnému rozdělení respondentů do skupin s různými větvemi.
        unit:
          type: [string, 'null']
          description: Jednotka u číselných otázek (např. „Kč", „let").
        customAnswerLabel:
          type: [string, 'null']
          maxLength: 255
          description: Popisek pole pro vlastní odpověď u polouzavřených otázek.
        minAnswers:
          type: integer
          minimum: 0
          default: 0
          description: |
            Nejmenší počet odpovědí, které musí respondent zvolit/vyplnit.
            Platí pro `multiple_choice`, `multiple_choice_semi_open` a
            `text_short_multiple`. U povinné otázky se hodnota menší než 1
            ignoruje (vyžaduje se alespoň jedna odpověď).
        maxAnswers:
          type: integer
          minimum: 0
          default: 0
          description: 'Největší počet odpovědí. Stejné typy jako `minAnswers`. `0` = bez omezení.'

    Choice:
      type: object
      required: [text]
      description: Nabízená odpověď. Pořadí je dané pořadím v seznamu `choices`.
      properties:
        text:
          type: string
          maxLength: 255
          description: |
            Text odpovědi (prostý text). U typů `text_short_multiple` a `ranking`
            slouží jako popisek jednotlivého pole / položky.
        nextQuestionOrder:
          type: integer
          minimum: 0
          description: |
            Pouze u rozdělujících otázek (`single_choice_branching`): kam
            pokračovat, zvolí-li respondent tuto odpověď. Číslo = pořadí otázky,
            `0` = konec dotazníku.
        image:
          type: [string, 'null']
          contentEncoding: base64
          description: |
            Obrázek u odpovědi (pouze typ `single_choice_media`). JPG nebo PNG
            v kódování base64, **nejvýše 50 KB**.

    MatrixRow:
      type: object
      required: [text]
      description: Řádek (podotázka) maticové otázky. Pořadí je dané pořadím v seznamu `rows`.
      properties:
        text:
          type: string
          maxLength: 255
          description: Text řádku / podotázky (prostý text).

    MatrixColumn:
      type: object
      required: [text]
      description: Sloupec (možnost) maticové otázky. Pořadí je dané pořadím v seznamu `columns`.
      properties:
        text:
          type: string
          maxLength: 255
          description: Popisek sloupce (prostý text).

    SemanticDifferential:
      type: object
      required: [leftLabel, rightLabel]
      description: Jeden řádek sémantického diferenciálu – dvojice protikladných pólů a škála mezi nimi.
      properties:
        leftLabel:
          type: string
          maxLength: 255
          description: Levý pól (např. „studený").
        rightLabel:
          type: string
          maxLength: 255
          description: Pravý pól (např. „teplý").
        scale:
          type: string
          default: alternative
          description: |
            Škála mezi póly; body se přiřazují postupně zleva (`leftLabel`)
            doprava (`rightLabel`):
            - `alternative` – volba jen mezi levou a pravou stranou (−1 / 1),
            - `symmetric_3` – tři body: −1, 0, 1,
            - `symmetric_5` – pět bodů: −2, −1, 0, 1, 2,
            - `linear_5` – pět bodů: 1 až 5,
            - `linear_7` – sedm bodů: 1 až 7.
          enum: [alternative, symmetric_3, symmetric_5, linear_5, linear_7]

    QuestionType:
      type: string
      description: |
        Typ otázky určuje, jak respondent odpovídá a jaká další pole otázka
        používá:

        **Výběr z možností** (pole `choices`):
        - `single_choice` – seznam, právě jedna odpověď,
        - `single_choice_branching` – rozdělující otázka: podle zvolené odpovědi
          se větví další průchod (viz `choices[].nextQuestionOrder`),
        - `single_choice_semi_open` – polouzavřená: možnosti + vlastní odpověď,
        - `single_choice_media` – možnosti s obrázky,
        - `multiple_choice` – seznam, alespoň jedna odpověď,
        - `multiple_choice_semi_open` – polouzavřená s výběrem více odpovědí,
        - `ranking` – seřazení nabízených možností podle preference.

        **Text a čísla:**
        - `text_short` – krátký text,
        - `text_short_multiple` – několik krátkých textových polí,
        - `text_long` – delší text,
        - `number` – číslo, `number_nonnegative` – nezáporné, `number_positive` – kladné.

        **Hotové škály a baterie** (bez dalších polí):
        - `yes_no`, `yes_unsure_no`,
        - `agree_unsure_disagree`, `agree_5point`,
        - `grade_verbal` – slovní známkování (výborný … nedostatečný),
        - `scale_1_5`, `scale_1_10`.

        **Sémantický diferenciál** (pole `semanticDifferential`):
        - `semantic_differential`.

        **Maticové otázky** (pole `rows`, u `matrix_free` i `columns`):
        - `matrix_1_4`, `matrix_1_5`, `matrix_1_7`,
        - `matrix_0_5`, `matrix_0_6`, `matrix_plusminus` (++/+/0/−/−−),
        - `matrix_numbers` – ke každému řádku respondent vyplní celočíselnou hodnotu,
        - `matrix_free` – volná matice s vlastními sloupci (`columns`), např.
          řádky „káva"/„čaj" a sloupce „nepiju"/„občas"/„denně".

        **Ostatní:**
        - `file_upload` – nahrání souboru.
      enum:
        - single_choice
        - single_choice_branching
        - single_choice_semi_open
        - single_choice_media
        - multiple_choice
        - multiple_choice_semi_open
        - ranking
        - text_short
        - text_short_multiple
        - text_long
        - number
        - number_nonnegative
        - number_positive
        - yes_no
        - yes_unsure_no
        - agree_unsure_disagree
        - agree_5point
        - grade_verbal
        - scale_1_5
        - scale_1_10
        - semantic_differential
        - matrix_1_4
        - matrix_1_5
        - matrix_1_7
        - matrix_0_5
        - matrix_0_6
        - matrix_plusminus
        - matrix_numbers
        - matrix_free
        - file_upload

    # =========================================================================
    #  EXPORT VYPLNĚNÍ (JSON Lines)
    # =========================================================================
    ResponsesHeader:
      type: object
      description: |
        První řádek exportu – hlavička se strukturou dotazníku. Umožňuje
        spárovat odpovědi (podle `questionOrder`) s otázkami.
      properties:
        slug:
          type: string
          example: spokojenost-2027
        title:
          type: string
        questions:
          type: array
          description: Přehled otázek dotazníku.
          items:
            type: object
            properties:
              order:
                type: integer
                description: Pořadí otázky (odkazuje na něj `answers[].questionOrder`).
              text:
                type: string
                description: Znění otázky.
              type:
                $ref: '#/components/schemas/QuestionType'

    ResponseRecord:
      type: object
      description: Jedno dokončené vyplnění dotazníku (jeden řádek exportu).
      properties:
        responseId:
          type: integer
          description: Jedinečné číslo vyplnění.
        valid:
          type: boolean
          description: |
            Platné vyplnění (`potvrzeno = ano`). Nepotvrzená/nedokončená a duplicitní
            vyplnění mají `false`; při vyhodnocení dotazníku se přesunou stranou.
        submittedAt:
          type: string
          description: Datum a čas odeslání dotazníku (ISO 8601 s časovým posunem).
          example: '2027-03-01T10:04:30+01:00'
        durationSeconds:
          type: integer
          description: Doba vyplňování v sekundách.
        queryParameters:
          type: [object, 'null']
          additionalProperties:
            type: string
          description: |
            Parametry z odkazu, kterým respondent na dotazník přišel (obsah
            query stringu za „?"), rozdělené na dvojice klíč–hodnota. Užitečné
            pro rozlišení zdrojů/kampaní. `null`, pokud odkaz žádné neměl.
          example:
            utm_source: facebook
            kampan: jaro
        answers:
          type: array
          description: |
            Odpovědi na zodpovězené otázky. **Přeskočené a nevyplněné otázky
            (i jednotlivé nevyplněné řádky matic, diferenciálů, rankingu či
            více­textových otázek) se neuvádějí** – v poli jsou jen skutečné
            odpovědi. Spárování s otázkou přes `questionOrder`.
          items:
            $ref: '#/components/schemas/Answer'

    Answer:
      type: object
      description: |
        Odpověď na jednu **zodpovězenou** otázku (přeskočené se v exportu
        neuvádějí, viz `ResponseRecord.answers`). Kromě `questionOrder` a `type`
        je přítomné vždy jen jedno pole podle typu otázky:
        - výběr jedné možnosti / škály → `selected` (text zvolené odpovědi),
        - výběr více možností → `selectedMultiple` (pole textů),
        - seřazení (`ranking`) → `ranking` (objekt „text možnosti → pořadí";
          nepovinné a neseřazené možnosti chybí),
        - text → `text`, číslo → `number`,
        - více­textová otázka (`text_short_multiple`) → `subAnswers`
          (text pro každou vyplněnou podotázku),
        - maticová otázka → `matrix` (hodnota pro každý vyplněný řádek),
        - sémantický diferenciál → `semanticDifferential` (pro každý vyplněný řádek),
        - nahrání souboru (`file_upload`) → `file` (název, velikost a odkaz ke stažení),
        - neznámý/nepodporovaný typ → `unsupported` (bezpečný fallback).
      properties:
        questionOrder:
          type: integer
          description: Pořadí otázky (odkaz do hlavičky exportu).
        type:
          $ref: '#/components/schemas/QuestionType'
        selected:
          type: string
          description: Text zvolené odpovědi (u otázek s jednou odpovědí/škálou).
        selectedMultiple:
          type: array
          items:
            type: string
          description: Texty zvolených odpovědí (u otázek s více odpověďmi).
        ranking:
          type: object
          additionalProperties:
            type: integer
          description: |
            Seřazení: klíč = text možnosti, hodnota = pořadí zvolené respondentem
            (1 = první). U nepovinné otázky nemusí být seřazené všechny možnosti –
            neseřazené v objektu chybí.
          example:
            R1: 1
            R3: 2
        text:
          type: string
          description: Textová odpověď.
        number:
          type: number
          description: Číselná odpověď.
        subAnswers:
          type: array
          description: Vyplněné podotázky u víc­textové otázky (`text_short_multiple`).
          items:
            type: object
            properties:
              subQuestion:
                type: string
                description: Popisek podotázky (chybí-li, uvede se „odpověď <pořadí>").
              text:
                type: string
                description: Zadaný text.
        matrix:
          type: array
          description: Odpovědi po vyplněných řádcích maticové otázky.
          items:
            type: object
            properties:
              row:
                type: string
                description: Text řádku (podotázky).
              value:
                description: Zvolená hodnota – číslo (škála) nebo text sloupce (`matrix_free`).
        semanticDifferential:
          type: array
          description: Odpovědi po vyplněných řádcích sémantického diferenciálu.
          items:
            type: object
            properties:
              left:
                type: string
              right:
                type: string
              scale:
                type: string
                description: Škála řádku (`alternative`, `symmetric_3`, `symmetric_5`, `linear_5`, `linear_7`).
              value:
                type: number
                description: Zvolený bod na škále.
        file:
          type: object
          description: |
            Nahraný soubor u otázky typu `file_upload`. Obsah souboru se
            **neexportuje inline** – stáhne se přes `downloadUrl` (viz endpoint
            *Stažení nahraného souboru*), autentizovaně stejným API tokenem.
          properties:
            filename:
              type: string
              description: Původní název souboru (od respondenta).
            sizeBytes:
              type: integer
              description: Velikost souboru v bajtech.
            downloadUrl:
              type: string
              description: Absolutní odkaz ke stažení souboru (vyžaduje API token).
              example: https://www.vyplnto.cz/api/v1/questionnaires/spokojenost-2027/responses/214/files/7
        unsupported:
          type: object
          description: Fallback pro neznámý/nepodporovaný typ otázky (dopředná kompatibilita).
          properties:
            message:
              type: string
              example: Nepodporovaný typ otázky v exportu.
            values:
              type: array
              items:
                type: string
              description: Syrové uložené hodnoty odpovědi.
