openapi: 3.0.3
info:
  title: Tikklok REST API
  version: "1.0.0"
  description: >
    Public REST API for third-party integrations. Stateless, API-key
    authenticated (HTTP Basic over HTTPS). Read your company's employees,
    locations, and performance data. Every request is scoped to the company
    that owns the API key; IDs from other companies return 404 not_found.
servers:
  - url: https://api.tikklok.app
    description: Production
  - url: http://localhost:8084
    description: Local development

security:
  - basicAuth: []

tags:
  - name: Meta
  - name: Employees
  - name: Locations

paths:
  /v1/health:
    get:
      tags: [Meta]
      summary: Uptime probe
      description: No authentication required.
      security: []
      responses:
        "200":
          description: Service healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  status:  { type: string, example: ok }
                  version: { type: string, example: v1 }
                  time:    { type: string, format: date-time, example: "2026-08-08T14:23:00Z" }

  /v1/me:
    get:
      tags: [Meta]
      summary: Identify the calling key
      responses:
        "200":
          description: Credentials valid
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:      { type: boolean, example: true }
                  company_id:   { type: integer, example: 1 }
                  company_name: { type: string, example: Acme Corp }
                  key_id:       { type: string, example: TK_PUB_LIVE_A1B2C3D4E5F60718 }
                  key_name:     { type: string, example: My Slack integration }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/employees:
    get:
      tags: [Employees]
      summary: List employees
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 100, minimum: 1, maximum: 500 } }
        - { name: offset, in: query, schema: { type: integer, default: 0, minimum: 0 } }
        - { name: location_id, in: query, schema: { type: integer }, description: Only employees actively assigned to this location. }
        - { name: role, in: query, schema: { type: string, enum: [owner, admin, employee] } }
        - { name: include_inactive, in: query, schema: { type: integer, enum: [0, 1], default: 0 } }
      responses:
        "200":
          description: Paginated roster
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  total:   { type: integer, example: 25 }
                  limit:   { type: integer, example: 100 }
                  offset:  { type: integer, example: 0 }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/EmployeeListItem" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/employees/{company_user_id}:
    get:
      tags: [Employees]
      summary: Get one employee
      parameters:
        - $ref: "#/components/parameters/CompanyUserId"
      responses:
        "200":
          description: Employee detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: "#/components/schemas/EmployeeDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/employees/{company_user_id}/locations/{location_id}:
    get:
      tags: [Employees]
      summary: Get an employee's assignment at one location
      parameters:
        - $ref: "#/components/parameters/CompanyUserId"
        - $ref: "#/components/parameters/LocationId"
      responses:
        "200":
          description: Assignment
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: "#/components/schemas/LocationAssignmentFull" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/employees/{company_user_id}/performance:
    get:
      tags: [Employees]
      summary: Employee performance, grouped by location
      description: >
        Per-day and per-location hours. expected_hours = planned - closed -
        location_holiday - user_holiday - sick; overtime_hours = worked -
        expected (may be negative).
      parameters:
        - $ref: "#/components/parameters/CompanyUserId"
        - { name: min_date, in: query, required: true, schema: { type: string, format: date }, example: "2025-01-01" }
        - { name: max_date, in: query, required: true, schema: { type: string, format: date }, example: "2025-01-31" }
      responses:
        "200":
          description: Performance breakdown
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: "#/components/schemas/Performance" }
        "400":
          description: invalid_date or range_too_large
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/locations:
    get:
      tags: [Locations]
      summary: List locations
      parameters:
        - { name: include_inactive, in: query, schema: { type: integer, enum: [0, 1], default: 0 } }
      responses:
        "200":
          description: Locations
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Location" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/locations/{location_id}:
    get:
      tags: [Locations]
      summary: Get one location
      parameters:
        - $ref: "#/components/parameters/LocationId"
      responses:
        "200":
          description: Location
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: "#/components/schemas/Location" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

components:
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic
      description: base64(KEY_ID:SECRET). KEY_ID is TK_PUB_LIVE_..., SECRET is TK_SEC_LIVE_...

  parameters:
    CompanyUserId:
      name: company_user_id
      in: path
      required: true
      schema: { type: integer }
    LocationId:
      name: location_id
      in: path
      required: true
      schema: { type: integer }

  responses:
    Unauthorized:
      description: Missing or invalid credentials
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { success: false, error: unauthorized }
    NotFound:
      description: Resource missing or not owned by this company
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { success: false, error: not_found }
    BadRequest:
      description: Invalid parameter
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { success: false, error: invalid_role }
    RateLimited:
      description: Rate limit exceeded
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { success: false, error: rate_limited }

  schemas:
    Error:
      type: object
      properties:
        success: { type: boolean, example: false }
        error:   { type: string, example: not_found }
    LocationRef:
      type: object
      properties:
        location_user_id: { type: integer, example: 15 }
        location_id:      { type: integer, example: 7 }
        location_name:    { type: string, example: Main Store }
    EmployeeListItem:
      type: object
      properties:
        company_user_id: { type: integer, example: 42 }
        first_name:      { type: string, example: Jane }
        last_name:       { type: string, example: Doe }
        is_active:       { type: boolean, example: true }
        locations:
          type: array
          items: { $ref: "#/components/schemas/LocationRef" }
    LocationAssignment:
      type: object
      properties:
        location_user_id: { type: integer, example: 15 }
        location_id:      { type: integer, example: 7 }
        location_name:    { type: string, example: Main Store }
        role:             { type: string, example: staff }
        function_id:      { type: integer, nullable: true, example: 3 }
        function_name:    { type: string, nullable: true, example: Sales }
        is_user_primary:  { type: boolean, example: true }
        clocking_allowed: { type: boolean, example: true }
        overtime:         { type: boolean, example: true }
        valid_from:       { type: string, nullable: true, example: null }
        valid_to:         { type: string, nullable: true, example: null }
        is_active:        { type: boolean, example: true }
    LocationAssignmentFull:
      allOf:
        - $ref: "#/components/schemas/LocationAssignment"
        - type: object
          properties:
            company_user_id: { type: integer, example: 42 }
            created: { type: string, example: "2025-01-15 10:30:00" }
            updated: { type: string, example: "2025-08-27 14:22:15" }
    EmployeeDetail:
      type: object
      properties:
        company_user_id: { type: integer, example: 42 }
        first_name: { type: string, example: Jane }
        last_name:  { type: string, example: Doe }
        email:      { type: string, example: jane@example.com }
        tel:        { type: string, example: "+32 470 00 00 00" }
        role:       { type: string, example: employee }
        contract:   { type: string, example: fixed_schedule }
        status:     { type: string, example: active }
        is_active:  { type: boolean, example: true }
        created:    { type: string, example: "2025-01-15 10:30:00" }
        updated:    { type: string, example: "2025-08-27 14:22:15" }
        locations:
          type: array
          items: { $ref: "#/components/schemas/LocationAssignment" }
    Location:
      type: object
      properties:
        location_id:        { type: integer, example: 7 }
        name:               { type: string, example: Main Store }
        street:             { type: string, example: Rue de la Paix }
        house_nr:           { type: string, example: "42" }
        postal_code:        { type: string, example: "1000" }
        city:               { type: string, example: Brussels }
        country:            { type: string, example: BE }
        timezone:           { type: string, example: Europe/Brussels }
        is_company_primary: { type: boolean, example: true }
        latitude:           { type: number, example: 50.8503 }
        longitude:          { type: number, example: 4.3517 }
        geo_radius_meters:  { type: integer, example: 100 }
        is_active:          { type: boolean, example: true }
        created:            { type: string, example: "2024-06-01 09:00:00" }
        updated:            { type: string, example: "2025-08-27 14:22:15" }
    PerfDay:
      type: object
      properties:
        date:                   { type: string, format: date, example: "2025-01-06" }
        planned_hours:          { type: number, example: 8 }
        worked_hours:           { type: number, example: 7.75 }
        closed_hours:           { type: number, example: 0 }
        location_holiday_hours: { type: number, example: 0 }
        user_holiday_hours:     { type: number, example: 0 }
        sick_hours:             { type: number, example: 0 }
        expected_hours:         { type: number, example: 8 }
        overtime_hours:         { type: number, example: -0.25 }
    Performance:
      type: object
      properties:
        company_user_id: { type: integer, example: 42 }
        employee_name:   { type: string, example: Jane Doe }
        period:
          type: object
          properties:
            min_date: { type: string, format: date, example: "2025-01-01" }
            max_date: { type: string, format: date, example: "2025-01-31" }
        locations:
          type: array
          items:
            type: object
            properties:
              location_id:   { type: integer, example: 7 }
              location_name: { type: string, example: Main Store }
              days:
                type: array
                items: { $ref: "#/components/schemas/PerfDay" }
              totals: { $ref: "#/components/schemas/PerfDay" }
