> ## 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.

# Overview

<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: "HR Overview"
description: "Modul HR SNISHOP ERP: absensi, cuti, rekrmen, payroll, dan manajemen karyawan lengkap dengan GPS tracking dan foto selfie."
-----------------------------------------------------------------------------------------------------------------------------------------

# HR Overview

<img src="https://mintlify.s3.us-west-1.amazonaws.com/quinnofspicy/docs/mintlify/screenshots/hr/overview.png" alt="HR Overview" />

Modul Human Resource (HR) SNISHOP ERP mengelola seluruh siklus hidup karyawan — dari rekrmen, onboarding, absensi harian, cuti dan izin, hingga payroll. Sistem ini dibangun untuk bisnis F\&B yang membutuhkan kontrol kehadiran ketat dengan verifikasi GPS dan foto selfie, serta integrasi langsung ke perhitungan gaji.

## Arsitektur Modul HR

```mermaid theme={null}
graph TD
    subgraph "Halaman HR"
        A[HR Overview<br/>Dashboard & Statistik]
        B[Attendance<br/>Absensi & Check-in]
        C[Leave Management<br/>Cuti & Izin]
        D[Recruitment<br/>Lowongan & Lamaran]
    end

    subgraph "Entity & Data"
        E[Employee<br/>Data Karyawan]
        F[AttendanceRecord<br/>Log Kehadiran]
        G[LeaveRequest<br/>Pengajuan Cuti]
        H[JobPosting<br/>Lowongan Kerja]
        I[JobApplication<br/>Lamaran]
    end

    subgraph "Integrasi"
        J[Cloudinary<br/>Upload Foto & Dokumen]
        K[GPS Geolocation<br/>Lokasi Check-in]
        L[Finance Module<br/>Payroll Integration]
        M[WhatsApp<br/>Notifikasi]
    end

    A --> E & F & G & H
    B --> F & J & K
    C --> G & M
    D --> H & I & J
    F --> L
    G --> L
```

## 7 Halaman HR

| Halaman | Fungsi | Komponen Utama |
| - | - | - |
| **Overview** | Dashboard & statistik HR | Summary cards, charts, quick actions |
| **Attendance** | Absensi harian dengan GPS & foto | Clock-in/out, calendar view, photo upload |
| **Leave Management** | Pengajuan cuti & izin | Form pengajuan, approval workflow, calendar |
| **Recruitment** | Manajemen lowongan & lamaran | Job posting, application tracking, interview scheduling |
| **Employees** | Data karyawan | Employee directory, profile management |
| **Payroll** | Perhitungan gaji | Salary computation, deduction, bonus |
| **Reports** | Laporan HR | Attendance summary, leave balance, headcount |

## Entity & Data Model

### Employee

| Field | Tipe | Deskripsi |
| - | - | - |
| `id` | UUID | Primary key |
| `employee_id` | String | NIK karyawan (unique) |
| `full_name` | String | Nama lengkap |
| `email` | String | Email perusahaan |
| `phone` | String | Nomor telepon/WhatsApp |
| `position` | String | Jabatan |
| `department` | String | Departemen |
| `join_date` | Date | Tanggal bergabung |
| `employment_type` | Enum | `full_time`, `part_time`, `contract`, `intern` |
| `salary` | Number | Gaji pokok bulanan |
| `bank_account` | String | Nomor rekening bank |
| `address` | Text | Alamat tempat tinggal |
| `emergency_contact` | Object | Nama & nomor darurat |
| `status` | Enum | `active`, `on_leave`, `terminated`, `resigned` |
| `company_id` | UUID | Multi-tenant scoping |
| `created_at` | Timestamp | Waktu registrasi |

### AttendanceRecord

| Field | Tipe | Deskripsi |
| - | - | - |
| `id` | UUID | Primary key |
| `employee_id` | FK → Employee | Karyawan yang absen |
| `date` | Date | Tanggal absensi |
| `clock_in` | Timestamp | Waktu check-in |
| `clock_out` | Timestamp | Waktu check-out |
| `work_hours` | Number | Total jam kerja (otomatis) |
| `status` | Enum | `present`, `late`, `absent`, `leave`, `sick` |
| `location` | Object | `{latitude, longitude}` dari GPS |
| `photo_url` | String | URL foto selfie (Cloudinary) |
| `notes` | Text | Catatan tambahan |
| `company_id` | UUID | Multi-tenant scoping |

### LeaveRequest

| Field | Tipe | Deskripsi |
| - | - | - |
| `id` | UUID | Primary key |
| `employee_id` | FK → Employee | Karyawan yang mengajukan |
| `leave_type` | Enum | `annual`, `sick`, `maternity`, `paternity`, `unpaid`, `personal` |
| `start_date` | Date | Tanggal mulai cuti |
| `end_date` | Date | Tanggal berakhir |
| `duration_days` | Number | Jumlah hari (otomatis) |
| `reason` | Text | Alasan cuti |
| `status` | Enum | `pending`, `approved`, `rejected`, `cancelled` |
| `approved_by` | FK → User | Manager yang approve |
| `approval_notes` | Text | Catatan approval |
| `attachment_url` | String | URL dokumen pendukung (surat dokter, dll) |
| `company_id` | UUID | Multi-tenant scoping |

### JobPosting

| Field | Tipe | Deskripsi |
| - | - | - |
| `id` | UUID | Primary key |
| `title` | String | Judul posisi |
| `department` | String | Departemen |
| `employment_type` | Enum | Jenis pekerjaan |
| `description` | Text | Deskripsi pekerjaan |
| `requirements` | Text | Kualifikasi |
| `salary_range` | Object | `{min, max}` |
| `location` | String | Lokasi kerja |
| `status` | Enum | `draft`, `published`, `closed` |
| `application_deadline` | Date | Batas waktu lamaran |
| `company_id` | UUID | Multi-tenant scoping |

### JobApplication

| Field | Tipe | Deskripsi |
| - | - | - |
| `id` | UUID | Primary key |
| `job_posting_id` | FK → JobPosting | Lowongan yang dilamar |
| `applicant_name` | String | Nama pelamar |
| `applicant_email` | String | Email pelamar |
| `applicant_phone` | String | Nomor telepon |
| `resume_url` | String | URL CV (Cloudinary) |
| `cover_letter` | Text | Surat lamaran |
| `status` | Enum | `submitted`, `reviewing`, `interview`, `offered`, `rejected`, `withdrawn` |
| `interview_date` | Timestamp | Jadwal interview |
| `interview_notes` | Text | Catatan interview |
| `rating` | Number | Rating pelamar (1-5) |
| `company_id` | UUID | Multi-tenant scoping |

## 13 Peran & Hak Akses

| Peran | Attendance | Leave | Recruitment | Payroll | Reports |
| - | - | - | - | - | - |
| **Super Admin** | Full | Full | Full | Full | Full |
| **Owner** | Full | Full | Full | Full | Full |
| **HR Manager** | Full | Full | Full | Read | Full |
| **HR Staff** | CRUD | CRUD | CRUD | No | Read |
| **Manager** | Read team | Approve team | Read | No | Read team |
| **Supervisor** | Read team | Approve team | No | No | Read team |
| **Employee** | Own only | Own only | No | Own only | No |

## Fitur Utama

### 1. Absensi dengan GPS & Foto Selfie

Sistem absensi menggunakan verifikasi ganda untuk memastikan karyawan benar-benar hadir di lokasi kerja:

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan
    participant App as Mobile App
    participant GPS as Geolocation API
    participant CAM as Camera
    participant CLD as Cloudinary
    participant DB as Database

    K->>App: Klik "Check-in"
    App->>GPS: Request current location
    GPS-->>App: {latitude, longitude}
    App->>CAM: Open camera for selfie
    K->>CAM: Ambil foto
    CAM-->>App: Image blob
    App->>App: Compress image (Canvas API)
    App->>CLD: Upload foto
    CLD-->>App: photo_url
    App->>DB: Save AttendanceRecord
    Note over DB: clock_in, location, photo_url
    DB-->>App: Confirmation
```

**Verifikasi Kehadiran**:

| Check | Metode | Tujuan |
| - | - | - |
| **GPS Location** | `navigator.geolocation.getCurrentPosition()` | Pastikan di lokasi kerja |
| **Photo Selfie** | Camera API + Cloudinary upload | Verifikasi identitas visual |
| **Timestamp** | Server-side validation | Cegah manipulasi waktu |

### 2. Leave Management dengan Approval Workflow

```mermaid theme={null}
stateDiagram-v2
    [*] --> Pending: Karyawan submit
    Pending --> Approved: Manager approve
    Pending --> Rejected: Manager reject
    Approved --> Cancelled: Karyawan cancel
    Rejected --> [*]
    Cancelled --> [*]
    Approved --> [*]: Cuti selesai
```

**Jenis Cuti**:

| Jenis | Durasi Default | Berbayar | Dokumen Required |
| - | - | - | - |
| **Annual Leave** | 12 hari/tahun | Ya | Tidak |
| **Sick Leave** | Unlimited | Ya | Surat dokter (>3 hari) |
| **Maternity Leave** | 90 hari | Ya | Surat dokter |
| **Paternity Leave** | 7 hari | Ya | Surat nikah |
| **Unpaid Leave** | Unlimited | Tidak | Tidak |
| **Personal Leave** | 3 hari | Tidak | Tidak |

### 3. Recruitment Pipeline

```mermaid theme={null}
flowchart LR
    A[Submitted] --> B[Reviewing]
    B --> C[Interview]
    C --> D[Offered]
    D --> E[Hired]
    B --> F[Rejected]
    C --> F
    D --> G[Withdrawn]
```

**Pipeline Stages**:

| Stage | Aksi | KPI |
| - | - | - |
| **Submitted** | Lamaran masuk via portal | Total applications |
| **Reviewing** | HR review CV & kualifikasi | Screening time |
| **Interview** | Jadwal & hasil interview | Interview-to-offer ratio |
| **Offered** | Surat penawaran dikirim | Offer acceptance rate |
| **Hired** | Karyawan baru bergabung | Time-to-hire |
| **Rejected** | Lamaran ditolak | Rejection rate |
| **Withdrawn** | Pelamar mengundurkan diri | Withdrawal rate |

### 4. Payroll Integration

Modul HR terintegrasi langsung dengan Finance untuk perhitungan gaji otomatis:

```mermaid theme={null}
flowchart TD
    A[Attendance Records] --> D[Payroll Calculation]
    B[Leave Records] --> D
    C[Employee Salary] --> D
    D --> E{Kalkulasi}
    E --> F[Gaji Pokok]
    E --> G[Tunjangan]
    E --> H[Potongan]
    F & G & H --> I[Total Gaji]
    I --> J[Generate Journal Entry]
    J --> K[Finance Module]
```

**Komponen Payroll**:

| Komponen | Sumber | Perhitungan |
| - | - | - |
| **Gaji Pokok** | Employee.salary | Fixed bulanan |
| **Tunjangan Makan** | Attendance × Rate | Rp 25.000 × hari hadir |
| **Tunjangan Transport** | Attendance × Rate | Rp 15.000 × hari hadir |
| **Bonus** | Manual input | Based on performance |
| **Potongan Absen** | Late count | Rp 50.000 × terlambat |
| **Potongan Cuti** | Unpaid leave | Daily rate × hari |
| **BPJS Kesehatan** | Salary × 1% | Ditanggung perusahaan |
| **BPJS Ketenagakerjaan** | Salary × 3.7% | Ditanggung perusahaan |

## Arsitektur Patterns

### Server-Function-First

Semua operasi CRUD menggunakan base44 entity functions untuk konsistensi dan auditability:

```javascript theme={null}
// Contoh: Create attendance record
await base44.entities.AttendanceRecord.create({
  employee_id: userId,
  date: new Date(),
  clock_in: new Date(),
  location: { latitude, longitude },
  photo_url: uploadedPhotoUrl,
  company_id: companyId
});
```

### Idempotency Keys

Operasi kritis menggunakan idempotency key untuk mencegah duplikasi:

| Operasi | Idempotency Key | Tujuan |
| - | - | - |
| Clock-in | `${employee_id}_${date}` | Cegah double check-in |
| Leave submission | `${employee_id}_${start_date}_${end_date}` | Cegah duplikasi pengajuan |
| Payroll generation | `${company_id}_${month}_${year}` | Cegah double payment |

### Global Caching

Data karyawan dan attendance di-cache di context untuk performa:

```mermaid theme={null}
graph LR
    A[EmployeeContext] --> B[Attendance Page]
    A --> C[Leave Page]
    A --> D[Payroll Page]
    E[CompanyContext] --> F[All HR Pages]
```

### BroadcastChannel Sync

Multi-tab sync untuk real-time update:

```javascript theme={null}
const channel = new BroadcastChannel('hr-updates');
channel.postMessage({ type: 'attendance_updated', employee_id: id });
```

## 12 Shared Components

| Komponen | Fungsi | Digunakan Di |
| - | - | - |
| `ERPAccessGuard` | Module-level access control | Semua halaman HR |
| `EmployeeSelector` | Dropdown pilih karyawan | Attendance, Leave, Payroll |
| `DateRangePicker` | Pilih rentang tanggal | Reports, Attendance |
| `Calendar` | Calendar view | Attendance, Leave |
| `PhotoUpload` | Upload foto dengan compress | Attendance, Recruitment |
| `DocumentUpload` | Upload dokumen PDF/DOC | Recruitment, Leave |
| `StatusBadge` | Badge status berwarna | Leave, Recruitment |
| `ApprovalWorkflow` | Approval UI component | Leave Management |
| `StatsCard` | Summary statistics | Overview, Reports |
| `DataTable` | Sortable, filterable table | Semua halaman |
| `FilterBar` | Filter controls | Attendance, Leave |
| `ExportButton` | Export ke CSV/PDF | Reports |

## Integrasi Cross-Module

| Modul | Data yang Dipertukarkan | Arah |
| - | - | - |
| **Finance** | Payroll → Journal entries | HR → Finance |
| **Attendance** | Check-in/out → Work hours | HR → Finance (payroll) |
| **Leave** | Leave balance → Deduction | HR → Finance (payroll) |
| **CRM** | Employee ↔ Customer relationship | Bidirectional |
| **Tasks** | Task assignment → Employee workload | Tasks → HR |
| **Analytics** | HR metrics → Executive dashboard | HR → Analytics |

## Cara Akses

Dari sidebar, klik menu **HR** untuk melihat dashboard overview, atau pilih sub-menu spesifik (Attendance, Leave, Recruitment, Employees, Payroll, Reports).

## Tips

* **Pastikan GPS aktif** saat check-in absensi — sistem menolak check-in jika lokasi tidak terdeteksi
* **Upload foto selfie dengan pencahayaan baik** — foto gelap atau blur menyulitkan verifikasi
* **Submit cuti jauh-jauh hari** — minimal H-3 untuk cuti tahunan, agar manager punya waktu approve
* **Lengkapi dokumen pendukung** untuk sick leave > 3 hari — surat dokter wajib di-upload
* **Monitor attendance report** setiap bulan untuk identifikasi pola keterlambatan atau absensi tidak wajar
* **Gunakan recruitment pipeline** untuk track semua lamaran secara terstruktur — jangan lewatkan kandidat potensial

***

## Entity Relationship Diagram

Berikut adalah diagram relasi seluruh entitas HR dalam sistem SNISHOP ERP. Setiap entitas terhubung melalui foreign key dan relasi multi-tenant berdasarkan `company_id`.

```mermaid theme={null}
erDiagram
    Employee {
        string company_id PK
        string user_id FK
        string employee_id UK
        string full_name
        string email
        string phone
        string department
        string position
        string description
        string employment_type
        date hire_date
        number salary
        object bank_account
        object emergency_contact
        string address
        date date_of_birth
        string status
        string avatar_url
    }

    AttendanceRecord {
        string user_id FK
        string workspace_id
        date date
        datetime clock_in_time
        datetime clock_out_time
        string clock_in_photo_url
        string clock_out_photo_url
        object clock_in_location
        object clock_out_location
        string status
        string notes
        number total_hours
    }

    CompanyAttendance {
        string company_id FK
        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 clock_in_photo_url
        string clock_out_photo_url
        object clock_in_location
        object clock_out_location
        string status
        string notes
        number total_hours
        number overtime_hours
        number distance_from_office
        string location_id
        string location_name
        number clock_in_accuracy
        number clock_out_accuracy
        boolean is_manual_override
        string override_reason
        string override_approved_by
        datetime override_approved_at
    }

    CompanyLeave {
        string company_id FK
        string employee_id FK
        string employee_name
        string employee_email
        string leave_type
        date start_date
        date end_date
        number total_days
        string reason
        string description
        string attachment_url
        string status
        string approver_id
        string approver_notes
        datetime approved_at
    }

    Leave {
        string company_id FK
        string employee_id FK
        string employee_name
        string leave_type
        date start_date
        date end_date
        number total_days
        string reason
        string description
        string attachment_url
        string status
        string approver_id
        string approver_notes
        datetime approved_at
    }

    Payroll {
        string company_id FK
        string employee_id FK
        string employee_name
        string period
        number basic_salary
        array allowances
        array deductions
        number overtime_hours
        number overtime_pay
        number gross_salary
        number net_salary
        string status
        date payment_date
        string payment_method
        string notes
    }

    CompanyPayroll {
        string company_id FK
        string employee_id FK
        string employee_name
        string period
        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
        date payment_date
        string payment_method
        string notes
    }

    JobPosting {
        string company_id PK
        string position_title
        string department
        string description
        array requirements
        number salary_range_min
        number salary_range_max
        string employment_type
        string job_location
        date posting_date
        date closing_date
        string status
        string posted_by
    }

    JobApplication {
        string applicant_name
        string applicant_email
        string applicant_phone
        string position_applied
        string position_id FK
        string cv_url
        string portfolio_url
        string linkedin_url
        string cover_letter
        string description
        number experience_years
        string current_salary
        string expected_salary
        string status
        string admin_notes
        string interview_link
        datetime interview_date
        string rejection_reason
        number rating
        array skills
        string reviewed_by
        datetime reviewed_date
    }

    InterviewSchedule {
        string company_id FK
        string applicant_id FK
        string interview_type
        datetime interview_date
        string interview_location
        array interviewer_ids
        string description
        string status
        string interview_notes
        number rating
        array feedback
        string recommendation
    }

    Training {
        string company_id FK
        string training_name
        string training_type
        string category
        string description
        string trainer
        datetime start_date
        datetime end_date
        number duration_hours
        string location
        number max_participants
        array participants
        number cost_per_person
        number total_cost
        string budget_code
        array materials
        string status
        object evaluation
    }

    KPI {
        string company_id FK
        string employee_id FK
        string period
        string description
        array metrics
        number overall_score
        string rating
        string reviewer_id
        string reviewer_notes
        string employee_notes
        string status
    }

    CompanyKPI {
        string company_id FK
        string employee_id FK
        string employee_name
        string employee_email
        string period
        string template_id FK
        string template_name
        string description
        array metrics
        number overall_score
        number attendance_score
        string rating
        string reviewer_id
        string reviewer_name
        string reviewer_notes
        string employee_notes
        string status
        boolean auto_generated
        datetime finalized_at
        string evidence_url
        string evidence_note
    }

    KPITemplate {
        string company_id FK
        string template_name
        string description
        array applicable_roles
        array applicable_departments
        array default_metrics
        boolean is_active
        boolean auto_calculate
        string calculation_frequency
    }

    TimeEntry {
        string company_id FK
        string user_id FK
        string project_id
        string task_id
        date entry_date
        time start_time
        time end_time
        number duration_minutes
        string description
        boolean billable
        boolean is_approved
        string approved_by
        string status
    }

    PayrollConfiguration {
        string company_id PK
        string description
        string basic_salary_formula
        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
    }

    PayslipTemplate {
        string company_id FK
        string template_name
        string description
        string logo_url
        string header_html
        string footer_html
        boolean show_company_info
        boolean show_breakdown
        boolean show_attendance_summary
        boolean show_kpi_score
        boolean is_default
    }

    CompanyAttendanceSettings {
        string company_id PK
        string description
        object office_location
        number office_radius_meters
        boolean require_office_location
        number default_accuracy_tolerance_meters
        array locations
        object working_hours
        array shifts
        object overtime_settings
        number late_tolerance_minutes
        array working_days
        boolean auto_clock_out_enabled
        string auto_clock_out_time
        boolean require_photo
        boolean require_notes
    }

    Applicant {
        string company_id FK
        string job_posting_id FK
        string full_name
        string email
        string phone
        string resume_url
        string cover_letter
        datetime application_date
        string current_status
        array education
        array experience
        array skills
        string assigned_to
        string notes
    }

    OfferLetter {
        string company_id FK
        string applicant_id FK
        string applicant_name
        string position_title
        string description
        date start_date
        string employment_type
        number salary
        string currency
        array benefits
        string reporting_to
        string department
        string office_location
        string contract_terms
        date offer_date
        date expiration_date
        string status
        date response_date
        string acceptance_status
        string offer_letter_url
        string issued_by
    }

    PerformanceReview {
        string company_id FK
        string employee_id FK
        string employee_name
        string review_period
        string review_type
        string reviewer_id FK
        string reviewer_name
        date review_date
        string description
        array competencies
        array kpi_achievement
        number overall_score
        string rating
        array strengths
        array areas_for_improvement
        array development_plan
        string reviewer_comments
        string employee_comments
        boolean employee_acknowledged
        datetime acknowledged_at
        string status
    }

    ApprovalWorkflow {
        string company_id FK
        string workflow_name
        string description
        string document_type
        array approval_levels
        object amount_limits
        boolean is_active
    }

    ApprovalRequest {
        string company_id FK
        string workflow_id FK
        string document_type
        string document_id
        string requester_id FK
        datetime submission_date
        number amount
        string description
        number current_approval_level
        array approval_history
        string overall_status
        datetime final_approval_date
    }

    Employee ||--o{ CompanyAttendance : "mencatat kehadiran"
    Employee ||--o{ CompanyLeave : "mengajukan cuti"
    Employee ||--o{ CompanyPayroll : "menerima gaji"
    Employee ||--o{ CompanyKPI : "dievaluasi KPI"
    Employee ||--o{ KPI : "penilaian performa"
    Employee ||--o{ TimeEntry : "mencatat waktu kerja"
    Employee }o--|| AttendanceRecord : "absensi via user"
    Employee }o--|| Leave : "pengajuan cuti legacy"
    Employee }o--|| Payroll : "slip gaji legacy"
    JobPosting ||--o{ JobApplication : "menerima lamaran"
    JobApplication ||--o{ InterviewSchedule : "dijadwalkan interview"
    KPITemplate ||--o{ CompanyKPI : "template KPI digunakan"
    PayrollConfiguration ||--o{ CompanyPayroll : "konfigurasi perhitungan"
    PayslipTemplate }o--|| CompanyPayroll : "format cetak slip"
    CompanyAttendanceSettings }o--o{ CompanyAttendance : "aturan absensi"
    Training }o--o{ Employee : "peserta pelatihan"
    JobPosting ||--o{ Applicant : "lowongan yang dilamar"
    Applicant ||--o{ InterviewSchedule : "kandidat yang diinterview"
    Applicant ||--o| OfferLetter : "surat penawaran kerja"
    OfferLetter ||--o| Employee : "menjadi karyawan baru (relasi logis)"
    Employee ||--o{ PerformanceReview : "dinilai performanya"
    CompanyKPI ||--o{ PerformanceReview : "capaian KPI dirangkum"
    ApprovalWorkflow ||--o{ ApprovalRequest : "alur persetujuan dipakai"
    ApprovalRequest }o--|| CompanyLeave : "mencatat approval cuti"
```

### Keterangan Relasi

Tabel di atas membedakan relasi *hard foreign key* (field eksplisit pada JSONC schema) dengan relasi *logis/embedded* yang dijaga oleh server-function.

| Relasi | Jenis | Field penghubung |
| - | - | - |
| `Applicant` → `JobPosting` | Hard FK | `Applicant.job_posting_id` |
| `InterviewSchedule` → `Applicant` | Hard FK | `InterviewSchedule.applicant_id` |
| `OfferLetter` → `Applicant` | Hard FK | `OfferLetter.applicant_id` |
| `PerformanceReview` → `Employee` | Hard FK | `PerformanceReview.employee_id` + `reviewer_id` |
| `CompanyAttendance` → `Employee` | Hard FK | `CompanyAttendance.employee_id` |
| `CompanyLeave` → `Employee` | Hard FK | `CompanyLeave.employee_id` + `employee_email` (RLS) |
| `CompanyPayroll` → `Employee` | Hard FK | `CompanyPayroll.employee_id` |
| `CompanyKPI` → `KPITemplate` | Hard FK | `CompanyKPI.template_id` |
| `ApprovalRequest` → `ApprovalWorkflow` | Hard FK | `ApprovalRequest.workflow_id` |
| `OfferLetter` → `Employee` | Logis | Tidak ada FK; dibuat manual oleh HR saat onboarding melalui server-function |
| `ApprovalRequest` → `CompanyLeave` | Logis (polymorphic) | `document_type` + `document_id`, divalidasi server-function |
| `PerformanceReview` → `CompanyKPI` | Embedded | Array `kpi_achievement[]` menyimpan snapshot target/realisasi |
| `Training` → `Employee` | Embedded | Array `participants[]` berisi `employee_id` & `attendance_status` |
| `CompanyAttendanceSettings` → `CompanyAttendance` | Logis | `location_id` & `shift_id` dicocokkan saat clock-in |

Seluruh entitas di atas juga ter-scoping secara multi-tenant lewat `company_id`, kecuali `AttendanceRecord` yang masih memakai `workspace_id` sebagai kunci scope.

***

## Entity Schema Reference (Referensi Skema Entitas)

Bagian ini mendokumentasikan seluruh field untuk setiap entitas HR. Semua deskripsi dalam Bahasa Indonesia. Field name mengikuti definisi JSONC entity secara persis.

### Employee — Data Karyawan

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan (multi-tenant scoping) |
| `user_id` | string | Tidak | Link ke entitas User untuk autentikasi |
| `employee_id` | string | Ya | Nomor Induk Karyawan (NIK), bersifat unik |
| `full_name` | string | Ya | Nama lengkap karyawan sesuai KTP |
| `email` | string | Ya | Alamat email perusahaan karyawan |
| `phone` | string | Tidak | Nomor telepon atau WhatsApp aktif |
| `department` | string | Ya | Departemen tempat karyawan bekerja |
| `position` | string | Ya | Jabatan atau posisi kerja karyawan |
| `description` | string | Tidak | Catatan tambahan mengenai keahlian dan tanggung jawab (maks 1000 karakter) |
| `employment_type` | string | Tidak | Jenis employment — `full_time`, `part_time`, `contract`, atau `intern` |
| `hire_date` | date | Ya | Tanggal bergabung dengan perusahaan |
| `salary` | number | Tidak | Gaji pokok bulanan dalam Rupiah |
| `bank_account` | object | Tidak | Informasi rekening bank untuk transfer gaji |
| `bank_account.bank_name` | string | — | Nama bank (misal: BCA, Mandiri, BNI) |
| `bank_account.account_number` | string | — | Nomor rekening bank |
| `bank_account.account_name` | string | — | Nama pemilik rekening |
| `emergency_contact` | object | Tidak | Kontak darurat yang dapat dihubungi |
| `emergency_contact.name` | string | — | Nama kontak darurat |
| `emergency_contact.relationship` | string | — | Hubungan dengan kontak darurat |
| `emergency_contact.phone` | string | — | Nomor telepon kontak darurat |
| `address` | string | Tidak | Alamat tempat tinggal karyawan |
| `date_of_birth` | date | Tidak | Tanggal lahir karyawan |
| `status` | string | Tidak | Status keaktifan karyawan — `active`, `on_leave`, atau `terminated` |
| `avatar_url` | string | Tidak | URL foto profil karyawan (Cloudinary) |

### AttendanceRecord — Log Absensi (User-Level)

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `user_id` | string | Ya | ID user yang melakukan absensi |
| `workspace_id` | string | Ya | ID workspace tempat absensi dilakukan |
| `date` | date | Ya | Tanggal absensi |
| `clock_in_time` | datetime | Ya | Waktu check-in karyawan |
| `clock_out_time` | datetime | Tidak | Waktu check-out karyawan |
| `clock_in_photo_url` | string | Tidak | URL foto selfie saat check-in |
| `clock_out_photo_url` | string | Tidak | URL foto selfie saat check-out |
| `clock_in_location` | object | Tidak | Koordinat GPS saat check-in |
| `clock_in_location.latitude` | number | — | Garis lintang lokasi check-in |
| `clock_in_location.longitude` | number | — | Garis bujur lokasi check-in |
| `clock_in_location.address` | string | — | Alamat teks lokasi check-in |
| `clock_out_location` | object | Tidak | Koordinat GPS saat check-out |
| `clock_out_location.latitude` | number | — | Garis lintang lokasi check-out |
| `clock_out_location.longitude` | number | — | Garis bujur lokasi check-out |
| `clock_out_location.address` | string | — | Alamat teks lokasi check-out |
| `status` | string | Tidak | Status kehadiran — `present`, `late`, `absent`, `sick`, atau `leave` |
| `notes` | string | Tidak | Catatan tambahan dari karyawan |
| `total_hours` | number | Tidak | Total jam kerja (dihitung otomatis) |

### CompanyAttendance — Log Kehadiran Perusahaan

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID karyawan yang melakukan absensi |
| `employee_name` | string | Tidak | Nama lengkap karyawan (denormalized) |
| `employee_email` | string | Tidak | Email karyawan untuk RLS |
| `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 check-in |
| `clock_out_time` | datetime | Tidak | Waktu check-out |
| `clock_in_photo_url` | string | Tidak | URL foto selfie saat check-in |
| `clock_out_photo_url` | string | Tidak | URL foto selfie saat check-out |
| `clock_in_location` | object | Tidak | Lokasi GPS saat check-in |
| `clock_in_location.latitude` | number | — | Garis lintang check-in |
| `clock_in_location.longitude` | number | — | Garis bujur check-in |
| `clock_in_location.accuracy` | number | — | Akurasi GPS dalam meter saat check-in |
| `clock_out_location` | object | Tidak | Lokasi GPS saat check-out |
| `clock_out_location.latitude` | number | — | Garis lintang check-out |
| `clock_out_location.longitude` | number | — | Garis bujur check-out |
| `clock_out_location.accuracy` | number | — | Akurasi GPS dalam meter saat check-out |
| `status` | string | Tidak | Status kehadiran — `present`, `late`, `absent`, `sick`, `leave`, atau `wfh` |
| `notes` | string | Tidak | Catatan tambahan |
| `total_hours` | number | Tidak | Total jam kerja pada hari tersebut |
| `overtime_hours` | number | Tidak | Jumlah jam lembur (default: 0) |
| `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 | 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 |

### CompanyLeave — Pengajuan Cuti Perusahaan

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID karyawan yang mengajukan cuti |
| `employee_name` | string | Tidak | Nama lengkap karyawan (denormalized) |
| `employee_email` | string | Tidak | Email karyawan untuk RLS |
| `leave_type` | string | Ya | Jenis cuti — `annual`, `sick`, `unpaid`, `maternity`, `paternity`, atau `emergency` |
| `start_date` | date | Ya | Tanggal mulai cuti |
| `end_date` | date | Ya | Tanggal berakhir cuti |
| `total_days` | number | Tidak | Jumlah hari cuti (dihitung otomatis) |
| `reason` | string | Ya | Alasan pengajuan cuti |
| `description` | string | Tidak | Keterangan tambahan atau dokumen pendukung (maks 1000 karakter) |
| `attachment_url` | string | Tidak | URL dokumen pendukung (surat dokter, dll) |
| `status` | string | Tidak | Status pengajuan — `pending`, `approved`, `rejected`, atau `cancelled` |
| `approver_id` | string | Tidak | ID approver (manager/HR) yang memproses |
| `approver_notes` | string | Tidak | Catatan dari approver saat approve/reject |
| `approved_at` | datetime | Tidak | Timestamp kapan pengajuan diproses |

### Leave — Pengajuan Cuti (Legacy)

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID karyawan yang mengajukan cuti |
| `employee_name` | string | Tidak | Nama lengkap karyawan |
| `leave_type` | string | Ya | Jenis cuti — `annual`, `sick`, `unpaid`, `maternity`, `paternity`, atau `emergency` |
| `start_date` | date | Ya | Tanggal mulai cuti |
| `end_date` | date | Ya | Tanggal berakhir cuti |
| `total_days` | number | Tidak | Jumlah hari cuti |
| `reason` | string | Ya | Alasan pengajuan cuti |
| `description` | string | Tidak | Detail tambahan mengenai pengajuan cuti (maks 1000 karakter) |
| `attachment_url` | string | Tidak | URL lampiran (surat dokter, dll) |
| `status` | string | Tidak | Status pengajuan — `pending`, `approved`, `rejected`, atau `cancelled` |
| `approver_id` | string | Tidak | ID approver |
| `approver_notes` | string | Tidak | Catatan dari approver |
| `approved_at` | datetime | Tidak | Waktu persetujuan |

### CompanyPayroll — Slip Gaji Perusahaan

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID karyawan penerima gaji |
| `employee_name` | string | Tidak | Nama lengkap karyawan (denormalized) |
| `period` | string | Ya | Periode gaji dalam format YYYY-MM |
| `basic_salary` | number | Ya | Gaji pokok bulanan |
| `attendance_days` | number | Tidak | Jumlah hari hadir dalam periode |
| `working_days` | number | Tidak | Total hari kerja di periode ini |
| `late_count` | number | Tidak | Jumlah keterlambatan (default: 0) |
| `absent_count` | number | Tidak | Jumlah hari tidak hadir (default: 0) |
| `overtime_hours` | number | Tidak | Total jam lembur (default: 0) |
| `kpi_score` | number | Tidak | Skor KPI periode ini |
| `allowances` | array | Tidak | Daftar tunjangan — setiap item berisi `name` dan `amount` |
| `deductions` | array | Tidak | Daftar potongan — setiap item berisi `name` dan `amount` |
| `overtime_pay` | number | Tidak | Upah lembur yang dihitung (default: 0) |
| `kpi_bonus` | number | Tidak | Bonus berdasarkan skor KPI (default: 0) |
| `gross_salary` | number | Ya | Gaji kotor (gaji pokok + tunjangan + lembur + bonus) |
| `net_salary` | number | Ya | Gaji bersih (gaji kotor - potongan) |
| `status` | string | Tidak | Status pembayaran — `draft`, `approved`, atau `paid` |
| `payment_date` | date | Tidak | Tanggal pembayaran gaji |
| `payment_method` | string | Tidak | Metode pembayaran — `bank_transfer`, `cash`, atau `check` |
| `notes` | string | Tidak | Catatan tambahan pada slip gaji |

### Payroll — Slip Gaji (Legacy)

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID karyawan |
| `employee_name` | string | Tidak | Nama lengkap karyawan |
| `period` | string | Ya | Periode gaji format YYYY-MM |
| `basic_salary` | number | Ya | Gaji pokok |
| `allowances` | array | Tidak | Daftar tunjangan `{name, amount}` |
| `deductions` | array | Tidak | Daftar potongan `{name, amount}` |
| `overtime_hours` | number | Tidak | Jam lembur (default: 0) |
| `overtime_pay` | number | Tidak | Upah lembur (default: 0) |
| `gross_salary` | number | Ya | Gaji kotor |
| `net_salary` | number | Ya | Gaji bersih |
| `status` | string | Tidak | Status — `draft`, `approved`, atau `paid` |
| `payment_date` | date | Tidak | Tanggal pembayaran |
| `payment_method` | string | Tidak | Metode — `bank_transfer`, `cash`, atau `check` |
| `notes` | string | Tidak | Catatan |

### JobPosting — Lowongan Kerja

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `position_title` | string | Ya | Judul posisi yang ditawarkan |
| `department` | string | Ya | Departemen penempatan |
| `description` | string | Ya | Deskripsi lengkap pekerjaan |
| `requirements` | array\[string] | Tidak | Daftar persyaratan kualifikasi |
| `salary_range_min` | number | Tidak | Batas bawah rentang gaji |
| `salary_range_max` | number | Tidak | Batas atas rentang gaji |
| `employment_type` | string | Tidak | Jenis pekerjaan — `full_time`, `part_time`, `contract`, atau `internship` |
| `job_location` | string | Tidak | Lokasi penempatan kerja |
| `posting_date` | date | Tidak | Tanggal lowongan dipublikasikan |
| `closing_date` | date | Ya | Batas akhir penerimaan lamaran |
| `status` | string | Tidak | Status lowongan — `open`, `closed`, atau `on_hold` |
| `posted_by` | string | Tidak | User ID HR yang membuat posting |

### Applicant — Pelamar Kerja (Pipeline Rekrutmen)

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan tempat lowongan dibuka |
| `job_posting_id` | string | Ya | ID lowongan yang dilamar (FK ke `JobPosting`) |
| `full_name` | string | Ya | Nama lengkap pelamar |
| `email` | string | Ya | Alamat email pelamar (format email) |
| `phone` | string | Tidak | Nomor telepon/WhatsApp pelamar |
| `resume_url` | string | Tidak | URL file CV/Resume yang diupload |
| `cover_letter` | string | Tidak | Surat lamaran / motivation letter |
| `application_date` | datetime | Tidak | Waktu lamaran dikirimkan |
| `current_status` | string | Tidak | Status tahap pipeline — `new`, `screening`, `interview_scheduled`, `interviewed`, `offer_extended`, `hired`, atau `rejected` (default: `new`) |
| `education` | array\[object] | Tidak | Riwayat pendidikan pelamar |
| `education[].school_name` | string | — | Nama sekolah / universitas |
| `education[].degree` | string | — | Gelar / jenjang pendidikan (S1, D3, SMA) |
| `education[].field` | string | — | Bidang / jurusan studi |
| `education[].graduation_year` | number | — | Tahun lulus |
| `experience` | array\[object] | Tidak | Riwayat pengalaman kerja |
| `experience[].company_name` | string | — | Nama perusahaan tempat bekerja |
| `experience[].position` | string | — | Jabatan / posisi terakhir |
| `experience[].duration_years` | number | — | Lama bekerja dalam tahun |
| `experience[].description` | string | — | Deskripsi tanggung jawab / pencapaian |
| `skills` | array\[string] | Tidak | Daftar keahlian yang dimiliki pelamar |
| `assigned_to` | string | Tidak | HR / Hiring Manager yang menangani pelamar ini |
| `notes` | string | Tidak | Catatan internal HR mengenai pelamar |

**Kondisi RLS** — Create/Read/Update/Delete: `data.company_id` = `user.data.active_company_id` (dan `company_id` tidak kosong) ATAU `created_by_id` = user saat ini ATAU `user_condition.role = admin`.

### JobApplication — Lamaran Kerja

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `applicant_name` | string | Ya | Nama lengkap pelamar |
| `applicant_email` | string | Ya | Alamat email pelamar |
| `applicant_phone` | string | Ya | Nomor telepon pelamar |
| `position_applied` | string | Ya | Posisi yang dilamar |
| `position_id` | string | Tidak | ID posisi dari CareerContent.open\_positions |
| `cv_url` | string | Ya | URL file CV/Resume yang diupload |
| `portfolio_url` | string | Tidak | URL portofolio (opsional) |
| `linkedin_url` | string | Tidak | URL profil LinkedIn pelamar |
| `cover_letter` | string | Tidak | Surat lamaran atau motivation letter |
| `description` | string | Tidak | Ringkasan latar belakang dan motivasi pelamar (maks 1000 karakter) |
| `experience_years` | number | Tidak | Jumlah tahun pengalaman kerja |
| `current_salary` | string | Tidak | Gaji saat ini (opsional) |
| `expected_salary` | string | Tidak | Ekspektasi gaji yang diinginkan |
| `status` | string | Tidak | Status lamaran — `submitted`, `screening`, `interview_scheduled`, `interview_completed`, `accepted`, atau `rejected` |
| `admin_notes` | string | Tidak | Catatan internal dari admin/HR |
| `interview_link` | string | Tidak | Link interview online (Google Meet, Zoom, dll) |
| `interview_date` | datetime | Tidak | Tanggal dan waktu interview |
| `rejection_reason` | string | Tidak | Alasan penolakan jika status rejected |
| `rating` | number | Tidak | Rating kandidat dari 1 sampai 5 |
| `skills` | array\[string] | Tidak | Daftar keahlian yang dimiliki pelamar |
| `reviewed_by` | string | Tidak | Email admin yang melakukan review |
| `reviewed_date` | datetime | Tidak | Tanggal lamaran di-review |

### InterviewSchedule — Jadwal Interview

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `applicant_id` | string | Ya | ID pelamar yang diinterview |
| `interview_type` | string | Tidak | Tipe wawancara — `phone_screening`, `first_round`, `second_round`, `final_round`, atau `hr_round` |
| `interview_date` | datetime | Ya | Tanggal dan waktu interview |
| `interview_location` | string | Tidak | Lokasi atau link wawancara |
| `interviewer_ids` | array\[string] | Tidak | User ID para interviewer |
| `description` | string | Tidak | Agenda dan topik pembahasan wawancara (maks 1000 karakter) |
| `status` | string | Tidak | Status jadwal — `scheduled`, `completed`, atau `cancelled` |
| `interview_notes` | string | Tidak | Catatan hasil wawancara |
| `rating` | number | Tidak | Rating hasil wawancara (1-5) |
| `feedback` | array\[object] | Tidak | Daftar feedback dari setiap interviewer |
| `feedback[].interviewer_id` | string | — | ID interviewer yang memberi feedback |
| `feedback[].feedback_text` | string | — | Teks feedback |
| `feedback[].rating` | number | — | Rating dari interviewer ini |
| `recommendation` | string | Tidak | Rekomendasi — `pass`, `reject`, atau `undecided` |

### OfferLetter — Surat Penawaran Kerja

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan penerbit penawaran |
| `applicant_id` | string | Ya | ID pelamar yang menerima penawaran (FK ke `Applicant`) |
| `applicant_name` | string | Tidak | Nama pelamar (denormalisasi untuk tampilan) |
| `position_title` | string | Ya | Jabatan yang ditawarkan |
| `description` | string | Tidak | Ringkasan penawaran kerja dan hal penting yang perlu diketahui calon karyawan (maks 1000 karakter) |
| `start_date` | date | Tidak | Tanggal mulai bekerja yang direncanakan |
| `employment_type` | string | Tidak | Jenis hubungan kerja — `full_time`, `part_time`, atau `contract` (default: `full_time`) |
| `salary` | number | Ya | Gaji pokok yang ditawarkan |
| `currency` | string | Tidak | Mata uang gaji (default: `IDR`) |
| `benefits` | array\[string] | Tidak | Daftar manfaat/tunjangan yang ditawarkan |
| `reporting_to` | string | Tidak | Direct manager / atasan langsung |
| `department` | string | Tidak | Departemen penempatan |
| `office_location` | string | Tidak | Lokasi kerja / kantor penempatan |
| `contract_terms` | string | Tidak | Syarat dan ketentuan kontrak kerja |
| `offer_date` | date | Ya | Tanggal penawaran dikeluarkan |
| `expiration_date` | date | Tidak | Tanggal penawaran kadaluarsa |
| `status` | string | Tidak | Status dokumen penawaran — `draft`, `sent`, `accepted`, `declined`, atau `expired` (default: `draft`) |
| `response_date` | date | Tidak | Tanggal pelamar memberikan respons |
| `acceptance_status` | string | Tidak | Status keputusan pelamar — `pending`, `accepted`, atau `declined` |
| `offer_letter_url` | string | Tidak | URL dokumen surat penawaran (PDF) |
| `issued_by` | string | Tidak | User ID yang membuat / menerbitkan penawaran |

**Kondisi RLS** — Create/Read/Update/Delete: `data.company_id` = `user.data.active_company_id` (dan `company_id` tidak kosong) ATAU `created_by_id` = user saat ini ATAU `user_condition.role = admin`.

### Training — Program Pelatihan

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `training_name` | string | Ya | Nama program pelatihan |
| `training_type` | string | Tidak | Jenis pelatihan — `internal`, `external`, `online`, `certification`, atau `workshop` |
| `category` | string | Tidak | Kategori pelatihan (Technical, Soft Skills, Leadership, dll) |
| `description` | string | Tidak | Deskripsi program pelatihan |
| `trainer` | string | Tidak | Nama trainer atau instruktur |
| `start_date` | datetime | Ya | Tanggal dan waktu mulai pelatihan |
| `end_date` | datetime | Ya | Tanggal dan waktu selesai pelatihan |
| `duration_hours` | number | Tidak | Total durasi pelatihan dalam jam |
| `location` | string | Tidak | Lokasi pelaksanaan pelatihan |
| `max_participants` | number | Tidak | Jumlah maksimum peserta |
| `participants` | array\[object] | Tidak | Daftar peserta pelatihan |
| `participants[].employee_id` | string | — | ID karyawan peserta |
| `participants[].employee_name` | string | — | Nama karyawan peserta |
| `participants[].attendance_status` | string | — | Status kehadiran — `registered`, `attended`, `absent`, atau `completed` |
| `participants[].completion_date` | date | — | Tanggal penyelesaian |
| `participants[].certificate_issued` | boolean | — | Apakah sertifikat telah diterbitkan |
| `participants[].certificate_url` | string | — | URL file sertifikat |
| `participants[].score` | number | — | Nilai hasil pelatihan |
| `participants[].feedback` | string | — | Umpan balik peserta |
| `cost_per_person` | number | Tidak | Biaya pelatihan per peserta |
| `total_cost` | number | Tidak | Total biaya pelatihan |
| `budget_code` | string | Tidak | Kode anggaran pelatihan |
| `materials` | array\[object] | Tidak | Materi pelatihan yang dibagikan |
| `materials[].title` | string | — | Judul materi |
| `materials[].file_url` | string | — | URL file materi |
| `status` | string | Tidak | Status pelatihan — `planned`, `ongoing`, `completed`, atau `cancelled` |
| `evaluation` | object | Tidak | Hasil evaluasi pelatihan |
| `evaluation.average_rating` | number | — | Rata-rata rating peserta |
| `evaluation.feedback_count` | number | — | Jumlah feedback yang masuk |
| `evaluation.would_recommend` | number | — | Persentase peserta yang merekomendasikan |

### KPI — Penilaian Key Performance Indicator

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID karyawan yang dinilai |
| `period` | string | Ya | Periode penilaian (misal: `2024-Q1`, `2024-01`) |
| `description` | string | Tidak | Penjelasan konteks penilaian dan target pencapaian (maks 1000 karakter) |
| `metrics` | array\[object] | Ya | Daftar metrik KPI yang dinilai |
| `metrics[].name` | string | — | Nama metrik |
| `metrics[].description` | string | — | Deskripsi metrik |
| `metrics[].target` | number | — | Target yang harus dicapai |
| `metrics[].actual` | number | — | Realisasi pencapaian aktual |
| `metrics[].unit` | string | — | Satuan pengukuran (%, rupiah, jumlah, dll) |
| `metrics[].weight` | number | — | Bobot persentase (0-100) |
| `overall_score` | number | Tidak | Skor rata-rata tertimbang (0-100) |
| `rating` | string | Tidak | Predikat — `outstanding`, `exceeds`, `meets`, `needs_improvement`, atau `unsatisfactory` |
| `reviewer_id` | string | Tidak | ID reviewer (manager) |
| `reviewer_notes` | string | Tidak | Catatan dari reviewer |
| `employee_notes` | string | Tidak | Catatan dari karyawan yang dinilai |
| `status` | string | Tidak | Status penilaian — `draft`, `submitted`, `reviewed`, atau `finalized` |

### CompanyKPI — KPI Perusahaan

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID member/karyawan yang dinilai |
| `employee_name` | string | Tidak | Nama lengkap karyawan (denormalized) |
| `employee_email` | string | Tidak | Email karyawan untuk RLS |
| `period` | string | Ya | Periode penilaian format YYYY-MM |
| `template_id` | string | Tidak | ID template KPI yang digunakan |
| `template_name` | string | Tidak | Nama template KPI (denormalized) |
| `description` | string | Tidak | Penjelasan konteks evaluasi KPI (maks 1000 karakter) |
| `metrics` | array\[object] | Ya | Daftar metrik KPI |
| `metrics[].name` | string | — | Nama metrik |
| `metrics[].description` | string | — | Deskripsi metrik |
| `metrics[].target` | number | — | Target |
| `metrics[].actual` | number | — | Realisasi aktual |
| `metrics[].unit` | string | — | Satuan |
| `metrics[].weight` | number | — | Bobot persentase (0-100) |
| `metrics[].score` | number | — | Skor tertimbang untuk metrik ini |
| `overall_score` | number | Tidak | Skor rata-rata tertimbang (0-100) |
| `attendance_score` | number | Tidak | Skor kehadiran (0-100) |
| `rating` | string | Tidak | Predikat — `outstanding`, `exceeds`, `meets`, `needs_improvement`, atau `unsatisfactory` |
| `reviewer_id` | string | Tidak | ID reviewer |
| `reviewer_name` | string | Tidak | Nama reviewer (denormalized) |
| `reviewer_notes` | string | Tidak | Catatan reviewer |
| `employee_notes` | string | Tidak | Catatan karyawan |
| `status` | string | Tidak | Status — `draft`, `submitted`, `reviewed`, atau `finalized` |
| `auto_generated` | boolean | Tidak | Apakah KPI dibuat otomatis oleh sistem |
| `finalized_at` | datetime | Tidak | Timestamp saat KPI difinalisasi |
| `evidence_url` | string | Tidak | URL bukti pendukung pencapaian KPI |
| `evidence_note` | string | Tidak | Catatan singkat tentang bukti yang dilampirkan (maks 500 karakter) |

### KPITemplate — Template KPI

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `template_name` | string | Ya | Nama template KPI |
| `description` | string | Tidak | Penjelasan tujuan dan metrik utama template (maks 1000 karakter) |
| `applicable_roles` | array\[string] | Tidak | Daftar role yang menggunakan template ini |
| `applicable_departments` | array\[string] | Tidak | Daftar departemen yang menggunakan template |
| `default_metrics` | array\[object] | Ya | Daftar metrik default dalam template |
| `default_metrics[].metric_name` | string | — | Nama metrik |
| `default_metrics[].description` | string | — | Deskripsi metrik |
| `default_metrics[].default_target` | number | — | Target default |
| `default_metrics[].unit` | string | — | Satuan pengukuran |
| `default_metrics[].weight` | number | — | Bobot default (0-100) |
| `default_metrics[].calculation_method` | string | — | Metode perhitungan — `manual`, `auto_from_attendance`, `auto_from_sales`, atau `auto_from_projects` |
| `default_metrics[].auto_source` | object | — | Konfigurasi sumber data otomatis |
| `default_metrics[].auto_source.entity` | string | — | Entity sumber data |
| `default_metrics[].auto_source.field` | string | — | Field yang diagregasi |
| `default_metrics[].auto_source.aggregation` | string | — | Metode agregasi — `sum`, `count`, `average`, atau `percentage` |
| `is_active` | boolean | Tidak | Status keaktifan template (default: true) |
| `auto_calculate` | boolean | Tidak | Otomatis menghitung KPI setiap periode (default: true) |
| `calculation_frequency` | string | Tidak | Frekuensi perhitungan — `daily`, `weekly`, atau `monthly` |

### PerformanceReview — Penilaian Performa Karyawan

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID karyawan yang dinilai (FK ke `Employee`) |
| `employee_name` | string | Tidak | Nama karyawan (denormalisasi untuk tampilan) |
| `review_period` | string | Ya | Periode review, misal `Q1 2026` atau `2025-Annual` |
| `review_type` | string | Tidak | Jenis penilaian — `probation`, `quarterly`, `mid_year`, atau `annual` (default: `quarterly`) |
| `reviewer_id` | string | Ya | ID reviewer, biasanya supervisor langsung |
| `reviewer_name` | string | Tidak | Nama reviewer |
| `review_date` | date | Tidak | Tanggal pelaksanaan review |
| `description` | string | Tidak | Ringkasan umum performa dan konteks evaluasi pada periode ini (maks 1000 karakter) |
| `competencies` | array\[object] | Tidak | Daftar penilaian kompetensi per aspek |
| `competencies[].competency_name` | string | — | Nama kompetensi (misal: Integritas, Teamwork) |
| `competencies[].description` | string | — | Definisi / indikator kompetensi |
| `competencies[].score` | number | — | Skor kompetensi (1–5) |
| `competencies[].comments` | string | — | Catatan reviewer untuk kompetensi ini |
| `kpi_achievement` | array\[object] | Tidak | Rangkuman capaian KPI karyawan |
| `kpi_achievement[].kpi_name` | string | — | Nama indikator KPI |
| `kpi_achievement[].target` | number | — | Nilai target yang ditetapkan |
| `kpi_achievement[].actual` | number | — | Nilai realisasi |
| `kpi_achievement[].achievement_percentage` | number | — | Persentase capaian `(actual/target) × 100` |
| `kpi_achievement[].weight` | number | — | Bobot indikator dalam penilaian keseluruhan |
| `overall_score` | number | Tidak | Skor keseluruhan (0–100), hasil agregasi bobot kompetensi & capaian KPI |
| `rating` | string | Tidak | Rating keseluruhan — `outstanding`, `exceeds_expectations`, `meets_expectations`, `needs_improvement`, atau `unsatisfactory` |
| `strengths` | array\[string] | Tidak | Daftar kekuatan karyawan |
| `areas_for_improvement` | array\[string] | Tidak | Daftar area yang perlu ditingkatkan |
| `development_plan` | array\[object] | Tidak | Rencana pengembangan karyawan pasca-review |
| `development_plan[].goal` | string | — | Tujuan pengembangan |
| `development_plan[].action` | string | — | Tindakan / program yang dijalankan |
| `development_plan[].timeline` | string | — | Target waktu penyelesaian |
| `development_plan[].support_needed` | string | — | Dukungan yang dibutuhkan dari perusahaan |
| `reviewer_comments` | string | Tidak | Komentar/kesimpulan akhir reviewer |
| `employee_comments` | string | Tidak | Tanggapan karyawan atas hasil penilaian |
| `employee_acknowledged` | boolean | Tidak | Apakah karyawan sudah acknowledge review (default: `false`) |
| `acknowledged_at` | datetime | Tidak | Waktu karyawan melakukan acknowledge |
| `status` | string | Tidak | Status dokumen review — `draft`, `submitted`, `acknowledged`, atau `finalized` (default: `draft`) |

**Kondisi RLS** — Create/Read/Update/Delete: `data.company_id` = `user.data.active_company_id` (dan `company_id` tidak kosong) ATAU `created_by_id` = user saat ini ATAU `user_condition.role = admin`.

### TimeEntry — Pencatatan Waktu Kerja

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `user_id` | string | Ya | ID pengguna yang mencatat waktu |
| `project_id` | string | Tidak | ID proyek yang dikerjakan |
| `task_id` | string | Tidak | ID tugas spesifik |
| `entry_date` | date | Ya | tanggal pencatatan waktu |
| `start_time` | time | Tidak | Waktu mulai (format HH:MM) |
| `end_time` | time | Tidak | Waktu selesai (format HH:MM) |
| `duration_minutes` | number | Ya | Durasi kerja dalam menit |
| `description` | string | Tidak | Deskripsi pekerjaan yang dilakukan |
| `billable` | boolean | Tidak | Apakah waktu dapat ditagihkan ke klien (default: false) |
| `is_approved` | boolean | Tidak | Status persetujuan (default: false) |
| `approved_by` | string | Tidak | ID user yang menyetujui |
| `status` | string | Tidak | Status entri — `submitted`, `approved`, atau `rejected` |

### PayrollConfiguration — Konfigurasi Penggajian

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `description` | string | Tidak | Penjelasan kebijakan penggajian perusahaan (maks 1000 karakter) |
| `basic_salary_formula` | string | Tidak | Formula gaji pokok — `fixed`, `hourly`, atau `daily` |
| `working_hours_per_day` | number | Tidak | Jam kerja per hari (default: 8) |
| `working_days_per_month` | number | Tidak | Hari kerja per bulan (default: 22) |
| `overtime_rate` | number | Tidak | Multiplier lembur, misal 1.5 = 150% (default: 1.5) |
| `late_penalty_per_minute` | number | Tidak | Denda keterlambatan per menit dalam Rupiah (default: 0) |
| `absence_deduction_per_day` | number | Tidak | Potongan per hari tidak hadir dalam Rupiah (default: 0) |
| `kpi_bonus_enabled` | boolean | Tidak | Apakah bonus KPI diaktifkan (default: true) |
| `kpi_bonus_formula` | object | Tidak | Persentase bonus KPI berdasarkan rating |
| `kpi_bonus_formula.outstanding` | number | — | Bonus persentase untuk rating outstanding (default: 20%) |
| `kpi_bonus_formula.exceeds` | number | — | Bonus persentase untuk rating exceeds (default: 15%) |
| `kpi_bonus_formula.meets` | number | — | Bonus persentase untuk rating meets (default: 10%) |
| `kpi_bonus_formula.needs_improvement` | number | — | Bonus persentase untuk rating needs improvement (default: 5%) |
| `kpi_bonus_formula.unsatisfactory` | number | — | Bonus persentase untuk rating unsatisfactory (default: 0%) |
| `tax_formula` | object | Tidak | Konfigurasi perhitungan PPh 21 |
| `tax_formula.ptkp` | number | — | Penghasilan Tidak Kena Pajak tahunan (default: 54.000.000) |
| `tax_formula.brackets` | array\[object] | — | Daftar bracket tarif pajak progresif `{min, max, rate}` |
| `bpjs_kesehatan` | object | Tidak | Kontribusi BPJS Kesehatan (persentase) |
| `bpjs_kesehatan.company_contribution` | number | — | Bagian perusahaan (default: 4%) |
| `bpjs_kesehatan.employee_contribution` | number | — | Bagian karyawan (default: 1%) |
| `bpjs_ketenagakerjaan` | object | Tidak | Kontribusi BPJS Ketenagakerjaan (persentase) |
| `bpjs_ketenagakerjaan.jht_company` | number | — | JHT bagian perusahaan (default: 3.7%) |
| `bpjs_ketenagakerjaan.jht_employee` | number | — | JHT bagian karyawan (default: 2%) |
| `bpjs_ketenagakerjaan.jkk` | number | — | Jaminan Kecelakaan Kerja (default: 0.24%) |
| `bpjs_ketenagakerjaan.jkm` | number | — | Jaminan Kematian (default: 0.3%) |
| `bpjs_ketenagakerjaan.jp_company` | number | — | Jaminan Pensiun bagian perusahaan (default: 2%) |
| `bpjs_ketenagakerjaan.jp_employee` | number | — | Jaminan Pensiun bagian karyawan (default: 1%) |
| `auto_generate` | boolean | Tidak | Otomatis generate payroll di akhir bulan (default: false) |
| `payment_date` | number | Tidak | Tanggal pembayaran per bulan, 1-31 (default: 25) |

### PayslipTemplate — Template Slip Gaji

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `template_name` | string | Ya | Nama template slip gaji |
| `description` | string | Tidak | Penjelasan tampilan dan komponen slip gaji (maks 1000 karakter) |
| `logo_url` | string | Tidak | URL logo perusahaan pada slip |
| `header_html` | string | Tidak | HTML kustom untuk bagian header |
| `footer_html` | string | Tidak | HTML kustom untuk bagian footer |
| `show_company_info` | boolean | Tidak | Tampilkan informasi perusahaan (default: true) |
| `show_breakdown` | boolean | Tidak | Tampilkan rincian komponen gaji (default: true) |
| `show_attendance_summary` | boolean | Tidak | Tampilkan ringkasan kehadiran (default: true) |
| `show_kpi_score` | boolean | Tidak | Tampilkan skor KPI (default: true) |
| `is_default` | boolean | Tidak | Apakah template ini dipakai secara default (default: false) |

### CompanyAttendanceSettings — Pengaturan Absensi Perusahaan

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `description` | string | Tidak | Penjelasan kebijakan absensi dan jam kerja (maks 1000 karakter) |
| `office_location` | object | Tidak | Lokasi kantor utama |
| `office_location.latitude` | number | — | Garis lintang kantor |
| `office_location.longitude` | number | — | Garis bujur kantor |
| `office_location.address` | string | — | Alamat teks kantor |
| `office_radius_meters` | number | Tidak | Radius geofence kantor dalam meter (default: 100) |
| `require_office_location` | boolean | Tidak | Wajib absen dari lokasi kantor (default: false) |
| `default_accuracy_tolerance_meters` | number | Tidak | Batas toleransi akurasi GPS dalam meter (default: 150) |
| `locations` | array\[object] | Tidak | Daftar multi-lokasi geofence absensi |
| `locations[].location_id` | string | — | ID unik area lokasi absensi |
| `locations[].name` | string | — | Nama lokasi (misal: Head Office, Outlet 2) |
| `locations[].address` | string | — | Alamat fisik lokasi |
| `locations[].latitude` | number | — | Garis lintang pusat geofence |
| `locations[].longitude` | number | — | Garis bujur pusat geofence |
| `locations[].radius_meters` | number | — | Radius geofence dalam meter (default: 100) |
| `locations[].accuracy_tolerance_meters` | number | — | Toleransi akurasi GPS maksimum (default: 150) |
| `locations[].assigned_employee_ids` | array\[string] | — | Daftar ID karyawan yang ditugaskan ke lokasi ini |
| `locations[].is_active` | boolean | — | Status aktif lokasi geofence (default: true) |
| `working_hours` | object | Tidak | Pengaturan jam kerja standar |
| `working_hours.start_time` | string | — | Jam mulai kerja (default: `09:00`) |
| `working_hours.end_time` | string | — | Jam selesai kerja (default: `17:00`) |
| `working_hours.break_duration_minutes` | number | — | Durasi istirahat dalam menit (default: 60) |
| `shifts` | array\[object] | Tidak | Daftar shift kerja yang tersedia |
| `shifts[].shift_id` | string | — | ID unik shift |
| `shifts[].shift_name` | string | — | Nama shift (misal: Shift Pagi) |
| `shifts[].start_time` | string | — | Jam mulai shift (HH:MM) |
| `shifts[].end_time` | string | — | Jam selesai shift (HH:MM) |
| `shifts[].break_duration_minutes` | number | — | Durasi istirahat shift dalam menit (default: 60) |
| `overtime_settings` | object | Tidak | Pengaturan lembur |
| `overtime_settings.enabled` | boolean | — | Apakah lembur diaktifkan (default: true) |
| `overtime_settings.start_after_hours` | number | — | Lembur dimulai setelah X jam kerja (default: 8) |
| `overtime_settings.max_overtime_hours_per_day` | number | — | Maksimal jam lembur per hari (default: 4) |
| `late_tolerance_minutes` | number | Tidak | Toleransi keterlambatan dalam menit (default: 15) |
| `working_days` | array\[number] | Tidak | Hari kerja, 1=Senin sampai 7=Minggu (default: \[1,2,3,4,5]) |
| `auto_clock_out_enabled` | boolean | Tidak | Otomatis clock-out jika karyawan lupa (default: false) |
| `auto_clock_out_time` | string | Tidak | Jam auto clock-out (default: `18:00`) |
| `require_photo` | boolean | Tidak | Wajib foto selfie saat absen (default: true) |
| `require_notes` | boolean | Tidak | Wajib catatan saat absen (default: false) |

### ApprovalWorkflow — Definisi Alur Persetujuan

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan pemilik alur persetujuan |
| `workflow_name` | string | Ya | Nama alur persetujuan (misal: "Approval Cuti 2 Level") |
| `description` | string | Tidak | Penjelasan proses & aturan persetujuan yang diterapkan workflow ini (maks 1000 karakter) |
| `document_type` | string | Ya | Tipe dokumen yang memerlukan persetujuan — `expense_request`, `leave_request`, `purchase_order`, `discount_request`, atau `budget_adjustment` |
| `approval_levels` | array\[object] | Ya | Daftar tingkat persetujuan berjenjang |
| `approval_levels[].level` | number | — | Urutan tingkat persetujuan (1, 2, 3, dst) |
| `approval_levels[].approver_role` | string | — | Role yang dapat melakukan persetujuan di level ini |
| `approval_levels[].approver_ids` | array\[string] | — | User ID spesifik sebagai approver (opsional) |
| `approval_levels[].required_approvals` | number | — | Jumlah persetujuan yang dibutuhkan di level ini (default: 1) |
| `approval_levels[].parallel_approval` | boolean | — | Apakah persetujuan level ini dapat berjalan paralel (default: false) |
| `amount_limits` | object | Tidak | Limit nominal untuk setiap level |
| `amount_limits.level_1` | number | — | Ambang nominal level 1 |
| `amount_limits.level_2` | number | — | Ambang nominal level 2 |
| `amount_limits.level_3` | number | — | Ambang nominal level 3 |
| `is_active` | boolean | Tidak | Apakah workflow dipakai untuk pengajuan baru (default: true) |

**Kondisi RLS** — Aturan `create`/`read`/`update`/`delete` didefinisikan kosong (`{}`) pada entitas ini; kontrol akses ditegakkan di lapisan server-function, bukan di level entitas.

### ApprovalRequest — Instance Pengajuan Persetujuan

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `workflow_id` | string | Ya | ID `ApprovalWorkflow` yang dipakai (FK) |
| `document_type` | string | Ya | Tipe dokumen yang diajukan persetujuannya |
| `document_id` | string | Ya | ID dokumen source, misal ID `CompanyLeave` (polymorphic reference) |
| `requester_id` | string | Ya | User yang mengajukan persetujuan |
| `submission_date` | datetime | Tidak | Waktu pengajuan dikirim ke approver |
| `amount` | number | Tidak | Nominal terkait (jika ada), dipakai untuk mengevaluasi `amount_limits` |
| `description` | string | Tidak | Ringkasan / alasan pengajuan |
| `current_approval_level` | number | Tidak | Tingkat persetujuan yang sedang berjalan (default: 1) |
| `approval_history` | array\[object] | Tidak | Riwayat keputusan di setiap level |
| `approval_history[].level` | number | — | Tingkat persetujuan yang diputuskan |
| `approval_history[].approver_id` | string | — | ID approver yang memutuskan |
| `approval_history[].approval_date` | datetime | — | Waktu keputusan diambil |
| `approval_history[].status` | string | — | Keputusan level — `approved`, `rejected`, atau `pending` |
| `approval_history[].comments` | string | — | Catatan approver (wajib diisi saat menolak) |
| `overall_status` | string | Tidak | Status akhir pengajuan — `pending`, `approved`, `rejected`, atau `on_hold` (default: `pending`) |
| `final_approval_date` | datetime | Tidak | Waktu seluruh tingkat persetujuan selesai |

**Kondisi RLS** — Sama seperti `ApprovalWorkflow`, aturan RLS kosong (`{}`); validasi approver dilakukan oleh server-function sebelum status berubah.

***

## State Diagrams — Siklus Hidup Data HR

### Siklus Hidup Status Karyawan

```mermaid theme={null}
stateDiagram-v2
    [*] --> Active: Karyawan baru bergabung
    Active --> OnLeave: Pengajuan cuti disetujui
    OnLeave --> Active: Cuti selesai, kembali kerja
    Active --> Terminated: PHK atau kontrak berakhir
    OnLeave --> Terminated: PHK selama cuti
    Terminated --> [*]
```

### Siklus Hidup Kehadiran Harian

```mermaid theme={null}
stateDiagram-v2
    [*] --> BelumAbsen: Awal hari kerja
    BelumAbsen --> Present: Clock-in tepat waktu
    BelumAbsen --> Late: Clock-in melewati toleransi
    BelumAbsen --> Sick: Karyawan input status sakit
    BelumAbsen --> Leave: Status cuti sudah disetujui
    BelumAbsen --> Absent: Tidak ada clock-in & tidak ada keterangan
    BelumAbsen --> WFH: Work from home disetujui
    Present --> Present: Clock-out tercatat
    Late --> Late: Clock-out tercatat
    WFH --> WFH: Clock-out tercatat
    Present --> KoreksiManual: Disetujui override
    Late --> KoreksiManual: Disetujui override
    Absent --> KoreksiManual: Disetujui override
    KoreksiManual --> Present: Status dikoreksi
    Present --> [*]
    Late --> [*]
    Sick --> [*]
    Leave --> [*]
    Absent --> [*]
    WFH --> [*]
```

### Siklus Hidup Pengajuan Cuti

```mermaid theme={null}
stateDiagram-v2
    [*] --> Pending: Karyawan submit pengajuan
    Pending --> Approved: Manager/HR approve
    Pending --> Rejected: Manager/HR reject dengan catatan
    Approved --> Cancelled: Karyawan membatalkan cuti
    Rejected --> Pending: Karyawan revisi & re-submit
    Approved --> [*]: Cuti selesai
    Cancelled --> [*]
    Rejected --> [*]
```

### Siklus Hidup Payroll Bulanan

```mermaid theme={null}
stateDiagram-v2
    [*] --> Draft: Payroll di-generate sistem
    Draft --> Draft: HR melakukan penyesuaian
    Draft --> Approved: HR Manager menyetujui
    Approved --> Paid: Pembayaran diproses & tercatat
    Paid --> [*]: Periode selesai
    Draft --> [*]: Dibatalkan sebelum approval
```

### Siklus Hidup Rekrutmen

```mermaid theme={null}
stateDiagram-v2
    [*] --> Submitted: Lamaran masuk dari portal
    Submitted --> Screening: HR mulai review CV
    Screening --> InterviewScheduled: Lolos screening, jadwal interview
    Screening --> Rejected: Tidak memenuhi kualifikasi
    InterviewScheduled --> InterviewCompleted: Interview selesai
    InterviewScheduled --> Rejected: Tidak lolos interview
    InterviewCompleted --> Accepted: Diterima sebagai karyawan
    InterviewCompleted --> Rejected: Tidak lolos tahap akhir
    Accepted --> [*]: Onboarding dimulai
    Rejected --> [*]
```

### Siklus Hidup Penilaian KPI

```mermaid theme={null}
stateDiagram-v2
    [*] --> Draft: KPI dibuat oleh reviewer atau auto-generated
    Draft --> Submitted: Karyawan mengisi self-assessment
    Submitted --> Reviewed: Reviewer memberikan penilaian
    Reviewed --> Finalized: Hasil KPI difinalisasi
    Finalized --> [*]: Skor masuk perhitungan payroll
    Draft --> [*]: Dibatalkan
```

### Siklus Hidup Performance Review (`PerformanceReview.status`)

Review performa memiliki siklus 4 tahap dengan satu gerbang wajib: acknowledge dari karyawan sebelum hasil bisa difinalisasi.

```mermaid theme={null}
stateDiagram-v2
    [*] --> Draft: Reviewer membuka form review
    Draft --> Draft: Perbaiki skor kompetensi & capaian KPI
    Draft --> Submitted: Reviewer mengirim hasil penilaian
    Submitted --> Acknowledged: Employee menekan Acknowledge
    Submitted --> Draft: Employee mengajukan keberatan, reviewer revisi
    Acknowledged --> Finalized: HR memfinalisasi penilaian
    Finalized --> [*]: Rating & overall_score masuk data payroll/bonus
    Draft --> [*]: Review dibatalkan sebelum dikirim

    note right of Submitted
        Field employee_comments dapat diisi
        karyawan saat review masih Submitted
    end note
    note right of Finalized
        overall_score dan rating bersifat read-only
        setelah Finalized
    end note
```

### Siklus Hidup Surat Penawaran Kerja (`OfferLetter.status`)

```mermaid theme={null}
stateDiagram-v2
    [*] --> Draft: HR menyusun penawaran
    Draft --> Sent: Surat penawaran dikirim ke pelamar
    Sent --> Accepted: Pelamar menandatangani sebelum expiration_date
    Sent --> Declined: Pelamar menolak penawaran
    Sent --> Expired: expiration_date terlewati tanpa respons
    Expired --> Sent: HR memperpanjang masa berlaku
    Accepted --> [*]: Applicant.current_status menjadi hired, Employee dibuat
    Declined --> [*]

    note right of Accepted
        acceptance_status ikut berubah menjadi
        accepted dan response_date terisi
    end note
```

### Siklus Hidup Pengajuan Persetujuan (`ApprovalRequest.overall_status`)

```mermaid theme={null}
stateDiagram-v2
    [*] --> Pending: Requester mengajukan dokumen
    Pending --> OnHold: Approver meminta perbaikan/data tambahan
    OnHold --> Pending: Requester melengkapi dokumen
    Pending --> Approved: Semua approval_levels menyetujui
    Pending --> Rejected: Salah satu level menolak
    Approved --> [*]: Dokumen source (misal cuti) otomatis di-update
    Rejected --> [*]

    state Pending {
        [*] --> Level1
        Level1 --> Level2: required_approvals level 1 terpenuhi
        Level2 --> LevelN: required_approvals level 2 terpenuhi
    }
```

***

## Sequence Diagrams — Alur Data Utama

### 1. Alur Check-in Absensi dengan Verifikasi GPS & Foto

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan
    participant App as Mobile App
    participant GPS as Geolocation API
    participant CAM as Camera API
    participant CLD as Cloudinary
    participant CAS as CompanyAttendanceSettings
    participant DB as Database

    K->>App: Klik tombol "Check-in"
    App->>GPS: Request lokasi terkini
    GPS-->>App: {latitude, longitude, accuracy}
    App->>CAS: Validasi lokasi terhadap geofence
    CAS-->>App: Status: dalam/t luar radius
    alt Lokasi di luar radius
        App-->>K: Tampilkan peringatan "Di luar area absensi"
        K->>App: Tetap lanjutkan dengan catatan
    end
    App->>CAM: Buka kamera untuk foto selfie
    K->>CAM: Ambil foto selfie
    CAM-->>App: Image blob
    App->>App: Compress gambar via Canvas API
    App->>CLD: Upload foto selfie
    CLD-->>App: clock_in_photo_url
    App->>DB: Simpan CompanyAttendance
    Note over DB: clock_in_time, clock_in_location,<br/>clock_in_photo_url, status, shift_id
    DB-->>App: Konfirmasi penyimpanan
    App-->>K: Tampilkan "Check-in berhasil"
```

### 2. Alur Pengajuan Cuti dengan Approval Workflow

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan
    participant App as Frontend App
    participant DB as Database
    participant WA as WhatsApp Notification
    participant M as Manager/HR

    K->>App: Isi form pengajuan cuti
    Note over K,App: Pilih leave_type, start_date,<br/>end_date, reason, attachment
    App->>App: Hitung total_days otomatis
    App->>DB: Simpan CompanyLeave (status: pending)
    DB-->>App: Konfirmasi pengajuan
    App->>WA: Kirim notifikasi ke Manager
    WA-->>M: "Pengajuan cuti baru dari [nama karyawan]"
    M->>App: Buka detail pengajuan cuti
    App->>DB: Fetch CompanyLeave by ID
    DB-->>App: Data pengajuan cuti
    alt Manager Approve
        M->>App: Klik "Approve" + isi approver_notes
        App->>DB: Update status → approved, set approved_at
        DB-->>App: Update berhasil
        App->>WA: Notifikasi ke karyawan
        WA-->>K: "Cuti Anda telah disetujui"
    else Manager Reject
        M->>App: Klik "Reject" + isi alasan penolakan
        App->>DB: Update status → rejected, set approver_notes
        DB-->>App: Update berhasil
        App->>WA: Notifikasi ke karyawan
        WA-->>K: "Cuti Anda ditolak. Alasan: [catatan]"
    end
```

### 3. Alur Pemrosesan Payroll Bulanan

```mermaid theme={null}
sequenceDiagram
    participant HR as HR Manager
    participant App as Frontend App
    participant ATT as CompanyAttendance
    participant LV as CompanyLeave
    participant KPI as CompanyKPI
    participant CFG as PayrollConfiguration
    participant EMP as Employee
    participant DB as Database
    participant FIN as Finance Module

    HR->>App: Pilih periode payroll (YYYY-MM)
    App->>CFG: Fetch konfigurasi penggajian
    CFG-->>App: Formula, rate, bracket pajak, BPJS
    App->>ATT: Fetch rekap absensi periode ini
    ATT-->>App: attendance_days, late_count, overtime_hours
    App->>LV: Fetch rekap cuti periode ini
    LV-->>App: unpaid_days, sick_days
    App->>KPI: Fetch skor KPI periode ini
    KPI-->>App: overall_score, rating
    App->>EMP: Fetch data gaji pokok karyawan
    EMP-->>App: basic_salary, bank_account
    App->>App: Hitung komponen payroll
    Note over App: gross = basic + allowances<br/>+ overtime_pay + kpi_bonus<br/>net = gross - deductions - BPJS - pajak
    App->>DB: Simpan CompanyPayroll (status: draft)
    DB-->>App: Daftar slip gaji draft
    HR->>App: Review & approve slip gaji
    App->>DB: Update status → approved
    HR->>App: Proses pembayaran
    App->>DB: Update status → paid, set payment_date
    App->>FIN: Generate journal entry
    FIN-->>App: Jurnal gaji tercatat
```

### 4. Alur Rekrutmen dari Lamaran hingga Interview

```mermaid theme={null}
sequenceDiagram
    participant P as Pelamar
    participant Portal as Career Portal
    participant DB as Database
    participant HR as HR Staff
    participant WA as WhatsApp
    participant INT as Interviewer

    P->>Portal: Upload CV & isi form lamaran
    Note over P,Portal: applicant_name, email, phone,<br/>cv_url, cover_letter, skills
    Portal->>DB: Simpan JobApplication (status: submitted)
    DB-->>Portal: Lamaran terkirim
    Portal-->>P: Konfirmasi lamaran berhasil
    HR->>DB: Fetch daftar lamaran baru
    DB-->>HR: List JobApplication status submitted
    HR->>DB: Review CV & update status → screening
    alt Lolos Screening
        HR->>DB: Update status → interview_scheduled
        HR->>DB: Buat InterviewSchedule
        Note over DB: interview_type, interview_date,<br/>interview_location, interviewer_ids
        HR->>WA: Kirim link interview ke pelamar
        WA-->>P: "Interview dijadwalkan pada [tanggal]"
        INT->>DB: Input feedback & rating setelah interview
        DB-->>HR: Hasil interview tercatat
        HR->>DB: Update InterviewSchedule status → completed
        HR->>DB: Update JobApplication → accepted / rejected
    else Tidak Lolos
        HR->>DB: Update status → rejected, isi rejection_reason
    end
```

### 5. Alur Onboarding Karyawan Baru (Offer Accepted → Aktif)

Diagram berikut menggambarkan transisi kandidat yang menerima penawaran menjadi karyawan aktif beserta provisioning akses, pengaturan absensi, dan KPI awal.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant HR as HR Staff
    participant DB as Database
    participant AUTH as Base44 Auth/User
    participant CFG as CompanyAttendanceSettings
    participant TM as KPITemplate
    participant FIN as Finance Module
    participant EMP as Karyawan Baru

    Note over HR: OfferLetter status = accepted,<br/>Applicant.current_status = hired
    HR->>DB: Buat Employee (employee_id, department, position, hire_date)
    HR->>DB: Isi bank_account & emergency_contact
    DB-->>AUTH: Undangan akun & pengaturan role HR/Staff
    AUTH->>EMP: Email aktivasi akun + kredensial awal
    EMP->>AUTH: Login pertama & ganti password
    AUTH-->>DB: employee.user_id di-link ke akun baru
    HR->>CFG: Tugaskan shift & location_id karyawan
    CFG-->>DB: locations[].assigned_employee_ids diperbarui
    HR->>TM: Terapkan template KPI masa percobaan
    TM-->>DB: CompanyKPI periode probation dibuat (status draft)
    HR->>DB: PerformanceReview (review_type = probation) dijadwalkan
    HR->>FIN: Sinkronkan gaji pokok ke PayrollConfiguration
    FIN-->>DB: PayrollConfiguration basic_salary & formula tersimpan
    EMP->>DB: Clock-in pertama (CompanyAttendance)
    Note over DB: employee.status = active,<br/> masa percobaan berjalan
```

**Catatan implementasi**

* Pembuatan `Employee` dan provisioning akun berjalan dalam satu transaksi logis; jika salah satu gagal, status `Applicant.current_status` tetap `offer_extended` agar dapat diulang.
* `hire_date` menjadi dasar perhitungan masa percobaan (`review_type = probation`) dan prorasi gaji bulan pertama pada `CompanyPayroll`.
* Penugasan `location_id` pada `CompanyAttendanceSettings.locations[].assigned_employee_ids` menentukan geofence mana yang valid untuk karyawan tersebut.

### 6. Alur Performance Review & KPI (Self-assessment → Finalized)

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant EMP as Karyawan
    participant RV as Reviewer/Atasan
    participant HR as HR Manager
    participant APP as Frontend App
    participant KPI as CompanyKPI
    participant PR as PerformanceReview
    participant DB as Database
    participant PAY as Payroll

    HR->>PR: Buka periode review (review_period, review_type)
    PR->>KPI: Ambil skor KPI periode berjalan
    KPI-->>PR: overall_score & achievement_percentage per metrik
    EMP->>APP: Isi self-assessment & employee_comments
    APP->>DB: Simpan PerformanceReview status = draft
    RV->>APP: Isi skor competencies[] (1-5) & catatan
    RV->>APP: Hitung overall_score berbobot + rating
    APP->>DB: Update PerformanceReview status = submitted
    DB-->>EMP: Notifikasi review menunggu acknowledge
    alt Karyawan setuju
        EMP->>APP: Tekan Acknowledge
        APP->>DB: employee_acknowledged = true, acknowledged_at terisi
        DB->>PR: status = acknowledged
    else Karyawan mengajukan keberatan
        EMP->>APP: Kirim employee_comments keberatan
        APP->>DB: status kembali ke draft
        RV->>APP: Revisi skor lalu submit ulang
    end
    HR->>DB: Finalisasi (status = finalized, field read-only)
    DB-->>PAY: rating & kpi_bonus periode dipakai perhitungan gaji
    HR->>PR: Turunkan development_plan ke Training
```

**Catatan implementasi**

* `overall_score` dihitung dari gabungan `competencies[].score` dan `kpi_achievement[].achievement_percentage` memakai `weight` masing-masing; hasilnya menentukan `rating`.
* Status `finalized` mengunci dokumen. Koreksi setelah finalisasi hanya dapat dilakukan Super Admin/HR Manager dan tercatat sebagai review baru.
* `development_plan[]` yang disetujui berubah menjadi entri `Training` agar progres pelatihan dapat dipantau.

***

## Enum Reference Tables — Tabel Referensi Enum

Berikut adalah seluruh nilai enum yang digunakan pada field-field entitas HR.

### Employee Status (`Employee.status`)

| Nilai | Deskripsi |
| - | - |
| `active` | Karyawan aktif bekerja |
| `on_leave` | Karyawan sedang menjalani cuti |
| `terminated` | Karyawan telah diberhentikan |

### Employment Type (`Employee.employment_type`, `JobPosting.employment_type`)

| Nilai | Deskripsi |
| - | - |
| `full_time` | Karyawan tetap penuh waktu |
| `part_time` | Karyawan paruh waktu |
| `contract` | Karyawan berbasis kontrak |
| `intern` / `internship` | Magang |

### Department (`Employee.department`)

| Nilai | Deskripsi |
| - | - |
| `Management` | Departemen manajemen |
| `Sales` | Departemen penjualan |
| `Marketing` | Departemen pemasaran |
| `Operations` | Departemen operasional |
| `Finance` | Departemen keuangan |
| `IT` | Departemen teknologi informasi |
| `HR` | Departemen sumber daya manusia |
| `Customer Service` | Departemen layanan pelanggan |

### Attendance Status (`AttendanceRecord.status`, `CompanyAttendance.status`)

| Nilai | Deskripsi |
| - | - |
| `present` | Hadir tepat waktu |
| `late` | Hadir terlambat |
| `absent` | Tidak hadir tanpa keterangan |
| `sick` | Sakit |
| `leave` | Cuti |
| `wfh` | Bekerja dari rumah (khusus CompanyAttendance) |

### Leave Type (`Leave.leave_type`, `CompanyLeave.leave_type`)

| Nilai | Deskripsi |
| - | - |
| `annual` | Cuti tahunan |
| `sick` | Cuti sakit |
| `unpaid` | Cuti tanpa gaji |
| `maternity` | Cuti melahirkan |
| `paternity` | Cuti melahirkan (ayah) |
| `emergency` | Cuti darurat |

### Leave Status (`Leave.status`, `CompanyLeave.status`)

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

### Payroll Status (`Payroll.status`, `CompanyPayroll.status`)

| Nilai | Deskripsi |
| - | - |
| `draft` | Slip gaji masih dalam tahap pembuatan |
| `approved` | Slip gaji telah disetujui HR Manager |
| `paid` | Gaji telah dibayarkan ke karyawan |

### Payment Method (`Payroll.payment_method`, `CompanyPayroll.payment_method`)

| Nilai | Deskripsi |
| - | - |
| `bank_transfer` | Transfer bank |
| `cash` | Tunai |
| `check` | Cek/giro |

### Basic Salary Formula (`PayrollConfiguration.basic_salary_formula`)

| Nilai | Deskripsi |
| - | - |
| `fixed` | Gaji tetap bulanan |
| `hourly` | Dihitung berdasarkan jam kerja |
| `daily` | Dihitung berdasarkan hari kerja |

### Job Posting Status (`JobPosting.status`)

| Nilai | Deskripsi |
| - | - |
| `open` | Lowongan aktif menerima lamaran |
| `closed` | Lowongan telah ditutup |
| `on_hold` | Lowongan ditunda sementara |

### Job Application Status (`JobApplication.status`)

| Nilai | Deskripsi |
| - | - |
| `submitted` | Lamaran baru masuk |
| `screening` | Sedang dalam tahap review CV |
| `interview_scheduled` | Interview telah dijadwalkan |
| `interview_completed` | Interview telah selesai dilaksanakan |
| `accepted` | Pelamar diterima |
| `rejected` | Pelamar ditolak |

### Interview Type (`InterviewSchedule.interview_type`)

| Nilai | Deskripsi |
| - | - |
| `phone_screening` | Screening awal via telepon |
| `first_round` | Interview putaran pertama |
| `second_round` | Interview putaran kedua |
| `final_round` | Interview putaran akhir |
| `hr_round` | Interview khusus dengan tim HR |

### Interview Status (`InterviewSchedule.status`)

| Nilai | Deskripsi |
| - | - |
| `scheduled` | Interview telah dijadwalkan |
| `completed` | Interview telah selesai |
| `cancelled` | Interview dibatalkan |

### Interview Recommendation (`InterviewSchedule.recommendation`)

| Nilai | Deskripsi |
| - | - |
| `pass` | Kandidat lolos ke tahap berikutnya |
| `reject` | Kandidat tidak lolos |
| `undecided` | Belum ada keputusan |

### Training Type (`Training.training_type`)

| Nilai | Deskripsi |
| - | - |
| `internal` | Pelatihan internal perusahaan |
| `external` | Pelatihan dari penyedia eksternal |
| `online` | Pelatihan daring/online |
| `certification` | Program sertifikasi profesi |
| `workshop` | Workshop atau seminar |

### Training Status (`Training.status`)

| Nilai | Deskripsi |
| - | - |
| `planned` | Pelatihan masih dalam perencanaan |
| `ongoing` | Pelatihan sedang berlangsung |
| `completed` | Pelatihan telah selesai |
| `cancelled` | Pelatihan dibatalkan |

### Training Participant Attendance (`Training.participants[].attendance_status`)

| Nilai | Deskripsi |
| - | - |
| `registered` | Terdaftar sebagai peserta |
| `attended` | Hadir mengikuti pelatihan |
| `absent` | Tidak hadir |
| `completed` | Menyelesaikan pelatihan |

### KPI Rating (`KPI.rating`, `CompanyKPI.rating`)

| Nilai | Deskripsi |
| - | - |
| `outstanding` | Luar biasa — pencapaian jauh melebihi target |
| `exceeds` | Melebihi ekspektasi — pencapaian di atas target |
| `meets` | Sesuai ekspektasi — pencapaian sesuai target |
| `needs_improvement` | Perlu perbaikan — pencapaian di bawah target |
| `unsatisfactory` | Tidak memuaskan — pencapaian sangat rendah |

### KPI Status (`KPI.status`, `CompanyKPI.status`)

| Nilai | Deskripsi |
| - | - |
| `draft` | KPI masih dalam tahap pembuatan |
| `submitted` | KPI telah disubmit oleh karyawan |
| `reviewed` | KPI telah direview oleh atasan |
| `finalized` | KPI telah difinalisasi dan tidak dapat diubah |

### KPI Calculation Method (`KPITemplate.default_metrics[].calculation_method`)

| Nilai | Deskripsi |
| - | - |
| `manual` | Input manual oleh reviewer |
| `auto_from_attendance` | Otomatis dari data absensi |
| `auto_from_sales` | Otomatis dari data penjualan |
| `auto_from_projects` | Otomatis dari data proyek |

### KPI Aggregation Method (`KPITemplate.default_metrics[].auto_source.aggregation`)

| Nilai | Deskripsi |
| - | - |
| `sum` | Total penjumlahan |
| `count` | Jumlah entri |
| `average` | Rata-rata |
| `percentage` | Persentase pencapaian |

### KPI Calculation Frequency (`KPITemplate.calculation_frequency`)

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

### Time Entry Status (`TimeEntry.status`)

| Nilai | Deskripsi |
| - | - |
| `submitted` | Entri waktu telah disubmit |
| `approved` | Entri waktu telah disetujui |
| `rejected` | Entri waktu ditolak |

### Applicant Pipeline Status (`Applicant.current_status`)

| Nilai | Deskripsi |
| - | - |
| `new` | Lamaran baru masuk, belum direview (default) |
| `screening` | HR sedang menyeleksi CV dan portofolio |
| `interview_scheduled` | Kandidat lolos screening dan interview dijadwalkan |
| `interviewed` | Rangkaian interview selesai, menunggu keputusan |
| `offer_extended` | Surat penawaran kerja (`OfferLetter`) telah dikirim |
| `hired` | Kandidat menerima penawaran dan menjadi karyawan |
| `rejected` | Kandidat tidak dilanjutkan pada proses rekrutmen |

### Offer Letter Status (`OfferLetter.status`)

| Nilai | Deskripsi |
| - | - |
| `draft` | Surat penawaran masih disusun HR, belum dikirim (default) |
| `sent` | Surat penawaran sudah dikirim ke kandidat |
| `accepted` | Kandidat menerima dan menandatangani penawaran |
| `declined` | Kandidat menolak penawaran kerja |
| `expired` | Batas `expiration_date` terlewati tanpa respons kandidat |

### Offer Acceptance Status (`OfferLetter.acceptance_status`)

| Nilai | Deskripsi |
| - | - |
| `pending` | Keputusan kandidat belum diterima |
| `accepted` | Kandidat secara eksplisit menerima penawaran |
| `declined` | Kandidat secara eksplisit menolak penawaran |

### Offer Employment Type (`OfferLetter.employment_type`)

Himpitan dari enum `Employee.employment_type` — tawaran kerja belum mendukung status magang.

| Nilai | Deskripsi |
| - | - |
| `full_time` | Kerja penuh waktu, tetap (default) |
| `part_time` | Kerja paruh waktu / tidak tetap |
| `contract` | Kerja dengan kontrak berjangka (PKWT) |

### Performance Review Type (`PerformanceReview.review_type`)

| Nilai | Deskripsi |
| - | - |
| `probation` | Penilaian masa percobaan karyawan baru |
| `quarterly` | Penilaian rutin per kuartal (default) |
| `mid_year` | Penilaian tengah tahun |
| `annual` | Penilaian tahunan untuk keputusan promosi/kenaikan gaji |

### Performance Review Rating (`PerformanceReview.rating`)

| Nilai | Rentang Skor | Deskripsi |
| - | - | - |
| `outstanding` | 91–100 | Performa jauh melampaui ekspektasi |
| `exceeds_expectations` | 81–90 | Performa melampaui ekspektasi |
| `meets_expectations` | 71–80 | Performa sesuai target yang ditetapkan |
| `needs_improvement` | 51–70 | Sebagian target tidak tercapai, perlu pendampingan |
| `unsatisfactory` | 0–50 | Performa di bawah standar, masuk program perbaikan serius |

### Performance Review Status (`PerformanceReview.status`)

| Nilai | Deskripsi |
| - | - |
| `draft` | Review sedang disusun reviewer, skor boleh diubah bebas |
| `submitted` | Hasil penilaian dikirim ke karyawan untuk di-read |
| `acknowledged` | Karyawan telah acknowledge hasil review |
| `finalized` | HR memfinalisasi, dokumen menjadi read-only |

### Approval Workflow Document Type (`ApprovalWorkflow.document_type`)

| Nilai | Deskripsi |
| - | - |
| `expense_request` | Pengajuan reimbursement / klaim biaya |
| `leave_request` | Pengajuan cuti karyawan |
| `purchase_order` | Purchase order pembelian |
| `discount_request` | Permintaan diskon di atas batas wewenang kasir |
| `budget_adjustment` | Perubahan alokasi anggaran yang sudah ditetapkan |

### Approval Request Status (`ApprovalRequest.overall_status`)

| Nilai | Deskripsi |
| - | - |
| `pending` | Pengajuan menunggu keputusan approver pada level aktif (default) |
| `approved` | Seluruh tingkat persetujuan telah menyetujui |
| `rejected` | Salah satu tingkat menolak, pengajuan berhenti |
| `on_hold` | Approver menahan pengajuan, data tambahan diminta requester |

### Approval History Level Status (`ApprovalRequest.approval_history[].status`)

| Nilai | Deskripsi |
| - | - |
| `pending` | Level tersebut belum diputuskan oleh approver |
| `approved` | Level tersebut telah disetujui |
| `rejected` | Level tersebut ditolak beserta catatan alasan |

### Kode Hari Kerja (`CompanyAttendanceSettings.working_days[]`)

| Nilai | Deskripsi |
| - | - |
| `1` | Senin |
| `2` | Selasa |
| `3` | Rabu |
| `4` | Kamis |
| `5` | Jumat |
| `6` | Sabtu |
| `7` | Minggu |

> Default `working_days` adalah `[1,2,3,4,5]` (Senin–Jumat). Nilai di luar array dianggap hari libur sehingga tidak dihitung sebagai `working_days` pada perhitungan prorasi `CompanyPayroll`.

***

## Hak Akses RBAC — Modul HR

Tabel berikut merinci hak akses setiap peran terhadap entitas dan fitur dalam modul HR.

| Peran | Employee | Attendance | Leave | Payroll | JobPosting | JobApplication | Interview | Training | KPI |
| - | - | - | - | - | - | - | - | - | - |
| **Super Admin** | Full | Full | Full | Full | Full | Full | Full | Full | Full |
| **Owner** | Full | Full | Full | Full | Full | Full | Full | Full | Full |
| **HR Manager** | Full | Full | Full | Read + Approve | Full | Full | Full | Full | Full |
| **HR Staff** | CRUD | CRUD | CRUD | No Access | CRUD | CRUD | CRUD | CRUD | Read |
| **Manager** | Read (tim) | Read (tim) | Approve (tim) | Read (tim) | Read | Read | Read + Feedback | Read (tim) | Read + Review (tim) |
| **Supervisor** | Read (tim) | Read (tim) | Approve (tim) | No Access | Read | Read | Read | Read (tim) | Read (tim) |
| **Employee** | Read (own) | CRUD (own) | CRUD (own) | Read (own) | Read | Create | Read (own) | Read (own) | Read (own) |

### Keterangan Hak Akses RBAC

| Level | Deskripsi |
| - | - |
| **Full** | Akses penuh — Create, Read, Update, Delete, dan Approve |
| **CRUD** | Create, Read, Update, Delete tanpa approval |
| **Read** | Hanya dapat melihat data |
| **Read (own)** | Hanya dapat melihat data milik sendiri |
| **Read (tim)** | Hanya dapat melihat data anggota tim |
| **CRUD (own)** | Dapat membuat dan mengelola data milik sendiri |
| **Approve** | Dapat memberikan approval pada pengajuan |
| **Read + Approve** | Dapat melihat semua data dan memberikan approval |
| **Read + Review** | Dapat melihat dan memberikan review/penilaian |
| **No Access** | Tidak memiliki akses sama sekali |

### Row-Level Security (RLS) Summary

Seluruh entitas HR menggunakan Row-Level Security (RLS) dari base44 untuk memastikan isolasi data multi-tenant:

| Entitas | Kondisi RLS Utama |
| - | - |
| `Employee` | `company_id` = user's `active_company_id` OR `user_id` = current user OR role = admin |
| `AttendanceRecord` | `user_id` = current user OR `created_by_id` = current user OR role = admin |
| `CompanyAttendance` | `company_id` = user's `active_company_id` OR `employee_email` = current user email OR role = admin |
| `CompanyLeave` | `company_id` = user's `active_company_id` OR `employee_email` = current user email OR role = admin |
| `CompanyPayroll` | `company_id` = user's `active_company_id` OR role = admin |
| `CompanyKPI` | `company_id` = user's `active_company_id` OR `employee_email` = current user email OR role = admin |
| `JobPosting` | Open read — semua user dapat melihat lowongan aktif |
| `JobApplication` | Create: public. Read/Update/Delete: role = admin only |
| `PayrollConfiguration` | `company_id` = user's `active_company_id` OR role = admin |
| `CompanyAttendanceSettings` | Aturan RLS kosong (`{}`) — hanya user terautentikasi, scoping perusahaan dijaga server-function |
| `Training` | Aturan RLS kosong (`{}`) — hanya user terautentikasi, scoping perusahaan dijaga server-function |
| `TimeEntry` | Aturan RLS kosong (`{}`) — hanya user terautentikasi, scoping perusahaan dijaga server-function |
| `InterviewSchedule` | Aturan RLS kosong (`{}`) — hanya user terautentikasi, scoping perusahaan dijaga server-function |
| `Applicant` | `company_id` = user's `active_company_id` (tidak kosong) OR `created_by_id` = current user OR role = admin |
| `OfferLetter` | `company_id` = user's `active_company_id` OR `created_by_id` = current user OR role = admin |
| `PerformanceReview` | `company_id` = user's `active_company_id` OR `created_by_id` = current user OR role = admin |
| `ApprovalWorkflow` | Aturan RLS kosong (`{}`), wewenang ditegakkan oleh server-function |
| `ApprovalRequest` | Aturan RLS kosong (`{}`), wewenang approver divalidasi oleh server-function |

> Pola `OR created_by_id = current user` pada entitas rekrutmen & penilaian berarti pembuat data selalu dapat membaca barisnya sendiri meskipun ia tidak memegang role HR.
>
> Entri bertanda **aturan RLS kosong (`{}`)** tidak memfilter baris berdasarkan field. Artinya: setiap user yang sudah login secara teknis memenuhi syarat RLS, sehingga validasi `company_id` dan role WAJIB tetap dilakukan di dalam server-function. Jangan pernah membaca entitas-entitas ini langsung dari client tanpa fungsi backend.

### Matriks RBAC Detail (Role × Entitas × Operasi)

Tabel berikut memecah matriks di atas menjadi level operasi CRUD per entitas. Simbol: ✅ = boleh, ❌ = tidak boleh, 🔃 = boleh dengan batasan scope (own/tim), ⭐ = hanya lewat server-function dengan validasi tambahan.

| Peran | Entitas | Create | Read | Update | Delete |
| - | - | - | - | - | - |
| **Super Admin** | `Employee` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `CompanyAttendance` | ✅ | ✅ | ✅ (termasuk manual override) | ✅ |
| **Super Admin** | `AttendanceRecord` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `CompanyAttendanceSettings` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `CompanyLeave` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `CompanyPayroll` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `PayrollConfiguration` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `PayslipTemplate` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `JobPosting` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `Applicant` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `JobApplication` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `InterviewSchedule` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `OfferLetter` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `Training` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `CompanyKPI` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `KPITemplate` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `PerformanceReview` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `TimeEntry` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `ApprovalWorkflow` | ✅ | ✅ | ✅ | ✅ |
| **Super Admin** | `ApprovalRequest` | ✅ | ✅ | ✅ | ✅ |
| **Owner** | `Employee` | ✅ | ✅ | ✅ | ❌ (arsipkan, jangan hapus) |
| **Owner** | `CompanyAttendance` | ❌ | ✅ | ⭐ (setujui koreksi manual) | ❌ |
| **Owner** | `CompanyAttendanceSettings` | ✅ | ✅ | ✅ | ❌ |
| **Owner** | `CompanyLeave` | ❌ | ✅ | ⭐ (approval level akhir) | ❌ |
| **Owner** | `CompanyPayroll` | ❌ | ✅ | ⭐ (setujui payroll) | ❌ |
| **Owner** | `PayrollConfiguration` | ❌ | ✅ | ✅ | ❌ |
| **Owner** | `JobPosting` | ✅ | ✅ | ✅ | ❌ |
| **Owner** | `Applicant` | ❌ | ✅ | ✅ | ❌ |
| **Owner** | `JobApplication` | ❌ | ✅ | ✅ | ❌ |
| **Owner** | `InterviewSchedule` | ❌ | ✅ | ✅ | ❌ |
| **Owner** | `OfferLetter` | ✅ | ✅ | ✅ | ❌ |
| **Owner** | `Training` | ✅ | ✅ | ✅ | ❌ |
| **Owner** | `CompanyKPI` | ❌ | ✅ | ✅ | ❌ |
| **Owner** | `KPITemplate` | ✅ | ✅ | ✅ | ❌ |
| **Owner** | `PerformanceReview` | ❌ | ✅ | ✅ | ❌ |
| **Owner** | `TimeEntry` | ❌ | ✅ | ❌ | ❌ |
| **Owner** | `ApprovalWorkflow` | ✅ | ✅ | ✅ | ❌ |
| **Owner** | `ApprovalRequest` | ✅ | ✅ | ⭐ (approve/reject) | ❌ |
| **HR Manager** | `Employee` | ✅ | ✅ | ✅ | ⭐ (soft delete/terminated) |
| **HR Manager** | `CompanyAttendance` | ✅ | ✅ | ✅ (rekap & koreksi) | ❌ |
| **HR Manager** | `CompanyAttendanceSettings` | ✅ | ✅ | ✅ | ❌ |
| **HR Manager** | `CompanyLeave` | ✅ | ✅ | ✅ (approve/reject) | ❌ |
| **HR Manager** | `CompanyPayroll` | ✅ | ✅ | ✅ (approve) | ❌ |
| **HR Manager** | `PayrollConfiguration` | ✅ | ✅ | ✅ | ❌ |
| **HR Manager** | `PayslipTemplate` | ✅ | ✅ | ✅ | ❌ |
| **HR Manager** | `JobPosting` | ✅ | ✅ | ✅ | ✅ |
| **HR Manager** | `Applicant` | ✅ | ✅ | ✅ | ✅ |
| **HR Manager** | `JobApplication` | ✅ | ✅ | ✅ | ✅ |
| **HR Manager** | `InterviewSchedule` | ✅ | ✅ | ✅ | ✅ |
| **HR Manager** | `OfferLetter` | ✅ | ✅ | ✅ | ✅ |
| **HR Manager** | `Training` | ✅ | ✅ | ✅ | ✅ |
| **HR Manager** | `CompanyKPI` | ✅ | ✅ | ✅ | ✅ |
| **HR Manager** | `KPITemplate` | ✅ | ✅ | ✅ | ✅ |
| **HR Manager** | `PerformanceReview` | ✅ | ✅ | ✅ (finalisasi) | ✅ |
| **HR Manager** | `TimeEntry` | ❌ | ✅ | ✅ | ❌ |
| **HR Manager** | `ApprovalWorkflow` | ✅ | ✅ | ✅ | ❌ |
| **HR Manager** | `ApprovalRequest` | ✅ | ✅ | ✅ (approve/reject/hold) | ❌ |
| **HR Staff** | `Employee` | ✅ | ✅ | ✅ | ❌ |
| **HR Staff** | `CompanyAttendance` | ✅ | ✅ | ✅ | ❌ |
| **HR Staff** | `CompanyAttendanceSettings` | ❌ | ✅ | ✅ (shift & lokasi) | ❌ |
| **HR Staff** | `CompanyLeave` | ✅ | ✅ | ✅ (draf & koreksi saldo) | ❌ |
| **HR Staff** | `CompanyPayroll` | ✅ (generate draf) | ✅ | ✅ (sebelum approved) | ✅ (hanya draf) |
| **HR Staff** | `PayrollConfiguration` | ❌ | ✅ | ❌ | ❌ |
| **HR Staff** | `PayslipTemplate` | ✅ | ✅ | ✅ | ✅ |
| **HR Staff** | `JobPosting` | ✅ | ✅ | ✅ | ❌ |
| **HR Staff** | `Applicant` | ✅ | ✅ | ✅ (status pipeline) | ❌ |
| **HR Staff** | `JobApplication` | ✅ | ✅ | ✅ (rating & admin\_notes) | ❌ |
| **HR Staff** | `InterviewSchedule` | ✅ | ✅ | ✅ | ❌ |
| **HR Staff** | `OfferLetter` | ✅ | ✅ | ✅ (draft → sent) | ❌ |
| **HR Staff** | `Training` | ✅ | ✅ | ✅ | ❌ |
| **HR Staff** | `CompanyKPI` | ❌ | ✅ | ❌ | ❌ |
| **HR Staff** | `KPITemplate` | ✅ | ✅ | ✅ | ❌ |
| **HR Staff** | `PerformanceReview` | ❌ | ✅ | ❌ (menyiapkan periode) | ❌ |
| **HR Staff** | `TimeEntry` | ❌ | ✅ | ❌ | ❌ |
| **HR Staff** | `ApprovalWorkflow` | ❌ | ✅ | ❌ | ❌ |
| **HR Staff** | `ApprovalRequest` | ✅ | ✅ | ❌ | ❌ |
| **Manager** | `Employee` | ❌ | 🔃 (tim) | ❌ | ❌ |
| **Manager** | `CompanyAttendance` | ❌ | 🔃 (tim) | ❌ | ❌ |
| **Manager** | `CompanyLeave` | ❌ | 🔃 (tim) | ⭐ (approve/reject level 1) | ❌ |
| **Manager** | `CompanyPayroll` | ❌ | 🔃 (tim, setelah paid) | ❌ | ❌ |
| **Manager** | `PayrollConfiguration` | ❌ | ❌ | ❌ | ❌ |
| **Manager** | `JobPosting` | ✅ (timnya) | ✅ | 🔃 (tim) | ❌ |
| **Manager** | `Applicant` | ❌ | 🔃 (lowongan tim) | 🔃 (feedback) | ❌ |
| **Manager** | `JobApplication` | ❌ | 🔃 (lowongan tim) | ❌ | ❌ |
| **Manager** | `InterviewSchedule` | ✅ (jadwalkan) | 🔃 (tim) | ✅ (feedback & rating) | ❌ |
| **Manager** | `OfferLetter` | ❌ | 🔃 (tim) | ❌ | ❌ |
| **Manager** | `Training` | ✅ (usulkan) | ✅ | 🔃 (tim) | ❌ |
| **Manager** | `CompanyKPI` | ✅ (draft tim) | 🔃 (tim) | ✅ (review tim) | ❌ |
| **Manager** | `KPITemplate` | ❌ | ✅ | ❌ | ❌ |
| **Manager** | `PerformanceReview` | ✅ (sebagai reviewer) | 🔃 (tim) | ✅ (skor & komentar) | ❌ |
| **Manager** | `TimeEntry` | ✅ | 🔃 (tim) | ✅ (approve/reject) | ❌ |
| **Manager** | `ApprovalWorkflow` | ❌ | ✅ | ❌ | ❌ |
| **Manager** | `ApprovalRequest` | ✅ | 🔃 (tim) | ⭐ (approve level aktif) | ❌ |
| **Supervisor** | `Employee` | ❌ | 🔃 (tim) | ❌ | ❌ |
| **Supervisor** | `CompanyAttendance` | ❌ | 🔃 (tim) | ⭐ (ajukan koreksi) | ❌ |
| **Supervisor** | `CompanyLeave` | ❌ | 🔃 (tim) | ⭐ (approve/reject level 1) | ❌ |
| **Supervisor** | `CompanyPayroll` | ❌ | ❌ | ❌ | ❌ |
| **Supervisor** | `JobPosting` | ❌ | ✅ | ❌ | ❌ |
| **Supervisor** | `Applicant` | ❌ | 🔃 (lowongan tim) | ❌ | ❌ |
| **Supervisor** | `JobApplication` | ❌ | 🔃 (lowongan tim) | ❌ | ❌ |
| **Supervisor** | `InterviewSchedule` | ❌ | 🔃 (tim) | ✅ (catatan wawancara) | ❌ |
| **Supervisor** | `OfferLetter` | ❌ | ❌ | ❌ | ❌ |
| **Supervisor** | `Training` | ❌ | 🔃 (tim) | ❌ | ❌ |
| **Supervisor** | `CompanyKPI` | ❌ | 🔃 (tim) | ✅ (isi capaian) | ❌ |
| **Supervisor** | `PerformanceReview` | ❌ | 🔃 (tim) | ✅ (skor kompetensi) | ❌ |
| **Supervisor** | `TimeEntry` | ✅ | 🔃 (tim) | ✅ (approve/reject) | ❌ |
| **Supervisor** | `ApprovalRequest` | ✅ | 🔃 (tim) | ⭐ (approve level 1) | ❌ |
| **Employee** | `Employee` | ❌ | 🔃 (own profile) | 🔃 (address, emergency\_contact) | ❌ |
| **Employee** | `AttendanceRecord` | ✅ (own clock-in/out) | 🔃 (own) | 🔃 (own, hari yang sama) | ❌ |
| **Employee** | `CompanyAttendance` | ✅ (own) | 🔃 (own) | 🔃 (own sebelum midnight) | ❌ |
| **Employee** | `CompanyLeave` | ✅ (own) | 🔃 (own) | ✅ (own saat pending/cancel) | ✅ (own saat pending) |
| **Employee** | `CompanyPayroll` | ❌ | 🔃 (own payslip) | ❌ | ❌ |
| **Employee** | `PayrollConfiguration` | ❌ | ❌ | ❌ | ❌ |
| **Employee** | `JobPosting` | ❌ | ✅ (publik) | ❌ | ❌ |
| **Employee** | `Applicant` | ✅ (saat melamar internal) | 🔃 (own lamaran) | ❌ | ❌ |
| **Employee** | `JobApplication` | ✅ (submit lamaran) | 🔃 (own) | ❌ | ❌ |
| **Employee** | `InterviewSchedule` | ❌ | 🔃 (own interview) | ❌ | ❌ |
| **Employee** | `OfferLetter` | ❌ | 🔃 (own, setelah sent) | ⭐ (accept/decline) | ❌ |
| **Employee** | `Training` | ✅ (daftar kelas terbuka) | 🔃 (own + daftar publik) | ✅ (attendance\_status own) | ❌ |
| **Employee** | `CompanyKPI` | ❌ | 🔃 (own) | ✅ (self-assessment saat draft) | ❌ |
| **Employee** | `PerformanceReview` | ❌ | 🔃 (own) | ✅ (employee\_comments, acknowledge) | ❌ |
| **Employee** | `TimeEntry` | ✅ (own) | 🔃 (own) | ✅ (own saat submitted) | ✅ (own saat draft) |
| **Employee** | `ApprovalRequest` | ✅ (ajukan cuti) | 🔃 (own) | ✅ (own saat pending) | ✅ (own saat pending) |

### Aturan Override & Eskalasi

| Skenario | Peran yang berwenang | Syarat tambahan |
| - | - | - |
| Koreksi absensi manual (`is_manual_override = true`) | HR Staff membuat, HR Manager/Owner menyetujui | `override_reason` wajib diisi, tercatat di `override_approved_by` dan `override_approved_at` |
| Persetujuan cuti multi-level | Supervisor/Manager pada level aktif | Naik ke `current_approval_level` berikutnya setelah `required_approvals` terpenuhi |
| Perubahan gaji & `PayrollConfiguration` | Owner / HR Manager | Server-function memverifikasi role sebelum write, tidak bisa dilakukan HR Staff |
| Finalisasi `CompanyPayroll` | HR Manager | Seluruh `ApprovalRequest` terkait berstatus `approved` |
| Pembatalan `OfferLetter` setelah `sent` | HR Manager | Hanya bila `acceptance_status = pending` |
| Revisi `PerformanceReview` setelah `finalized` | Super Admin | Membuat review baru, dokumen lama tetap tersimpan sebagai audit trail |
| Penghapusan data `Employee` | Super Admin | Direkomendasikan `status = terminated` (soft delete), bukan hapus baris |


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