openapi: 3.1.0
info:
  title: CompanyBrain API Aplikasi Karyawan
  version: "1.0.0"
  description: |
    Permukaan API yang dipakai aplikasi karyawan CompanyBrain.

    Perusahaan ditentukan backend dari header `Host`, bukan dari isi permintaan.
    Ganti variabel server `host` dengan host perusahaan yang dituju, misalnya
    `os.perusahaan-contoh.id`. Tidak ada parameter tenant di mana pun.

    Dokumentasi naratif: https://docs.sikaryawan.com

servers:
  - url: https://{host}/api/v1
    variables:
      host:
        default: os.perusahaan-contoh.id
        description: Host perusahaan. Menentukan tenant.

tags:
  - name: Autentikasi
  - name: Profil
  - name: Kehadiran
  - name: Pengajuan
  - name: Tugas
  - name: Info
  - name: Notifikasi

security:
  - bearer: []

paths:
  /public/branding:
    get:
      tags: [Autentikasi]
      summary: Identitas visual perusahaan
      security: []
      responses:
        "200":
          description: Ditemukan, atau host portal bersama
          content:
            application/json:
              schema:
                type: object
                properties:
                  found: { type: boolean }
                  portal: { type: boolean, description: Benar bila host dipakai banyak perusahaan }
                  name: { type: string }
                  slug: { type: string }
                  branding: { type: object, additionalProperties: true }

  /auth/employee-login:
    post:
      tags: [Autentikasi]
      summary: Masuk dengan kode karyawan dan PIN
      description: |
        Tidak berlaku pada host portal bersama maupun domain generik: kode
        karyawan hanya unik di dalam satu perusahaan.

        Seluruh kegagalan identitas dijawab dengan pesan yang sama, disengaja
        agar endpoint ini tidak dapat dipakai memastikan kode karyawan yang
        berlaku. Lima kali salah mengunci akses 15 menit.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [employee_code, pin]
              properties:
                employee_code: { type: string, example: "EMP-0010" }
                pin: { type: string, pattern: '^\d{6}$', example: "735291" }
      responses:
        "200":
          description: Berhasil
          content:
            application/json:
              schema:
                type: object
                properties:
                  tokens: { $ref: "#/components/schemas/PasanganToken" }
                  tenant_id: { type: string, format: uuid }
                  employee_id: { type: string, format: uuid }
                  must_change_pin:
                    type: boolean
                    description: Bila benar, paksa layar ganti PIN sebelum apa pun yang lain.
        "400": { $ref: "#/components/responses/Galat" }

  /auth/login:
    post:
      tags: [Autentikasi]
      summary: Masuk dengan surel dan kata sandi
      description: |
        Ketika verifikasi dua langkah masih diperlukan, balasannya berstatus
        200 dengan `requires_2fa: true`, bukan galat.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string }
                totp_code: { type: string }
      responses:
        "200":
          description: Berhasil, atau masih menunggu kode 2FA
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    properties:
                      tokens: { $ref: "#/components/schemas/PasanganToken" }
                      tenant_id: { type: string, format: uuid }
                  - type: object
                    properties:
                      requires_2fa: { type: boolean, const: true }
        "400": { $ref: "#/components/responses/Galat" }

  /auth/refresh:
    post:
      tags: [Autentikasi]
      summary: Menukar refresh token
      description: Balasannya tidak dibungkus `tokens`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refresh_token]
              properties:
                refresh_token: { type: string }
      responses:
        "200":
          description: Sepasang token baru
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PasanganToken" }
        "401": { $ref: "#/components/responses/Galat" }

  /me:
    get:
      tags: [Profil]
      summary: Profil, perusahaan, peran, izin, modul
      responses:
        "200":
          description: Profil lengkap
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Profil" }

  /me/app-pin:
    get:
      tags: [Profil]
      summary: Status PIN pemakai
      responses:
        "200":
          description: Status
          content:
            application/json:
              schema:
                type: object
                properties:
                  punya_data_karyawan: { type: boolean }
                  punya_pin: { type: boolean }
                  employee_code: { type: string }
    put:
      tags: [Profil]
      summary: Mengganti PIN sendiri
      description: |
        `pin_lama` boleh dihilangkan hanya ketika PIN sedang dalam keadaan
        wajib ganti karena baru disetel bagian SDM.

        PIN wajib enam angka, dan ditolak bila seluruh angkanya sama atau
        berurutan naik maupun turun.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [pin_baru]
              properties:
                pin_lama: { type: string, pattern: '^\d{6}$' }
                pin_baru: { type: string, pattern: '^\d{6}$' }
      responses:
        "200": { description: Berhasil }
        "400": { $ref: "#/components/responses/Galat" }

  /me/payslips:
    get:
      tags: [Profil]
      summary: Slip gaji milik pemakai
      description: Hanya periode yang sudah difinalisasi, terbaru lebih dahulu.
      parameters:
        - { name: per_page, in: query, schema: { type: integer, default: 24, minimum: 1, maximum: 120 } }
      responses:
        "200":
          description: Daftar slip
          content:
            application/json:
              schema:
                type: object
                properties:
                  punya_data_karyawan: { type: boolean }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/SlipGaji" }

  /attendance:
    get:
      tags: [Kehadiran]
      summary: Riwayat kehadiran
      description: Tersaring menurut cakupan izin pemanggil. Tidak perlu mengirim employee_id.
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: employee_id, in: query, schema: { type: string, format: uuid } }
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: per_page, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses:
        "200":
          description: Daftar kehadiran
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Halaman"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Kehadiran" }

  /attendance/shifts:
    get:
      tags: [Kehadiran]
      summary: Shift yang tersedia
      responses:
        "200":
          description: Daftar shift
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Shift" }

  /attendance/check-in:
    post:
      tags: [Kehadiran]
      summary: Absen masuk
      description: |
        Pagar lokasi punya tiga mode. Pada mode `enforce`, absen di luar area
        ditolak, dan tidak adanya koordinat sama sekali juga ditolak.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PermintaanAbsen" }
      responses:
        "200":
          description: Baris kehadiran
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Kehadiran" }
        "400": { $ref: "#/components/responses/Galat" }
        "409":
          description: |
            Sudah absen masuk hari ini. Pesannya berbunyi "duplicate value",
            berasal dari batasan unik basis data dan bukan kalimat yang layak
            dibaca pemakai. Ganti di sisi aplikasi.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Galat" }

  /attendance/check-out:
    post:
      tags: [Kehadiran]
      summary: Absen pulang
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PermintaanAbsen" }
      responses:
        "200":
          description: Baris kehadiran
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Kehadiran" }
        "400":
          description: Belum pernah absen masuk hari ini.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Galat" }
        "409":
          description: Sudah absen pulang hari ini.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Galat" }

  /attendance/overtime:
    get:
      tags: [Pengajuan]
      summary: Daftar lembur
      parameters:
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: per_page, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses:
        "200":
          description: Daftar lembur
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Halaman"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Lembur" }
    post:
      tags: [Pengajuan]
      summary: Mengajukan lembur
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [work_date, minutes]
              properties:
                employee_id:
                  type: string
                  format: uuid
                  description: Dihilangkan berarti atas nama diri sendiri.
                work_date: { type: string, format: date }
                minutes: { type: integer, minimum: 1 }
                reason: { type: string }
      responses:
        "200":
          description: Pengajuan tersimpan
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Lembur" }
        "400": { $ref: "#/components/responses/Galat" }

  /leave-types:
    get:
      tags: [Pengajuan]
      summary: Jenis cuti
      responses:
        "200":
          description: Daftar jenis cuti
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/JenisCuti" }

  /leave-balances:
    get:
      tags: [Pengajuan]
      summary: Saldo cuti
      description: |
        PERINGATAN: endpoint ini belum menyaring menurut cakupan izin. Tanpa
        parameter `employee_id`, balasannya memuat saldo seluruh karyawan.
        Kirimkan selalu employee_id milik pemakai sendiri sampai diperbaiki.

        Sisa cuti dihitung sebagai entitled_days dikurangi used_days.
      parameters:
        - { name: employee_id, in: query, schema: { type: string, format: uuid } }
        - { name: leave_type_id, in: query, schema: { type: string, format: uuid } }
        - { name: year, in: query, schema: { type: integer } }
      responses:
        "200":
          description: Daftar saldo
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/SaldoCuti" }

  /leave-requests:
    get:
      tags: [Pengajuan]
      summary: Daftar pengajuan cuti
      parameters:
        - { name: employee_id, in: query, schema: { type: string, format: uuid } }
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: per_page, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses:
        "200":
          description: Daftar pengajuan
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Halaman"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/PengajuanCuti" }
    post:
      tags: [Pengajuan]
      summary: Mengajukan cuti
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [leave_type_id, start_date, end_date]
              properties:
                employee_id:
                  type: string
                  format: uuid
                  description: Dihilangkan berarti atas nama diri sendiri.
                leave_type_id: { type: string, format: uuid }
                start_date: { type: string, format: date }
                end_date: { type: string, format: date }
                reason: { type: string }
      responses:
        "200":
          description: Pengajuan tersimpan
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PengajuanCuti" }
        "400": { $ref: "#/components/responses/Galat" }

  /reimbursement-types:
    get:
      tags: [Pengajuan]
      summary: Jenis penggantian
      responses:
        "200":
          description: Daftar jenis
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        name: { type: string }
                        created_at: { type: string, format: date-time }

  /reimbursements:
    get:
      tags: [Pengajuan]
      summary: Daftar pengajuan penggantian
      parameters:
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: per_page, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses:
        "200":
          description: Daftar pengajuan
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Halaman"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Reimbursement" }
    post:
      tags: [Pengajuan]
      summary: Mengajukan penggantian
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type_id, amount, description]
              properties:
                employee_id: { type: string, format: uuid }
                type_id: { type: string, format: uuid }
                amount: { type: integer, minimum: 1 }
                description: { type: string }
                receipt_url: { type: string }
      responses:
        "200":
          description: Pengajuan tersimpan
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Reimbursement" }
        "400": { $ref: "#/components/responses/Galat" }

  /hr/loans:
    get:
      tags: [Pengajuan]
      summary: Daftar kasbon
      description: Menuntut izin payroll:read. Tersaring menurut cakupan.
      parameters:
        - { name: employee_id, in: query, schema: { type: string, format: uuid } }
        - { name: status, in: query, schema: { type: string } }
      responses:
        "200":
          description: Daftar kasbon
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Kasbon" }
    post:
      tags: [Pengajuan]
      summary: Mengajukan kasbon
      description: |
        PERINGATAN: belum dapat dipakai karyawan biasa. Berbeda dari pengajuan
        lain, employee_id di sini wajib, dan izin yang dituntut adalah
        payroll:create yang tidak termasuk izin dasar karyawan.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [employee_id, principal, installments, start_period]
              properties:
                employee_id: { type: string, format: uuid }
                principal: { type: integer, minimum: 1 }
                installments: { type: integer, minimum: 1 }
                start_period: { type: string, example: "2026-09" }
                reason: { type: string }
      responses:
        "200":
          description: Kasbon tersimpan
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Kasbon" }
        "403": { $ref: "#/components/responses/Galat" }
        "422": { description: Badan JSON tidak dapat diurai. Balasannya teks biasa. }

  /me/tasks:
    get:
      tags: [Tugas]
      summary: Tugas milik pemakai dari seluruh proyek
      description: |
        Bidang `selesai` disimpulkan dari nama kolom papan, bukan dari penanda
        yang tercatat. Kolom yang dianggap selesai: done, selesai, completed,
        closed, finished, beres.
      parameters:
        - { name: selesai, in: query, schema: { type: boolean, default: false }, description: Sertakan tugas yang sudah selesai }
        - { name: per_page, in: query, schema: { type: integer, default: 50, maximum: 200 } }
      responses:
        "200":
          description: Daftar tugas
          content:
            application/json:
              schema:
                type: object
                properties:
                  punya_data_karyawan: { type: boolean }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Tugas" }

  /tasks/{task_id}/checklist:
    parameters:
      - { name: task_id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Tugas]
      summary: Butir daftar periksa
      description: Judul butir ada pada `label`, bukan `title`.
      responses:
        "200":
          description: Daftar butir
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        task_id: { type: string, format: uuid }
                        label: { type: string }
                        is_done: { type: boolean }
                        position: { type: integer }

  /tasks/{task_id}/checklist/{item_id}/toggle:
    parameters:
      - { name: task_id, in: path, required: true, schema: { type: string, format: uuid } }
      - { name: item_id, in: path, required: true, schema: { type: string, format: uuid } }
    patch:
      tags: [Tugas]
      summary: Membalik status satu butir
      description: Hanya menerima PATCH. POST menjawab 405.
      responses:
        "200": { description: Berhasil }

  /tasks/{task_id}/comments:
    parameters:
      - { name: task_id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Tugas]
      summary: Komentar tugas
      description: Yang dikirim author_membership_id, bukan nama penulis.
      responses:
        "200":
          description: Daftar komentar, terlama lebih dahulu
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        task_id: { type: string, format: uuid }
                        author_membership_id: { type: string, format: uuid }
                        body: { type: string }
                        created_at: { type: string, format: date-time }
    post:
      tags: [Tugas]
      summary: Menulis komentar
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body: { type: string }
                mentions:
                  type: array
                  items: { type: string, format: uuid }
                  description: membership_id yang disebut, untuk pemberitahuan.
      responses:
        "200": { description: Komentar tersimpan }

  /announcements:
    get:
      tags: [Info]
      summary: Pengumuman perusahaan
      description: Menuntut izin comms:read, dimiliki seluruh peran.
      parameters:
        - { name: is_active, in: query, schema: { type: boolean } }
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: per_page, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses:
        "200":
          description: Daftar pengumuman
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Halaman"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string, format: uuid }
                            title: { type: string }
                            content: { type: string }
                            target_type: { type: string }
                            is_active: { type: boolean }
                            author_membership_id: { type: string, format: uuid }
                            created_at: { type: string, format: date-time }

  /briefings:
    get:
      tags: [Info]
      summary: Briefing lapangan
      description: |
        Menuntut izin briefing:read, yang hanya diberikan pada paket
        distribusi untuk pengemudi dan kernet. Bagi peran lain, 403 adalah
        jawaban yang benar dan berarti "tidak berlaku bagi Anda". Ambil
        sebagai endpoint opsional.
      parameters:
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: per_page, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses:
        "200":
          description: Daftar briefing
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Halaman"
                  - type: object
                    properties:
                      data: { type: array, items: { type: object, additionalProperties: true } }
        "403": { $ref: "#/components/responses/Galat" }

  /briefings/{id}/ack:
    post:
      tags: [Info]
      summary: Menyatakan sudah membaca briefing
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                lat: { type: number }
                lng: { type: number }
      responses:
        "200": { description: Tercatat }

  /devices/fcm:
    post:
      tags: [Notifikasi]
      summary: Mendaftarkan token perangkat
      description: |
        Idempoten pada token. Token terikat pada perusahaan, pemakai, dan data
        karyawan yang sedang aktif, jadi wajib didaftarkan ulang setiap kali
        berpindah perusahaan.

        Catatan: jalur pengiriman ke Firebase belum terpasang.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
                platform: { type: string, enum: [android, ios] }
      responses:
        "200": { description: Terdaftar }
        "400": { $ref: "#/components/responses/Galat" }
    delete:
      tags: [Notifikasi]
      summary: Mencabut token perangkat
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
      responses:
        "200": { description: Dicabut }

components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Access token berlaku 30 menit, refresh token 14 hari.

  responses:
    Galat:
      description: Galat
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Galat" }

  schemas:
    Galat:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum: [bad_request, unauthorized, forbidden, not_found, conflict, internal, db_error]
            message:
              type: string
              description: Ditulis untuk dibaca pemakai. Tampilkan apa adanya.

    Halaman:
      type: object
      properties:
        page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }

    PasanganToken:
      type: object
      properties:
        access_token: { type: string }
        refresh_token: { type: string }
        token_type: { type: string, const: Bearer }

    Profil:
      type: object
      properties:
        user:
          type: object
          properties:
            id: { type: string, format: uuid }
            email: { type: string }
            name: { type: string }
            avatar_url: { type: [string, "null"] }
            totp_enabled: { type: boolean }
            preferences: { type: object, additionalProperties: true }
        employee_id: { type: [string, "null"], format: uuid }
        employee:
          type: [object, "null"]
          properties:
            id: { type: string, format: uuid }
            code: { type: string }
            name: { type: string }
            email: { type: [string, "null"] }
            phone: { type: [string, "null"] }
        tenant:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            slug: { type: string }
            logo_url: { type: [string, "null"] }
            white_label: { type: boolean }
            hidden_nav: { type: array, items: { type: string } }
            connected_providers: { type: array, items: { type: string } }
        roles:
          type: array
          items:
            type: object
            properties:
              key: { type: string }
              name: { type: string }
        permissions:
          type: array
          items: { $ref: "#/components/schemas/Izin" }
        denials:
          type: array
          items:
            type: object
            properties:
              resource: { type: string }
              action: { type: string }
        modules:
          type: array
          items:
            type: object
            properties:
              module_key: { type: string }
              enabled: { type: boolean }
        is_platform_admin: { type: boolean }

    Izin:
      type: object
      properties:
        resource: { type: string }
        action: { type: string }
        scope:
          type: string
          enum: [own, team, dept, branch, all]
          description: Berjenjang dari own yang paling sempit sampai all.

    Kehadiran:
      type: object
      properties:
        id: { type: string, format: uuid }
        employee_id: { type: string, format: uuid }
        employee_name: { type: [string, "null"], description: Hanya pada endpoint daftar. }
        work_date:
          type: string
          format: date
          description: Tanggal kalender menurut zona waktu perusahaan.
        check_in: { type: [string, "null"], format: date-time }
        check_out: { type: [string, "null"], format: date-time }
        status: { type: [string, "null"], enum: [present, late, null] }
        latitude: { type: [number, "null"] }
        longitude: { type: [number, "null"] }
        note: { type: [string, "null"] }
        shift_id: { type: [string, "null"], format: uuid }
        method: { type: [string, "null"] }
        photo_file_id: { type: [string, "null"], format: uuid }
        out_of_geofence:
          type: boolean
          description: Benar bila absen di luar area dan mode pagar lokasi warn.

    PermintaanAbsen:
      type: object
      properties:
        employee_id: { type: string, format: uuid, description: Dihilangkan berarti atas nama diri sendiri. }
        shift_id: { type: string, format: uuid, description: Tanpa ini status selalu present. }
        latitude: { type: number }
        longitude: { type: number }
        note: { type: string }
        method: { type: string, example: mobile }
        photo_file_id: { type: string, format: uuid }

    Shift:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        start_time: { type: string, example: "08:00:00" }
        end_time: { type: string, example: "17:00:00" }
        grace_minutes: { type: integer }

    JenisCuti:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        quota_days: { type: integer }
        is_paid: { type: boolean }

    SaldoCuti:
      type: object
      properties:
        id: { type: string, format: uuid }
        employee_id: { type: string, format: uuid }
        leave_type_id: { type: string, format: uuid }
        year: { type: integer }
        entitled_days: { type: integer }
        used_days: { type: integer }

    PengajuanCuti:
      type: object
      properties:
        id: { type: string, format: uuid }
        employee_id: { type: string, format: uuid }
        employee_name: { type: [string, "null"] }
        leave_type_id: { type: string, format: uuid }
        leave_type_name: { type: [string, "null"] }
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        days: { type: integer }
        reason: { type: [string, "null"] }
        approval_request_id: { type: [string, "null"], format: uuid }
        approval_status:
          type: [string, "null"]
          description: Null berarti tidak melalui alur persetujuan, bukan menunggu.

    Lembur:
      type: object
      properties:
        id: { type: string, format: uuid }
        employee_id: { type: string, format: uuid }
        work_date: { type: string, format: date }
        minutes: { type: integer }
        reason: { type: [string, "null"] }
        approval_request_id: { type: [string, "null"], format: uuid }
        approval_status: { type: [string, "null"] }

    Reimbursement:
      type: object
      properties:
        id: { type: string, format: uuid }
        employee_id: { type: string, format: uuid }
        type_id: { type: string, format: uuid }
        amount: { type: integer }
        description: { type: string }
        receipt_url: { type: [string, "null"] }
        approval_request_id: { type: [string, "null"], format: uuid }
        approval_status: { type: [string, "null"] }
        current_step: { type: [integer, "null"] }
        created_at: { type: string, format: date-time }

    Kasbon:
      type: object
      properties:
        id: { type: string, format: uuid }
        employee_id: { type: string, format: uuid }
        employee_name: { type: [string, "null"] }
        principal: { type: integer }
        installments: { type: integer }
        installment_amount: { type: integer }
        remaining: { type: integer }
        start_period: { type: string, example: "2026-09" }
        reason: { type: [string, "null"] }
        status: { type: string }
        paid_count: { type: integer }

    Tugas:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: [string, "null"], format: uuid }
        project_name: { type: [string, "null"] }
        title: { type: string }
        description: { type: [string, "null"] }
        status_name: { type: [string, "null"] }
        due_date: { type: [string, "null"], format: date }
        priority: { type: [string, "null"] }
        selesai: { type: boolean, description: Disimpulkan dari nama kolom papan. }

    SlipGaji:
      type: object
      properties:
        id: { type: string, format: uuid }
        run_id: { type: string, format: uuid }
        period: { type: string, example: "2026-08" }
        base: { type: integer }
        allowances: { type: integer }
        deductions: { type: integer }
        gross: { type: integer }
        net: { type: integer }
        tax: { type: integer }
