> ## Documentation Index
> Fetch the complete documentation index at: https://docs.quinnsambal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Payroll

<style>
  {`
    [class*="max-w-none"][class*="table"] {
      display: block !important;
      overflow-x: auto !important;
      max-width: 100% !important;
      width: 100% !important;
      flex-grow: 0 !important;
    }
    [class*="max-w-none"][class*="table"] > table {
      width: 100% !important;
      max-width: 100% !important;
      table-layout: fixed !important;
    }
    .mermaid {
      max-width: 100% !important;
      overflow-x: auto !important;
    }
    article svg[role="img"] {
      max-width: 100% !important;
      height: auto !important;
    }
    article img, .prose img {
      max-width: 100% !important;
      height: auto !important;
    }
    img[src*="LOGO"], img[src*="logo"] {
      max-width: 120px !important;
      max-height: 40px !important;
      width: auto !important;
      height: auto !important;
      object-fit: contain !important;
    }
    `}
</style>

***

title: "Payroll Automation — Otomasi Penggajian"
description: "Otomasi penggajian dengan kalkulator real-time, AI insights, konfigurasi BPJS/PPh 21, dan export PDF di SNISHOP ERP."
-----------------------------------------------------------------------------------------------------------------------------------

# Payroll Automation — Otomasi Penggajian

<img src="https://mintcdn.com/quinnofspicy/e4f_upKhVWcjsUmM/docs/mintlify/screenshots/hr/payroll.png?fit=max&auto=format&n=e4f_upKhVWcjsUmM&q=85&s=39721d5c39c8aab696fb956e4072f0fa" alt="Payroll" width="1920" height="1080" data-path="docs/mintlify/screenshots/hr/payroll.png" />

Payroll Automation mengotomatisasi seluruh proses penggajian dari perhitungan gaji pokok, lembur, bonus KPI, potongan BPJS, PPh 21, hingga generate slip gaji PDF. Dilengkapi AI assistant untuk optimasi biaya dan deteksi anomali.

## Arsitektur Payroll

```mermaid theme={null}
flowchart TB
    subgraph INPUT["Data Input"]
        ATT[CompanyAttendance<br/>Hari hadir, lembur, terlambat]
        KPI[CompanyKPI<br/>Skor & rating KPI]
        CFG[PayrollConfiguration<br/>Formula & tarif]
        MEM[CompanyMember<br/>Gaji pokok karyawan]
    end

    subgraph ENGINE["Payroll Engine — generatePayroll()"]
        PRORATE[Prorata Gaji<br/>basicSalary × attendance/working_days]
        OT[Upah Lembur<br/>overtimeHours × hourlyRate × overtime_rate]
        BONUS[Bonus KPI<br/>basicSalary × rating_percentage]
        GROSS[Gaji Kotor<br/>basic + overtime + bonus]
        BPJS_K[BPJS Kesehatan<br/>Emp 1% / Co 4%]
        BPJS_T[BPJS TK<br/>JHT, JKK, JKM, JP]
        TAX[PPh 21<br/>Progressive + PTKP]
        NET[Gaji Bersih<br/>gross - bpjs - tax]
    end

    subgraph OUTPUT["Output"]
        SLIP[CompanyPayroll<br/>Record]
        PDF[Slip Gaji PDF]
        AI[AI Insights<br/>InvokeLLM]
    end

    ATT --> PRORATE
    KPI --> BONUS
    CFG --> OT
    CFG --> BPJS_K
    CFG --> BPJS_T
    CFG --> TAX
    MEM --> PRORATE
    PRORATE --> GROSS
    OT --> GROSS
    BONUS --> GROSS
    GROSS --> BPJS_K
    GROSS --> BPJS_T
    BPJS_K --> TAX
    BPJS_T --> TAX
    TAX --> NET
    NET --> SLIP
    SLIP --> PDF
    SLIP --> AI
```

## Akses Halaman

URL: `/payroll`

## Empat Tab Utama

| Tab | Label | Komponen | Fungsi |
| - | - | - | - |
| `realtime` | Realtime | `RealtimePayrollCalculator` | Kalkulator pro-rated hingga hari ini |
| `payroll` | Payroll | Payroll List | Daftar payroll resmi + mark-as-paid |
| `ai_insights` | AI Insights | `PayrollAIAssistant` | Analisis AI untuk optimasi biaya |
| `config` | Konfigurasi | `PayrollConfigForm` | Formula, tarif, BPJS, PPh 21 |

## Tab Realtime: Payroll Calculator

Kalkulator real-time menghitung proyeksi gaji **hingga hari ini** (bukan akhir bulan):

| Fitur | Deskripsi |
| - | - |
| **Pro-rated Salary** | Gaji pokok diprorata berdasarkan hari yang sudah berlalu |
| **Overtime Calculation** | Jam lembur yang sudah tercatat |
| **KPI Projection** | Bonus berdasarkan KPI terakhir |
| **Save as Draft** | Hasil kalkulasi bisa disimpan sebagai payroll resmi (status: draft) |
| **Full Month Projection** | Proyeksi gaji penuh jika tren berlanjut hingga akhir bulan |

## Tab Payroll: Daftar Resmi

Daftar payroll resmi dengan aksi:

| Aksi | Deskripsi |
| - | - |
| **Lihat Detail** | Buka dialog detail slip gaji |
| **Mark as Paid** | Tandai sudah dibayar (status → `paid`) |
| **Generate PDF** | Download slip gaji dalam format PDF |
| **Bulk PDF** | Generate PDF untuk seluruh karyawan sekaligus |

## Tab AI Insights: PayrollAIAssistant

AI assistant menggunakan `base44.integrations.Core.InvokeLLM` untuk analisis mendalam:

### 1. Cost-Saving Detection

| Deteksi | Kondisi | Rekomendasi |
| - | - | - |
| **High Overtime** | Overtime > threshold | Evaluasi beban kerja atau tambah karyawan |
| **BPJS Optimization** | Tarif tidak optimal | Sesuaikan basis perhitungan |

### 2. Anomaly Detection

| Anomali | Kondisi | Alert |
| - | - | - |
| **Salary Outlier** | Gaji karyawan jauh dari rata-rata departemen | Review struktur gaji |
| **Zero Attendance + Pay** | Tidak ada absensi tapi ada gaji | Investigasi kemungkinan error |

### 3. KPI Impact Analysis

Menganalisis korelasi antara skor KPI dan bonus yang diberikan per periode.

### 4. Tax Optimization

Saran optimasi PPh 21 berdasarkan bracket pajak dan PTKP.

### 4. Per-Employee Explanation

Untuk setiap karyawan, AI menghasilkan penjelasan natural language tentang komponen gaji:

```
LLM Input:  { basic_salary, overtime, kpi, bpjs, tax, net }
LLM Output: "Gaji bersih Budi bulan Januari Rp 6.187.500. 
             Bonus KPI menambah 10% dari gaji pokok karena 
             rating 'meets expectations'. PPh 21 dihitung 
             progresif setelah pengurangan PTKP Rp 54.000.000."
```

## Tab Konfigurasi: PayrollConfigForm

### Entitas: PayrollConfiguration

| Field | Tipe | Deskripsi |
| - | - | - |
| `company_id` | string | ID perusahaan |
| `basic_salary_formula` | enum | `fixed`, `hourly`, `daily` |
| `working_hours_per_day` | number | Jam kerja per hari |
| `working_days_per_month` | number | Hari kerja per bulan |
| `overtime_rate` | number | Tarif lembur (multiplier) |
| `late_penalty_per_minute` | number | Denda terlambat per menit |
| `absence_deduction_per_day` | number | Potongan absen per hari |
| `kpi_bonus_enabled` | boolean | Aktifkan bonus KPI |
| `kpi_bonus_formula` | object | Persentase per rating |
| `tax_formula` | object | PTKP dan bracket pajak |
| `bpjs_kesehatan` | object | Tarif BPJS Kesehatan |
| `bpjs_ketenagakerjaan` | object | Tarif BPJS Ketenagakerjaan |
| `auto_generate` | boolean | Auto-generate payroll setiap bulan |
| `payment_date` | number | Tanggal pembayaran (1-28) |

## Perhitungan Payroll: generatePayroll()

### Langkah 1: Gaji Pokok Pro-rated

```
hourlyRate = basicSalary / (working_days_per_month × working_hours_per_day)

Jika basic_salary_formula = "fixed":
  proratedSalary = basicSalary × (attendance_days / working_days_per_month)

Jika basic_salary_formula = "hourly":
  proratedSalary = hourlyRate × total_hours_worked

Jika basic_salary_formula = "daily":
  proratedSalary = dailyRate × attendance_days
```

### Langkah 2: Upah Lembur

```
overtimePay = overtime_hours × hourlyRate × overtime_rate
```

### Langkah 3: Bonus KPI

Bonus berdasarkan rating KPI:

| Rating | Persentase Bonus |
| - | - |
| `outstanding` | 20% dari gaji pokok |
| `exceeds_expectations` | 15% |
| `meets_expectations` | 10% |
| `needs_improvement` | 5% |
| `unsatisfactory` | 0% |

```
kpiBonus = basicSalary × (kpi_bonus_formula[rating] / 100)
```

### Langkah 4: Gaji Kotor

```
grossSalary = proratedSalary + overtimePay + kpiBonus
```

### Langkah 5: BPJS Kesehatan

| Komponen | Tarif Karyawan | Tarif Perusahaan |
| - | - | - |
| BPJS Kesehatan | 1% | 4% |

```
bpjs_kesehatan_employee = grossSalary × 1%
bpjs_kesehatan_company = grossSalary × 4%
```

### Langkah 6: BPJS Ketenagakerjaan

| Komponen | Tarif Karyawan | Tarif Perusahaan |
| - | - | - |
| JHT (Jaminan Hari Tua) | 2% | 3.7% |
| JKK (Jaminan Kecelakaan Kerja) | — | 0.24% |
| JKM (Jaminan Kematian) | — | 0.3% |
| JP (Jaminan Pensiun) | 1% | 2% |

```
bpjs_tk_employee = grossSalary × (2% + 1%) = grossSalary × 3%
bpjs_tk_company = grossSalary × (3.7% + 0.24% + 0.3% + 2%) = grossSalary × 6.24%
```

### Langkah 7: PPh 21 (Pajak Penghasilan)

Perhitungan PPh 21 progresif dengan PTKP:

| Komponen | Nilai |
| - | - |
| **PTKP** (Penghasilan Tidak Kena Pajak) | Rp 54.000.000/tahun |

**Bracket Pajak Progresif:**

| Penghasilan Kena Pajak | Tarif |
| - | - |
| 0 — Rp 60.000.000 | 5% |
| Rp 60.000.001 — Rp 250.000.000 | 15% |
| Rp 250.000.001 — Rp 500.000.000 | 25% |
| > Rp 500.000.000 | 30% |

```
annualIncome = grossSalary × 12
annualBPJS = (bpjs_kesehatan_employee + bpjs_tk_employee) × 12
taxableIncome = annualIncome - annualBPJS - PTKP

PPh 21 tahunan = hitung progresif dari taxableIncome
PPh 21 bulanan = PPh 21 tahunan / 12
```

### Langkah 8: Gaji Bersih

```
netSalary = grossSalary - bpjs_kesehatan_employee - bpjs_tk_employee - PPh_21_bulanan
```

## Export PDF

### Single Slip

`PayrollPDFGenerator` menghasilkan PDF slip gaji individual dengan format resmi.

### Bulk Export

`BulkPayrollPDFButton` generate PDF untuk seluruh karyawan dalam satu periode. Berguna untuk arsip dan distribusi slip gaji.

## Integrasi dengan Modul Lain

```mermaid theme={null}
flowchart LR
    ATT[Attendance] -->|attendance_days<br/>overtime_hours<br/>late_count| PAY[Payroll]
    KPI[KPI] -->|kpi_score<br/>rating| PAY
    LV[Leave] -->|attendance_days<br/>dikurangi| PAY
    LN[Loan] -->|deduction| PAY
    PAY -->|net_salary<br/>bpjs<br/>tax| FIN[Finance]
    PAY -->|PDF slip| EMP[Employee Portal]
```

***

## Entity Relationship Diagram

```mermaid theme={null}
erDiagram
    Company ||--o{ CompanyMember : "memiliki anggota"
    Company ||--o{ CompanyPayroll : "memiliki payroll"
    Company ||--o{ PayrollConfiguration : "memiliki konfigurasi"
    Company ||--o{ CompanyAttendance : "memiliki absensi"
    Company ||--o{ CompanyKPI : "memiliki penilaian"
    Company ||--o{ CompanyLoan : "memiliki kasbon"
    Company ||--o{ Employee : "memiliki karyawan"

    CompanyMember ||--o{ CompanyPayroll : "employee_id"
    CompanyMember ||--o{ CompanyAttendance : "employee_id"
    CompanyMember ||--o{ CompanyKPI : "employee_id"
    CompanyMember ||--o{ CompanyLoan : "employee_id"

    Employee ||--o{ CompanyMember : "linked via employee_id"
    Employee ||--o{ CompanyPayroll : "employee_id"

    CompanyPayroll }o--|| PayrollConfiguration : "menggunakan formula & tarif"
    CompanyPayroll }o--o{ CompanyAttendance : "attendance_days, overtime_hours"
    CompanyPayroll }o--o{ CompanyKPI : "kpi_score, rating bonus"
    CompanyPayroll }o--o{ CompanyLoan : "monthly_deduction"

    CompanyPayroll {
        string company_id PK
        string employee_id FK
        string employee_name
        string period "YYYY-MM"
        number basic_salary
        number attendance_days
        number working_days
        number late_count
        number absent_count
        number overtime_hours
        number kpi_score
        array allowances
        array deductions
        number overtime_pay
        number kpi_bonus
        number gross_salary
        number net_salary
        string status "draft|approved|paid"
        date payment_date
        string payment_method
        string notes
    }

    PayrollConfiguration {
        string company_id PK
        string description
        string basic_salary_formula "fixed|hourly|daily"
        number working_hours_per_day
        number working_days_per_month
        number overtime_rate
        number late_penalty_per_minute
        number absence_deduction_per_day
        boolean kpi_bonus_enabled
        object kpi_bonus_formula
        object tax_formula
        object bpjs_kesehatan
        object bpjs_ketenagakerjaan
        boolean auto_generate
        number payment_date
    }

    CompanyLoan {
        string company_id PK
        string employee_id FK
        string employee_name
        string employee_email
        number amount
        string reason
        string description
        string attachment_url
        number installment_months
        number monthly_deduction
        number remaining_amount
        string status "pending|approved|rejected|paid_off"
        string approver_id
        string approver_notes
        datetime approved_at
        datetime disbursed_at
    }

    CompanyAttendance {
        string company_id PK
        string employee_id FK
        string employee_name
        string employee_email
        date date
        string shift_id
        string shift_name
        datetime clock_in_time
        datetime clock_out_time
        string status "present|late|absent|sick|leave|wfh"
        number total_hours
        number overtime_hours
    }

    CompanyKPI {
        string company_id PK
        string employee_id FK
        string employee_name
        string period "YYYY-MM"
        array metrics
        number overall_score
        string rating "outstanding|exceeds|meets|needs_improvement|unsatisfactory"
        string status "draft|submitted|reviewed|finalized"
    }

    CompanyMember {
        string company_id PK
        string user_id FK
        string user_email
        string user_name
        string role "owner|admin|supervisor|hr_admin|employee|..."
        string employee_id FK
        string department
        string position
        number salary
        string status "active|inactive|pending"
    }
```

***

## Tabel Schema

### CompanyPayroll

Entitas utama untuk menyimpan record penggajian per karyawan per periode.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan pemilik payroll |
| `employee_id` | string | Ya | — | ID karyawan (CompanyMember.id) |
| `employee_name` | string | Tidak | — | Nama lengkap karyawan (denormalized) |
| `period` | string | Ya | — | Periode penggajian format `YYYY-MM` |
| `basic_salary` | number | Ya | — | Gaji pokok sebelum prorata |
| `attendance_days` | number | Tidak | — | Jumlah hari hadir di periode ini |
| `working_days` | number | Tidak | — | Total hari kerja di periode ini |
| `late_count` | number | Tidak | `0` | Jumlah keterlambatan |
| `absent_count` | number | Tidak | `0` | Jumlah hari absen tanpa keterangan |
| `overtime_hours` | number | Tidak | `0` | Total jam lembur |
| `kpi_score` | number | Tidak | — | Skor KPI periode ini (0-100) |
| `allowances` | array\[{name, amount}] | Tidak | — | Daftar tunjangan (nama + jumlah) |
| `deductions` | array\[{name, amount}] | Tidak | — | Daftar potongan (nama + jumlah) |
| `overtime_pay` | number | Tidak | `0` | Upah lembur terkalkulasi |
| `kpi_bonus` | number | Tidak | `0` | Bonus KPI berdasarkan rating |
| `gross_salary` | number | Ya | — | Gaji kotor (basic + overtime + bonus) |
| `net_salary` | number | Ya | — | Gaji bersih setelah potongan BPJS & pajak |
| `status` | enum | Tidak | `draft` | Status payroll: `draft`, `approved`, `paid` |
| `payment_date` | date | Tidak | — | Tanggal pembayaran aktual |
| `payment_method` | enum | Tidak | — | Metode bayar: `bank_transfer`, `cash`, `check` |
| `notes` | string | Tidak | — | Catatan tambahan untuk payroll ini |

**Required fields:** `company_id`, `employee_id`, `period`, `basic_salary`, `gross_salary`, `net_salary`

### PayrollConfiguration

Konfigurasi penggajian per perusahaan — menyimpan formula, tarif BPJS, bracket pajak, dan parameter kalkulasi.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan |
| `description` | string | Tidak | — | Penjelasan kebijakan penggajian (max 1000 char) |
| `basic_salary_formula` | enum | Tidak | `fixed` | Formula gaji pokok: `fixed`, `hourly`, `daily` |
| `working_hours_per_day` | number | Tidak | `8` | Jam kerja per hari |
| `working_days_per_month` | number | Tidak | `22` | Hari kerja per bulan |
| `overtime_rate` | number | Tidak | `1.5` | Multiplier lembur (1.5 = 150%) |
| `late_penalty_per_minute` | number | Tidak | `0` | Denda terlambat per menit (dalam IDR) |
| `absence_deduction_per_day` | number | Tidak | `0` | Potongan absen per hari (dalam IDR) |
| `kpi_bonus_enabled` | boolean | Tidak | `true` | Aktifkan bonus KPI |
| `kpi_bonus_formula` | object | Tidak | — | Persentase bonus per rating KPI |
| `kpi_bonus_formula.outstanding` | number | — | `20` | Bonus 20% untuk outstanding |
| `kpi_bonus_formula.exceeds` | number | — | `15` | Bonus 15% untuk exceeds expectations |
| `kpi_bonus_formula.meets` | number | — | `10` | Bonus 10% untuk meets expectations |
| `kpi_bonus_formula.needs_improvement` | number | — | `5` | Bonus 5% untuk needs improvement |
| `kpi_bonus_formula.unsatisfactory` | number | — | `0` | Bonus 0% untuk unsatisfactory |
| `tax_formula` | object | Tidak | — | Konfigurasi PTKP dan bracket pajak |
| `tax_formula.ptkp` | number | — | `54000000` | Penghasilan Tidak Kena Pajak per tahun |
| `tax_formula.brackets` | array\[{min, max, rate}] | — | — | Bracket pajak progresif |
| `bpjs_kesehatan` | object | Tidak | — | Tarif BPJS Kesehatan |
| `bpjs_kesehatan.company_contribution` | number | — | `4` | Kontribusi perusahaan (%) |
| `bpjs_kesehatan.employee_contribution` | number | — | `1` | Kontribusi karyawan (%) |
| `bpjs_ketenagakerjaan` | object | Tidak | — | Tarif BPJS Ketenagakerjaan |
| `bpjs_ketenagakerjaan.jht_company` | number | — | `3.7` | JHT kontribusi perusahaan (%) |
| `bpjs_ketenagakerjaan.jht_employee` | number | — | `2` | JHT kontribusi karyawan (%) |
| `bpjs_ketenagakerjaan.jkk` | number | — | `0.24` | JKK kontribusi perusahaan (%) |
| `bpjs_ketenagakerjaan.jkm` | number | — | `0.3` | JKM kontribusi perusahaan (%) |
| `bpjs_ketenagakerjaan.jp_company` | number | — | `2` | JP kontribusi perusahaan (%) |
| `bpjs_ketenagakerjaan.jp_employee` | number | — | `1` | JP kontribusi karyawan (%) |
| `auto_generate` | boolean | Tidak | `false` | Auto-generate payroll setiap akhir bulan |
| `payment_date` | number | Tidak | `25` | Tanggal pembayaran (1-31) |

**Required fields:** `company_id`

### CompanyLoan

Entitas kasbon/pinjaman karyawan yang potongannya diterapkan pada payroll.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan |
| `employee_id` | string | Ya | — | ID karyawan peminjam |
| `employee_name` | string | Tidak | — | Nama karyawan (denormalized) |
| `employee_email` | string | Tidak | — | Email karyawan (denormalized) |
| `amount` | number | Ya | — | Jumlah kasbon yang diajukan |
| `reason` | string | Ya | — | Alasan pengajuan kasbon |
| `description` | string | Tidak | — | Detail tambahan & rencana pelunasan (max 1000 char) |
| `attachment_url` | string | Tidak | — | URL bukti pendukung |
| `installment_months` | number | Tidak | `1` | Jumlah cicilan (bulan) |
| `monthly_deduction` | number | Tidak | — | Potongan per bulan (amount / installment\_months) |
| `remaining_amount` | number | Tidak | — | Sisa pinjaman yang belum dilunasi |
| `status` | enum | Tidak | `pending` | Status: `pending`, `approved`, `rejected`, `paid_off` |
| `approver_id` | string | Tidak | — | ID user yang menyetujui |
| `approver_notes` | string | Tidak | — | Catatan dari approver |
| `approved_at` | datetime | Tidak | — | Waktu persetujuan |
| `disbursed_at` | datetime | Tidak | — | Waktu pencairan dana |

**Required fields:** `company_id`, `employee_id`, `amount`, `reason`

***

## State Machine — Siklus Hidup Payroll

```mermaid theme={null}
stateDiagram-v2
    [*] --> draft : generatePayroll()\natau Realtime Calculator\n"Save as Draft"

    draft --> draft : Edit komponen\n(basic, overtime, KPI, allowances)
    draft --> approved : HR Admin review\n& approve
    draft --> draft : Recalculate\n(ubah input)

    approved --> paid : Mark as Paid\n(payment_date, payment_method)
    approved --> draft : Revisi\n(batal approve)

    paid --> [*] : Selesai\n(generate PDF slip)

    note right of draft
        Payroll dapat di-edit selama
        status masih draft.
        Semua komponen dihitung ulang
        saat input berubah.
    end note

    note right of approved
        Setelah approved, payroll
        siap untuk dibayar.
        Trigger notifikasi ke finance.
    end note

    note right of paid
        Generate slip gaji PDF.
        Integrasi ke modul Finance
        untuk pencatatan pengeluaran.
    end note
```

### State Machine — Siklus Hidup CompanyLoan

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending : Karyawan mengajukan kasbon
    pending --> approved : HR/Admin approve
    pending --> rejected : HR/Admin reject
    rejected --> [*] : Ditolak

    approved --> approved : Disburse dana\n(disbursed_at)
    approved --> paid_off : Cicilan selesai\n(remaining_amount = 0)

    note right of pending
        Karyawan mengajukan via
        employee portal.
        Menunggu persetujuan.
    end note

    note right of approved
        Dana dicairkan.
        monthly_deduction dipotong
        otomatis dari payroll tiap bulan.
    end note

    note right of paid_off
        Semua cicilan telah dilunasi.
        Tidak ada potongan lagi
        di payroll berikutnya.
    end note
```

***

## Sequence Diagram — Perhitungan Payroll

```mermaid theme={null}
sequenceDiagram
    participant HR as HR Admin
    participant RT as Realtime Calculator
    participant ATT as CompanyAttendance
    participant KPI as CompanyKPI
    participant CFG as PayrollConfiguration
    participant MEM as CompanyMember
    participant LN as CompanyLoan
    participant ENG as Payroll Engine
    participant DB as CompanyPayroll

    HR->>RT: Pilih periode & klik "Hitung"
    RT->>MEM: Fetch semua karyawan aktif
    MEM-->>RT: List karyawan + basic_salary

    loop Untuk setiap karyawan
        RT->>ATT: Fetch absensi periode ini
        ATT-->>RT: attendance_days, overtime_hours, late_count

        RT->>KPI: Fetch KPI periode ini
        KPI-->>RT: overall_score, rating

        RT->>CFG: Fetch konfigurasi payroll
        CFG-->>RT: formula, tarif BPJS, bracket pajak

        RT->>ENG: generatePayroll(inputs)
        Note over ENG: 1. proratedSalary = basic × (attendance/working_days)
        Note over ENG: 2. overtimePay = hours × hourlyRate × overtime_rate
        Note over ENG: 3. kpiBonus = basic × kpi_bonus_formula[rating]%
        Note over ENG: 4. grossSalary = prorated + overtime + kpiBonus
        Note over ENG: 5. bpjsKesehatan = gross × (1% emp + 4% co)
        Note over ENG: 6. bpjsTK = gross × (3% emp + 6.24% co)
        Note over ENG: 7. PPh21 = progresif(annual - PTKP) / 12

        RT->>LN: Fetch pinjaman aktif
        LN-->>RT: monthly_deduction

        Note over ENG: 8. netSalary = gross - bpjs_emp - pph21 - loan_deduction
        ENG-->>RT: Komponen gaji lengkap
    end

    HR->>RT: "Save as Draft"
    RT->>DB: Insert CompanyPayroll (status: draft)
    DB-->>RT: Payroll records created
```

## Sequence Diagram — Generate Slip Gaji PDF

```mermaid theme={null}
sequenceDiagram
    participant HR as HR Admin
    participant UI as Payroll List
    participant PDF as PayrollPDFGenerator
    participant DB as CompanyPayroll
    participant MEM as CompanyMember
    participant CFG as PayrollConfiguration

    HR->>UI: Pilih payroll & klik "Generate PDF"
    UI->>DB: Fetch CompanyPayroll by ID
    DB-->>UI: Payroll record + komponen

    UI->>MEM: Fetch data karyawan
    MEM-->>UI: Nama, posisi, departemen

    UI->>CFG: Fetch konfigurasi perusahaan
    CFG-->>UI: Nama perusahaan, logo

    UI->>PDF: Render PDF slip gaji
    Note over PDF: Format: Header perusahaan,<br/>data karyawan, tabel komponen,<br/>total potongan, gaji bersih
    PDF-->>UI: PDF Blob

    UI-->>HR: Download slip gaji PDF

    Note over HR,PDF: Bulk Export: iterate semua payroll<br/>dalam periode → generate ZIP
```

***

## Enum Tables

### payroll\_status

Status siklus hidup record payroll.

| Nilai | Deskripsi |
| - | - |
| `draft` | Payroll baru dihitung, belum di-approve. Masih bisa diedit semua komponennya. |
| `approved` | HR Admin sudah mereview dan menyetujui perhitungan. Siap untuk dibayar. |
| `paid` | Gaji sudah dibayarkan ke karyawan. Tanggal & metode pembayaran tercatat. |

### payroll\_period

Format periode penggajian.

| Format | Contoh | Deskripsi |
| - | - | - |
| `YYYY-MM` | `2025-01` | Periode bulan penuh (default) |
| `YYYY-MM` | `2025-06` | Periode Juni 2025 |

Periode selalu dalam format ISO 8601 year-month. Satu record payroll per karyawan per periode.

### basic\_salary\_formula

Formula perhitungan gaji pokok yang dikonfigurasi di PayrollConfiguration.

| Nilai | Deskripsi | Formula |
| - | - | - |
| `fixed` | Gaji tetap bulanan | `proratedSalary = basicSalary × (attendance_days / working_days_per_month)` |
| `hourly` | Gaji berdasarkan jam | `proratedSalary = (basicSalary / (working_days × hours_per_day)) × total_hours_worked` |
| `daily` | Gaji berdasarkan hari | `proratedSalary = (basicSalary / working_days) × attendance_days` |

### payment\_method

Metode pembayaran gaji.

| Nilai | Deskripsi |
| - | - |
| `bank_transfer` | Transfer bank ke rekening karyawan |
| `cash` | Pembayaran tunai |
| `check` | Cek/giro |

### loan\_status

Status siklus hidup kasbon/pinjaman karyawan.

| Nilai | Deskripsi |
| - | - |
| `pending` | Pengajuan menunggu persetujuan HR/Admin |
| `approved` | Disetujui, menunggu pencairan dana |
| `rejected` | Ditolak oleh HR/Admin |
| `paid_off` | Semua cicilan telah dilunasi |

### kpi\_rating

Rating hasil penilaian KPI yang menentukan persentase bonus.

| Rating | Persentase Bonus | Deskripsi |
| - | - | - |
| `outstanding` | 20% | Kinerja luar biasa, melampaui semua target |
| `exceeds` | 15% | Melampaui sebagian besar target |
| `meets` | 10% | Memenuhi semua target yang ditetapkan |
| `needs_improvement` | 5% | Belum memenuhi beberapa target |
| `unsatisfactory` | 0% | Tidak memenuhi target, tidak ada bonus |

### attendance\_status

Status absensi harian karyawan.

| Nilai | Deskripsi |
| - | - |
| `present` | Hadir tepat waktu |
| `late` | Hadir terlambat |
| `absent` | Tidak hadir tanpa keterangan |
| `sick` | Sakit (dengan surat dokter) |
| `leave` | Cuti (sudah disetujui) |
| `wfh` | Work from home |

### employment\_type

Tipe kepegawaian karyawan.

| Nilai | Deskripsi |
| - | - |
| `full_time` | Karyawan tetap penuh waktu |
| `part_time` | Karyawan paruh waktu |
| `contract` | Karyawan kontrak (PKWT) |
| `intern` | Magang |

***

## RBAC — Hak Akses Payroll

Kontrol akses berdasarkan role di CompanyMember.

| Operasi | Owner | Admin | HR Admin | Supervisor | Employee |
| - | - | - | - | - | - |
| **Lihat daftar payroll** | Ya | Ya | Ya | Ya (departemen) | Ya (milik sendiri) |
| **Lihat detail payroll** | Ya | Ya | Ya | Ya (departemen) | Ya (milik sendiri) |
| **Hitung realtime** | Ya | Ya | Ya | Tidak | Tidak |
| **Save as Draft** | Ya | Ya | Ya | Tidak | Tidak |
| **Edit payroll draft** | Ya | Ya | Ya | Tidak | Tidak |
| **Approve payroll** | Ya | Ya | Ya | Tidak | Tidak |
| **Mark as Paid** | Ya | Ya | Ya (view only) | Tidak | Tidak |
| **Generate PDF** | Ya | Ya | Ya | Ya (departemen) | Ya (milik sendiri) |
| **Bulk PDF Export** | Ya | Ya | Ya | Tidak | Tidak |
| **Edit konfigurasi** | Ya | Ya | Tidak | Tidak | Tidak |
| **Lihat AI Insights** | Ya | Ya | Ya | Tidak | Tidak |
| **Ajukan kasbon** | Ya | Ya | Ya | Ya | Ya |
| **Approve kasbon** | Ya | Ya | Ya | Tidak | Tidak |

**Permission flags** di CompanyMember.permissions:

| Permission | Deskripsi |
| - | - |
| `can_view_hr` | Akses baca modul HR (termasuk payroll) |
| `can_edit_hr` | Akses tulis modul HR (create, edit, approve payroll) |
| `can_view_finance` | Akses baca modul Finance (untuk integrasi pembayaran) |
| `can_manage_members` | Kelola anggota perusahaan |

**RLS Policy:** Semua entitas payroll menggunakan filter `company_id = user.data.active_company_id` untuk isolasi data antar perusahaan. Karyawan hanya bisa melihat data milik sendiri via `employee_email = user.email`.

***

## Tabel Perhitungan BPJS

### BPJS Kesehatan

| Komponen | Tarif Karyawan | Tarif Perusahaan | Dasar Perhitungan |
| - | - | - | - |
| BPJS Kesehatan | **1%** | **4%** | `gross_salary` (gaji kotor) |

```
bpjs_kesehatan_karyawan = gross_salary × 1%
bpjs_kesehatan_perusahaan = gross_salary × 4%
total_bpjs_kesehatan = gross_salary × 5%
```

### BPJS Ketenagakerjaan

| Komponen | Tarif Karyawan | Tarif Perusahaan | Dasar Perhitungan |
| - | - | - | - |
| JHT (Jaminan Hari Tua) | **2%** | **3.7%** | `gross_salary` |
| JKK (Jaminan Kecelakaan Kerja) | — | **0.24%** | `gross_salary` |
| JKM (Jaminan Kematian) | — | **0.3%** | `gross_salary` |
| JP (Jaminan Pensiun) | **1%** | **2%** | `gross_salary` |
| **Total** | **3%** | **6.24%** | — |

```
bpjs_tk_karyawan = gross_salary × (2% + 1%) = gross_salary × 3%
bpjs_tk_perusahaan = gross_salary × (3.7% + 0.24% + 0.3% + 2%) = gross_salary × 6.24%
total_bpjs_tk = gross_salary × 9.24%
```

### Ringkasan Total Potongan & Kontribusi

| Komponen | Ditanggung Karyawan | Ditanggung Perusahaan |
| - | - | - |
| BPJS Kesehatan | 1% | 4% |
| JHT | 2% | 3.7% |
| JKK | — | 0.24% |
| JKM | — | 0.3% |
| JP | 1% | 2% |
| **Total BPJS** | **3%** | **6.24%** |

### Contoh Perhitungan (Gaji Pokok Rp 10.000.000)

| Komponen | Perhitungan | Jumlah (IDR) |
| - | - | - |
| Gaji Pokok | — | 10.000.000 |
| BPJS Kesehatan (karyawan) | 10.000.000 × 1% | 100.000 |
| BPJS Kesehatan (perusahaan) | 10.000.000 × 4% | 400.000 |
| JHT (karyawan) | 10.000.000 × 2% | 200.000 |
| JHT (perusahaan) | 10.000.000 × 3.7% | 370.000 |
| JKK (perusahaan) | 10.000.000 × 0.24% | 24.000 |
| JKM (perusahaan) | 10.000.000 × 0.3% | 30.000 |
| JP (karyawan) | 10.000.000 × 1% | 100.000 |
| JP (perusahaan) | 10.000.000 × 2% | 200.000 |
| **Total potongan karyawan** | — | **400.000** |
| **Total kontribusi perusahaan** | — | **1.024.000** |

***

## Diagram Relasi Entitas Payroll — Schema Lengkap

```mermaid theme={null}
erDiagram
    Company ||--o{ PayrollConfiguration : "memiliki konfigurasi penggajian"
    Company ||--o{ CompanyPayroll : "memiliki record payroll"
    Company ||--o{ Payroll : "memiliki payroll legacy"
    Company ||--o{ Employee : "memiliki data karyawan"
    Company ||--o{ CompanyMember : "memiliki anggota"
    Company ||--o{ CompanyAttendance : "memiliki absensi"
    Company ||--o{ CompanyKPI : "memiliki penilaian KPI"
    Company ||--o{ CompanyLoan : "memiliki kasbon"
    Company ||--o{ CompanyLeave : "memiliki cuti"
    Company ||--o{ PayslipTemplate : "memiliki template slip"
    Company ||--o{ KPITemplate : "memiliki template KPI"
    Company ||--|| CompanyAttendanceSettings : "memiliki pengaturan absensi"

    Employee ||--o{ CompanyMember : "terhubung via employee_id"
    Employee ||--o{ CompanyPayroll : "memiliki record gaji"
    CompanyMember ||--o{ CompanyPayroll : "employee_id"
    CompanyMember ||--o{ CompanyAttendance : "employee_id"
    CompanyMember ||--o{ CompanyKPI : "employee_id"
    CompanyMember ||--o{ CompanyLoan : "employee_id"
    CompanyMember ||--o{ CompanyLeave : "employee_id"

    CompanyPayroll }o--|| PayrollConfiguration : "menggunakan formula & tarif"
    CompanyPayroll }o--o{ CompanyAttendance : "attendance_days, overtime_hours"
    CompanyPayroll }o--o{ CompanyKPI : "kpi_score, rating bonus"
    CompanyPayroll }o--o{ CompanyLoan : "monthly_deduction"
    CompanyPayroll }o--o{ CompanyLeave : "pengurangan hari hadir"
    CompanyPayroll }o--|| PayslipTemplate : "cetak menggunakan template"

    CompanyKPI }o--o| KPITemplate : "menggunakan template"
    CompanyAttendance }o--|| CompanyAttendanceSettings : "mengikuti pengaturan shift & geofence"
```

***

## Schema Entitas Lengkap — Dikelompokkan per Komponen

### Komponen Gaji Pokok (Income)

Field-field yang membentuk penghasilan karyawan sebelum potongan.

#### CompanyPayroll — Field Gaji Pokok

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `basic_salary` | number | Ya | — | Gaji pokok bulanan sebelum prorata (dalam IDR) |
| `attendance_days` | number | Tidak | — | Jumlah hari hadir di periode ini |
| `working_days` | number | Tidak | — | Total hari kerja di periode ini (dari konfigurasi) |
| `overtime_hours` | number | Tidak | `0` | Total jam lembur yang tercatat |
| `overtime_pay` | number | Tidak | `0` | Upah lembur terkalkulasi: `overtime_hours × hourlyRate × overtime_rate` |
| `kpi_score` | number | Tidak | — | Skor KPI periode ini (skala 0-100) |
| `kpi_bonus` | number | Tidak | `0` | Bonus KPI berdasarkan rating: `basicSalary × persentase_rating` |
| `gross_salary` | number | Ya | — | Gaji kotor: `proratedSalary + overtimePay + kpiBonus` |
| `net_salary` | number | Ya | — | Gaji bersih setelah semua potongan BPJS dan pajak |

#### Payroll (Legacy) — Field Gaji Pokok

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `basic_salary` | number | Ya | — | Gaji pokok bulanan (dalam IDR) |
| `overtime_hours` | number | Tidak | `0` | Total jam lembur |
| `overtime_pay` | number | Tidak | `0` | Upah lembur terkalkulasi |
| `gross_salary` | number | Ya | — | Gaji kotor total |
| `net_salary` | number | Ya | — | Gaji bersih setelah potongan |

### Komponen Tunjangan (Allowances)

Tunjangan tambahan yang diberikan kepada karyawan, disimpan sebagai array objek.

#### CompanyPayroll.allowances

| Sub-field | Tipe | Deskripsi |
| - | - | - |
| `name` | string | Nama tunjangan (misal: "Tunjangan Makan", "Tunjangan Transport", "Tunjangan Komunikasi") |
| `amount` | number | Jumlah tunjangan dalam IDR |

**Contoh data allowances:**

```json theme={null}
[
  { "name": "Tunjangan Makan", "amount": 500000 },
  { "name": "Tunjangan Transport", "amount": 300000 },
  { "name": "Tunjangan Komunikasi", "amount": 150000 }
]
```

#### Payroll.allowances

| Sub-field | Tipe | Deskripsi |
| - | - | - |
| `name` | string | Nama tunjangan |
| `amount` | number | Jumlah tunjangan dalam IDR |

### Komponen Potongan (Deductions)

Potongan-potongan yang mengurangi gaji kotor menjadi gaji bersih.

#### CompanyPayroll.deductions

| Sub-field | Tipe | Deskripsi |
| - | - | - |
| `name` | string | Nama potongan (misal: "BPJS Kesehatan", "BPJS JHT", "PPh 21", "Potongan Kasbon") |
| `amount` | number | Jumlah potongan dalam IDR |

**Contoh data deductions:**

```json theme={null}
[
  { "name": "BPJS Kesehatan (Karyawan)", "amount": 100000 },
  { "name": "BPJS JHT (Karyawan)", "amount": 200000 },
  { "name": "BPJS JP (Karyawan)", "amount": 100000 },
  { "name": "PPh 21", "amount": 75000 },
  { "name": "Potongan Kasbon", "amount": 500000 }
]
```

#### Payroll.deductions

| Sub-field | Tipe | Deskripsi |
| - | - | - |
| `name` | string | Nama potongan |
| `amount` | number | Jumlah potongan dalam IDR |

### Komponen Status & Pembayaran

Field yang mengatur siklus hidup dan metode pembayaran payroll.

#### CompanyPayroll — Status & Pembayaran

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `status` | enum | Tidak | `draft` | Status siklus hidup: `draft`, `approved`, `paid` |
| `payment_date` | date | Tidak | — | Tanggal pembayaran aktual (diisi saat status berubah ke `paid`) |
| `payment_method` | enum | Tidak | — | Metode pembayaran: `bank_transfer`, `cash`, `check` |
| `notes` | string | Tidak | — | Catatan tambahan untuk payroll ini |
| `period` | string | Ya | — | Periode penggajian format `YYYY-MM` |
| `employee_name` | string | Tidak | — | Nama lengkap karyawan (denormalized untuk performa) |
| `late_count` | number | Tidak | `0` | Jumlah keterlambatan di periode ini |
| `absent_count` | number | Tidak | `0` | Jumlah hari absen tanpa keterangan |

#### Payroll (Legacy) — Status & Pembayaran

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `status` | enum | Tidak | `draft` | Status: `draft`, `approved`, `paid` |
| `payment_date` | date | Tidak | — | Tanggal pembayaran |
| `payment_method` | enum | Tidak | — | Metode: `bank_transfer`, `cash`, `check` |
| `notes` | string | Tidak | — | Catatan tambahan |

***

### Employee — Data Karyawan

Entitas yang menyimpan data lengkap karyawan termasuk informasi perbankan dan kontak darurat.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan pemilik data karyawan |
| `user_id` | string | Tidak | — | Link ke entitas User (akun login) |
| `employee_id` | string | Ya | — | ID unik karyawan (NIP/NIK internal) |
| `full_name` | string | Ya | — | Nama lengkap karyawan |
| `email` | string | Ya | — | Alamat email karyawan |
| `phone` | string | Tidak | — | Nomor telepon/HP karyawan |
| `department` | enum | Ya | — | Departemen: `Management`, `Sales`, `Marketing`, `Operations`, `Finance`, `IT`, `HR`, `Customer Service` |
| `position` | string | Ya | — | Jabatan/posisi karyawan |
| `description` | string | Tidak | — | Catatan/deskripsi tambahan termasuk keahlian dan tanggung jawab (max 1000 char) |
| `employment_type` | enum | Tidak | `full_time` | Tipe kepegawaian: `full_time`, `part_time`, `contract`, `intern` |
| `hire_date` | date | Ya | — | Tanggal mulai bekerja |
| `salary` | number | Tidak | — | Gaji bulanan (dalam IDR) |
| `address` | string | Tidak | — | Alamat tempat tinggal karyawan |
| `date_of_birth` | date | Tidak | — | Tanggal lahir karyawan |
| `avatar_url` | string | Tidak | — | URL foto profil karyawan |
| `status` | enum | Tidak | `active` | Status kepegawaian: `active`, `on_leave`, `terminated` |

**Required fields:** `company_id`, `employee_id`, `full_name`, `email`, `department`, `position`, `hire_date`

#### Employee.bank\_account — Informasi Rekening Bank

Objek nested yang menyimpan detail rekening bank untuk transfer gaji.

| Sub-field | Tipe | Deskripsi |
| - | - | - |
| `bank_name` | string | Nama bank (misal: "BCA", "Mandiri", "BNI") |
| `account_number` | string | Nomor rekening bank |
| `account_name` | string | Nama pemilik rekening (harus sesuai nama karyawan) |

**Contoh data bank\_account:**

```json theme={null}
{
  "bank_name": "BCA",
  "account_number": "1234567890",
  "account_name": "Budi Santoso"
}
```

#### Employee.emergency\_contact — Kontak Darurat

Objek nested yang menyimpan informasi kontak darurat karyawan.

| Sub-field | Tipe | Deskripsi |
| - | - | - |
| `name` | string | Nama kontak darurat |
| `relationship` | string | Hubungan dengan karyawan (misal: "Istri", "Suami", "Orang Tua") |
| `phone` | string | Nomor telepon kontak darurat |

**Contoh data emergency\_contact:**

```json theme={null}
{
  "name": "Siti Rahayu",
  "relationship": "Istri",
  "phone": "081234567890"
}
```

***

### CompanyAttendance — Data Absensi

Entitas yang mencatat absensi harian karyawan termasuk lokasi GPS dan foto selfie.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan |
| `employee_id` | string | Ya | — | ID karyawan yang absen |
| `employee_name` | string | Tidak | — | Nama karyawan (denormalized) |
| `employee_email` | string | Tidak | — | Email karyawan (denormalized) |
| `date` | date | Ya | — | Tanggal absensi |
| `shift_id` | string | Tidak | — | ID shift yang dipilih karyawan |
| `shift_name` | string | Tidak | — | Nama shift (misal: "Pagi", "Siang", "Malam") |
| `clock_in_time` | datetime | Ya | — | Waktu clock in (tanggal + jam) |
| `clock_out_time` | datetime | Tidak | — | Waktu clock out (tanggal + jam) |
| `clock_in_photo_url` | string | Tidak | — | URL foto selfie saat clock in |
| `clock_out_photo_url` | string | Tidak | — | URL foto selfie saat clock out |
| `status` | enum | Tidak | `present` | Status absensi: `present`, `late`, `absent`, `sick`, `leave`, `wfh` |
| `notes` | string | Tidak | — | Catatan absensi (misal: alasan terlambat) |
| `total_hours` | number | Tidak | — | Total jam kerja hari ini |
| `overtime_hours` | number | Tidak | `0` | Jam lembur hari ini |
| `distance_from_office` | number | Tidak | — | Jarak dari kantor/geofence pusat dalam meter |
| `location_id` | string | Tidak | — | ID lokasi geofence absensi terpilih |
| `location_name` | string | Tidak | — | Nama lokasi geofence (misal: "Head Office", "Outlet 2") |
| `clock_in_accuracy` | number | Tidak | — | Akurasi GPS saat clock in dalam meter |
| `clock_out_accuracy` | number | Tidak | — | Akurasi GPS saat clock out dalam meter |
| `is_manual_override` | boolean | Tidak | `false` | Flag apakah absensi ini hasil koreksi manual berizin |
| `override_reason` | string | Tidak | — | Alasan pengajuan koreksi manual absensi |
| `override_approved_by` | string | Tidak | — | ID user/manager yang menyetujui koreksi manual |
| `override_approved_at` | datetime | Tidak | — | Waktu persetujuan koreksi manual |

**Required fields:** `company_id`, `employee_id`, `date`, `clock_in_time`

#### CompanyAttendance.clock\_in\_location / clock\_out\_location — Koordinat GPS

Objek nested yang menyimpan koordinat GPS saat clock in/out.

| Sub-field | Tipe | Deskripsi |
| - | - | - |
| `latitude` | number | Garis lintang lokasi absensi |
| `longitude` | number | Garis bujur lokasi absensi |
| `accuracy` | number | Akurasi GPS dalam meter |

***

### CompanyKPI — Penilaian Kinerja

Entitas yang menyimpan penilaian KPI karyawan per periode, termasuk metrik terbobor dan bukti pencapaian.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan |
| `employee_id` | string | Ya | — | ID member (CompanyMember.id) yang dinilai |
| `employee_name` | string | Tidak | — | Nama karyawan (denormalized) |
| `employee_email` | string | Tidak | — | Email karyawan (denormalized) |
| `period` | string | Ya | — | Periode penilaian format `YYYY-MM` (misal: "2025-04") |
| `template_id` | string | Tidak | — | ID template KPI yang digunakan |
| `template_name` | string | Tidak | — | Nama template KPI |
| `description` | string | Tidak | — | Penjelasan konteks evaluasi KPI (max 1000 char) |
| `metrics` | array | Ya | — | Daftar metrik KPI (lihat sub-tabel di bawah) |
| `overall_score` | number | Tidak | — | Skor rata-rata terbobot (0-100) |
| `attendance_score` | number | Tidak | — | Skor kehadiran (0-100) |
| `rating` | enum | Tidak | — | Rating akhir: `outstanding`, `exceeds`, `meets`, `needs_improvement`, `unsatisfactory` |
| `reviewer_id` | string | Tidak | — | ID reviewer/penilai |
| `reviewer_name` | string | Tidak | — | Nama reviewer |
| `reviewer_notes` | string | Tidak | — | Catatan dari reviewer |
| `employee_notes` | string | Tidak | — | Catatan/self-assessment dari karyawan |
| `status` | enum | Tidak | `draft` | Status: `draft`, `submitted`, `reviewed`, `finalized` |
| `auto_generated` | boolean | Tidak | `false` | Apakah KPI digenerate otomatis oleh sistem |
| `finalized_at` | datetime | Tidak | — | Waktu KPI difinalisasi |
| `evidence_url` | string | Tidak | — | URL bukti/file pendukung pencapaian KPI |
| `evidence_note` | string | Tidak | — | Catatan singkat tentang bukti yang dilampirkan (max 500 char) |

**Required fields:** `company_id`, `employee_id`, `period`, `metrics`

#### CompanyKPI.metrics\[] — Detail Metrik KPI

Setiap elemen array menyimpan satu metrik penilaian.

| Sub-field | Tipe | Deskripsi |
| - | - | - |
| `name` | string | Nama metrik (misal: "Target Penjualan", "Customer Satisfaction") |
| `description` | string | Deskripsi detail metrik |
| `target` | number | Target yang harus dicapai |
| `actual` | number | Realisasi/capaian aktual |
| `unit` | string | Satuan pengukuran (misal: "rupiah", "persen", "jumlah") |
| `weight` | number | Bobot persentase (0-100), total semua bobot = 100 |
| `score` | number | Skor terbobot untuk metrik ini |

**Contoh data metrics:**

```json theme={null}
[
  { "name": "Target Penjualan", "target": 100000000, "actual": 120000000, "unit": "rupiah", "weight": 40, "score": 85 },
  { "name": "Kepuasan Pelanggan", "target": 90, "actual": 88, "unit": "persen", "weight": 30, "score": 78 },
  { "name": "Produktivitas", "target": 100, "actual": 95, "unit": "persen", "weight": 30, "score": 80 }
]
```

***

### CompanyMember — Anggota Perusahaan

Entitas yang menghubungkan User dengan Company, menyimpan role, permission, dan data kerja.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan |
| `user_id` | string | Tidak | — | Link ke entitas User |
| `user_email` | string | Ya | — | Email user |
| `user_name` | string | Tidak | — | Nama user |
| `role` | enum | Tidak | `employee` | Role dalam perusahaan (lihat tabel enum di bawah) |
| `employee_id` | string | Tidak | — | Link ke entitas Employee |
| `department` | string | Tidak | — | Departemen tempat bekerja |
| `position` | string | Tidak | — | Jabatan/posisi |
| `status` | enum | Tidak | `active` | Status keanggotaan: `active`, `inactive`, `pending` |
| `joined_date` | date | Tidak | — | Tanggal bergabung di perusahaan |
| `invited_by` | string | Tidak | — | Email yang mengundang |
| `permissions` | object | Tidak | — | Hak akses detail (lihat sub-tabel di bawah) |
| `assigned_locations` | array\[string] | Tidak | — | Daftar ID lokasi gudang/outlet yang diizinkan (RBAC-01) |
| `working_hours` | object | Tidak | — | Jam kerja karyawan |
| `salary` | number | Tidak | — | Gaji karyawan (dalam IDR) |
| `notes` | string | Tidak | — | Catatan tambahan |

**Required fields:** `company_id`, `user_email`

#### CompanyMember.working\_hours — Jam Kerja

| Sub-field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `start` | string | `09:00` | Jam mulai kerja (format HH:MM) |
| `end` | string | `17:00` | Jam selesai kerja (format HH:MM) |

#### CompanyMember.permissions — Hak Akses Detail

| Permission | Tipe | Default | Deskripsi |
| - | - | - | - |
| `can_view_dashboard` | boolean | `true` | Akses baca dashboard |
| `can_view_tasks` | boolean | `true` | Akses baca tugas |
| `can_create_tasks` | boolean | `true` | Akses buat tugas |
| `can_edit_tasks` | boolean | `true` | Akses edit tugas |
| `can_delete_tasks` | boolean | `false` | Akses hapus tugas |
| `can_view_notes` | boolean | `true` | Akses baca catatan |
| `can_create_notes` | boolean | `true` | Akses buat catatan |
| `can_edit_notes` | boolean | `true` | Akses edit catatan |
| `can_delete_notes` | boolean | `false` | Akses hapus catatan |
| `can_view_hr` | boolean | `false` | Akses baca modul HR (termasuk payroll) |
| `can_edit_hr` | boolean | `false` | Akses tulis modul HR (create, edit, approve payroll) |
| `can_view_finance` | boolean | `false` | Akses baca modul Finance |
| `can_edit_finance` | boolean | `false` | Akses tulis modul Finance |
| `can_view_inventory` | boolean | `false` | Akses baca modul Inventori |
| `can_edit_inventory` | boolean | `false` | Akses tulis modul Inventori |
| `can_view_projects` | boolean | `false` | Akses baca modul Proyek |
| `can_edit_projects` | boolean | `false` | Akses tulis modul Proyek |
| `can_view_pos` | boolean | `false` | Akses baca modul POS |
| `can_use_pos` | boolean | `false` | Akses gunakan modul POS (kasir) |
| `can_view_reports` | boolean | `false` | Akses baca laporan |
| `can_manage_members` | boolean | `false` | Kelola anggota perusahaan |
| `can_manage_roles` | boolean | `false` | Kelola role dan permission |
| `can_view_settings` | boolean | `false` | Akses baca pengaturan |
| `can_edit_settings` | boolean | `false` | Akses edit pengaturan |
| `can_manage_cashier_shift` | boolean | `false` | Kelola shift kasir |
| `can_approve_stock_opname` | boolean | `false` | Approve stock opname |
| `can_count_stock_opname` | boolean | `false` | Hitung stock opname |
| `can_transfer_inventory` | boolean | `false` | Transfer inventori |
| `can_create_production_batch` | boolean | `false` | Buat batch produksi |
| `can_release_production_qc` | boolean | `false` | Release QC produksi |
| `can_view_hpp` | boolean | `false` | Akses baca HPP |
| `can_manage_channel_pricing` | boolean | `false` | Kelola harga per channel |
| `can_view_distribution` | boolean | `false` | Akses baca distribusi |
| `can_create_distribution_shipment` | boolean | `false` | Buat pengiriman distribusi |
| `can_confirm_distribution_shipment` | boolean | `false` | Konfirmasi pengiriman distribusi |
| `can_view_b2b_invoices` | boolean | `false` | Akses baca invoice B2B |
| `can_create_b2b_invoice` | boolean | `false` | Buat invoice B2B |
| `can_verify_b2b_payment` | boolean | `false` | Verifikasi pembayaran B2B |

***

### PayslipTemplate — Template Slip Gaji

Entitas yang menyimpan template untuk cetak slip gaji PDF dengan kustomisasi tampilan.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan pemilik template |
| `template_name` | string | Ya | — | Nama template slip gaji |
| `description` | string | Tidak | — | Penjelasan tampilan dan komponen yang dicetak (max 1000 char) |
| `logo_url` | string | Tidak | — | URL logo perusahaan untuk slip |
| `header_html` | string | Tidak | — | Custom HTML untuk header slip |
| `footer_html` | string | Tidak | — | Custom HTML untuk footer slip |
| `show_company_info` | boolean | Tidak | `true` | Tampilkan informasi perusahaan di slip |
| `show_breakdown` | boolean | Tidak | `true` | Tampilkan rincian komponen gaji |
| `show_attendance_summary` | boolean | Tidak | `true` | Tampilkan ringkasan absensi |
| `show_kpi_score` | boolean | Tidak | `true` | Tampilkan skor KPI |
| `is_default` | boolean | Tidak | `false` | Apakah ini template default perusahaan |

**Required fields:** `company_id`, `template_name`

***

### KPITemplate — Template Penilaian KPI

Entitas yang menyimpan template KPI reusable berdasarkan role dan departemen.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan |
| `template_name` | string | Ya | — | Nama template KPI |
| `description` | string | Tidak | — | Penjelasan template dan target pencapaian (max 1000 char) |
| `applicable_roles` | array\[string] | Tidak | — | Role yang menggunakan template (misal: "Sales", "Engineer") |
| `applicable_departments` | array\[string] | Tidak | — | Departemen yang menggunakan template |
| `default_metrics` | array | Ya | — | Daftar metrik default template |
| `is_active` | boolean | Tidak | `true` | Status aktif template |
| `auto_calculate` | boolean | Tidak | `true` | Otomatis hitung KPI secara berkala |
| `calculation_frequency` | enum | Tidak | `daily` | Frekuensi kalkulasi: `daily`, `weekly`, `monthly` |

**Required fields:** `company_id`, `template_name`, `default_metrics`

#### KPITemplate.default\_metrics\[] — Metrik Default Template

| Sub-field | Tipe | Deskripsi |
| - | - | - |
| `metric_name` | string | Nama metrik |
| `description` | string | Deskripsi metrik |
| `default_target` | number | Target default |
| `unit` | string | Satuan pengukuran |
| `weight` | number | Bobot persentase default (0-100) |
| `calculation_method` | enum | Metode hitung: `manual`, `auto_from_attendance`, `auto_from_sales`, `auto_from_projects` |
| `auto_source.entity` | string | Entity sumber data otomatis |
| `auto_source.field` | string | Field yang di-aggregate |
| `auto_source.aggregation` | enum | Jenis agregasi: `sum`, `count`, `average`, `percentage` |

***

### CompanyAttendanceSettings — Pengaturan Absensi

Entitas yang menyimpan konfigurasi absensi perusahaan termasuk geofence, shift, dan pengaturan lembur.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan |
| `description` | string | Tidak | — | Penjelasan kebijakan absensi (max 1000 char) |
| `office_location` | object | Tidak | — | Lokasi kantor utama (latitude, longitude, address) |
| `office_radius_meters` | number | Tidak | `100` | Radius geofence kantor pusat dalam meter (legacy fallback) |
| `require_office_location` | boolean | Tidak | `false` | Wajib absen dari lokasi kantor |
| `default_accuracy_tolerance_meters` | number | Tidak | `150` | Batas toleransi akurasi GPS dalam meter |
| `locations` | array | Tidak | `[]` | Daftar multi-lokasi geofence absensi |
| `working_hours` | object | Tidak | — | Jam kerja default |
| `shifts` | array | Tidak | `[]` | Daftar shift kerja yang tersedia |
| `overtime_settings` | object | Tidak | — | Pengaturan lembur |
| `late_tolerance_minutes` | number | Tidak | `15` | Toleransi terlambat dalam menit |
| `working_days` | array\[number] | Tidak | `[1,2,3,4,5]` | Hari kerja (1=Senin, 7=Minggu) |
| `auto_clock_out_enabled` | boolean | Tidak | `false` | Otomatis clock out jika karyawan lupa |
| `auto_clock_out_time` | string | Tidak | `18:00` | Jam auto clock out |
| `require_photo` | boolean | Tidak | `true` | Wajib foto selfie saat absen |
| `require_notes` | boolean | Tidak | `false` | Wajib catatan saat absen |

**Required fields:** `company_id`

#### CompanyAttendanceSettings.locations\[] — Lokasi Geofence

| Sub-field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `location_id` | string | Ya | — | ID unik area/lokasi absensi |
| `name` | string | Ya | — | Nama lokasi (misal: "Head Office", "Outlet 2") |
| `address` | string | Tidak | — | Alamat fisik lokasi |
| `latitude` | number | Ya | — | Garis lintang pusat geofence |
| `longitude` | number | Ya | — | Garis bujur pusat geofence |
| `radius_meters` | number | Tidak | `100` | Radius lingkaran geofence dalam meter |
| `accuracy_tolerance_meters` | number | Tidak | `150` | Toleransi akurasi GPS maksimum |
| `assigned_employee_ids` | array\[string] | Tidak | `[]` | ID karyawan yang ditugaskan (kosong = semua) |
| `is_active` | boolean | Tidak | `true` | Status aktif lokasi geofence |

#### CompanyAttendanceSettings.shifts\[] — Shift Kerja

| Sub-field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `shift_id` | string | Ya | — | ID unik shift |
| `shift_name` | string | Ya | — | Nama shift (misal: "Shift Pagi") |
| `start_time` | string | Ya | — | Jam mulai shift (format HH:MM) |
| `end_time` | string | Ya | — | Jam selesai shift (format HH:MM) |
| `break_duration_minutes` | number | Tidak | `60` | Durasi istirahat dalam menit |

#### CompanyAttendanceSettings.overtime\_settings — Pengaturan Lembur

| Sub-field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `enabled` | boolean | `true` | Aktifkan fitur lembur |
| `start_after_hours` | number | `8` | Lembur dimulai setelah X jam kerja |
| `max_overtime_hours_per_day` | number | `4` | Maksimal jam lembur per hari |

***

### CompanyLeave — Pengajuan Cuti

Entitas yang menyimpan pengajuan cuti karyawan yang mempengaruhi hari hadir di payroll.

| Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan |
| `employee_id` | string | Ya | — | ID karyawan yang mengajukan cuti |
| `employee_name` | string | Tidak | — | Nama karyawan (denormalized) |
| `employee_email` | string | Tidak | — | Email karyawan (denormalized) |
| `leave_type` | enum | Ya | `annual` | Jenis cuti: `annual`, `sick`, `unpaid`, `maternity`, `paternity`, `emergency` |
| `start_date` | date | Ya | — | Tanggal mulai cuti |
| `end_date` | date | Ya | — | Tanggal akhir cuti |
| `total_days` | number | Tidak | — | Total hari cuti |
| `reason` | string | Ya | — | Alasan pengajuan cuti |
| `description` | string | Tidak | — | Keterangan tambahan (max 1000 char) |
| `attachment_url` | string | Tidak | — | URL surat dokter/dokumen pendukung |
| `status` | enum | Tidak | `pending` | Status: `pending`, `approved`, `rejected`, `cancelled` |
| `approver_id` | string | Tidak | — | ID user yang menyetujui |
| `approver_notes` | string | Tidak | — | Catatan dari approver |
| `approved_at` | datetime | Tidak | — | Waktu persetujuan |

**Required fields:** `company_id`, `employee_id`, `leave_type`, `start_date`, `end_date`, `reason`

***

## State Diagram — Siklus Hidup Lengkap Payroll Processing

```mermaid theme={null}
stateDiagram-v2
    [*] --> draft : generatePayroll()\natau Realtime Calculator\n"Save as Draft"

    state "Draft" as draft {
        [*] --> menghitung
        menghitung --> input_absensi : Fetch CompanyAttendance
        input_absensi --> input_kpi : Fetch CompanyKPI
        input_kpi --> hitung_gaji : Hitung prorata + lembur + bonus
        hitung_gaji --> hitung_bpjs : Hitung BPJS Kesehatan & TK
        hitung_bpjs --> hitung_pajak : Hitung PPh 21 progresif
        hitung_pajak --> hitung_bersih : Hitung gaji bersih
        hitung_bersih --> menunggu_review : Simpan record
    }

    menunggu_review --> approved : HR Admin approve
    menunggu_review --> draft : Recalculate\n(ubah input)

    approved --> proses_bayar : Finance proses\npembayaran
    proses_bayar --> paid : Mark as Paid\n(payment_date, payment_method)

    paid --> generate_pdf : Generate slip PDF\n(PayslipTemplate)
    generate_pdf --> [*] : Selesai

    approved --> draft : Revisi\n(batal approve)

    note right of draft
        Semua komponen dihitung ulang
        secara real-time saat input berubah.
        Include: gaji prorata, lembur,
        bonus KPI, BPJS, PPh 21.
    end note

    note right of approved
        Payroll siap dibayar.
        Notifikasi dikirim ke
        modul Finance.
    end note

    note right of paid
        Slip gaji PDF digenerate
        menggunakan PayslipTemplate.
        Data terintegrasi ke Finance.
    end note
```

## State Diagram — Siklus Hidup KPI Assessment

```mermaid theme={null}
stateDiagram-v2
    [*] --> draft : KPI dibuat\n(manual atau auto_generated)

    draft --> draft : Edit metrik\n(target, actual, weight)
    draft --> submitted : Karyawan submit\nself-assessment

    submitted --> reviewed : Reviewer evaluasi\n& berikan rating
    reviewed --> finalized : HR Admin finalisasi\n(rating mengunci)

    finalized --> [*] : Rating digunakan\nuntuk bonus KPI\n di payroll

    note right of draft
        Metrik bisa diisi manual
        atau otomatis dari sumber data
        (attendance, sales, projects).
    end note

    note right of reviewed
        Rating ditentukan:
        outstanding (>90), exceeds (>80),
        meets (>70), needs_improvement (>50),
        unsatisfactory (<50).
    end note
```

## State Diagram — Siklus Hidup CompanyLeave

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending : Karyawan mengajukan cuti
    pending --> approved : HR/Admin approve
    pending --> rejected : HR/Admin reject
    pending --> cancelled : Karyawan batalkan

    rejected --> [*] : Ditolak
    cancelled --> [*] : Dibatalkan

    approved --> [*] : Cuti berjalan\n(hari hadir berkurang\ndi payroll)

    note right of pending
        Pengajuan cuti via
        employee portal.
        Menunggu persetujuan.
    end note

    note right of approved
        Cuti disetujui.
        Hari cuti mengurangi
        attendance_days di payroll.
        Cuti sakit (sick) tidak
        mengurangi jatah cuti tahunan.
    end note
```

***

## Sequence Diagram — Perhitungan PPh 21 Progresif

```mermaid theme={null}
sequenceDiagram
    participant ENG as Payroll Engine
    participant CFG as PayrollConfiguration
    participant CALC as Tax Calculator

    ENG->>CFG: Fetch tax_formula
    CFG-->>ENG: PTKP = 54.000.000, brackets[]

    ENG->>CALC: Hitung PPh 21(grossSalary, PTKP, brackets)

    Note over CALC: annualIncome = grossSalary × 12
    Note over CALC: annualBPJS = (bpjsKes_karyawan + bpjsTK_karyawan) × 12
    Note over CALC: taxableIncome = annualIncome - annualBPJS - PTKP

    alt taxableIncome <= 0
        CALC-->>ENG: PPh 21 = 0 (tidak kena pajak)
    else taxableIncome > 0
        Note over CALC: Bracket 1: min(60jt, taxable) × 5%
        Note over CALC: Bracket 2: min(190jt, remaining) × 15%
        Note over CALC: Bracket 3: min(250jt, remaining) × 25%
        Note over CALC: Bracket 4: remaining × 30%
        CALC-->>ENG: PPh 21 tahunan
        Note over ENG: PPh 21 bulanan = PPh 21 tahunan / 12
    end
```

## Sequence Diagram — Pencairan Kasbon & Potongan Payroll

```mermaid theme={null}
sequenceDiagram
    participant EMP as Karyawan
    participant LN as CompanyLoan
    participant HR as HR Admin
    participant FIN as Finance
    participant PAY as Payroll Engine

    EMP->>LN: Ajukan kasbon (amount, reason)
    LN-->>EMP: Status: pending

    HR->>LN: Review pengajuan
    alt Disetujui
        HR->>LN: Approve (approver_id, approved_at)
        LN-->>HR: Status: approved
        FIN->>LN: Cairkan dana (disbursed_at)
        LN-->>FIN: Status: approved, dana dicairkan
        Note over LN: monthly_deduction = amount / installment_months
        Note over LN: remaining_amount = amount
    else Ditolak
        HR->>LN: Reject (approver_notes)
        LN-->>EMP: Status: rejected
    end

    Note over PAY, LN: Setiap periode payroll berjalan

    PAY->>LN: Fetch pinjaman aktif karyawan
    LN-->>PAY: monthly_deduction, remaining_amount

    Note over PAY: deductions[] += {name: "Potongan Kasbon", amount: monthly_deduction}
    Note over PAY: netSalary = grossSalary - bpjs - pph21 - monthly_deduction
    Note over PAY: remaining_amount -= monthly_deduction

    alt remaining_amount <= 0
        Note over LN: Status → paid_off
        Note over PAY: Tidak ada potongan kasbon di periode berikutnya
    end
```

## Sequence Diagram — Integrasi Absensi ke Payroll

```mermaid theme={null}
sequenceDiagram
    participant EMP as Karyawan
    participant ATT as CompanyAttendance
    participant SET as CompanyAttendanceSettings
    participant PAY as Payroll Engine
    participant CFG as PayrollConfiguration

    EMP->>ATT: Clock in (foto + GPS)
    ATT->>SET: Validasi lokasi geofence
    SET-->>ATT: distance_from_office, location

    alt distance > radius + tolerance
        ATT-->>EMP: Ditolak - di luar jangkauan
    else distance <= radius + tolerance
        ATT->>ATT: Tentukan status (present/late)
        ATT->>SET: Cek jam kerja & shift
        alt clock_in > start_time + late_tolerance
            ATT->>ATT: Status = late
        else clock_in <= start_time + late_tolerance
            ATT->>ATT: Status = present
        end
    end

    EMP->>ATT: Clock out (foto + GPS)
    ATT->>ATT: Hitung total_hours & overtime_hours
    ATT->>SET: Cek overtime_settings
    Note over ATT: overtime = total_hours - working_hours\n(cap: max_overtime_hours_per_day)

    Note over PAY: Saat generatePayroll()
    PAY->>ATT: Fetch absensi periode ini
    ATT-->>PAY: attendance_days, overtime_hours, late_count
    PAY->>CFG: Fetch late_penalty_per_minute
    CFG-->>PAY: Tarif denda
    Note over PAY: late_penalty = late_count × late_penalty_per_minute
    Note over PAY: absence_deduction = absent_count × absence_deduction_per_day
```

***

## Referensi Enum Lengkap

### member\_role

Role yang tersedia untuk CompanyMember dalam perusahaan.

| Nilai | Deskripsi |
| - | - |
| `owner` | Pemilik perusahaan, akses penuh ke semua modul |
| `admin` | Administrator, akses hampir penuh ke semua modul |
| `supervisor` | Supervisor departemen, akses terbatas ke departemen yang diawasi |
| `store_admin` | Admin toko/outlet |
| `stock_admin` | Admin gudang/stok |
| `finance_admin` | Admin keuangan |
| `hr_admin` | Admin HR, mengelola payroll, absensi, KPI, dan kasbon |
| `transaction_admin` | Admin transaksi |
| `employee` | Karyawan biasa, akses terbatas ke data milik sendiri |
| `production_operator` | Operator produksi |
| `qc_inspector` | Inspector quality control |
| `sales_marketing` | Sales dan marketing |
| `partner_distributor` | Partner/distributor |

### employee\_status

Status kepegawaian karyawan di entitas Employee.

| Nilai | Deskripsi |
| - | - |
| `active` | Karyawan aktif bekerja |
| `on_leave` | Karyawan sedang cuti panjang |
| `terminated` | Karyawan sudah diberhentikan |

### member\_status

Status keanggotaan di CompanyMember.

| Nilai | Deskripsi |
| - | - |
| `active` | Anggota aktif |
| `inactive` | Anggota nonaktif (tidak bisa login) |
| `pending` | Undangan belum diterima |

### department

Departemen yang tersedia di entitas Employee.

| Nilai | Deskripsi |
| - | - |
| `Management` | Manajemen perusahaan |
| `Sales` | Penjualan |
| `Marketing` | Pemasaran |
| `Operations` | Operasional |
| `Finance` | Keuangan |
| `IT` | Teknologi Informasi |
| `HR` | Sumber Daya Manusia |
| `Customer Service` | Layanan Pelanggan |

### leave\_type

Jenis-jenis cuti yang tersedia di CompanyLeave.

| Nilai | Deskripsi |
| - | - |
| `annual` | Cuti tahunan (mengurangi jatah cuti) |
| `sick` | Cuti sakit (dengan surat dokter) |
| `unpaid` | Cuti tidak dibayar |
| `maternity` | Cuti melahirkan |
| `paternity` | Cuti ayah (melahirkan) |
| `emergency` | Cuti darurat |

### leave\_status

Status pengajuan cuti di CompanyLeave.

| Nilai | Deskripsi |
| - | - |
| `pending` | Menunggu persetujuan |
| `approved` | Disetujui oleh HR/Admin |
| `rejected` | Ditolak oleh HR/Admin |
| `cancelled` | Dibatalkan oleh karyawan |

### kpi\_status

Status penilaian KPI di CompanyKPI dan KPI.

| Nilai | Deskripsi |
| - | - |
| `draft` | KPI sedang dibuat, belum disubmit |
| `submitted` | Karyawan sudah submit self-assessment |
| `reviewed` | Reviewer sudah mengevaluasi dan memberikan rating |
| `finalized` | KPI difinalisasi oleh HR Admin, rating tidak bisa diubah |

### kpi\_calculation\_method

Metode perhitungan metrik di KPITemplate.

| Nilai | Deskripsi |
| - | - |
| `manual` | Diisi manual oleh reviewer/karyawan |
| `auto_from_attendance` | Otomatis dari data absensi (CompanyAttendance) |
| `auto_from_sales` | Otomatis dari data penjualan |
| `auto_from_projects` | Otomatis dari data proyek |

### kpi\_aggregation

Jenis agregasi untuk auto\_source di KPITemplate.

| Nilai | Deskripsi |
| - | - |
| `sum` | Total kumulatif |
| `count` | Jumlah kejadian |
| `average` | Rata-rata |
| `percentage` | Persentase pencapaian |

### calculation\_frequency

Frekuensi kalkulasi otomatis KPI.

| Nilai | Deskripsi |
| - | - |
| `daily` | Dihitung setiap hari |
| `weekly` | Dihitung setiap minggu |
| `monthly` | Dihitung setiap bulan |

### company\_industry

Industri perusahaan di entitas Company.

| Nilai | Deskripsi |
| - | - |
| `retail` | Ritel |
| `manufacturing` | Manufaktur |
| `services` | Jasa |
| `technology` | Teknologi |
| `food_beverage` | Makanan & Minuman |
| `healthcare` | Kesehatan |
| `education` | Pendidikan |
| `other` | Lainnya |

***

## RBAC Lengkap — Hak Akses Semua Entitas Payroll

### Hak Akses CompanyPayroll & Payroll

| Operasi | Owner | Admin | HR Admin | Supervisor | Employee |
| - | - | - | - | - | - |
| **Create** | Ya | Ya | Ya | Tidak | Tidak |
| **Read** | Ya | Ya | Ya | Ya (departemen) | Ya (milik sendiri) |
| **Update** | Ya | Ya | Ya | Tidak | Tidak |
| **Delete** | Ya | Ya | Ya | Tidak | Tidak |

### Hak Akses Employee

| Operasi | Owner | Admin | HR Admin | Supervisor | Employee |
| - | - | - | - | - | - |
| **Create** | Ya | Ya | Ya | Tidak | Tidak |
| **Read** | Ya | Ya | Ya | Ya (departemen) | Ya (milik sendiri) |
| **Update** | Ya | Ya | Ya | Tidak | Ya (milik sendiri) |
| **Delete** | Ya | Ya | Ya | Tidak | Tidak |

### Hak Akses CompanyAttendance

| Operasi | Owner | Admin | HR Admin | Supervisor | Employee |
| - | - | - | - | - | - |
| **Create (clock in/out)** | Ya | Ya | Ya | Ya | Ya (milik sendiri) |
| **Read** | Ya | Ya | Ya | Ya (departemen) | Ya (milik sendiri) |
| **Update** | Ya | Ya | Ya | Tidak | Ya (milik sendiri) |
| **Delete** | Ya | Ya | Ya | Tidak | Tidak |

### Hak Akses CompanyKPI

| Operasi | Owner | Admin | HR Admin | Supervisor | Employee |
| - | - | - | - | - | - |
| **Create** | Ya | Ya | Ya | Ya (review departemen) | Ya (self-assessment) |
| **Read** | Ya | Ya | Ya | Ya (departemen) | Ya (milik sendiri) |
| **Update** | Ya | Ya | Ya | Ya (review departemen) | Ya (milik sendiri) |
| **Delete** | Ya | Ya | Ya | Tidak | Tidak |

### Hak Akses CompanyLoan

| Operasi | Owner | Admin | HR Admin | Supervisor | Employee |
| - | - | - | - | - | - |
| **Create (ajukan)** | Ya | Ya | Ya | Ya | Ya |
| **Read** | Ya | Ya | Ya | Ya (departemen) | Ya (milik sendiri) |
| **Update (approve/reject)** | Ya | Ya | Ya | Tidak | Tidak |
| **Delete** | Ya | Ya | Ya | Tidak | Tidak |

### Hak Akses CompanyLeave

| Operasi | Owner | Admin | HR Admin | Supervisor | Employee |
| - | - | - | - | - | - |
| **Create (ajukan)** | Ya | Ya | Ya | Ya | Ya |
| **Read** | Ya | Ya | Ya | Ya (departemen) | Ya (milik sendiri) |
| **Update (approve/reject)** | Ya | Ya | Ya | Tidak | Tidak |
| **Delete** | Ya | Ya | Ya | Tidak | Tidak |

### Hak Akses PayrollConfiguration

| Operasi | Owner | Admin | HR Admin | Supervisor | Employee |
| - | - | - | - | - | - |
| **Create** | Ya | Ya | Tidak | Tidak | Tidak |
| **Read** | Ya | Ya | Ya | Ya | Ya |
| **Update** | Ya | Ya | Tidak | Tidak | Tidak |
| **Delete** | Ya | Ya | Tidak | Tidak | Tidak |

### Hak Akses PayslipTemplate

| Operasi | Owner | Admin | HR Admin | Supervisor | Employee |
| - | - | - | - | - | - |
| **Create** | Ya | Ya | Ya | Tidak | Tidak |
| **Read** | Ya | Ya | Ya | Ya | Ya |
| **Update** | Ya | Ya | Ya | Tidak | Tidak |
| **Delete** | Ya | Ya | Ya | Tidak | Tidak |

### Hak Akses KPITemplate

| Operasi | Owner | Admin | HR Admin | Supervisor | Employee |
| - | - | - | - | - | - |
| **Create** | Ya | Ya | Ya | Tidak | Tidak |
| **Read** | Ya | Ya | Ya | Ya | Ya |
| **Update** | Ya | Ya | Ya | Tidak | Tidak |
| **Delete** | Ya | Ya | Ya | Tidak | Tidak |

### RLS Policy — Ringkasan Isolasi Data

| Entitas | Filter Utama | Filter Tambahan |
| - | - | - |
| `CompanyPayroll` | `company_id = user.data.active_company_id` | `created_by_id = user.id` atau `role = admin` |
| `Payroll` | `company_id = user.data.active_company_id` | `created_by_id = user.id` atau `role = admin` |
| `PayrollConfiguration` | `company_id = user.data.active_company_id` | `created_by_id = user.id` atau `role = admin` |
| `Employee` | `company_id = user.data.active_company_id` | `user_id = user.id` atau `role = admin` |
| `CompanyMember` | `company_id = user.data.active_company_id` | `user_email = user.email` atau `user_id = user.id` |
| `CompanyAttendance` | `company_id = user.data.active_company_id` | `employee_email = user.email` atau `role = admin` |
| `CompanyKPI` | `company_id = user.data.active_company_id` | `employee_email = user.email` atau `role = admin` |
| `CompanyLoan` | `company_id = user.data.active_company_id` | `employee_email = user.email` atau `role = admin` |
| `CompanyLeave` | `company_id = user.data.active_company_id` | `employee_email = user.email` atau `role = admin` |

***

## Ringkasan Alur Data Payroll

```mermaid theme={null}
flowchart LR
    subgraph INPUT["Input Data"]
        E[Employee<br/>Data karyawan & gaji]
        A[CompanyAttendance<br/>Absensi & lembur]
        K[CompanyKPI<br/>Penilaian kinerja]
        L[CompanyLeave<br/>Cuti karyawan]
        LN[CompanyLoan<br/>Kasbon/pinjaman]
    end

    subgraph CONFIG["Konfigurasi"]
        CFG[PayrollConfiguration<br/>Formula, tarif, bracket]
        ATS[CompanyAttendanceSettings<br/>Shift, geofence, lembur]
        KT[KPITemplate<br/>Template metrik]
        PT[PayslipTemplate<br/>Template slip PDF]
    end

    subgraph PROCESS["Proses"]
        GEN[generatePayroll<br/>Payroll Engine]
        TAX[PPh 21 Calculator<br/>Progresif + PTKP]
        BPJS[BPJS Calculator<br/>Kesehatan + TK]
    end

    subgraph OUTPUT["Output"]
        CP[CompanyPayroll<br/>Record gaji]
        PDF[Slip Gaji PDF]
        AI[AI Insights<br/>Analisis & anomali]
        FIN[Integrasi Finance<br/>Pencatatan pengeluaran]
    end

    E --> GEN
    A --> GEN
    K --> GEN
    L --> GEN
    LN --> GEN
    CFG --> GEN
    CFG --> TAX
    CFG --> BPJS
    ATS --> A
    KT --> K

    GEN --> CP
    CP --> PDF
    CP --> AI
    CP --> FIN
    PT --> PDF
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.