openapi: 3.0.3
info:
  title: Freelo API
  description: |
    API documentation for [Freelo.io](https://www.freelo.io) - Project Management Tool.
    
    ## Request Format
    All requests point to URL that begins with `https://api.freelo.io/v1/`.
    Each request must include a `User-Agent` HTTP header.
    
    ### Example: Projects List
    ```bash
    curl -u your@email.tld:API_KEY -H 'User-Agent: Your_App_Id (email@company.tld)' \
        https://api.freelo.io/v1/projects
    ```
    
    ## Response Format
    The API only supports JSON format in UTF-8 encoding.
    
    ## Authentication
    Authentication is done using HTTP Basic Authentication. Use your email as username and API key as password.
    You can find the API key in [settings](https://app.freelo.io/profil/nastaveni).
    
    ## Rate Limiting
    The API enforces per-user rate limits. Because these limits may change over time, clients should read them from the response headers rather than hardcoding values.

    At a minimum, clients must honor the `Retry-After` header, which alone is sufficient for correct use of the API. When a client exceeds the limit, the API responds with `429 Too Many Requests` and a `Retry-After: <seconds>` header indicating the number of seconds to wait before the next request is accepted. Clients should wait at least that long before retrying and apply exponential backoff if 429 responses persist.

    The IETF `RateLimit` headers (structured fields) are informational and do not constitute a service level agreement. The server may reject a request even when remaining quota is advertised, and clients should not depend on these headers being available indefinitely. `Retry-After` and exponential backoff should therefore be treated as the authoritative fallback. When present, the headers may be used to pace requests and reduce the likelihood of 429 responses:
    - `RateLimit-Policy: "default";q=60;w=20` — the active policy, permitting `q` requests per `w` seconds.
    - `RateLimit: "default";r=42;t=5` — the current state, with `r` requests remaining and `t` seconds until the full quota is restored.
    
    ## Currencies Format
    All currency amounts must be strings with exactly 2 decimal places multiplied by 100.
    
    Supported currencies: CZK, EUR, USD
    
    Examples:
    - `1,000.25` = `"100025"`
    - `1,000` = `"100000"`
    - `1` = `"100"`
    
    ## Pagination
    Pages are set via GET parameter `p` starting from 0.
    
    ## Ordering
    Use `order_by` and `order` (asc/desc) parameters where supported.

    ## Timestamp Format
    All response and request fields with `format: date-time` are returned and accepted as
    **naive ISO8601 strings without a timezone designator** — for example `2026-04-24T11:12:38`.
    Values represent **Europe/Prague local time** (CET / CEST, observing DST). This is **not
    RFC3339-compliant**, so strict parsers (e.g. Go's `time.Parse(time.RFC3339, ...)`) will fail.

    Recommended client handling:

    1. Parse with a permissive layout (Go: `"2006-01-02T15:04:05"`, Python: `datetime.fromisoformat`).
    2. Attach the `Europe/Prague` location explicitly (e.g. Go: `time.LoadLocation("Europe/Prague")`).
    3. Convert to UTC or the desired zone afterwards.

    Send request `date-time` values in the same naive format and timezone.

  version: "1.0.0"
  contact:
    name: Freelo Support
    url: https://www.freelo.io

servers:
  - url: "https://api.freelo.io/v1"
    description: "Production server"

security:
  - basicAuth: []

tags:
  - name: Projects
    description: Project management operations
  - name: Project Labels
    description: Project label management
  - name: Pinned Items
    description: Pinned items in projects
  - name: Tasklists
    description: Tasklist operations
  - name: Tasks
    description: Task management
  - name: Subtasks
    description: Subtask operations
  - name: Task Labels
    description: Task label management
  - name: Comments
    description: Comment operations
  - name: Time Tracking
    description: Time tracking operations
  - name: Work Reports
    description: Work report management
  - name: Invoicing
    description: Invoice operations
  - name: Users
    description: User management
  - name: Notifications
    description: Notification operations
  - name: Events
    description: Event operations
  - name: Files
    description: File operations
  - name: States
    description: State definitions
  - name: Custom Fields
    description: Custom field management
  - name: Notes
    description: Note operations
  - name: Search
    description: Search operations
paths:
  # ==================== USERS ====================
  /users/me:
    get:
      tags:
        - Users
      summary: Authentication health check
      description: |
        Verifies that the provided credentials are valid.
        Returns 200 with the authenticated user's information.
        Returns 401 when credentials are invalid or missing.
      operationId: getUsersMe
      responses:
        "200":
          description: Authentication is valid
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    type: string
                    example: success
                  user:
                    type: object
                    properties:
                      id:
                        type: integer
                        description: ID of the authenticated user
                        example: 12345
                      email:
                        type: string
                        format: email
                        description: Email address of the authenticated user
                        example: jane.doe@example.com
                      fullname:
                        type: string
                        description: Full name of the authenticated user (first name + last name, or email if name is empty).
                        example: Jane Doe
                      mention_key:
                        type: string
                        description: |
                          Normalized form of the authenticated user's `fullname` (whitespace stripped, diacritics removed) used in comments as the visible text after `@` inside a mention span. Build a mention with:
                          `<span data-freelo-mention="1" data-freelo-user-id="{id}">@{mention_key}</span>`
                        example: JaneDoe
                    required:
                      - id
                      - email
                      - fullname
                      - mention_key
                required:
                  - result
                  - user
        "401":
          description: Invalid or missing credentials
          content:
            application/json:
              schema:
                type: object
                properties:
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        message:
                          type: string

  # ==================== PROJECTS ====================
  /projects:
    get:
      tags:
        - Projects
      summary: Get own active projects
      description: |
        Returns active projects **owned** by the authenticated user (not projects they were only invited to), each with its active tasklists eagerly loaded.

        **Use cases:**
        - Primary project picker in dashboards / onboarding — show projects the user owns and manages
        - Populating a "my projects" widget without paging (this endpoint is **not** paginated)
        - Bulk tooling that needs owner-scoped projects before branching into tasklists

        **Behavior notes:**
        - Scope is owner-only. Projects where the user is only a worker / guest are **not** returned — use `GET /all-projects` or `GET /invited-projects` for those.
        - Filters only projects in the **active** state (state_id=1). Archived / template projects are excluded — use `/archived-projects` or `/template-projects`.
        - Response is a flat array (not paginated). For large accounts consider `GET /all-projects` with paging.
        - Tasklists embedded in each project also include only active tasklists.
      operationId: getProjects
      parameters:
        - name: order_by
          in: query
          description: Order column
          schema:
            type: string
            enum: [name, date_add, date_edited_at]
            default: name
        - name: order
          in: query
          description: Order direction
          schema:
            type: string
            enum: [asc, desc]
            default: asc
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ProjectWithTasklists'
    post:
      tags:
        - Projects
      summary: Create project
      description: |
        Creates a new active project. The authenticated user becomes the project **author** automatically. If `project_owner_id` is omitted, the author is also the owner; otherwise the referenced user must already exist.

        **Use cases:**
        - Provisioning a new project from an integration (e.g. after a deal closes in CRM)
        - Bulk-creating projects when onboarding a client
        - Delegating ownership: the API caller creates the project and immediately assigns a different owner via `project_owner_id`

        **Side effects:**
        - Business-account captains are auto-invited as commanders/workers (via `BusinessAccountCaptainProjectInviter`).
        - Emits `project_owner_assigner` and `project_commander_promote` events (webhooks, notifications).
        - If `project_owner_id` does not map to an owner-eligible user, the request fails with `400` (`project_owner_id X is not valid`) — the business rule is enforced by `IProjectCreator`.
      operationId: createProject
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - currency_iso
              properties:
                name:
                  type: string
                  example: "Vegetable growing"
                currency_iso:
                  type: string
                  enum: [CZK, EUR, USD]
                  description: Currency used for budgets and invoicing in this project. Cannot be changed afterwards.
                project_owner_id:
                  type: integer
                  description: |
                    ID of user assigned as owner. Must be an owner-eligible user in the caller's account.
                    If omitted, the authenticated caller becomes the owner.
      responses:
        '200':
          description: Project created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectBasic'

  /all-projects:
    get:
      tags:
        - Projects
      summary: Get all accessible projects (owned + invited)
      description: |
        Paginated collection of all projects the authenticated user can see — both owned and those they were invited to, filtered by state and tag.

        **Use cases:**
        - Primary project list for end-user UI where both owned and invited projects are shown
        - Backfills / syncing to external systems that need every project regardless of ownership
        - Narrow search by state (active / archived / template) combined with tag and owner filters

        **Behavior notes:**
        - Pagination is **required** for large accounts — use `p` (or its alias `page`); page size is fixed server-side.
        - `states_ids[]` accepts any combination of `1=active`, `2=archived`, `3=template`. When omitted, the server applies a default (typically active only) — always pass it explicitly if you need archived/templates.
        - `tags[]` matches any of the specified tags; pass the literal string `"without"` to get projects **without** any tag (this is a magic value, not a real tag name).
        - `users_ids[]` filters by project **owner** only, not by workers.
        - Default ordering is `date_add asc`.
      operationId: getAllProjects
      parameters:
        - name: order_by
          in: query
          schema:
            type: string
            enum: [name, date_add, date_edited_at]
            default: date_add
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
        - name: tags[]
          in: query
          description: Filter by tags. Use "without" to get projects without tags.
          schema:
            type: array
            items:
              type: string
        - name: states_ids[]
          in: query
          description: "Project states: 1=active, 2=archived, 3=template"
          schema:
            type: array
            items:
              type: integer
              enum: [1, 2, 3]
        - name: users_ids[]
          in: query
          description: Filter by project owner IDs
          schema:
            type: array
            items:
              type: integer
        - name: created_in_range[date_from]
          in: query
          schema:
            type: string
            format: date
        - name: created_in_range[date_to]
          in: query
          schema:
            type: string
            format: date
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          projects:
                            type: array
                            items:
                              $ref: '#/components/schemas/ProjectFull'

  /invited-projects:
    get:
      tags:
        - Projects
      summary: Get projects where I am a worker (not owner)
      description: |
        Paginated list of active projects where the authenticated user is a worker — i.e. they were **invited** to the project, not its owner. Each project includes its active tasklists.

        **Use cases:**
        - "Projects shared with me" view, separating them from my own projects
        - Freelancer dashboards that show only client work
        - Cross-account collaboration views

        **Behavior notes:**
        - Returns only **active** projects. Archived invited projects do not appear here.
        - Ordering and filtering parameters are **not** accepted — only pagination.
      operationId: getInvitedProjects
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          invited_projects:
                            type: array
                            items:
                              $ref: '#/components/schemas/ProjectWithTasklists'

  /archived-projects:
    get:
      tags:
        - Projects
      summary: Get archived projects
      description: |
        Paginated list of projects the authenticated user can see that are in the **archived** state.

        **Use cases:**
        - Archive browser UI (read-only view of finished work)
        - Exporting historical project data for reporting / accounting
        - Restoring an archived project — find its ID here, then call `POST /project/{id}/activate`

        **Behavior notes:**
        - Includes both owned and invited archived projects.
        - Each project comes with its tasklists embedded (active and archived tasklists alike — the project's archived state does not restrict the embedded tasklists).
        - Only pagination parameters are accepted.
      operationId: getArchivedProjects
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          archived_projects:
                            type: array
                            items:
                              $ref: '#/components/schemas/ProjectWithTasklists'

  /template-projects:
    get:
      tags:
        - Projects
      summary: Get project templates
      description: |
        Paginated list of **project templates** (projects in state 3 — template) the caller can use as a source for new projects via `POST /project/create-from-template/{template_id}`.

        **Use cases:**
        - Populating a "create from template" picker in a client UI
        - Syncing templates to an external project-provisioning tool
        - Reporting on which template tags are in use

        **Behavior notes:**
        - The returned templates can be picked for copying regardless of whether the caller owns them, as long as they have view access.
        - `users_ids[]` filters by template **owner**, not by invitees.
        - Default order: `date_add asc`.
      operationId: getTemplateProjects
      parameters:
        - name: order_by
          in: query
          schema:
            type: string
            enum: [name, date_add, date_edited_at]
            default: date_add
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
        - name: tags[]
          in: query
          schema:
            type: array
            items:
              type: string
        - name: users_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - name: created_in_range[date_from]
          in: query
          schema:
            type: string
            format: date
        - name: created_in_range[date_to]
          in: query
          schema:
            type: string
            format: date
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          template_projects:
                            type: array
                            items:
                              $ref: '#/components/schemas/ProjectWithTasklists'

  /user/{user_id}/all-projects:
    get:
      tags:
        - Projects
      summary: Get projects of another user
      description: |
        Paginated list of projects where the given `user_id` is the **owner**, intersected with what the caller has permission to see.

        **Use cases:**
        - Viewing teammate's workload before reassigning tasks
        - HR / PM views showing all projects a specific person owns
        - Filtering projects by owner in cross-functional reporting

        **Behavior notes:**
        - The caller sees only projects they themselves have access to — if the target user owns projects the caller can't see, they are silently omitted.
        - `states_ids[]` combines states (1=active, 2=archived, 3=template). Omitting returns the server default.
        - Default order: `date_add desc` (newest first).
      operationId: getUserProjects
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
        - name: states_ids[]
          in: query
          description: "States: 1=active, 2=archived, 3=template"
          schema:
            type: array
            items:
              type: integer
              enum: [1, 2, 3]
        - name: order_by
          in: query
          schema:
            type: string
            enum: [name, date_add, date_edited_at]
            default: date_add
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          projects:
                            type: array
                            items:
                              $ref: '#/components/schemas/ProjectFull'

  /project/{project_id}:
    get:
      tags:
        - Projects
      summary: Get project detail
      description: |
        Returns full detail of a single project — metadata, tasklists (filtered by caller's ACL), date-edited timestamp, the caller's hourly rate on the project, and current budget / spent totals.

        **Use cases:**
        - Opening the project detail page in a UI
        - Pulling budget vs. spent numbers for reporting
        - Retrieving the list of tasklists the caller can see before drilling into tasks

        **Behavior notes:**
        - Embedded tasklists respect the caller's ACL (tasklists the caller is not authorized for are filtered out).
        - Budget / spent numbers are computed on the fly for the **calling** user's view — different users may see different totals depending on their hourly rates and worker relationships.
        - Works for any project state (active, archived, template) as long as the caller has access.
      operationId: getProject
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectDetail'
    delete:
      tags:
        - Projects
      summary: Delete project (soft-delete)
      description: |
        Marks the project as deleted. The project disappears from all listings, but is retained in the database.

        **Use cases:**
        - Removing a test / mistakenly-created project
        - Permanent cleanup when archiving is not enough (e.g. client contract terminated)

        **Behavior notes:**
        - This is a **soft-delete** — the project row stays, but `deletedAt` is set. `POST /project/{id}/activate` restores it (the activate endpoint un-archives and un-deletes).
        - Side effects: cascades to tasks/tasklists that inherit the deletion; running timetrackings may be stopped server-side; webhooks fire.
        - Requires the caller to have project delete permissions (usually owner / commander).
      operationId: deleteProject
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /project/{project_id}/workers:
    get:
      tags:
        - Projects
      summary: Get project workers
      description: |
        Paginated list of all users (workers + owner + guests) assigned to the given project.

        **Use cases:**
        - Worker picker when assigning tasks / time estimates / work reports
        - Validating that an email is already a member before calling `POST /users/manage-workers`
        - Audit / export of project membership

        **Behavior notes:**
        - Returns basic user data only — it is **not** filtered by ACL-tasklist membership (a worker assigned only to some tasklists still appears in the full list).
        - Deleted (former) workers do not appear.
      operationId: getProjectWorkers
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          workers:
                            type: array
                            items:
                              $ref: '#/components/schemas/UserWithEmail'

  /project/{project_id}/archive:
    post:
      tags:
        - Projects
      summary: Archive project
      description: |
        Moves an active project into the **archived** state (state_id=2). Archived projects are hidden from the default lists but are still readable and can be reactivated via `POST /project/{id}/activate`.

        **Use cases:**
        - Completing a client engagement and removing it from active views
        - Freezing a project at year-end without losing data

        **Behavior notes:**
        - No request body is expected.
        - Archiving is idempotent: calling it on an already archived project succeeds (200) without side effects.
        - Archiving **does not** stop running timetrackings automatically — check timetracking state separately if needed.
        - Requires project-admin level permission (owner / commander).
      operationId: archiveProject
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /project/{project_id}/activate:
    post:
      tags:
        - Projects
      summary: Activate (unarchive / undelete) project
      description: |
        Restores a project into the **active** state. Works as a single entry point for both "unarchive" and "undelete".

        **Use cases:**
        - Re-opening an archived project for follow-up work
        - Recovering a project that was soft-deleted by mistake

        **Behavior notes (non-obvious):**
        - The endpoint inspects the project's current state and performs the appropriate transition: if archived → unarchive; if deleted → undelete; otherwise no-op returning 200.
        - Can fail with `PlanExceededException` — restoring a project counts against the caller's plan limits and may be refused if the plan is already at its project cap.
        - Requires project-admin permission.
      operationId: activateProject
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /project/{project_id}/remove-workers/by-ids:
    post:
      tags:
        - Projects
      summary: Remove workers from project by user IDs
      description: |
        Removes one or more users (by their internal user IDs) from the project's worker list.

        **Use cases:**
        - Off-boarding teammates from a specific project
        - Cleaning up external collaborators after an engagement ends
        - Batch deprovisioning as part of automated HR flows

        **Behavior notes:**
        - All given IDs are checked at once by the remove-workers ACL checker — if the caller lacks rights to remove any single user, the whole request fails (no partial removal).
        - The project **owner** cannot be removed via this endpoint; attempting to do so results in an error.
        - Removing a user also cleans up their task assignments and ACL tasklists in this project.
      operationId: removeProjectWorkersByIds
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - users_ids
              properties:
                users_ids:
                  type: array
                  items:
                    type: integer
                  example: [305, 150, 820]
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /project/{project_id}/remove-workers/by-emails:
    post:
      tags:
        - Projects
      summary: Remove workers from project by email
      description: |
        Same behavior as `/remove-workers/by-ids`, but the caller references workers by their email instead of user ID. Useful when IDs are not available (e.g. integration receives emails from CRM).

        **Use cases:**
        - Removing invited externals by their email address
        - Integrations that sync membership with email-based identity sources

        **Behavior notes:**
        - Every email **must** belong to a user currently in the project — otherwise the request fails (pre-check via `IProjectWorkersByEmailChecker`). No partial success.
        - Emails are resolved to user IDs server-side and then the ID-based ACL check is run. Same "owner cannot be removed" rule applies.
      operationId: removeProjectWorkersByEmails
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - users_emails
              properties:
                users_emails:
                  type: array
                  items:
                    type: string
                    format: email
                  example: ["user1@freelo.io", "user2@freelo.io"]
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /project/create-from-template/{template_id}:
    post:
      tags:
        - Projects
      summary: Create project from a template
      description: |
        Clones a project template (state_id=3) into a brand-new active project, copying its tasklists, tasks, subtasks, and optionally shifting floating due-dates based on `preset_date_from`.

        **Use cases:**
        - Spinning up a standardized client-onboarding project
        - Mass-provisioning projects from a shared blueprint
        - Kick-starting recurring engagements

        **Behavior notes (non-obvious):**
        - `currency_iso` is optional — if omitted, the server derives it from the caller's locale (CZ → CZK, EN → USD, etc.) via `LanguageCurrencyMapper`. Pass it explicitly if you need a predictable result.
        - `project_owner_id` defaults to the authenticated caller if not provided. The user must be owner-eligible; otherwise `400 InvalidArgumentException`.
        - `preset_date_from` shifts any "relative" due dates defined in the template (e.g. "+3 days") to absolute dates anchored at this value.
        - `users_ids` is a list of users **from the template's member list** you want to carry over as invitees; it is validated against the template's membership, not arbitrary user IDs.
        - `name` defaults to the template's name (often with a suffix applied in front-end flows) — pass it to override.
      operationId: createProjectFromTemplate
      parameters:
        - name: template_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                project_owner_id:
                  type: integer
                currency_iso:
                  type: string
                  enum: [CZK, EUR, USD]
                preset_date_from:
                  type: string
                  format: date
                  description: Date to set floating due dates in templates
                general_settings:
                  type: object
                  properties:
                    layout:
                      type: string
                      enum: [rows, kanban]
                      default: rows
                users_ids:
                  type: array
                  items:
                    type: integer
                  description: Users from template to invite
      responses:
        '200':
          description: Project created from template
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                  owner:
                    $ref: '#/components/schemas/UserBasic'
                  currency_iso:
                    type: string

  # ==================== PROJECT LABELS ====================
  /project-labels/find-available:
    get:
      tags:
        - Project Labels
      summary: Get project labels usable by caller
      description: |
        Returns all project labels (tags) the authenticated user can assign — their own private labels plus public labels from projects they participate in.

        **Use cases:**
        - Populating a label picker before calling `/project-labels/add-to-project/{projectId}`
        - Showing the caller which labels already exist so they don't create a duplicate

        **Behavior notes:**
        - Response field is `labels` — an array. The tag's `tag` entity property is exposed as `name` in API shape (the TagNameKeyReplacer maps it).
      operationId: findAvailableProjectLabels
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  labels:
                    type: array
                    items:
                      $ref: '#/components/schemas/ProjectLabel'

  /project-labels/{labelId}:
    post:
      tags:
        - Project Labels
      summary: Edit project label
      description: |
        Updates an existing project label identified by path parameter `labelId`. Renames it, changes its color, and/or toggles private/public visibility.

        **Use cases:**
        - Recoloring / renaming a label across all projects that use it
        - Flipping a label from private (owner-only) to public once a team is ready to share it

        **Behavior notes:**
        - The label is global — the edit propagates to **every project** where the label is attached.
        - ACL: only the label's owner (or a user passing the `IProjectTagAclChecker`) may edit private labels. Editing public labels requires the relevant project-manager permission.
        - All fields in the body are optional; omit a field to leave it unchanged.
      operationId: editProjectLabel
      parameters:
        - name: labelId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                color:
                  type: string
                  pattern: '^#[0-9a-fA-F]{6}$'
                is_private:
                  type: boolean
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
    delete:
      tags:
        - Project Labels
      summary: Delete project label (global)
      description: |
        Removes the label entirely — it is detached from all projects it was attached to and the tag entity itself is deleted.

        **Use cases:**
        - Cleaning up unused / obsolete labels
        - Consolidating duplicates (delete one, re-attach projects to the other)

        **Behavior notes:**
        - This is a **hard delete of the global label**, not a "detach from one project". To just unlink a label from a single project use `POST /project-labels/remove-from-project/{projectId}`.
        - ACL applies: only the owner of a private label (or a user passing `IProjectTagAclChecker`) can delete it.
      operationId: deleteProjectLabel
      parameters:
        - name: labelId
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /project-labels/add-to-project/{projectId}:
    post:
      tags:
        - Project Labels
      summary: Attach label to project (fetch-or-create)
      description: |
        Attaches a project label to the given project. The request body selects the label in **one of two mutually exclusive modes**:

        1. **By ID** — pass `id` of an existing label. In this mode `name`, `color` and `is_private` are **ignored** (even if sent).
        2. **By data** — pass `name` + `is_private` (and optionally `color`). The server looks up an existing label owned by the correct user with the same data and re-uses it; if none exists, it **creates a new label** and attaches it.

        > Non-standard behavior: the presence of `id` completely overrides the other fields. Sending `{id: 123, name: "typo", color: "#ff0000"}` will attach label 123 and silently ignore the name/color. If you want a new label with a specific name, omit `id`.

        **Use cases:**
        - Organizing projects by client / status / priority using a shared label
        - Bulk-tagging projects during an import

        **Behavior notes:**
        - Attaching a label that is already on the project swallows the `UniqueConstraintViolationException` and returns 200 (idempotent).
        - ACL: private labels can only be attached by their owner; public labels require the caller to be a project manager of the target project.
      operationId: addProjectLabelToProject
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: |
                Provide **either** `id` (to reference an existing label) **or** `name` + `is_private` (to fetch-or-create). See endpoint description — `id` takes precedence and other fields are ignored when present.
              properties:
                name:
                  type: string
                  description: Label name. Used only when `id` is omitted.
                color:
                  type: string
                  description: Hex color or named color from the server enum. Used only when `id` is omitted.
                is_private:
                  type: boolean
                  description: Whether the label is private to its owner. Used only when `id` is omitted. Required in data mode.
                id:
                  type: integer
                  description: ID of an existing label. When present, `name`/`color`/`is_private` are ignored.
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /project-labels/remove-from-project/{projectId}:
    post:
      tags:
        - Project Labels
      summary: Detach label from project
      description: |
        Detaches a label from a single project. The label itself continues to exist and remains attached to other projects.

        **Use cases:**
        - Re-categorizing a project without deleting the label from the workspace
        - Bulk cleanup of mis-applied labels

        **Behavior notes (non-obvious):**
        - Request body follows the same two-mode rule as `/add-to-project`: if `id` is present, `name`/`color`/`is_private` are **ignored**; otherwise the server looks the label up by data (name + is_private + owner).
        - In data mode (no `id`), the label is matched by owner + data; in ID mode, any referenced label is targeted.
        - If the label is not attached to the project, `ITagForRemoveFromProjectFetcher` throws `NotFoundException` → HTTP 404.
      operationId: removeProjectLabelFromProject
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: |
                Same mutually-exclusive ID-or-data selection rule as `/add-to-project`: when `id` is present, name/color/is_private are ignored.
              properties:
                name:
                  type: string
                color:
                  type: string
                is_private:
                  type: boolean
                id:
                  type: integer
                  description: ID of the label to detach. When present, `name`/`color`/`is_private` are ignored.
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  # ==================== PINNED ITEMS ====================
  /project/{project_id}/pinned-items:
    get:
      tags:
        - Pinned Items
      summary: Get pinned items of project
      description: |
        Returns all pinned items (pinned links, tasks, documents, files, project-links, directories) attached to the given project, filtered by the caller's ACL.

        **Use cases:**
        - Rendering the "pinned" sidebar of a project detail page
        - Exporting quick-access resources for reporting / documentation

        **Behavior notes:**
        - The result is an ACL-filtered list — pinned items whose target (a file, a task, a document) the caller cannot see are omitted silently.
        - The response is a flat array, not paginated.
      operationId: getPinnedItems
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PinnedItem'
    post:
      tags:
        - Pinned Items
      summary: Pin an external link to project
      description: |
        Pins a user-supplied external URL to the project. The returned item is either the newly created pin or — if an equivalent internal pin (same task / document / file / project-link / directory) already exists — the pre-existing one.

        **Use cases:**
        - Quick-attach of reference material (spec doc, drive link) to a project
        - Programmatic pinning from integrations (e.g. Slack → Freelo bot)

        **Behavior notes (non-obvious):**
        - The endpoint accepts an **external link** (schema shown here), but in the internal code path it is a dispatcher: if the URL is recognized as an internal Freelo resource (task, document, file, project-link, project-directory), the PinnedItemCreator performs a **fetch-or-create** — returning the existing internal pin if one already exists for that target, instead of creating a duplicate. So a `POST` with the same internal resource is idempotent.
        - For purely external URLs, each POST creates a new row even if the same URL was pinned before.
        - `title` is optional; when omitted the server derives a display name from the link target.
      operationId: pinItemToProject
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - link
              properties:
                link:
                  type: string
                  format: uri
                  description: Full URL to pin. If the URL matches an internal Freelo resource, the endpoint is idempotent (see description).
                title:
                  type: string
                  description: Optional display label. If omitted, a default is derived from the target.
      responses:
        '200':
          description: Item pinned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PinnedItem'

  /pinned-item/{pinned_item_id}:
    delete:
      tags:
        - Pinned Items
      summary: Delete pinned item
      description: |
        Removes a single pinned item from its project. The underlying target (task / document / file / link) is **not** affected — only the pin is deleted.

        **Use cases:**
        - Un-pinning outdated references
        - Cleanup flows after a resource was moved / renamed

        **Behavior notes:**
        - ACL: the caller must have rights to modify pinned items in the owning project (usually worker+).
        - Returns 404 if the pinned item does not exist or the caller has no access to its project.
      operationId: deletePinnedItem
      parameters:
        - name: pinned_item_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  # ==================== TASKLISTS ====================
  /project/{project_id}/tasklists:
    post:
      tags:
        - Tasklists
      summary: Create tasklist in project
      description: |
        Creates a new tasklist inside the given project. The tasklist inherits project-level ACL and becomes visible to all project workers unless the tasklist's own ACL is narrowed later.

        **Use cases:**
        - Creating a new phase / milestone inside a project
        - Bulk-provisioning tasklists from an external system (imported project structure)

        **Behavior notes:**
        - Requires the caller to be a project manager or higher — otherwise `AclForbiddenException` / `RoleActionForbiddenException`.
        - `budget` is optional and uses the stringified-currency format (e.g. "100000" = 1000.00 of the project's currency).
      operationId: createTasklist
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                budget:
                  type: string
                  description: Currency amount (2 decimal places, no separator)
                priority:
                  type: integer
                  minimum: 1
                  description: |
                    Position (order) of the tasklist within the project, 1 = first. Tasklists currently at this position or below shift down by 1 to make room. Values past the end of the list are clamped to `last + 1`. Omit to append at the end.

                    Note: despite the name, this is positional ordering — not to be confused with task `priority_enum` (low/medium/high importance).
      responses:
        '200':
          description: Tasklist created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasklistWithBudget'

  /tasklist/{tasklist_id}/edit:
    post:
      tags:
        - Tasklists
      summary: Edit tasklist
      description: |
        Updates one or more fields of an existing tasklist. Every field is optional — only the keys present in the request body are applied.

        **Use cases:**
        - Renaming a tasklist, adjusting its budget / time fund
        - Reordering tasklists within a project (`priority` field)
        - Managing followers (`tracking_users_ids`) and the default worker

        **Behavior notes:**
        - `budget`: send `null` or `0` to clear it.
        - `time_budget_minutes`: send `null` to clear; values must be >= 0.
        - `priority`: new position (order) of the tasklist within the project, 1 = first. Other tasklists in the moved-over range shift by ±1 to fill the gap. Values past the end are clamped to the last position. Despite the name, this is positional ordering — not task importance (`priority_enum`).

          **Best-effort semantics:** the priority renumber runs outside the transaction that commits `name`, `budget`, `time_budget_minutes`, `tracking_users_ids` and `worker_id`. A failure of the priority renumber does NOT roll back the other fields. The response contains a `priorityApplied` flag — if `false`, the rest of the edit succeeded but the priority change did not; the client may retry the priority update separately.
        - `tracking_users_ids`: send `[]` to clear all followers. IDs of users without access to the tasklist are silently filtered out. Combine with `should_change_existing_tasks: true` to also propagate the change to every existing task.
        - `worker_id`: send `null` to clear the default worker.
      operationId: editTasklist
      parameters:
        - name: tasklist_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                budget:
                  type: string
                  nullable: true
                  description: Integer amount in minor currency units, encoded as a string (e.g. "100000" for 1000.00). `null` or `"0"` clears the budget. Decimal strings ("100.50") are rejected with HTTP 400.
                time_budget_minutes:
                  type: integer
                  nullable: true
                  minimum: 0
                priority:
                  type: integer
                  minimum: 1
                tracking_users_ids:
                  type: array
                  items:
                    type: integer
                should_change_existing_tasks:
                  type: boolean
                  default: false
                worker_id:
                  type: integer
                  nullable: true
                  minimum: 1
      responses:
        '200':
          description: Tasklist updated
          content:
            application/json:
              schema:
                type: object
                required:
                  - priorityApplied
                properties:
                  priorityApplied:
                    type: boolean
                    description: |
                      `true` when the priority renumber finished (or was not requested). `false` when other fields committed but the priority renumber failed (the client may retry the priority field alone).

  /all-tasklists:
    get:
      tags:
        - Tasklists
      summary: Get all tasklists (across projects)
      description: |
        Paginated list of tasklists visible to the caller, across all accessible projects. Can be filtered to a subset of projects via `projects_ids[]`.

        **Use cases:**
        - Cross-project reporting (e.g. "all tasklists touching client X")
        - Building a global tasklist picker in tooling
        - Dashboards that aggregate progress across a portfolio

        **Behavior notes:**
        - ACL is applied — tasklists the caller can't see are filtered out, even if `projects_ids[]` includes their project.
        - Default order: `date_add asc`.
      operationId: getAllTasklists
      parameters:
        - name: projects_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - name: order_by
          in: query
          schema:
            type: string
            enum: [name, date_add, date_edited_at]
            default: date_add
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          tasklists:
                            type: array
                            items:
                              $ref: '#/components/schemas/TasklistFull'

  /project/{project_id}/tasklist/{tasklist_id}/assignable-workers:
    get:
      tags:
        - Tasklists
      summary: List users who can be assigned tasks in this tasklist
      description: |
        Returns the workers who are allowed to be set as `worker` on tasks inside this tasklist — i.e. the intersection of project membership and the tasklist's ACL (if the tasklist is ACL-restricted).

        **Use cases:**
        - Populating the assignee picker when creating / editing a task
        - Validating `worker_id` before calling `POST /tasks` or `POST /task/{id}`

        **Behavior notes:**
        - If the tasklist is NOT ACL-restricted, the result equals the project's full worker list.
        - If the tasklist IS ACL-restricted, only users explicitly granted tasklist ACL (plus the project owner/commander) appear.
      operationId: getAssignableWorkers
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - $ref: '#/components/parameters/TasklistIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/UserWithEmail'

  /tasklist/{tasklist_id}:
    get:
      tags:
        - Tasklists
      summary: Get tasklist detail
      description: |
        Returns metadata for a single tasklist — name, budget, parent project reference, the default worker (`worker_id`), and the latest `date_edited` / `date_add` audit timestamps.

        **Use cases:**
        - Opening a tasklist detail view
        - Re-fetching metadata after an edit to confirm the state
        - Reading budget for reporting purposes
        - Reading the default worker set via `POST /tasklist/{tasklist_id}/edit` (`worker_id`)

        **Behavior notes:**
        - Performs both a tasklist fetch (ACL-checked) and a project fetch (ACL-checked). If the caller has no access to either, returns 404.
        - `worker_id` is `null` when no default worker is set, or when the configured default worker is no longer among the tasklist's assignable workers (project membership / tasklist ACL).
      operationId: getTasklist
      parameters:
        - $ref: '#/components/parameters/TasklistIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TasklistDetail'

  /tasklist/create-from-template/{template_id}:
    post:
      tags:
        - Tasklists
      summary: Copy a tasklist from a project template
      description: |
        Copies a specific tasklist from a **project template** into either a brand-new project or an existing target. Path parameter `template_id` identifies the source project template; body `tasklist_id` identifies which tasklist inside that template to copy.

        **Use cases:**
        - Re-using a standardized tasklist (e.g. "QA checklist") across projects
        - Seeding a new client project with a curated subset of blueprints

        **Behavior notes (non-obvious):**
        - Body field `tasklist_id` is **required** — it's the source tasklist's ID inside the template project referenced by path `template_id`. Mixing path + body IDs like this is deliberate.
        - If `target_project_id` is **not** provided, a new project is created as the target (copying from the template).
        - If `target_tasklist_id` is provided together with `target_project_id`, tasks are copied into that existing tasklist instead of a fresh one.
        - `preset_date_from` shifts floating due-dates relative to this date (same semantics as project template copy).
        - `users_ids` lists which template members to invite into the target — must be a subset of the template's members.
      operationId: createTasklistFromTemplate
      parameters:
        - name: template_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tasklist_id
              properties:
                tasklist_id:
                  type: integer
                  description: ID of the tasklist from template
                target_project_id:
                  type: integer
                target_tasklist_id:
                  type: integer
                preset_date_from:
                  type: string
                  format: date
                users_ids:
                  type: array
                  items:
                    type: integer
      responses:
        '200':
          description: Tasklist created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                  tasks:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        name:
                          type: string

  # ==================== TASKS ====================
  /project/{project_id}/tasklist/{tasklist_id}/tasks:
    get:
      tags:
        - Tasks
      summary: Get active tasks in a tasklist
      description: |
        Returns all **active** tasks in the specified tasklist, ordered by the requested criterion. To fetch finished tasks use `GET /tasklist/{tasklist_id}/finished-tasks`.

        **Use cases:**
        - Rendering the task board for a given tasklist
        - Iterating tasks for bulk operations (assign, move, etc.)
        - Feeding integrations that mirror the task state

        **Behavior notes:**
        - Response is a flat (non-paginated) array. For very large tasklists, consider `GET /all-tasks` with filters for pagination.
        - ACL-filtered: if the tasklist is ACL-restricted and the caller has no access to it, 404.
      operationId: getTasksInTasklist
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - $ref: '#/components/parameters/TasklistIdParam'
        - name: order_by
          in: query
          schema:
            type: string
            enum: [priority, name, date_add, date_edited_at]
            default: priority
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskSummary'
    post:
      tags:
        - Tasks
      summary: Create task in tasklist
      description: |
        Creates a new task in the given tasklist. The authenticated caller becomes the task's author; `worker` defaults to a value derived from the tasklist's default-worker rules if not supplied.

        **Use cases:**
        - Creating a ticket from an integration (Sentry → Freelo, Slack → Freelo, etc.)
        - Programmatic task provisioning
        - Migrating tasks from another system

        **Behavior notes:**
        - The assignee (`worker`) must be one of the tasklist's `assignable-workers` (see that endpoint). A user outside the ACL scope results in `WorkerHasNoAccessToTasklistException` → 403.
        - `tracking_users_ids` defaults to the assignee + author if omitted (via `TaskDataDefaultTrackingUsersFiller`).
        - Creates a `task_created` event (→ webhooks, notifications, calendar sync).
      operationId: createTask
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - $ref: '#/components/parameters/TasklistIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskCreate'
      responses:
        '200':
          description: Task created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskCreated'

  /all-tasks:
    get:
      tags:
        - Tasks
      summary: Get all tasks (paginated, filterable)
      description: |
        Paginated global task search. Combines fulltext search (via Elasticsearch), structural filters (projects, tasklists, worker, state), label filters, and date-range filters.

        **Use cases:**
        - Primary task search / dashboard filter
        - Reporting queries ("all tasks due next week for worker X")
        - Cross-project views (e.g. "all open tasks with label blocker")

        **Behavior notes (non-obvious):**
        - `search_query` is a fulltext match on the task name through Elasticsearch — it prefilters the task set before other filters are applied; supplying only `search_query` without any `projects_ids[]` restricts across all visible projects.
        - `with_label` is a single-value legacy alias for `with_labels[]` — when both are sent, `with_label` is merged into the array (no preemption). `with_label` is deprecated; prefer `with_labels[]`.
        - `state_id` filters by task state — omit to get tasks in all states the caller can see (typically active + finished; depends on ACL).
        - `no_due_date=true` returns only tasks without a due date; combining with `due_date_range` is effectively contradictory and the range is ignored.
        - `finished_overdue=true` filters for tasks finished **after** their due date — a reporting lens for delivery SLAs.
        - `worker_id` filters by assignee only (not by tracking users).
        - `my_priorities=1` returns only tasks the authenticated user has added to their priorities.
        - `priority_enum` filters by task priority level — `l` (low), `m` (medium) or `h` (high); omit to return tasks of any priority.
      operationId: getAllTasks
      parameters:
        - name: search_query
          in: query
          description: Fulltext search query for the task name
          schema:
            type: string
        - name: state_id
          in: query
          description: ID of the tasks state
          schema:
            type: integer
        - name: projects_ids[]
          in: query
          description: Filter tasks by project IDs. If empty, tasks from all accessible projects are returned.
          schema:
            type: array
            items:
              type: integer
        - name: tasklists_ids[]
          in: query
          description: Filter tasks by tasklist IDs
          schema:
            type: array
            items:
              type: integer
        - name: order_by
          in: query
          schema:
            type: string
            enum: [priority, name, date_add, date_edited_at]
            default: date_add
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
        - name: with_labels[]
          in: query
          description: Filter tasks that have at least one of the specified labels (case insensitive). Can be combined with with_label.
          schema:
            type: array
            items:
              type: string
        - name: with_label
          in: query
          description: Filter tasks by a single label name (case insensitive). If with_labels[] is also provided, this value is merged into that array.
          schema:
            type: string
          deprecated: true
        - name: without_label
          in: query
          description: Exclude tasks that have the specified label (case insensitive)
          schema:
            type: string
        - name: no_due_date
          in: query
          description: >-
            Only tasks with no due date. Pass `1` to enable, `0` to disable —
            string values like `true`/`false` are not accepted and silently
            fall back to the default.
          schema:
            type: integer
            enum: [0, 1]
            default: 0
        - name: due_date_range[date_from]
          in: query
          description: Filter tasks with due date on or after this date
          schema:
            type: string
            format: date
        - name: due_date_range[date_to]
          in: query
          description: Filter tasks with due date on or before this date
          schema:
            type: string
            format: date
        - name: finished_overdue
          in: query
          description: >-
            Only tasks finished after due date. Pass `1` to enable, `0` to
            disable — string values like `true`/`false` are not accepted and
            silently fall back to the default.
          schema:
            type: integer
            enum: [0, 1]
            default: 0
        - name: finished_date_range[date_from]
          in: query
          description: Filter tasks finished on or after this date
          schema:
            type: string
            format: date
        - name: finished_date_range[date_to]
          in: query
          description: Filter tasks finished on or before this date
          schema:
            type: string
            format: date
        - name: worker_id
          in: query
          description: Filter by worker ID
          schema:
            type: integer
        - name: my_priorities
          in: query
          description: >-
            Only tasks in the authenticated user's priorities ("my
            priorities"). Pass `1` to enable, `0` to disable — string values
            like `true`/`false` are not accepted and silently fall back to the
            default.
          schema:
            type: integer
            enum: [0, 1]
            default: 0
        - name: priority_enum
          in: query
          description: >-
            Filter by task priority level: `l` (low), `m` (medium) or `h`
            (high). Omit to return tasks of any priority.
          schema:
            type: string
            enum: [l, m, h]
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          tasks:
                            type: array
                            items:
                              $ref: '#/components/schemas/TaskFull'

  /tasklist/{tasklist_id}/finished-tasks:
    get:
      tags:
        - Tasks
      summary: Get finished tasks in a tasklist
      description: |
        Paginated list of **finished** (closed) tasks inside a given tasklist. Optionally narrow by a fulltext search of the task name.

        **Use cases:**
        - Archive / history view of what's been completed in a tasklist
        - Retrospective reports on delivered scope
        - Finding a specific closed task by name for reactivation

        **Behavior notes:**
        - `search_query` is an Elasticsearch-backed fulltext match on the task name; when omitted, all finished tasks in the tasklist are returned (paginated).
        - Active tasks are **not** included — use `/project/{pid}/tasklist/{tid}/tasks` for those.
      operationId: getFinishedTasks
      parameters:
        - $ref: '#/components/parameters/TasklistIdParam'
        - name: search_query
          in: query
          schema:
            type: string
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          finished_tasks:
                            type: array
                            items:
                              $ref: '#/components/schemas/TaskFinished'

  /tasks/relations:
    post:
      tags:
        - Tasks
      summary: Find task relations in bulk
      description: |
        Returns relations for a list of tasks. Each item in the response contains the task ID
        and its relations (types: `blocked_by`, `blocks`, `related_to`, `duplicate_of`).

        Tasks the caller cannot access (missing project ACL, insufficient plan, or the ID being
        a multi-project child) are silently omitted from the response — the endpoint never
        reports per-task 403/404. This mirrors the single-task GET which returns 404 for the
        same cases.

        Plan-gated types: `related_to` and `duplicate_of` require team features;
        `blocked_by` / `blocks` additionally require business features. On lower plans the
        corresponding items are simply missing from the per-task relation list.

        Duplicate task_ids are deduplicated internally; the response contains at most one
        entry per task.
      operationId: findTaskRelationsBulk
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - task_ids
              properties:
                task_ids:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: integer
                  description: List of task IDs (1–100 items).
      responses:
        '200':
          description: Relations grouped by task ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  tasks:
                    type: array
                    items:
                      type: object
                      properties:
                        task_id:
                          type: integer
                        relations:
                          type: array
                          items:
                            $ref: '#/components/schemas/TaskRelation'
        '400':
          description: Invalid request body

  /task/{task_id}:
    get:
      tags:
        - Tasks
      summary: Get task detail
      description: |
        Returns full task detail — metadata, labels, worker, tracking users, due dates, subtasks count, time estimates, custom field values, and (for non-commanders) computed spent minutes and cost.

        **Use cases:**
        - Opening a task in the UI
        - Fetching every detail before an edit (for diffing)
        - Pulling full context for AI / reporting pipelines

        **Behavior notes (non-obvious):**
        - For multi-project tasks, the response contains a `multi_project_task` block mapping the task across its projects and may expose a `parent_task_id` if this is a subtask linked to a multi-project parent.
        - Spent minutes (`minutes`) and `cost.amount` are only included when the caller is **not** a project commander (commanders see account-wide billing elsewhere).
        - Labels include labels inherited from a multi-project parent.
        - `copied_from_task` references the origin task if the task was created from a template or as a multi-project copy.
      operationId: getTask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskDetail'
    post:
      tags:
        - Tasks
      summary: Edit task (partial update)
      description: |
        Partially updates a task. Only the fields listed below are editable through this endpoint — any other key in the body is **silently ignored** (the facade uses `array_intersect_key` against a fixed whitelist).

        **Editable fields:** `name`, `worker`, `due_date`, `due_date_end`, `labels`, `priority_enum`, `tracking_users_ids`, `add_tracking_users_ids`, `remove_tracking_users_ids`.

        **Use cases:**
        - Reassigning a task
        - Changing priority / due date
        - Renaming or relabeling from an integration
        - Adjusting tracking users (add / remove / replace)

        **Behavior notes (non-obvious):**
        - **`worker_id` accepted as alias for `worker`:** the facade contains a make.com-compatibility HACK — if the body has `worker_id`, it is copied into `worker`. This is undocumented elsewhere but is the same field.
        - **Tracking users have three mutually-exclusive update shapes:**
          - `tracking_users_ids` **replaces** the full set (pass `[]` to clear all).
          - `add_tracking_users_ids` **merges** the given IDs into the current set.
          - `remove_tracking_users_ids` **removes** the given IDs from the current set.
          Mixing `tracking_users_ids` (replace) with add/remove in one call is accepted but the final state is determined by the facade's order of operations — keep it to one shape per call to be deterministic.
        - **Labels must reference existing labels or be fully-formed new label DTOs** — the behavior matches the task-labels add-to-task semantics.
        - The endpoint responds with the task's full detail (same shape as `GET /task/{id}`).
        - Note the HTTP method: `POST` is used for edits here (historical REST shape), not `PUT`/`PATCH`.
      operationId: editTask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: object
                  description:
                    Task description as an object, identical to the
                    POST /task/{task_id}/description request body. Files are
                    downloaded from download_url, attached to the description,
                    and the description file set is reconciled (files no longer
                    present are archived).
                  required:
                    - content
                  properties:
                    content:
                      type: string
                    files:
                      type: array
                      items:
                        $ref: '#/components/schemas/FileUpload'
                due_date:
                  type: string
                  format: date-time
                  description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                  example: '2026-04-24T11:12:38'
                due_date_end:
                  type: string
                  format: date-time
                  description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                  example: '2026-04-24T11:12:38'
                worker:
                  type: integer
                priority_enum:
                  type: string
                  enum: [l, m, h]
                  nullable: true
                  description: Allowed options are l, m, h. Set to null to remove priority.
                tracking_users_ids:
                  type: array
                  nullable: true
                  description: Set (replace) all tracking users. Pass an empty array to remove all.
                  items:
                    type: integer
                add_tracking_users_ids:
                  type: array
                  description: Add tracking users by user ID (merged with existing).
                  items:
                    type: integer
                remove_tracking_users_ids:
                  type: array
                  description: Remove tracking users by user ID.
                  items:
                    type: integer
      responses:
        '200':
          description: Task updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskDetail'
    delete:
      tags:
        - Tasks
      summary: Delete task (soft-delete)
      description: |
        Soft-deletes a task. It disappears from listings and cannot be reactivated via `/activate` (activation only un-finishes a finished task, it does **not** undelete).

        **Use cases:**
        - Removing spam / mistaken tasks
        - Cleaning up a noisy tasklist

        **Behavior notes:**
        - Cascades to subtasks (they are also hidden).
        - Emits `task_deleted` event — webhooks and notifications fire.
        - Requires delete permission (owner / commander / author, per role rules).
      operationId: deleteTask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /task/{task_id}/activate:
    post:
      tags:
        - Tasks
      summary: Reopen a finished task
      description: |
        Moves a task from the **finished** state back to **active**. Use to reverse a `finish` operation.

        **Use cases:**
        - Reopening a task that was closed prematurely
        - Reverting an incorrect "finish" action triggered by an integration

        **Behavior notes (non-obvious):**
        - Only works on finished tasks. On an active task, returns 200 without changes. On a **deleted** task, returns 404 (this endpoint does **not** un-delete — it's not symmetric with the project activate endpoint).
        - Emits `task_activated` event / webhooks.
      operationId: activateTask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /task/{task_id}/finish:
    post:
      tags:
        - Tasks
      summary: Mark task as finished
      description: |
        Closes a task — moves it to the **finished** state.

        **Use cases:**
        - Closing a task via an integration (e.g. Zapier "when ticket closed → finish Freelo task")
        - Bulk-closing tasks after a release

        **Behavior notes:**
        - Any running timetracking **on this specific task** is stopped as part of the finish flow.
        - Emits `task_finished` event / webhooks.
        - Requires the caller to be the assignee, author, or a project manager (Role rules). Otherwise `RoleActionForbiddenException` → 403.
      operationId: finishTask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /taskcheck/{taskcheck_id}:
    post:
      tags:
        - Tasks
      summary: Edit a simple checklist item
      description: |
        Updates a simple checklist item (a `tasks_checks` row with no smart-task counterpart). Only `name` and `worker` are editable; sending `priority_enum`, `priority`, `due_date` or `due_date_end` returns 400. A smart taskcheck id (one whose checklist item has its own `tasks.id`) returns 404 — use `POST /task/{task_id}` for those.
      operationId: editTaskcheck
      parameters:
        - $ref: '#/components/parameters/TaskcheckIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                worker:
                  type: integer
                  nullable: true
                  description: User id of the worker to assign. Pass `null` to clear.
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
    delete:
      tags:
        - Tasks
      summary: Delete a simple checklist item
      description: |
        Soft-deletes a simple checklist item (a `tasks_checks` row with no smart-task counterpart). A smart taskcheck id returns 404 — use `DELETE /task/{task_id}` for those.
      operationId: deleteTaskcheck
      parameters:
        - $ref: '#/components/parameters/TaskcheckIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /taskcheck/{taskcheck_id}/finish:
    post:
      tags:
        - Tasks
      summary: Mark a simple checklist item as finished
      description: |
        Moves a simple checklist item to the **finished** state. A smart taskcheck id returns 404 — use `POST /task/{task_id}/finish` for those.
      operationId: finishTaskcheck
      parameters:
        - $ref: '#/components/parameters/TaskcheckIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /taskcheck/{taskcheck_id}/activate:
    post:
      tags:
        - Tasks
      summary: Reopen a finished simple checklist item
      description: |
        Moves a simple checklist item from the **finished** state back to **active**. A smart taskcheck id returns 404 — use `POST /task/{task_id}/activate` for those.
      operationId: activateTaskcheck
      parameters:
        - $ref: '#/components/parameters/TaskcheckIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /task/{task_id}/move/{tasklist_id}:
    post:
      tags:
        - Tasks
      summary: Move task to another tasklist (optionally cross-project)
      description: |
        Moves a task into a different tasklist. Target tasklist may belong to the same or a different project.

        **Use cases:**
        - Re-phasing a task (moving from "Backlog" to "In progress" tasklist)
        - Escalating a task between projects (e.g. support → engineering)
        - Re-organizing multi-project tasks

        **Behavior notes (non-obvious):**
        - For **multi-project tasks**, the optional body field `multi_project_task.source_tasklist_id` picks which project-instance to move. When omitted (or set to the primary task's own tasklist), the cross-project move flow runs and applies `work_reports_action` / `custom_fields_action` rules. When it points to a **child task's** tasklist, only that child is moved within its own project, and `work_reports_action` / `custom_fields_action` are **ignored**.
        - `work_reports_action` decides what happens to existing work reports on cross-project moves: `move_to_target_project` (default) rebinds them, `keep_on_origin_project` leaves them tied to the origin.
        - `custom_fields_action` controls custom-field-value handling when the target project does not have the same custom fields — destructive options (`delete_*`) lose data; `move_to_comments_*` preserves it as a comment.
        - Required field on multi-project: if the caller has no ACL on the source tasklist's project, 403.
      operationId: moveTask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
        - $ref: '#/components/parameters/TasklistIdParam'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                work_reports_action:
                  type: string
                  enum: [move_to_target_project, keep_on_origin_project]
                  default: move_to_target_project
                custom_fields_action:
                  type: string
                  enum: [nothing, delete_what_cant_be_keep, move_to_comments_what_cant_be_keep, delete_all, move_to_comments_all]
                  default: nothing
                multi_project_task:
                  type: object
                  description: Optional multi-project task context for moving a specific project instance
                  properties:
                    source_tasklist_id:
                      type: integer
                      description: ID of the source tasklist identifying which project instance to move
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /task/{task_id}/projects:
    post:
      tags:
        - Tasks
      summary: Assign task to an additional project (make it multi-project)
      description: |
        Promotes a single-project task into a multi-project task (UVVP) by creating a **child task** in another project, linked to the same logical parent.

        **Use cases:**
        - Sharing a ticket across departments (e.g. one ticket visible in Sales and Engineering projects)
        - Cross-team visibility for long-running initiatives

        **Behavior notes:**
        - Target project is **derived from the `tasklist_id`** — you pass the tasklist, not the project. The target tasklist must belong to a project the caller has access to, otherwise 403.
        - Subsequent content (comments, worker) operations on the parent and child task may diverge depending on the multi-project architecture (see project docs `docs/feature/multi-project-tasks.md`).
      operationId: assignTaskToProject
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tasklist_id
              properties:
                tasklist_id:
                  type: integer
                  description: Target tasklist ID (the project is derived from it)
      responses:
        '200':
          description: Task assigned to project
          content:
            application/json:
              schema:
                type: object
                properties:
                  task:
                    type: object
                    properties:
                      id:
                        type: integer
                      uuid:
                        type: string
        '403':
          description: Forbidden
        '404':
          description: Task or tasklist not found

  /task/{task_id}/relations:
    get:
      tags:
        - Tasks
      summary: Get task relations
      description: |
        Returns all relations for a task (types: `blocked_by`, `blocks`, `related_to`, `duplicate_of`).
        Relations to tasks the caller cannot access are filtered out.

        Relation type visibility depends on the project owner's plan: `related_to` and `duplicate_of`
        require team features; `blocked_by` and `blocks` additionally require business features.
        On lower plans the corresponding buckets are returned empty.

        Multi-project child task IDs are not queryable here — a direct child ID returns 404.
      operationId: getTaskRelations
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      responses:
        '200':
          description: Task relations
          content:
            application/json:
              schema:
                type: object
                properties:
                  relations:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskRelation'
        '404':
          description: |
            Returned when the task does not exist, when the caller has no access to the task
            or its project, when the project owner's plan has no team features, or when the
            requested ID is a multi-project child. No separate 403 is emitted — access is
            indistinguishable from not-found by design.

  /task/{task_id}/projects/{project_id}:
    delete:
      tags:
        - Tasks
      summary: Remove task from a secondary project
      description: |
        Reverses a prior "assign to project" call by deleting the child task that belonged to the specified secondary project. The primary task continues to exist.

        **Use cases:**
        - Revoking cross-team visibility once a handoff is done
        - Cleanup after an accidental multi-project assignment

        **Behavior notes:**
        - Attempting to remove a task from its **primary** project (the one where it was originally created) is not allowed — that requires `DELETE /task/{task_id}` instead. The endpoint returns 403 `AclException` in that case.
        - Returns 404 if the task is not present in the given project at all.
      operationId: removeTaskFromProject
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
        - $ref: '#/components/parameters/ProjectIdParam'
      responses:
        '200':
          description: Task removed from project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '403':
          description: Forbidden
        '404':
          description: Task or project not found

  /task/{task_id}/description:
    get:
      tags:
        - Tasks
      summary: Get task description
      description: |
        Returns the task's description — the first "pinned" comment that serves as the canonical rich-text body of the task.

        **Use cases:**
        - Loading the long-form body in a task detail view
        - Extracting the description for reporting or AI summarization

        **Behavior notes:**
        - If the task has no description yet, the response is still 200 but fields may be empty / null — use the edit endpoint to create one.
      operationId: getTaskDescription
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Comment'
    post:
      tags:
        - Tasks
      summary: Create or overwrite task description (upsert)
      description: |
        Creates the task description if none exists, or **overwrites** the existing one — there is no "append" or "history" behavior. File attachments given in `files` are attached to the description comment.

        **Use cases:**
        - Filling a task body from a ticketing integration
        - Editing the task's main description in a UI
        - Attaching files that belong to the task body (not to a separate comment)

        **Behavior notes (non-obvious):**
        - Upsert semantics: first call creates, subsequent call **replaces** the content entirely. Any previous content is lost — not stored in history.
        - `files` expects already-uploaded file UUIDs (see `POST /file/upload`).
      operationId: editTaskDescription
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                content:
                  type: string
                files:
                  type: array
                  items:
                    $ref: '#/components/schemas/FileUpload'
      responses:
        '200':
          description: Description updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Comment'

  /task/{task_id}/reminder:
    post:
      tags:
        - Tasks
      summary: Set reminder on task (for caller)
      description: |
        Schedules a **personal** reminder for the calling user on a specific task. The reminder fires a notification to the caller at `remind_at`.

        **Use cases:**
        - "Ping me about this task at 9 AM tomorrow"
        - Snoozing a task for later follow-up

        **Behavior notes (non-obvious):**
        - Reminders are **per-user** — this endpoint sets a reminder for the caller only. Other tracking users are not affected.
        - Calling this endpoint on a task that already has a reminder by the caller **overwrites** the existing `remind_at` (upsert behavior).
        - `remind_at` is expected in ISO 8601; the server normalizes it internally.
      operationId: createTaskReminder
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - remind_at
              properties:
                remind_at:
                  type: string
                  format: date-time
                  description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                  example: '2026-04-24T11:12:38'
      responses:
        '200':
          description: Reminder created
          content:
            application/json:
              schema:
                type: object
                properties:
                  remind_at:
                    type: string
                    format: date-time
                    description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                    example: '2026-04-24T11:12:38'
                  task:
                    type: object
                    properties:
                      id:
                        type: integer
                      name:
                        type: string
    delete:
      tags:
        - Tasks
      summary: Clear caller's task reminder
      description: |
        Removes the calling user's personal reminder for the given task.

        **Behavior notes:**
        - Only the caller's own reminder is deleted — reminders set by other users are unaffected.
        - Idempotent: calling with no reminder present returns 200.
      operationId: deleteTaskReminder
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /public-link/task/{task_id}:
    get:
      tags:
        - Tasks
      summary: Get (or create) a public share link for a task
      description: |
        Returns a public, unauthenticated URL that lets anyone with the link view the task. If no public link exists yet, one is **created on-the-fly** and returned.

        **Use cases:**
        - Sharing a task with a client who has no Freelo account
        - Embedding a shareable link in a status update

        **Behavior notes (non-obvious):**
        - This is a **GET that creates** — first call to this endpoint creates the link; subsequent calls return the same URL. To invalidate, use `DELETE /public-link/task/{task_id}`.
        - The URL exposes the task's content read-only to anyone holding it. Rotating is done by DELETE + GET (creates a new URL).
      operationId: getPublicLinkToTask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
    delete:
      tags:
        - Tasks
      summary: Revoke public share link for a task
      description: |
        Deletes the task's public link, immediately invalidating any previously shared URL.

        **Use cases:**
        - Rotating a compromised link
        - Ending external sharing when a client engagement ends
      operationId: deletePublicLinkToTask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /task/create-from-template/{template_id}:
    post:
      tags:
        - Tasks
      summary: Create task by copying from a template task
      description: |
        Copies a single task out of a project template into a target tasklist. Mirrors the structure of the tasklist / project template copy endpoints.

        **Use cases:**
        - Adding a standard boilerplate task (e.g. "Kickoff checklist") to an existing project
        - Cloning a canonical bug-report template

        **Behavior notes (non-obvious):**
        - Body field `task_id` is **required** — it identifies the source task inside the template referenced by path `template_id`.
        - If `target_tasklist_id` is omitted, the copied task lands in the **same tasklist ID** it had in the template — which only works if `target_project_id` (or an auto-created project) has a tasklist with that ID. Safer to always pass both.
        - `preset_date_from` shifts floating due-dates (same as other template endpoints).
        - `users_ids` is a list of template members to invite into the destination.
      operationId: createTaskFromTemplate
      parameters:
        - name: template_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - task_id
              properties:
                task_id:
                  type: integer
                  description: ID of the task from template
                target_project_id:
                  type: integer
                target_tasklist_id:
                  type: integer
                preset_date_from:
                  type: string
                  format: date
                users_ids:
                  type: array
                  items:
                    type: integer
      responses:
        '200':
          description: Task created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                  tasklist:
                    type: object
                    properties:
                      id:
                        type: integer
                      name:
                        type: string

  /task/{task_id}/total-time-estimate:
    post:
      tags:
        - Tasks
      summary: Create or update the task's total time estimate (upsert)
      description: |
        Sets the total expected effort for a task (aggregated across the team). If a total estimate already exists, it is **updated**; otherwise a new one is created.

        **Use cases:**
        - Capturing the team-wide effort budget for capacity planning
        - Refreshing an estimate after re-scoping

        **Behavior notes:**
        - Upsert semantics (`TimeEstimateFacade::createOrUpdate`). Calling this endpoint multiple times is safe.
        - Per-user estimates are managed separately via `/task/{id}/users-time-estimates/{user_id}` — the total is not automatically derived from per-user sums.
      operationId: setTotalTimeEstimate
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - minutes
              properties:
                minutes:
                  type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
    delete:
      tags:
        - Tasks
      summary: Remove the task's total time estimate
      description: |
        Clears the total time estimate for the task.

        **Behavior notes:**
        - Per-user estimates are **not** removed by this call — delete them separately.
        - Idempotent: calling on a task without an estimate returns 200.
      operationId: deleteTotalTimeEstimate
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /task/{task_id}/users-time-estimates/{user_id}:
    post:
      tags:
        - Tasks
      summary: Set per-user time estimate on a task (upsert)
      description: |
        Upserts a **per-user** time estimate — how much effort the given `user_id` is expected to spend on this task.

        **Use cases:**
        - Project-manager capacity planning (assigning hours to each teammate)
        - Feeding a billing estimate where each worker has a different rate

        **Behavior notes:**
        - Upsert semantics (`TimeEstimateUserFacade::createOrUpdate`).
        - Does **not** automatically update the total time estimate — manage totals separately via `/task/{id}/total-time-estimate`.
        - The `user_id` must be an assignable worker of the task's tasklist; otherwise 403 / 404 depending on ACL.
      operationId: setUserTimeEstimate
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - minutes
              properties:
                minutes:
                  type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
    delete:
      tags:
        - Tasks
      summary: Remove per-user time estimate
      description: |
        Clears the per-user time estimate of the given user on this task.

        **Behavior notes:**
        - The task's total estimate is unaffected.
        - Idempotent: calling on a missing estimate returns 200.
      operationId: deleteUserTimeEstimate
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  # ==================== SUBTASKS ====================
  /task/{task_id}/subtasks:
    get:
      tags:
        - Subtasks
      summary: Get subtasks (taskchecks) of a task
      description: |
        Paginated list of subtasks ("taskchecks") under the given task. Subtasks come in two flavors — **smart taskchecks** (full tasks with their own worker, due date, comments) and **simple taskchecks** (checklist items with just a label). The response represents both uniformly.

        **Use cases:**
        - Rendering the subtask checklist inside a task view
        - Iterating subtasks for completion reporting

        **Behavior notes:**
        - Use the `states_ids[]` filter (if supported by the schema) to narrow by active / finished subtasks.
        - Subtasks returned are ACL-filtered by the parent task's tasklist rules.
      operationId: getSubtasksInTask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          subtasks:
                            type: array
                            items:
                              $ref: '#/components/schemas/Subtask'
    post:
      tags:
        - Subtasks
      summary: Create subtask (smart or simple, auto-fallback)
      description: |
        Creates a subtask under the given task. The endpoint automatically picks the best representation.

        **Behavior notes (non-obvious):**
        - The server first attempts to create a **smart taskcheck** — a full-featured subtask with worker, due date, tracking users, etc.
        - If the parent task is not eligible for smart taskchecks (e.g. it's a multi-project parent, or a nested smart taskcheck), the code catches `SmartTaskcheckCanNotBeCreatedException` and silently falls back to creating a **simple taskcheck** (a checkbox item with just a name). The body you sent may be partially discarded in that case — extra fields like `worker`, `due_date`, `tracking_users_ids` are ignored for simple taskchecks.
        - `tracking_users_ids` is **ACL-filtered** — user IDs without access to the parent task's tasklist are silently removed from the set via `ITrackingUsersIdsPrepender::prependWithAcl()`.
        - If you need deterministic smart-taskcheck creation, verify the parent task's eligibility first (e.g. make sure it's not already a taskcheck itself).
      operationId: createSubtask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubtaskCreate'
      responses:
        '200':
          description: Subtask created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Subtask'

  # ==================== TASK LABELS ====================
  /task-labels/find-available:
    get:
      tags:
        - Task Labels
      summary: Get task labels usable by caller
      description: |
        Returns all task labels usable by the authenticated user — labels attached to tasks across the caller's owned and invited projects in `ACTIVE`, `ARCHIVED`, or `TEMPLATE` state.

        **Use cases:**
        - Populating a label picker before calling `/task-labels/add-to-task/{task_id}`
        - Showing the caller which task labels already exist so they don't create a duplicate via the name+color fetch-or-create modes

        **Behavior notes:**
        - Sorted by `name` ascending.
        - If the caller has no accessible projects, returns `{ "labels": [] }`.
      operationId: findAvailableTaskLabels
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  labels:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskLabel'

  /task-label-colors:
    get:
      tags:
        - Task Labels
      summary: List the accepted task-label colors
      description: Returns the fixed palette of colors accepted when creating or assigning task labels.
      operationId: getTaskLabelColors
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  colors:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskLabelColor'

  /task-labels:
    post:
      tags:
        - Task Labels
      summary: Bulk-create task labels in the caller's workspace
      description: |
        Creates task-label definitions (not assignments). Use this before `/task-labels/add-to-task/{taskId}` when you want to provision a set of labels upfront.

        **Use cases:**
        - Seeding a shared label palette during onboarding
        - Importing labels from another system

        **Behavior notes (non-obvious):**
        - This is a **fetch-or-create** — labels with an existing matching name are **re-used**, not duplicated. The endpoint only creates those that don't already exist.
        - The label is scoped to the caller's account and available across their accessible projects via the "task-labels-used" relation.
        - If the caller has **no projects at all**, the "used" relation is silently skipped (no `NoProjectsException` bubbles up to the caller).
        - The response does not explicitly report which labels were new vs. reused — query `/project-labels/find-available` or the task detail to verify.
      operationId: createTaskLabels
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                labels:
                  type: array
                  items:
                    type: object
                    properties:
                      name:
                        type: string
                      color:
                        type: string
      responses:
        '200':
          description: Labels created

  /task-labels/add-to-task/{task_id}:
    post:
      tags:
        - Task Labels
      summary: Attach labels to task (UUID or name+color)
      description: |
        Assigns one or more task labels to the given task. Labels can be addressed by UUID (existing) or by name/color (fetch-or-create).

        **Two input modes per label:**
        1. **UUID only** — assigns an existing label by UUID as-is.
        2. **Name-based** — provide `name` (required), optionally `color` and `uuid`. If `color` is omitted it defaults to `#77787a` (gray). If `uuid` is omitted it is auto-generated. An existing label is **reused when both name AND color match**; otherwise a new label is created.

        **Use cases:**
        - Tagging a task with a known label from the palette (UUID mode — reliable, no ambiguity)
        - Quick-labeling by name from an integration without pre-creating labels (name mode — fetch-or-create)

        **Behavior notes (non-obvious):**
        - Name+color matching is **case-sensitive**. `"bug"` and `"Bug"` are different labels.
        - If you pass a UUID that doesn't match any existing label, `CannotCreateWithProvidedUuidException` is thrown.
        - When labels are actually added (vs. already present), a `task_labels_change` event is emitted (→ webhooks, audit log).
        - Calling with an empty array short-circuits — no event, no ACL check, 200 response.
        - Bad colors return 400 with `Unsupported color (X) provided.` using the server's color enum.
      operationId: addTaskLabelsToTask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - labels
              properties:
                labels:
                  type: array
                  items:
                    $ref: '#/components/schemas/TaskLabelAddInput'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /task-labels/remove-from-task/{task_id}:
    post:
      tags:
        - Task Labels
      summary: Detach labels from task
      description: |
        Removes one or more task labels from the given task. The label definitions themselves are **not** deleted globally — they remain available for reuse.

        **Three input modes per label:**
        1. **UUID** — removes the label identified by UUID.
        2. **Name only** — removes all labels with that name regardless of color (can affect multiple labels).
        3. **Name + color** — removes only the label matching **both** name and color.

        **Use cases:**
        - Cleanup after a relabeling operation
        - Removing a mis-assigned label

        **Behavior notes (non-obvious):**
        - **Name-only mode is aggressive** — it removes every label with that name, even if they have different colors. Use name+color or UUID if you want precision.
        - Emits a `task_labels_change` event only if the task's label set actually changed.
        - An empty `labels` array short-circuits: no ACL check, no event, 200.
      operationId: removeTaskLabelsFromTask
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - labels
              properties:
                labels:
                  type: array
                  items:
                    $ref: '#/components/schemas/TaskLabelRemoveInput'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  # ==================== COMMENTS ====================
  /task/{task_id}/comments:
    post:
      tags:
        - Comments
      summary: Add a comment to a task
      description: |
        Posts a new comment on the given task. Text is passed as `content` (HTML / plain text); attachments are passed as `files` (referencing previously uploaded file UUIDs).

        **Use cases:**
        - Logging progress or questions on a task
        - Posting automated status updates from integrations
        - Attaching supporting files to a task body

        **Behavior notes (non-obvious):**
        - **If the task has no comments yet, this call creates the task's description instead of a regular comment** (the `ICommentIsDescriptionFiller` auto-flips `is_description=true` on the first comment). From the second comment onward this endpoint behaves like a normal comment.
        - Subsequent calls are always regular comments; the description is managed separately via `/task/{id}/description`.
        - Fires notifications to the task's tracking users and a `comment_created` event.

        **Mentioning users:**
        Embed a mention in `content` as an HTML span:
        `<span data-freelo-mention="1" data-freelo-user-id="{id}">@{mention_key}</span>`
        where `{id}` is the user's id and `{mention_key}` is the visible text after `@` (the normalized `fullname` — whitespace stripped, diacritics removed). Get both from the user's `UserBasic` object (e.g. `GET /users/me`). The mentioned user is notified.
      operationId: createComment
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                content:
                  type: string
                  description: |
                    Comment body (HTML / plain text). To mention a user, embed a span:
                    `<span data-freelo-mention="1" data-freelo-user-id="{id}">@{mention_key}</span>`
                    (`id` and `mention_key` come from the user's `UserBasic` object, e.g. `GET /users/me`).
                files:
                  type: array
                  items:
                    $ref: '#/components/schemas/FileUpload'
      responses:
        '200':
          description: Comment created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Comment'

  /comment/{comment_id}:
    post:
      tags:
        - Comments
      summary: Edit an existing comment
      description: |
        Overwrites the text and / or attachments of an existing comment.

        **Use cases:**
        - Correcting typos in a posted comment
        - Updating a status comment with new info

        **Behavior notes:**
        - `files` replaces the full attachment set — pass the complete list of file UUIDs you want attached, not a delta.
        - ACL: only the comment's author can edit (or project owner / commander depending on role rules). Otherwise 404 `NotFoundException` is returned (not 403, to avoid leaking the existence of inaccessible comments).
        - The method used is `POST` for historical reasons, not `PUT`/`PATCH`.
      operationId: editComment
      parameters:
        - name: comment_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                content:
                  type: string
                files:
                  type: array
                  items:
                    $ref: '#/components/schemas/FileUpload'
      responses:
        '200':
          description: Comment updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Comment'

  /all-comments:
    get:
      tags:
        - Comments
      summary: Get all comments (paginated, filterable)
      description: |
        Global comment feed across all accessible projects / tasks / files / docs / links. Supports filtering by `type` (task comments, document comments, etc.).

        **Use cases:**
        - Activity-feed widgets
        - Auditing recent comments across a portfolio
        - Extracting comment history for AI summarization

        **Behavior notes:**
        - Response is ACL-filtered — only comments on entities the caller can read are returned.
        - Default order: `date_add desc` (newest first).
        - `type=all` is the default and combines every comment category; narrow down as needed.
      operationId: getAllComments
      parameters:
        - name: projects_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - name: type
          in: query
          description: Comment type
          schema:
            type: string
            enum: [all, task, document, file, link]
            default: all
        - name: order_by
          in: query
          schema:
            type: string
            enum: [date_add, date_edited_at]
            default: date_add
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          comments:
                            type: array
                            items:
                              $ref: '#/components/schemas/CommentFull'

  # ==================== TIME TRACKING ====================
  /timetracking/start:
    post:
      tags:
        - Time Tracking
      summary: Start a time tracking session
      description: |
        Starts a new timer ("work running") for the authenticated caller. Each user may have **at most one** active session at any time.

        **Use cases:**
        - User begins working on a task
        - Integration starts timer when a ticket moves to "in progress"

        **Behavior notes (non-obvious):**
        - Only one running session per user — attempting to start while one is already running returns HTTP **409 Conflict** with message `"Timetracking is already running."`. Call `/timetracking/stop` first (or `/timetracking/edit` to reassign the current session).
        - All body fields are **optional**. `task_id` is nullable — you can track general work not tied to a specific task.
        - `date_reported` defaults to "now" (server time) if not provided. Passing an explicit `date_reported` backdates the session's start time.
        - Returns the UUID of the newly created running-work record.
      operationId: startTimeTracking
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                task_id:
                  type: integer
                  nullable: true
                  description: ID of the task to track time for. Optional — if not provided, time tracking starts without a task assignment.
                note:
                  type: string
                  nullable: true
                  description: Optional note for the time tracking session.
      responses:
        '200':
          description: Time tracking started successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  uuid:
                    type: string
                    format: uuid
                    description: UUID of the created time tracking session
        '409':
          description: Time tracking is already running for this user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /timetracking/stop:
    post:
      tags:
        - Time Tracking
      summary: Stop the running time tracking session
      description: |
        Stops the caller's currently running timer and converts it into a finalized **work report**. Returns the resulting work report.

        **Use cases:**
        - User ends work on a task
        - Integration stops timer on ticket closure

        **Behavior notes:**
        - No request body. The endpoint always targets the caller's own active session (one per user).
        - Returns HTTP **409 Conflict** with `"Timetracking is not running."` when no session is active.
        - The produced work report inherits the task, note, and `date_reported` set at start / edit time. Minutes are computed from the start time to now.
      operationId: stopTimeTracking
      responses:
        '200':
          description: Time tracking stopped and work report created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkReport'
        '409':
          description: No time tracking is currently running for this user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /timetracking/edit:
    post:
      tags:
        - Time Tracking
      summary: Edit the running time tracking session
      description: |
        Updates the caller's currently running session — typically used to switch the tracked task or change the note mid-flight without losing elapsed time.

        **Use cases:**
        - Switching context (another task) without stopping + starting
        - Fixing a wrong `note` entered at start
        - Reassigning general (no-task) tracking to a task

        **Behavior notes:**
        - There is no session ID — the endpoint always targets the caller's single active session.
        - Returns HTTP **409 Conflict** with `"Timetracking is not running."` if no session is active.
        - Setting `task_id=null` disassociates the session from any task (continues as general work).
        - All body fields are partial — only fields present in the payload are updated; omitted fields keep their current value.
        - Passing `date_reported` rewrites the session's start time (e.g. to backdate a forgotten timer). Elapsed duration on `/timetracking/stop` is computed from this value to "now".
      operationId: editTimeTracking
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                task_id:
                  type: integer
                  nullable: true
                  description: ID of the task to reassign the session to. Can be used to switch tasks on an active session.
                note:
                  type: string
                  nullable: true
                  description: Updated note for the time tracking session.
                date_reported:
                  type: string
                  format: date-time
                  description: New start timestamp for the running session (ISO 8601, e.g. `2026-05-07T09:00:00+02:00`). Used to correct or backdate the timer's start time. The work report produced by `/timetracking/stop` will compute its duration from this value.
                  example: '2026-05-07T09:00:00+02:00'
      responses:
        '200':
          description: Time tracking session updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  uuid:
                    type: string
                    format: uuid
                    description: UUID of the updated time tracking session
        '409':
          description: No time tracking is currently running for this user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /timetracking/status:
    get:
      tags:
        - Time Tracking
      summary: Get current time tracking status
      description: |
        Returns the caller's currently running session with task context, note, labels, billability, and cost info.

        **Use cases:**
        - Polling whether the user is currently tracking time
        - Rendering a "you are tracking X since HH:MM" indicator
        - Verifying state before issuing a `/timetracking/edit` / `/stop`

        **Behavior notes (non-obvious):**
        - **Returns HTTP 204 No Content** when no session is active — **not 404 and not a 200 with empty body**. Callers must treat 204 as a valid "nothing running" state.
        - `cost`, `is_cost_fixed`, `is_billable`, `project_setting` reflect what would land in a work report if stopped right now.
      operationId: getTimeTrackingStatus
      responses:
        '200':
          description: Active time tracking session details.
          content:
            application/json:
              schema:
                type: object
                properties:
                  uuid:
                    type: string
                    format: uuid
                    description: UUID of the active time tracking session
                  date_reported:
                    type: string
                    format: date-time
                    description: 'Timestamp when the session was started. Naive ISO8601 in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                    example: '2026-04-24T11:12:38'
                  task:
                    nullable: true
                    description: Associated task, or null if tracking without a task
                    type: object
                    properties:
                      id:
                        type: integer
                      name:
                        type: string
                      project:
                        type: object
                        nullable: true
                        properties:
                          id:
                            type: integer
                          name:
                            type: string
                      tasklist:
                        type: object
                        nullable: true
                        properties:
                          id:
                            type: integer
                          name:
                            type: string
                  note:
                    type: string
                    nullable: true
                    description: Note associated with the session
                  cost:
                    type: object
                    description: Cost information
                  is_cost_fixed:
                    type: boolean
                  labels:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                  is_billable:
                    type: boolean
                  project_setting:
                    type: object
                    nullable: true
                    description: Project setting for the associated task's project
        '204':
          description: No time tracking session is currently running.

  # ==================== WORK REPORTS ====================
  /work-reports:
    get:
      tags:
        - Work Reports
      summary: Get work reports (paginated, filterable)
      description: |
        Returns work reports — finalized time entries — filtered by project, user, task, label, or date.

        **Use cases:**
        - Time-sheet exports
        - Billing calculations (combine with `currency` filter)
        - Utilization reporting per user

        **Behavior notes (non-obvious):**
        - `currency` defaults to `CZK` if not specified — costs are converted to the chosen currency for comparability across projects. Always pass it explicitly if mixing multiple currencies.
        - `with_own_taskless=true` **automatically scopes the query to the caller's own user** — i.e. it implicitly adds caller to `users_ids[]`. If you need all users' taskless reports, you need elevated permissions and a different endpoint (not exposed here).
        - `tasks_labels[]` accepts label UUIDs, not names or IDs.
        - `date_edited_from` returns reports with `date_edited >= value` — useful for incremental sync.
      operationId: getWorkReports
      parameters:
        - name: projects_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - name: users_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - name: tasks_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - name: tasks_labels[]
          in: query
          description: UUIDs for task labels
          schema:
            type: array
            items:
              type: string
              format: uuid
        - name: date_reported_range[date_from]
          in: query
          schema:
            type: string
            format: date
        - name: date_reported_range[date_to]
          in: query
          schema:
            type: string
            format: date
        - name: date_add_range[date_from]
          in: query
          schema:
            type: string
            format: date
        - name: date_add_range[date_to]
          in: query
          schema:
            type: string
            format: date
        - name: date_edited_from
          in: query
          schema:
            type: string
            format: date
        - name: with_own_taskless
          in: query
          description: >-
            Include the authenticated user's work reports without an associated
            task. Automatically filters by the authenticated user. Pass `1` to
            enable, `0` to disable — string values like `true`/`false` are not
            accepted and silently fall back to the default.
            Mutually exclusive with `users_ids[]`.
          schema:
            type: integer
            enum: [0, 1]
            default: 0
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          reports:
                            type: array
                            items:
                              $ref: '#/components/schemas/WorkReportFull'

  /task/{task_id}/work-reports:
    post:
      tags:
        - Work Reports
      summary: Log a work report on a task
      description: |
        Creates a finalized work report (time entry) directly on a task — bypassing the timetracking flow. Useful for retroactively logging work.

        **Use cases:**
        - Logging hours after the fact (Monday morning timesheet entry)
        - Importing time data from external timesheet systems
        - Manual adjustments on behalf of another worker

        **Behavior notes:**
        - `worker_id` defaults to the caller if omitted. To log time for a different user, the caller must be the project's owner / commander / have reporting rights; otherwise `WorkerHasNoAccessToTasklistException` → 400.
        - `date_reported` defaults to today if omitted; pass an explicit date to backdate.
        - `cost` uses the string currency-amount format (e.g. `"100025"` = 1000.25). If omitted, the server derives it from the worker's hourly rate × minutes.
        - 400 `WorkReportCanNotBeCreatedException` fires if the combination (project state, tasklist ACL) disallows logging time.
      operationId: createWorkReport
      parameters:
        - $ref: '#/components/parameters/TaskIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - minutes
              properties:
                date_reported:
                  type: string
                  description: |
                    Start timestamp of the work session. Accepts either a full ISO 8601 datetime
                    (`2026-05-08T08:00:00+02:00`) or a date-only string (`2026-05-08`).
                    Datetime form records the exact start moment; date-only defaults to start of day.
                  example: '2026-05-08T08:00:00+02:00'
                worker_id:
                  type: integer
                minutes:
                  type: integer
                cost:
                  type: string
                  description: "Currency amount (2 decimal places × 100)"
                note:
                  type: string
      responses:
        '200':
          description: Work report created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkReport'

  /work-reports/{work_report_id}:
    post:
      tags:
        - Work Reports
      summary: Edit an existing work report
      description: |
        Updates minutes, cost, date, note, or re-targets the report at a different task.

        **Use cases:**
        - Fixing a mistyped duration
        - Reassigning a work report to the correct task
        - Adjusting billable cost manually

        **Behavior notes:**
        - `task_id` can be changed to **re-parent** the report to a different task — ACL is re-checked against the new task.
        - ACL rules: the report author and the project owner/commander can edit; other users get `NotFoundException` (hiding existence).
        - If the report's parent project has been marked as invoiced, edits may be blocked — see `/issued-invoice/{id}/mark-as-invoiced`.
      operationId: editWorkReport
      parameters:
        - name: work_report_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                minutes:
                  type: integer
                cost:
                  type: string
                date_reported:
                  type: string
                  description: |
                    Start timestamp of the work session. Accepts either a full ISO 8601 datetime
                    (`2026-05-08T08:00:00+02:00`) or a date-only string (`2026-05-08`).
                    Datetime form records the exact start moment; date-only defaults to start of day.
                  example: '2026-05-08T08:00:00+02:00'
                note:
                  type: string
                  nullable: true
                  description: Note for the work report. Pass `null` to clear an existing note. Empty string is rejected.
                task_id:
                  type: integer
                  nullable: true
                  description: ID of the task this work report belongs to. Pass `null` to detach the report from its current task.
      responses:
        '200':
          description: Work report updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkReport'
    delete:
      tags:
        - Work Reports
      summary: Delete a work report
      description: |
        Permanently removes a work report.

        **Use cases:**
        - Correcting duplicate entries
        - Removing a report logged by mistake

        **Behavior notes:**
        - ACL: only the report author or project admin (owner / commander) can delete. Unauthorized callers get 400 with `UserCannotDeleteWorkReport` message (not 403).
        - If the report is tied to a project that has been **marked as invoiced**, the deletion may be refused — invoices freeze underlying reports.
      operationId: deleteWorkReport
      parameters:
        - name: work_report_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  # ==================== INVOICING ====================
  /issued-invoices:
    get:
      tags:
        - Invoicing
      summary: List issued invoices (paginated)
      description: |
        Returns a paginated list of invoice draft groups ("issued invoices") — each representing a set of work reports grouped for billing on a project in a date range.

        **Use cases:**
        - Displaying an invoicing queue in a client-facing UI
        - Exporting unbilled-but-ready batches to an external accounting tool

        **Behavior notes:**
        - Scope: only invoices from projects the caller has billing access to.
        - Filtering by `date_range` narrows to invoices whose reporting period overlaps the range (not by `date_issued`).
      operationId: getIssuedInvoices
      parameters:
        - name: date_range[date_from]
          in: query
          schema:
            type: string
            format: date
        - name: date_range[date_to]
          in: query
          schema:
            type: string
            format: date
        - name: projects_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          issued_invoices:
                            type: array
                            items:
                              $ref: '#/components/schemas/IssuedInvoice'

  /issued-invoice/{invoice_id}:
    get:
      tags:
        - Invoicing
      summary: Get an issued invoice detail
      description: |
        Full detail of a single issued invoice — total amount, currency, period, linked project, and metadata.

        **Use cases:**
        - Opening an invoice in the billing UI
        - Fetching metadata to pre-fill an external accounting entry
      operationId: getIssuedInvoiceDetail
      parameters:
        - name: invoice_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuedInvoiceDetail'

  /issued-invoice/{invoice_id}/reports:
    get:
      tags:
        - Invoicing
      summary: Download invoice's work reports as CSV
      description: |
        Streams a **CSV download** of all work reports included in the invoice. Content-Type is `text/csv`.

        **Use cases:**
        - Attaching the work-report backup to a physical invoice
        - Manual review before marking invoiced

        **Behavior notes:**
        - This is a file download — the body is not JSON. Response framework sets a Content-Disposition header with a server-chosen filename.
        - For programmatic access prefer `/reports-json` which returns structured data.
      operationId: downloadIssuedInvoiceReports
      parameters:
        - name: invoice_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: CSV file
          content:
            text/csv:
              schema:
                type: string
                format: binary

  /issued-invoice/{invoice_id}/reports-json:
    get:
      tags:
        - Invoicing
      summary: Get invoice's work reports as JSON
      description: |
        Returns the work reports composing the invoice as a JSON array — programmatic equivalent of the `/reports` CSV endpoint.

        **Use cases:**
        - Feeding invoice data into an external accounting or BI system
        - Rendering a detail breakdown in a custom UI
      operationId: getIssuedInvoiceReportsJson
      parameters:
        - name: invoice_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: JSON array of work report rows
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WorkReportExtended'

  /issued-invoice/{invoice_id}/mark-as-invoiced:
    post:
      tags:
        - Invoicing
      summary: Mark invoice as actually invoiced (via external app)
      description: |
        Records that an external invoicing tool (Fakturoid, iDoklad, etc.) has issued the real invoice. Associates the invoice with the external URL + subject and freezes the underlying work reports so they can't be edited or re-billed.

        **Use cases:**
        - After creating an invoice in an external accounting tool, close the loop in Freelo
        - Prevent accidental re-invoicing of already billed work

        **Behavior notes (non-obvious):**
        - This action is **not reversible** via API. Once marked, the underlying work reports are effectively frozen — edit / delete calls on those reports may be refused.
        - `url` is stored verbatim; it should point to the external invoice detail.
        - `subject` is the display title shown in Freelo's billing UI.
      operationId: markAsInvoiced
      parameters:
        - name: invoice_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
                - subject
              properties:
                url:
                  type: string
                  format: uri
                subject:
                  type: string
      responses:
        '200':
          description: Invoice marked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuedInvoiceDetail'

  # ==================== USERS ====================
  /users:
    get:
      tags:
        - Users
      summary: Get all coworkers visible to caller
      description: |
        Paginated list of users the authenticated caller shares at least one **active** project with — effectively their current "coworkers book".

        **Use cases:**
        - Populating an assignee picker outside of a project context
        - Building a company-wide people directory scoped to the caller's network
        - Resolving email → user ID for offline users

        **Behavior notes:**
        - Returns only **active sharing connections**: users who are currently workers or owners of at least one of the caller's active projects (workers' user accounts must also be in active state).
        - Archived / deleted / template projects are **not** considered when computing the result set.
        - Historical co-workers (people the caller used to share a project with but no longer does) are **not** returned. For a full historical view of past collaborators, use the Freelo web application.
      operationId: getAllUsers
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          users:
                            type: array
                            items:
                              $ref: '#/components/schemas/UserWithEmail'

  /users/project-manager-of:
    get:
      tags:
        - Users
      summary: List users who made me their project manager
      description: |
        Returns the list of users (project owners) who have promoted the authenticated caller to be **their** project manager. Use to discover on whose behalf the caller may act across projects.

        **Use cases:**
        - UI "Acting on behalf of" selector for PMs managing multiple clients
        - Scoping automations to users the caller has delegated authority for
      operationId: getProjectManagerOf
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/UserBasic'

  /users/manage-workers:
    post:
      tags:
        - Users
      summary: Invite users (by email or ID) to one or more projects
      description: |
        Invites existing users (by `users_ids`) and/or external people (by `emails`) to one or more projects. Emails for users who don't exist yet trigger user creation.

        **Use cases:**
        - Onboarding a batch of new teammates to a shared project
        - Granting access to an existing teammate in additional projects
        - Programmatic project invitations from an HR / CRM integration

        **Behavior notes (non-obvious):**
        - Exactly one of `emails` or `users_ids` must be non-empty. Sending both empty → `400` with message "At least one of the following fields must be filled: emails, users_ids".
        - When `users_ids` is non-empty, `projects_ids` **must** also be non-empty (you can't invite an existing user to "nothing"). For email-only invitations, `projects_ids` is still required logically because invites target projects.
        - Emails that do not match any existing user trigger **user creation** — the new users are returned in `newly_created_users`. This is the primary way to provision external collaborators.
        - `acl_tasklists` scopes the invitation to a subset of tasklists in the target projects (ACL workers). Omit to grant full project access.
        - The endpoint enforces `api_only_invite=true` internally — plan-limit side effects match the same flow as email-based invites from the UI. Exceeding the account's user-seat plan throws `PlanExceededException` (429/403 depending on context).
        - The `removed_users_from_projects` key in the response is populated only when some ACL adjustment implicitly removed workers (e.g. narrowing tasklist ACLs). It is not used for "deletion" requests.
      operationId: inviteUsersToProjects
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - projects_ids
              properties:
                projects_ids:
                  type: array
                  items:
                    type: integer
                emails:
                  type: array
                  items:
                    type: string
                    format: email
                users_ids:
                  type: array
                  items:
                    type: integer
      responses:
        '200':
          description: Users invited
          content:
            application/json:
              schema:
                type: object
                properties:
                  newly_invited_users_to_projects:
                    type: array
                    items:
                      type: object
                  newly_created_users:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        email:
                          type: string
                  newly_invited_users:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        projects_ids:
                          type: array
                          items:
                            type: integer
                        email:
                          type: string
                  removed_users_from_projects:
                    type: array
                    items:
                      type: object

  /user/{user_id}/out-of-office:
    get:
      tags:
        - Users
      summary: Get out-of-office status of a user
      description: |
        Returns the out-of-office (OOO) period set for the given user, or `null` if they are not currently marked as away.

        **Use cases:**
        - Showing OOO badge next to assignees
        - Letting automations re-route assignments while the user is away

        **Behavior notes:**
        - The caller must be the **target user themselves** or a valid coworker (share at least one project). Otherwise 404 — the endpoint does **not** differentiate "not found" from "not authorized" to avoid user enumeration.
        - Dates are returned in UTC.
      operationId: getOutOfOffice
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  out_of_office:
                    type: object
                    nullable: true
                    properties:
                      date_from:
                        type: string
                        format: date-time
                        description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                        example: '2026-04-24T11:12:38'
                      date_to:
                        type: string
                        format: date-time
                        description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                        example: '2026-04-24T11:12:38'
    post:
      tags:
        - Users
      summary: Enable / overwrite user's out-of-office
      description: |
        Sets or overwrites the out-of-office window for the given user.

        **Use cases:**
        - Self-service: user sets their own vacation
        - HR / PM sets a coworker's OOO on their behalf

        **Behavior notes:**
        - ACL: caller must be the target user or a valid coworker (same rule as GET).
        - Calling this on a user who already has an OOO **overwrites** the existing window (not appended).
        - `date_to` must be `>= date_from`, otherwise 400.
        - Dates are stored normalized to UTC.
      operationId: enableOutOfOffice
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - out_of_office
              properties:
                out_of_office:
                  type: object
                  required:
                    - date_from
                    - date_to
                  properties:
                    date_from:
                      type: string
                      format: date-time
                      description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                      example: '2026-04-24T11:12:38'
                    date_to:
                      type: string
                      format: date-time
                      description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                      example: '2026-04-24T11:12:38'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
    delete:
      tags:
        - Users
      summary: Disable user's out-of-office
      description: |
        Clears the user's OOO window. Idempotent — calling on a user who is not OOO returns 200.

        **Use cases:**
        - Ending OOO early when the user returns sooner than planned
        - Cleanup flows after a scheduled OOO has elapsed

        **Behavior notes:**
        - Same ACL as GET/POST on this path: caller must be the target user or a valid coworker.
      operationId: disableOutOfOffice
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  # ==================== NOTIFICATIONS ====================
  /all-notifications:
    get:
      tags:
        - Notifications
      summary: Get my notifications (paginated, filterable)
      description: |
        Paginated list of notifications addressed to the authenticated caller.

        **Use cases:**
        - Rendering the notifications dropdown
        - Digest / email summaries
        - Integrations that mirror Freelo activity into Slack / Teams

        **Behavior notes:**
        - Notifications are **always scoped to the caller** — there is no way to read another user's notifications through this endpoint.
        - `users_ids[]` filters by **authors** of the notification-triggering events, not recipients.
        - `teams_uuids[]` filters by the team context of the notification (e.g. all notifications from Team X).
        - `only_unread=true` is useful for badge counters.
        - Default order: `date_add desc` (newest first).
      operationId: getAllNotifications
      parameters:
        - name: projects_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - name: users_ids[]
          in: query
          description: Authors of notifications
          schema:
            type: array
            items:
              type: integer
        - name: teams_uuids[]
          in: query
          schema:
            type: array
            items:
              type: string
              format: uuid
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
        - name: notification_types[]
          in: query
          schema:
            type: array
            items:
              type: string
        - name: only_unread
          in: query
          description: >-
            Only return unread notifications. Pass `1` to enable, `0` to
            disable — string values like `true`/`false` are not accepted and
            silently fall back to the default.
          schema:
            type: integer
            enum: [0, 1]
            default: 0
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          notifications:
                            type: array
                            items:
                              $ref: '#/components/schemas/Notification'

  /notification/{notification_id}/mark-as-read:
    post:
      tags:
        - Notifications
      summary: Mark a notification as read
      description: |
        Marks a single notification (addressed to the caller) as read.

        **Use cases:**
        - Acknowledging an item from the bell dropdown
        - Auto-read flow in an integration when a user interacts with the linked entity

        **Behavior notes:**
        - Idempotent — calling on an already-read notification returns 200.
        - 404 if the notification does not exist **or** does not belong to the caller.
      operationId: markNotificationAsRead
      parameters:
        - name: notification_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /notification/{notification_id}/mark-as-unread:
    post:
      tags:
        - Notifications
      summary: Mark a notification as unread
      description: |
        Reverts a previous "read" on a notification — re-surfaces it in the unread feed.

        **Use cases:**
        - Restoring a notification the user wants to revisit
        - Bulk "unread" automation after a snooze

        **Behavior notes:**
        - Idempotent; 404 if notification does not belong to caller.
      operationId: markNotificationAsUnread
      parameters:
        - name: notification_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  # ==================== EVENTS ====================
  /events:
    get:
      tags:
        - Events
      summary: Get activity events (audit log)
      description: |
        Paginated activity feed across projects, tasks, users, and event types the caller has access to. This is effectively the audit / history log.

        **Use cases:**
        - Activity timeline UI
        - Webhook-like polling for "what changed since last sync"
        - Reconstructing a project's history

        **Behavior notes (non-obvious):**
        - The caller's accessible `projects_ids`, `users_ids`, and allowed event types are **injected implicitly** into the filter as a safety net. Requests filtered for projects / users / types the caller can't see are silently constrained; you won't see data you shouldn't, even if you pass the IDs explicitly.
        - Use `/events-types` (see `Events:findTypes` in the router) to discover valid `events_types[]` values.
        - Default order: `date desc` (newest first). No sort-by-type support.
      operationId: getAllEvents
      parameters:
        - name: projects_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - name: users_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - name: events_types[]
          in: query
          schema:
            type: array
            items:
              type: string
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
        - name: date_range[date_from]
          in: query
          schema:
            type: string
            format: date
        - name: date_range[date_to]
          in: query
          schema:
            type: string
            format: date
        - name: tasks_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          events:
                            type: array
                            items:
                              $ref: '#/components/schemas/Event'

  # ==================== FILES ====================
  /file/{file_uuid}:
    get:
      tags:
        - Files
      summary: Download a file by UUID
      description: |
        Streams the raw file content (any MIME type) identified by UUID. ACL-checked: the caller must have access to a project the file belongs to.

        **Use cases:**
        - Rendering an attachment preview in a client
        - Proxying file downloads through an integration

        **Behavior notes:**
        - Content-Type is derived from the stored MIME type. Content-Disposition header carries the original filename.
        - Returns 404 if the file does not exist, was deleted, or the caller has no access to any project it is attached to.
      operationId: downloadFile
      parameters:
        - name: file_uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: File content
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary

  /file/upload:
    post:
      tags:
        - Files
      summary: Upload a file (multipart)
      description: |
        Uploads a single file (**max 100 MB**). Returns a UUID that can be referenced from other endpoints.

        **Use cases:**
        - Attaching an image to a comment: embed `<a data-freelo-uuid="{uuid}">caption</a>` in the comment content
        - Attaching files to a task description via `POST /task/{id}/description`
        - Pinning files to a project

        **Behavior notes:**
        - The upload does **not** automatically attach the file anywhere — it produces a UUID you then reference.
        - `multipart/form-data` is mandatory; JSON body is rejected.
        - Size / content-type checks come from `FileUploadChecker`; oversize or forbidden types return 400.
      operationId: uploadFile
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: File uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  uuid:
                    type: string
                    format: uuid

  /all-docs-and-files:
    get:
      tags:
        - Files
      summary: List docs, files, links, and directories (paginated)
      description: |
        Global listing of **all four types of project assets** — directories, links, files, and documents — across accessible projects. Use `type` to narrow to a single category.

        **Use cases:**
        - Building a cross-project "library" search
        - Export / backup of non-task assets

        **Behavior notes:**
        - ACL-filtered by the caller's visible projects.
        - Without `projects_ids[]`, spans every project the caller can see.
      operationId: getAllDocsAndFiles
      parameters:
        - name: projects_ids[]
          in: query
          schema:
            type: array
            items:
              type: integer
        - name: type
          in: query
          schema:
            type: string
            enum: [directory, link, file, document]
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PageAliasParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          items:
                            type: array
                            items:
                              $ref: '#/components/schemas/FileItem'

  # ==================== STATES ====================
  /states:
    get:
      tags:
        - States
      summary: List all state definitions (active/archived/template/deleted…)
      description: |
        Returns the reference list of entity states used across Freelo (projects, tasks, tasklists) — a static enumeration you can use to interpret numeric state IDs in other endpoints.

        **Use cases:**
        - Translating state IDs returned by list endpoints into human-readable names in a UI
        - Building filter pickers that use `state_id` (tasks) or `states_ids[]` (projects)

        **Behavior notes:**
        - This is a global lookup; response is the same for every caller. Cache aggressively on the client.
      operationId: getAllStates
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  states:
                    type: array
                    items:
                      $ref: '#/components/schemas/State'

  # ==================== CUSTOM FIELDS ====================
  /custom-field/get-types:
    get:
      tags:
        - Custom Fields
      summary: List supported custom-field types
      description: |
        Returns the catalog of custom-field type definitions (e.g. text, number, enum) available for use in `POST /custom-field/create/{project_id}`. Response includes the UUID of each type — that's the value you pass in `type` when creating a custom field.

        **Use cases:**
        - Populating a type picker in a custom-field creation UI
        - Validating `type` UUIDs before sending a create request
      operationId: getCustomFieldTypes
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  custom_field_types:
                    type: array
                    items:
                      type: object
                      properties:
                        uuid:
                          type: string
                          format: uuid
                        name:
                          type: string

  /custom-field/create/{project_id}:
    post:
      tags:
        - Custom Fields
      summary: Define a custom field on a project
      description: |
        Creates a custom-field definition (column) on the specified project. Tasks in this project then expose this field in their custom field values.

        **Use cases:**
        - Adding "Estimated points", "Client ID", "Severity" fields to projects
        - Structured metadata for reporting

        **Behavior notes (non-obvious):**
        - Caller must be a **project commander** of the target project. Otherwise `UserIsNotProjectCommander` → 403.
        - `type` is a **UUID referencing a predefined type** from `GET /custom-field/get-types` (text / number / enum). Invalid UUIDs → 404.
        - If `uuid` is provided in the body it is honored (useful for reproducible provisioning); otherwise the server generates one.
        - Plan limits apply — creating a field beyond the account's allowance throws `PlanExceededException` (typically 402/429).
      operationId: createCustomField
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - type
              properties:
                uuid:
                  type: string
                  format: uuid
                name:
                  type: string
                type:
                  type: string
                  format: uuid
                  description: |
                    UUID of custom field type:
                    - `2f7bfe3a-c950-470e-b910-95b4caf5dc4f` - text
                    - `b1e56fa9-a97a-429b-8ab4-82bebe58933a` - number
                    - `f9729a8f-d340-40e4-b2c0-dc46c37e18ce` - enum
      responses:
        '200':
          description: Custom field created
          content:
            application/json:
              schema:
                type: object
                properties:
                  custom_field:
                    $ref: '#/components/schemas/CustomField'

  /custom-field/rename/{uuid}:
    post:
      tags:
        - Custom Fields
      summary: Rename a custom field
      description: |
        Changes the display name of an existing custom field.

        **Behavior notes:**
        - ACL: caller must be project commander of the field's project; otherwise 403.
        - The field's UUID / type are immutable via this endpoint; only `name` changes.
      operationId: renameCustomField
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
      responses:
        '200':
          description: Custom field renamed
          content:
            application/json:
              schema:
                type: object
                properties:
                  custom_field:
                    $ref: '#/components/schemas/CustomField'

  /custom-field/delete/{uuid}:
    delete:
      tags:
        - Custom Fields
      summary: Soft-delete custom field
      description: |
        Marks a custom-field definition as deleted. Existing task values of this field are preserved but hidden; can be restored via `/custom-field/restore/{uuid}`.

        **Use cases:**
        - Retiring an obsolete field without losing historical data

        **Behavior notes:**
        - Soft-delete; use `/restore` to undo.
        - ACL: requires project commander of the field's project.
      operationId: deleteCustomField
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /custom-field/restore/{uuid}:
    post:
      tags:
        - Custom Fields
      summary: Restore a soft-deleted custom field
      description: |
        Reverses a prior `/custom-field/delete`. Previously preserved values become visible again.

        **Behavior notes:**
        - 404 if the custom field doesn't exist or was never soft-deleted.
        - ACL: requires project commander.
      operationId: restoreCustomField
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Custom field restored
          content:
            application/json:
              schema:
                type: object
                properties:
                  custom_field:
                    $ref: '#/components/schemas/CustomField'

  /custom-field/add-or-edit-value:
    post:
      tags:
        - Custom Fields
      summary: Upsert scalar custom-field value on a task
      description: |
        Sets the value of a **non-enum** (text / number) custom field on a task. Upsert semantics — if the task already has a value for this `custom_field_uuid`, it is updated; otherwise a new value is created.

        **Use cases:**
        - Syncing structured metadata from an external system into Freelo tasks
        - Allowing a user to set / change a custom field value without knowing whether one already exists

        **Behavior notes (non-obvious):**
        - Matching key for upsert is the **pair (`task_id`, `custom_field_uuid`)**, not a UUID. Callers cannot specify the resulting value's UUID — it is generated server-side on create and preserved on update.
        - The task and the custom field must belong to the **same project** — otherwise HTTP 409 Conflict with `"Custom field is in the different project than the task."`.
        - For enum-typed custom fields, use `/custom-field/add-or-edit-enum-value` instead. Using this endpoint on an enum field produces unexpected `checker` validation errors.
        - Writes a custom-field-value-history row on every call (even no-op updates). Use sparingly for heavy polling loads.
        - Response is wrapped: `{ "custom_field_value": { ... } }`.
      operationId: addOrEditCustomFieldValue
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - custom_field_uuid
                - task_id
                - value
              properties:
                custom_field_uuid:
                  type: string
                  format: uuid
                task_id:
                  type: integer
                value:
                  type: string
      responses:
        '200':
          description: Value set
          content:
            application/json:
              schema:
                type: object
                properties:
                  custom_field_value:
                    $ref: '#/components/schemas/CustomFieldValue'

  /custom-field/add-or-edit-enum-value:
    post:
      tags:
        - Custom Fields
      summary: Upsert enum custom-field value on a task
      description: |
        Sets the value of an **enum-typed** custom field on a task by referencing one of the field's predefined enum options (by UUID). Upsert semantics — creates or updates based on the (`task_id`, `customFieldUuid`) pair.

        **Use cases:**
        - Selecting a dropdown / status value on a task programmatically
        - Bulk-updating categorical fields during data imports

        **Behavior notes (non-obvious):**
        - `value` is the **UUID of an enum option** (fetched from `/custom-field-enum/get-for-custom-field/{uuid}`), **not** the display string.
        - Field name casing mismatch: body uses camelCase `customFieldUuid` (unlike the scalar endpoint which uses snake_case `custom_field_uuid`). Matches the server's internal data key.
        - Same cross-project rule applies: custom field and task must be in the same project; otherwise 409.
        - If the enum option UUID does not exist or belongs to a different custom field, 404 with "Enum was not found.".
        - Response uses the camelCase key `customFieldEnum` for the value wrapper.
      operationId: addOrEditEnumValue
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customFieldUuid
                - task_id
                - value
              properties:
                customFieldUuid:
                  type: string
                  format: uuid
                task_id:
                  type: integer
                value:
                  type: string
                  format: uuid
                  description: UUID of enum option
      responses:
        '200':
          description: Enum value set
          content:
            application/json:
              schema:
                type: object
                properties:
                  customFieldEnum:
                    $ref: '#/components/schemas/CustomFieldEnumValue'

  /custom-field/delete-value/{uuid}:
    delete:
      tags:
        - Custom Fields
      summary: Remove a custom-field value from a task
      description: |
        Deletes the specific custom-field value by its UUID. The field definition itself and other tasks' values are unaffected.

        **Use cases:**
        - Clearing a field on a task (differentiating "no value" from "empty string")

        **Behavior notes:**
        - A history row is written capturing the previous value.
        - 404 if the value UUID doesn't exist or belongs to a deleted custom field.
      operationId: deleteCustomFieldValue
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /custom-field-enum/get-for-custom-field/{custom_field_uuid}:
    get:
      tags:
        - Custom Fields
      summary: List enum options of a custom field
      description: |
        Returns the list of enum options (dropdown values) defined for an enum-typed custom field. Use the returned `uuid`s as `value` in `/custom-field/add-or-edit-enum-value`.

        **Use cases:**
        - Populating a dropdown picker in a UI
        - Resolving display labels to option UUIDs when importing

        **Behavior notes:**
        - Only non-deleted options are returned; soft-deleted options are filtered out.
      operationId: getEnumOptionsForCustomField
      parameters:
        - name: custom_field_uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  custom_field_enum:
                    type: array
                    items:
                      $ref: '#/components/schemas/CustomFieldEnumOption'

  /custom-field-enum/create/{custom_field_uuid}:
    post:
      tags:
        - Custom Fields
      summary: Add an option to an enum custom field
      description: |
        Creates a new enum option (dropdown value) on an enum-typed custom field.

        **Use cases:**
        - Extending a "Status" dropdown with a new option ("Blocked", "In review")
        - Importing categorical data from an external system

        **Behavior notes:**
        - Caller-supplied `uuid` is respected if present; otherwise server-generated.
        - Calling on a non-enum custom field produces a validation error.
        - ACL: project commander.
      operationId: createEnumOption
      parameters:
        - name: custom_field_uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - value
              properties:
                uuid:
                  type: string
                  format: uuid
                value:
                  type: string
      responses:
        '200':
          description: Enum option created
          content:
            application/json:
              schema:
                type: object
                properties:
                  custom_field_enum:
                    type: object
                    properties:
                      uuid:
                        type: string
                        format: uuid
                      value:
                        type: string

  /custom-field-enum/change/{custom_field_enum_uuid}:
    post:
      tags:
        - Custom Fields
      summary: Rename an enum option
      description: |
        Updates the display `value` (label) of an enum option. The option's UUID is preserved, so any existing task values referencing it continue to work.

        **Use cases:**
        - Fixing a typo in a dropdown value across all tasks using it
        - Re-labeling a status category

        **Behavior notes:**
        - Only `value` is editable via this endpoint; the order and type are managed elsewhere.
      operationId: editEnumOption
      parameters:
        - name: custom_field_enum_uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - value
              properties:
                value:
                  type: string
      responses:
        '200':
          description: Enum option updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  custom_field_enum:
                    type: object
                    properties:
                      uuid:
                        type: string
                        format: uuid
                      value:
                        type: string

  /custom-field-enum/delete/{custom_field_enum_uuid}:
    delete:
      tags:
        - Custom Fields
      summary: Delete an unused enum option (safe)
      description: |
        Removes an enum option **only if no tasks currently reference it**.

        **Use cases:**
        - Safe cleanup of never-used options
        - Quality gating: if this succeeds, no data loss happened

        **Behavior notes (non-obvious):**
        - If the option is in use by any task value, the delete is **refused** with a `UserVisibleErrorMessageException`. To delete anyway (and null out the referencing values), use `/custom-field-enum/force-delete/{id}`.
        - Even non-used soft-deleted references may block deletion — inspect carefully.
      operationId: deleteEnumOption
      parameters:
        - name: custom_field_enum_uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /custom-field-enum/force-delete/{custom_field_enum_uuid}:
    delete:
      tags:
        - Custom Fields
      summary: Force-delete enum option (destructive)
      description: |
        Deletes the enum option even if it is currently used by task values. Referencing task values are cleared.

        **Use cases:**
        - Cleanup after a renamed / merged option that can't be resolved with a regular delete
        - Workspace housekeeping when preserving data in tasks is not required

        **Behavior notes (non-obvious):**
        - **Destructive:** any task value that referenced this option is cleared. The `custom_field_value_history` row is kept for audit, but the current value becomes null/empty.
        - There is no undo. Prefer `/custom-field-enum/delete` when in doubt.
        - ACL: project commander.
      operationId: forceDeleteEnumOption
      parameters:
        - name: custom_field_enum_uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

  /custom-field/find-by-project/{project_id}:
    get:
      tags:
        - Custom Fields
      summary: List custom fields of a project
      description: |
        Returns all custom-field definitions configured on a project, plus a boolean indicating whether the caller is the project's commander (relevant for the admin-level mutation endpoints in this tag).

        **Use cases:**
        - Rendering the custom-field columns on a task board
        - Deciding in UI whether to show "Create custom field" button (based on `is_commander`)

        **Behavior notes:**
        - Soft-deleted custom fields are excluded.
        - Includes enum fields with their options embedded.
      operationId: findCustomFieldsByProject
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  custom_fields:
                    type: array
                    items:
                      $ref: '#/components/schemas/CustomField'
                  is_commander:
                    type: boolean

  # ==================== NOTES ====================
  /project/{project_id}/note:
    post:
      tags:
        - Notes
      summary: Create a note in a project
      description: |
        Creates a project-level **note** (a Document entity internally — notes and documents share the same storage and presenter). Notes are rich-text blocks attached to the project rather than to a task.

        **Use cases:**
        - Capturing meeting minutes on a project
        - Storing shared reference docs without needing a separate doc tool

        **Behavior notes (non-obvious):**
        - Internally handled by `DocumentPresenter` — the `/note/*` paths are aliases of `/document/*`. The response shape is a Document. `name` maps to the note title, `content` to the body.
      operationId: createNote
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                content:
                  type: string
      responses:
        '200':
          description: Note created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Note'

  /note/{note_id}:
    get:
      tags:
        - Notes
      summary: Get a note detail
      description: |
        Fetches a single note by ID. As with create, notes are backed by the Document entity.

        **Behavior notes:**
        - ACL-checked via the note's project.
      operationId: getNote
      parameters:
        - name: note_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Note'
    post:
      tags:
        - Notes
      summary: Edit a note
      description: |
        Updates an existing note's title (`name`) and / or body (`content`).

        **Behavior notes:**
        - Overwrites content — there is no history / diff tracking exposed via the API.
      operationId: editNote
      parameters:
        - name: note_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                content:
                  type: string
      responses:
        '200':
          description: Note updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Note'
    delete:
      tags:
        - Notes
      summary: Delete a note
      description: |
        Soft-deletes the note. It is hidden from listings but retained in the database for audit.

        **Behavior notes:**
        - Response returns the (now-deleted) note's state for confirmation. This is a quirk — most delete endpoints return a SuccessResponse; this one returns the Note.
      operationId: deleteNote
      parameters:
        - name: note_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Note deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Note'

  # ==================== SEARCH ====================
  /search:
    post:
      tags:
        - Search
      summary: Fulltext search across Freelo (Elasticsearch)
      description: |
        Unified fulltext search powered by Elasticsearch. Combines a required text query with a rich set of filters: entity-type scope, structural narrowing (projects / tasklists / tasks / authors / workers), state, subtask flag, due-date range and sorting.

        ## What you can filter on

        | Filter | Type | Effect |
        | --- | --- | --- |
        | `search_query` | string | Required fulltext query (2–200 chars after trim). Wrap a fragment in **double quotes** for an ordered exact-match phrase, e.g. `"sentry issue 123"`. |
        | `entity_type` | string | Narrow to **one** entity category. See **Entity types** table below. |
        | `exclude_entity_types` | array of string | Exclude one or more categories. Same value set as `entity_type`. Useful e.g. to search everything *except* comments. |
        | `state_ids` | array of string | Restrict by lifecycle state. Defaults to `["active"]` — pass explicit values to also get archived / finished / template results. See **State IDs** table below. |
        | `projects_ids` | array of int | Only hits inside (or attached to) these projects. |
        | `tasklists_ids` | array of int | Only hits inside (or attached to) these tasklists. |
        | `tasks_ids` | array of int | Only hits inside (or attached to) these tasks (e.g. comments / files of a given task). |
        | `authors_ids` | array of int | Filter by author/creator (`entityOwner`). |
        | `workers_ids` | array of int | Filter by assignee. Meaningful for tasks/subtasks only. |
        | `is_subtask` | bool | When `true`, restricts to smart-subtask documents (subtasks, checklist items and comments under a subtask). Default `false`. |
        | `due_date.date_from` / `due_date.date_to` | date (`YYYY-MM-DD`) | Restrict by due-date range. At least one of the two must be set when the `due_date` object is provided. |
        | `sort` | object | Optional `{ order_by, order }`. Only sort field currently supported is `last_updated`; `order` is `asc` or `desc`. Omitting `sort` returns results by relevance score. |
        | `lang` | string | Language used for the Elasticsearch analyzer / stemmer. `cs_cz` (default) or `en_us`. |
        | `page` / `limit` | int | Pagination — `page` is zero-based; `limit` default `100`. |

        ## Entity types (`entity_type` / `exclude_entity_types`)

        The seven accepted input values expand to one or more underlying Elasticsearch document types.

        | Input value | Matches ES document types |
        | --- | --- |
        | `task` | `task` (top-level tasks only — excludes smart subtasks) |
        | `subtask` | smart subtasks (`task` with `isSmart=true`) **and** `taskcheck` (checklist items) |
        | `taskcheck` | `taskcheck` (checklist items only) |
        | `project` | `project` |
        | `tasklist` | `tasklist` |
        | `file` | `file`, `link`, `note` |
        | `comment` | `task_comment`, `note_comment`, `file_comment`, `link_comment` |


        ## State IDs

        | Value | Meaning |
        | --- | --- |
        | `active` | Default — open / in-progress tasks, active projects/tasklists. |
        | `finished` | Tasks closed via *Finish*. |
        | `archived` | Archived projects / tasklists. |
        | `archived_finished` / `archived_unfinished` | Sub-states of archived for finer reporting. |
        | `template` | Project / tasklist templates only. |
        | `not_template` | Everything except templates. |

        State filtering also shapes which **parent entities** are required/forbidden — e.g. `["archived"]` forces results to belong to an archived project/tasklist; `["template"]` restricts to template projects/tasklists.

        ## Use cases

        - Global search bar in the UI
        - Content-discovery integrations ("find comments about Sentry issue 123")
        - Reporting that starts from a text query ("find all tasks mentioning keyword X in last quarter")
        - "Search everywhere except comments" — `exclude_entity_types: ["comment"]`
        - "Find checklist items in this project" — `entity_type: "taskcheck"` + `projects_ids`

        ## Behavior notes (non-obvious)

        - `POST` method despite being a read — request body is JSON because filters are too complex for query strings.
        - `search_query` is **required** (2–200 chars after trim). Passing only filters without a text query is not supported here — use tag-specific list endpoints (e.g. `/all-tasks`) for filter-only queries.
        - ACL-filtered — Elasticsearch returns only documents the caller can read based on project membership.
        - Query length over 200 chars → `400` with a user-visible message (`ElasticsearchQueryLengthExceededException`).
        - Unknown `entity_type` / `exclude_entity_types` / `lang` value → `400` with a user-visible message listing accepted values.
      operationId: search
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - search_query
              properties:
                search_query:
                  type: string
                  minLength: 2
                  maxLength: 200
                  description: |
                    Fulltext query (2–200 chars after trimming). Double-quoted fragments are matched as ordered exact phrases; the rest as loose terms.
                  example: "sentry issue 123"
                entity_type:
                  type: string
                  enum: [task, subtask, taskcheck, project, tasklist, file, comment]
                  description: |
                    Narrow to a single category. See the **Entity types** table in the endpoint description for the underlying ES document types each value maps to. Omit for mixed results.
                exclude_entity_types:
                  type: array
                  description: Exclude one or more categories (same vocabulary as `entity_type`).
                  items:
                    type: string
                    enum: [task, subtask, taskcheck, project, tasklist, file, comment]
                state_ids:
                  type: array
                  description: Lifecycle states to include. Defaults to `["active"]` if omitted or empty.
                  items:
                    type: string
                    enum: [active, archived, finished, template, not_template, archived_finished, archived_unfinished]
                  default: [active]
                projects_ids:
                  type: array
                  description: Restrict to these projects (or to documents attached to them).
                  items:
                    type: integer
                tasklists_ids:
                  type: array
                  description: Restrict to these tasklists (or to documents attached to them).
                  items:
                    type: integer
                tasks_ids:
                  type: array
                  description: Restrict to these tasks (or to documents attached to them — e.g. comments / files of the listed tasks).
                  items:
                    type: integer
                authors_ids:
                  type: array
                  description: Filter by author (entity owner) user IDs.
                  items:
                    type: integer
                workers_ids:
                  type: array
                  description: Filter by assignee user IDs. Meaningful for tasks / subtasks.
                  items:
                    type: integer
                is_subtask:
                  type: boolean
                  default: false
                  description: When `true`, restrict to smart-subtask documents (subtasks, checklist items and their comments).
                due_date:
                  type: object
                  description: Due-date range. At least one of `date_from` / `date_to` must be set when this object is present.
                  properties:
                    date_from:
                      type: string
                      format: date
                    date_to:
                      type: string
                      format: date
                sort:
                  type: object
                  description: Optional sort. Omit for relevance-score ordering.
                  properties:
                    order_by:
                      type: string
                      enum: [last_updated]
                    order:
                      type: string
                      enum: [ASC, DESC]
                      description: Direction — note the uppercase values (`OrderEnum::ASC` / `OrderEnum::DESC`).
                lang:
                  type: string
                  enum: [cs_cz, en_us]
                  default: cs_cz
                  description: Language used by the Elasticsearch analyzer / stemmer.
                page:
                  type: integer
                  default: 0
                  description: Zero-based page index.
                limit:
                  type: integer
                  default: 100
                  description: Page size.
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          items:
                            type: array
                            items:
                              $ref: '#/components/schemas/SearchResult'

components:
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic
      description: Use your email as username and API key as password

  parameters:
    PageParam:
      name: p
      in: query
      description: Page number (starting from 0). Alias of `page` — `p` takes precedence when both are provided.
      schema:
        type: integer
        default: 0

    PageAliasParam:
      name: page
      in: query
      description: Page number (starting from 0). Alias of `p` — `p` takes precedence when both are provided.
      schema:
        type: integer
        default: 0

    ProjectIdParam:
      name: project_id
      in: path
      required: true
      schema:
        type: integer

    TasklistIdParam:
      name: tasklist_id
      in: path
      required: true
      schema:
        type: integer

    TaskIdParam:
      name: task_id
      in: path
      required: true
      schema:
        type: integer

    TaskcheckIdParam:
      name: taskcheck_id
      in: path
      required: true
      description: ID of the taskcheck (`tasks_checks.id`)
      schema:
        type: integer

  schemas:
    SuccessResponse:
      type: object
      properties:
        result:
          type: string
          example: success

    ErrorResponse:
      type: object
      properties:
        errors:
          type: array
          items:
            type: string
      example:
        errors:
          - "Error message."

    PaginatedResponse:
      type: object
      properties:
        total:
          type: integer
        count:
          type: integer
        page:
          type: integer
        per_page:
          type: integer

    Currency:
      type: object
      properties:
        amount:
          type: string
          description: Amount multiplied by 100
        currency:
          type: string
          enum: [CZK, EUR, USD]

    TaskBasic:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string

    TaskRelation:
      type: object
      properties:
        type:
          type: string
          enum: [blocked_by, blocks, related_to, duplicate_of]
        related_task_id:
          type: integer
        related_task_name:
          type: string

    TaskWork:
      type: object
      properties:
        id:
          type: integer
        reported:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        minutes:
          type: integer
        cost:
          $ref: '#/components/schemas/Currency'
        notice:
          type: string
          nullable: true

    State:
      type: object
      properties:
        id:
          type: integer
        state:
          type: string
          enum: [active, archived, finished, deleted, template]

    UserBasic:
      type: object
      required:
        - id
        - fullname
        - mention_key
      properties:
        id:
          type: integer
        fullname:
          type: string
        mention_key:
          type: string
          description: |
            Normalized form of `fullname` (whitespace stripped, diacritics removed) used as the visible text after `@` inside a mention span. Build a mention with:
            `<span data-freelo-mention="1" data-freelo-user-id="{id}">@{mention_key}</span>`

    UserWithEmail:
      allOf:
        - $ref: '#/components/schemas/UserBasic'
        - type: object
          properties:
            email:
              type: string
              format: email
          required:
            - email

    Client:
      type: object
      properties:
        id:
          type: integer
        email:
          type: string
        name:
          type: string
        company:
          type: string
        company_id:
          type: string
        company_tax_id:
          type: string
        street:
          type: string
        town:
          type: string
        zip:
          type: string

    TasklistBasic:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string

    ProjectBasic:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string

    ProjectWithTasklists:
      allOf:
        - $ref: '#/components/schemas/ProjectBasic'
        - type: object
          properties:
            date_add:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            date_edited_at:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            tasklists:
              type: array
              items:
                $ref: '#/components/schemas/TasklistBasic'
            client:
              $ref: '#/components/schemas/Client'

    ProjectFull:
      allOf:
        - $ref: '#/components/schemas/ProjectBasic'
        - type: object
          properties:
            date_add:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            date_edited_at:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            owner:
              $ref: '#/components/schemas/UserBasic'
            state:
              $ref: '#/components/schemas/State'
            minutes_budget:
              type: integer
              nullable: true
            budget:
              $ref: '#/components/schemas/Currency'
            real_minutes_spent:
              type: integer
            real_cost:
              $ref: '#/components/schemas/Currency'

    ProjectDetail:
      allOf:
        - $ref: '#/components/schemas/ProjectFull'
        - type: object
          properties:
            tasklists:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                  tasks:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        name:
                          type: string
                        due_date:
                          type: string
                          format: date-time
                          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                          example: '2026-04-24T11:12:38'
                          nullable: true
                        due_date_end:
                          type: string
                          format: date-time
                          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                          example: '2026-04-24T11:12:38'
                          nullable: true
                        worker:
                          $ref: '#/components/schemas/UserBasic'
                        parent_task_id:
                          type: integer
                          nullable: true
            workers:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: integer
                  fullname:
                    type: string
                  hour_rate:
                    type: object
                    nullable: true
                    properties:
                      amount:
                        type: integer
                      currency:
                        type: string
                      is_fixed:
                        type: boolean

    ProjectLabel:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        color:
          type: string
          nullable: true
        is_private:
          type: boolean
        users_id:
          type: integer
        usage_count:
          type: integer
        can_be_public:
          type: boolean
        can_be_edited:
          type: boolean

    PinnedItem:
      type: object
      properties:
        id:
          type: integer
        link:
          type: string
          format: uri
        title:
          type: string

    TasklistWithBudget:
      allOf:
        - $ref: '#/components/schemas/TasklistBasic'
        - type: object
          properties:
            budget:
              $ref: '#/components/schemas/Currency'

    TasklistFull:
      allOf:
        - $ref: '#/components/schemas/TasklistBasic'
        - type: object
          properties:
            date_add:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            date_edited_at:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            state:
              $ref: '#/components/schemas/State'
            project:
              allOf:
                - $ref: '#/components/schemas/ProjectBasic'
                - type: object
                  properties:
                    state:
                      $ref: '#/components/schemas/State'
            real_minutes_spent:
              type: integer
            budget:
              $ref: '#/components/schemas/Currency'
            real_cost:
              $ref: '#/components/schemas/Currency'

    TasklistDetail:
      allOf:
        - $ref: '#/components/schemas/TasklistBasic'
        - type: object
          properties:
            project_id:
              type: integer
            worker_id:
              type: integer
              nullable: true
              description: 'Default worker of the tasklist (same field as in POST /tasklist/{tasklist_id}/edit). `null` when not set or when the worker is no longer among the tasklist''s assignable workers.'
            date_add:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            date_edited_at:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            tasks:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                  due_date:
                    type: string
                    format: date-time
                    description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                    example: '2026-04-24T11:12:38'
                    nullable: true
                  due_date_end:
                    type: string
                    format: date-time
                    description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
                    example: '2026-04-24T11:12:38'
                    nullable: true
                  worker:
                    $ref: '#/components/schemas/UserBasic'
                  parent_task_id:
                    type: integer
                    nullable: true

    TaskLabel:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string
        color:
          type: string

    TaskLabelColor:
      type: object
      properties:
        color:
          type: string
          description: Hex value to send as the label color (e.g. "#15acc0").
        display_name:
          type: string
          description: Human-readable color name, for display only; not accepted as input.
        is_default:
          type: boolean
          description: True for the color applied when a label is created without a color.

    TaskLabelAddInput:
      description: >
        Two input modes:
        (1) UUID only — reference an existing label by UUID, assigned as-is.
        (2) Name-based — name is required; color defaults to #77787a if omitted; uuid is auto-generated if omitted.
        Matching is by name+color — if an existing label matches both, it is reused; otherwise a new label is created.
      oneOf:
        - type: object
          description: Reference an existing label by UUID.
          required:
            - uuid
          properties:
            uuid:
              type: string
              format: uuid
          additionalProperties: false
        - type: object
          description: Create or match a label by name and color.
          required:
            - name
          properties:
            name:
              type: string
            color:
              type: string
              default: '#77787a'
              description: 'Label color hex code. Defaults to #77787a (gray) if not provided.'
            uuid:
              type: string
              format: uuid
              description: 'Optional UUID for the label. Auto-generated if omitted.'

    TaskLabelRemoveInput:
      description: >
        Three input modes:
        (1) UUID — removes the label identified by UUID.
        (2) Name only — removes all labels with that name regardless of color.
        (3) Name + color — removes the label matching both name and color.
      oneOf:
        - type: object
          description: Remove a label by UUID.
          required:
            - uuid
          properties:
            uuid:
              type: string
              format: uuid
          additionalProperties: false
        - type: object
          description: Remove all labels with the given name (any color).
          required:
            - name
          properties:
            name:
              type: string
          additionalProperties: false
        - type: object
          description: Remove the label matching both name and color.
          required:
            - name
            - color
          properties:
            name:
              type: string
            color:
              type: string

    TimeEstimate:
      type: object
      properties:
        minutes:
          type: integer

    UserTimeEstimate:
      type: object
      properties:
        minutes:
          type: integer
        user:
          $ref: '#/components/schemas/UserBasic'

    TaskSummary:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        date_edited_at:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        due_date:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        due_date_end:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        count_comments:
          type: integer
        count_subtasks:
          type: integer
        author:
          $ref: '#/components/schemas/UserBasic'
        worker:
          $ref: '#/components/schemas/UserBasic'
        labels:
          type: array
          items:
            $ref: '#/components/schemas/TaskLabel'
        parent_task_id:
          type: integer
          nullable: true
        total_time_estimate:
          $ref: '#/components/schemas/TimeEstimate'
        users_time_estimates:
          type: array
          items:
            $ref: '#/components/schemas/UserTimeEstimate'

    TaskFull:
      allOf:
        - $ref: '#/components/schemas/TaskSummary'
        - type: object
          properties:
            type:
              type: string
              enum: [task, subtask]
              description: 'Task kind discriminator: `task` = top-level task, `subtask` = smart subtask (has its own task id).'
            state:
              $ref: '#/components/schemas/State'
            project:
              allOf:
                - $ref: '#/components/schemas/ProjectBasic'
                - type: object
                  properties:
                    state:
                      $ref: '#/components/schemas/State'
            tasklist:
              allOf:
                - $ref: '#/components/schemas/TasklistBasic'
                - type: object
                  properties:
                    state:
                      $ref: '#/components/schemas/State'
            priority_enum:
              type: string
              enum: [h, m, l]
              nullable: true
              description: "Task priority level: 'h' = high, 'm' = medium, 'l' = low."
            custom_fields:
              type: array
              items:
                $ref: '#/components/schemas/CustomFieldWithValue'

    TaskFinished:
      allOf:
        - $ref: '#/components/schemas/TaskSummary'
        - type: object
          properties:
            date_finished:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            finished_by:
              $ref: '#/components/schemas/UserBasic'

    TaskCreate:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        due_date:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        due_date_end:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        worker:
          type: integer
        priority_enum:
          type: string
          enum: [h, m, l]
        comment:
          type: object
          properties:
            content:
              type: string
        labels:
          type: array
          items:
            $ref: '#/components/schemas/TaskLabelAddInput'
        tracking_users_ids:
          type: array
          items:
            type: integer
        turn_off_authors_tracking:
          type: boolean
          default: false
        subtasks:
          type: array
          items:
            $ref: '#/components/schemas/SubtaskCreate'

    TaskCreated:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        due_date:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        due_date_end:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        worker:
          $ref: '#/components/schemas/UserBasic'
        priority_enum:
          type: string
        labels:
          type: array
          items:
            $ref: '#/components/schemas/TaskLabel'
        tracking_users:
          type: array
          items:
            $ref: '#/components/schemas/UserBasic'
        subtasks:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              task_id:
                type: integer
              name:
                type: string

    TaskDetail:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        date_edited_at:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        due_date:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        due_date_end:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        date_finished:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        minutes:
          type: integer
        priority_enum:
          type: string
        count_subtasks:
          type: integer
        parent_task_id:
          type: integer
          nullable: true
        cost:
          $ref: '#/components/schemas/Currency'
        author:
          $ref: '#/components/schemas/UserBasic'
        worker:
          $ref: '#/components/schemas/UserBasic'
        state:
          $ref: '#/components/schemas/State'
        comments:
          type: array
          items:
            $ref: '#/components/schemas/CommentWithFiles'
        labels:
          type: array
          items:
            $ref: '#/components/schemas/TaskLabel'
        project:
          $ref: '#/components/schemas/ProjectBasic'
        tasklist:
          $ref: '#/components/schemas/TasklistBasic'
        custom_fields:
          type: array
          items:
            $ref: '#/components/schemas/CustomFieldWithValue'
        total_time_estimate:
          $ref: '#/components/schemas/TimeEstimate'
        users_time_estimates:
          type: array
          items:
            $ref: '#/components/schemas/UserTimeEstimate'
        tracking_users:
          type: array
          items:
            $ref: '#/components/schemas/UserBasic'

    SubtaskCreate:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        due_date:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        due_date_end:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        worker:
          type: integer
        priority_enum:
          type: string
          enum: [h, m, l]
        comment:
          type: object
          properties:
            content:
              type: string
        labels:
          type: array
          items:
            $ref: '#/components/schemas/TaskLabelAddInput'
        tracking_users_ids:
          type: array
          items:
            type: integer

    Subtask:
      type: object
      properties:
        id:
          type: integer
        type:
          type: string
          enum: [subtask, taskcheck]
          description: 'Subtask kind discriminator: `subtask` = smart subtask (has its own `task_id`), `taskcheck` = simple checklist item (`task_id` is null). On the public API a `taskcheck` supports read, create, edit (name/worker only), finish, activate and delete via the `/api/v1/taskcheck/{taskcheck_id}/*` endpoints.'
        task_id:
          type: integer
          nullable: true
        name:
          type: string
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        due_date:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        due_date_end:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        count_comments:
          type: integer
        count_subtasks:
          type: integer
        author:
          $ref: '#/components/schemas/UserBasic'
        worker:
          $ref: '#/components/schemas/UserBasic'
        state:
          $ref: '#/components/schemas/State'
        project:
          allOf:
            - $ref: '#/components/schemas/ProjectBasic'
            - type: object
              properties:
                state:
                  $ref: '#/components/schemas/State'
        tasklist:
          allOf:
            - $ref: '#/components/schemas/TasklistBasic'
            - type: object
              properties:
                state:
                  $ref: '#/components/schemas/State'
        labels:
          type: array
          items:
            $ref: '#/components/schemas/TaskLabel'

    FileBasic:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
          format: uuid
        filename:
          type: string
        size:
          type: integer

    FileFull:
      allOf:
        - $ref: '#/components/schemas/FileBasic'
        - type: object
          properties:
            caption:
              type: string
            description:
              type: string
            date_add:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            date_edited_at:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            state:
              $ref: '#/components/schemas/State'

    FileUpload:
      type: object
      required:
        - download_url
      properties:
        download_url:
          type: string
          format: uri
        filename:
          type: string

    Comment:
      type: object
      properties:
        id:
          type: integer
        content:
          type: string
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        files:
          type: array
          items:
            $ref: '#/components/schemas/FileBasic'

    CommentWithFiles:
      allOf:
        - $ref: '#/components/schemas/Comment'
        - type: object
          properties:
            author:
              $ref: '#/components/schemas/UserBasic'
            is_description:
              type: boolean
            comments_reactions:
              type: array
              items:
                $ref: '#/components/schemas/UserBasic'
            files:
              type: array
              items:
                $ref: '#/components/schemas/FileFull'

    CommentFull:
      type: object
      properties:
        id:
          type: integer
          nullable: true
        uuid:
          type: string
          format: uuid
          nullable: true
        content:
          type: string
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        date_edited_at:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        author:
          $ref: '#/components/schemas/UserBasic'
        task:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
        tasklist:
          $ref: '#/components/schemas/TasklistBasic'
        project:
          $ref: '#/components/schemas/ProjectBasic'
        document:
          type: object
          nullable: true
          properties:
            uuid:
              type: string
              format: uuid
            name:
              type: string
        link:
          type: object
          nullable: true
          properties:
            uuid:
              type: string
              format: uuid
            name:
              type: string
        file:
          type: object
          nullable: true
          properties:
            uuid:
              type: string
              format: uuid
        files:
          type: array
          items:
            $ref: '#/components/schemas/FileFull'

    WorkReport:
      type: object
      properties:
        id:
          type: integer
        date_add:
          type: string
          format: date-time
          description: 'Insertion timestamp of the work report (when the row was created in the DB). Naive ISO8601 in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        date_reported:
          type: string
          format: date-time
          description: 'Start timestamp of the work session. Naive ISO8601 in Europe/Prague timezone (no offset). End time is derived as `date_reported + minutes`. See "Timestamp Format" in API description.'
          example: '2026-04-24T08:00:00'
        note:
          type: string
          nullable: true
        minutes:
          type: integer
        cost:
          $ref: '#/components/schemas/Currency'
        author:
          $ref: '#/components/schemas/UserBasic'
        worker:
          $ref: '#/components/schemas/UserBasic'
        task:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string

    WorkReportExtended:
      allOf:
        - $ref: '#/components/schemas/WorkReport'
        - type: object
          properties:
            project:
              $ref: '#/components/schemas/ProjectBasic'
            tasklist:
              $ref: '#/components/schemas/TasklistBasic'
            work_report_id:
              type: integer
              nullable: true

    WorkReportFull:
      allOf:
        - $ref: '#/components/schemas/WorkReport'
        - type: object
          properties:
            date_edited_at:
              type: string
              format: date-time
              description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
              example: '2026-04-24T11:12:38'
            task:
              nullable: true
              type: object
              properties:
                id:
                  type: integer
                name:
                  type: string
                minutes:
                  type: integer
                parent_task_id:
                  type: integer
                  nullable: true
                cost:
                  $ref: '#/components/schemas/Currency'
                labels:
                  type: array
                  items:
                    $ref: '#/components/schemas/TaskLabel'
                total_time_estimate:
                  $ref: '#/components/schemas/TimeEstimate'
                users_time_estimates:
                  type: array
                  items:
                    $ref: '#/components/schemas/UserTimeEstimate'
            tasklist:
              nullable: true
              allOf:
                - $ref: '#/components/schemas/TasklistBasic'
            project:
              nullable: true
              type: object
              properties:
                id:
                  type: integer
                name:
                  type: string
                labels:
                  type: array
                  items:
                    type: string

    IssuedInvoice:
      type: object
      properties:
        id:
          type: integer
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        note:
          type: string
          nullable: true
        currency:
          type: string
          enum: [CZK, EUR, USD]
        minutes:
          type: integer
        price:
          $ref: '#/components/schemas/Currency'
        subject:
          type: object
          properties:
            company_name:
              type: string
            invoice_url:
              type: string
        inv_items:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              name:
                type: string
              minutes:
                type: integer
              price:
                $ref: '#/components/schemas/Currency'

    IssuedInvoiceDetail:
      allOf:
        - $ref: '#/components/schemas/IssuedInvoice'
        - type: object
          properties:
            inv_items:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                  minutes:
                    type: integer
                  price:
                    $ref: '#/components/schemas/Currency'
                  reports:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        work_report_id:
                          type: integer
                          nullable: true
                        project_name:
                          type: string
                        tasklist_name:
                          type: string
                        name:
                          type: string
                        minutes:
                          type: integer
                        price:
                          $ref: '#/components/schemas/Currency'

    Notification:
      type: object
      properties:
        id:
          type: integer
        type:
          type: string
        date_action:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        author:
          $ref: '#/components/schemas/UserBasic'
        who:
          $ref: '#/components/schemas/UserBasic'
        is_unread:
          type: boolean
        is_new:
          type: boolean
        task:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
        tasklist:
          $ref: '#/components/schemas/TasklistBasic'
        project:
          $ref: '#/components/schemas/ProjectBasic'
        comment:
          type: object
          nullable: true
          properties:
            id:
              type: integer
        document:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
        file:
          type: object
          nullable: true
          properties:
            uuid:
              type: string
              format: uuid
            filename:
              type: string
            caption:
              type: string
        more_comments:
          type: boolean
        more_users:
          type: array
          items:
            $ref: '#/components/schemas/UserBasic'

    Event:
      type: object
      properties:
        id:
          type: integer
        date_action:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        type:
          type: string
        author:
          $ref: '#/components/schemas/UserBasic'
        who:
          $ref: '#/components/schemas/UserBasic'
        comment:
          type: object
          nullable: true
          properties:
            id:
              type: integer
        task:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
        task_check:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
        tasklist:
          $ref: '#/components/schemas/TasklistBasic'
        project:
          $ref: '#/components/schemas/ProjectBasic'
        document:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
        file:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            uuid:
              type: string
              format: uuid
            filename:
              type: string
            caption:
              type: string
        due_date:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        due_date_end:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true

    FileItem:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string
        author:
          $ref: '#/components/schemas/UserBasic'
        project:
          $ref: '#/components/schemas/ProjectBasic'
        directory_uuid:
          type: string
          format: uuid
          nullable: true
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        order:
          type: integer
        type:
          type: string
          enum: [directory, link, file, document]
        filename:
          type: string
          nullable: true
        caption:
          type: string
          nullable: true
        mime_type:
          type: string
          nullable: true
        extension:
          type: string
          nullable: true
        size:
          type: integer
        color:
          type: string
          nullable: true
        items_count:
          type: integer
          nullable: true
        link:
          type: string
          format: uri
          nullable: true
        link_type:
          type: string
          nullable: true
        note:
          type: string
          nullable: true

    CustomField:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
        custom_fields_types_uuid:
          type: string
          format: uuid
        project_id:
          type: integer
        author_id:
          type: integer
        name:
          type: string
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        priority:
          type: integer

    CustomFieldValue:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
        value:
          type: string
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        date_edited_at:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        author_id:
          type: integer
        task_id:
          type: integer
        custom_field_uuid:
          type: string
          format: uuid

    CustomFieldEnumValue:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
        task_id:
          type: integer
        custom_field_uuid:
          type: string
          format: uuid
        value:
          type: string
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        date_edited_at:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true
        author_id:
          type: integer

    CustomFieldEnumOption:
      type: object
      properties:
        enum_uuid:
          type: string
          format: uuid
        enum_value:
          type: string
        custom_field_uuid:
          type: string
          format: uuid
        custom_field_name:
          type: string

    CustomFieldWithValue:
      type: object
      properties:
        field_uuid:
          type: string
          format: uuid
        custom_fields_types_uuid:
          type: string
          format: uuid
        project_id:
          type: integer
        name:
          type: string
        priority:
          type: integer
        field_date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        value_uuid:
          type: string
          format: uuid
        value_author_id:
          type: integer
        value:
          type: string
        value_date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        value_date_edited_at:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
          nullable: true

    Note:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        date_add:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        date_edited_at:
          type: string
          format: date-time
          description: 'Naive ISO8601 timestamp in Europe/Prague timezone (no offset). See "Timestamp Format" in API description.'
          example: '2026-04-24T11:12:38'
        state:
          $ref: '#/components/schemas/State'
        content:
          type: string
        author:
          $ref: '#/components/schemas/UserBasic'
        project:
          $ref: '#/components/schemas/ProjectBasic'
        files:
          type: array
          items:
            $ref: '#/components/schemas/FileFull'
        comments:
          type: array
          items:
            $ref: '#/components/schemas/CommentWithFiles'

    SearchResult:
      type: object
      description: |
        Single search hit. Fields marked **conditional** below appear only for specific `type` values:

        | Conditional field | Appears when `type` is |
        | --- | --- |
        | `is_smart` | `task` |
        | `task` | `task` (only if it's a smart subtask), `taskcheck`, `task_comment` |
        | `smart_task` | `taskcheck`, `task_comment` (the smart-task ancestor, if any) |
        | `note` | `note_comment` |
        | `file` | `file_comment` |
        | `link` | `link_comment` |

      properties:
        score:
          type: number
          nullable: true
          description: Elasticsearch relevance score (`null` if missing from the hit).
        id:
          type: integer
          nullable: true
          description: |
            Entity ID. For `task` hits whose entity is a child of a multi-project parent, this is rewritten to the **multi-project parent's task ID** (the original child ID is not exposed here).
        uuid:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
          nullable: true
        author_id:
          type: integer
          nullable: true
          description: ID of the root entity's owner.
        type:
          type: string
          description: |
            Underlying Elasticsearch entity type for this hit (underscore form of `ElasticsearchEntityTypeEnum`). Broader than the `entity_type` request filter.
          enum:
            - task
            - taskcheck
            - task_comment
            - note_comment
            - file_comment
            - link_comment
            - project
            - tasklist
            - file
            - link
            - note
            - directory
            - user
        highlight_name:
          type: array
          description: Highlighted snippets from the entity name (HTML, with `<em>` markers).
          items:
            type: string
        highlight_content:
          type: array
          description: Highlighted snippets from the entity content (HTML, with `<em>` markers).
          items:
            type: string
        project:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ProjectBasic'
        tasklist:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/TasklistBasic'
        state:
          type: integer
          description: Numeric state ID of the root entity (active / archived / finished / template).
        is_smart:
          type: boolean
          description: |
            **Conditional** — present only for `type=task`. `true` when the task is a smart subtask, `false` for a regular top-level task.
        task:
          type: object
          description: |
            **Conditional** — parent task of the hit. Present for `type` of `taskcheck`, `task_comment`, and for `task` hits that are smart subtasks (in which case it carries the parent task).
          properties:
            id:
              type: integer
            name:
              type: string
        smart_task:
          type: object
          nullable: true
          description: |
            **Conditional** — smart-task ancestor (if any) for `type` of `taskcheck` or `task_comment`. `null` when the hit is not under a smart task.
          properties:
            id:
              type: integer
            name:
              type: string
        note:
          type: object
          description: '**Conditional** — present only for `type=note_comment`.'
          properties:
            id:
              type: integer
              nullable: true
            name:
              type: string
              nullable: true
        file:
          type: object
          description: '**Conditional** — present only for `type=file_comment`.'
          properties:
            uuid:
              type: string
              nullable: true
            name:
              type: string
              nullable: true
        link:
          type: object
          description: '**Conditional** — present only for `type=link_comment`.'
          properties:
            uuid:
              type: string
              nullable: true
            name:
              type: string
              nullable: true
