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

# Employee portal

<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: "Employee Portal — Portal Karyawan"
description: "Portal self-service karyawan: slip gaji, kalender absensi, pengajuan cuti, dan profil di SNISHOP ERP."
--------------------------------------------------------------------------------------------------------------------

# Employee Portal — Portal Karyawan

<img src="https://mintcdn.com/quinnofspicy/ny1xnfpEa_OBdv6T/docs/mintlify/screenshots/hr/employee-portal.png?fit=max&auto=format&n=ny1xnfpEa_OBdv6T&q=85&s=742907d05bb2161631f9bfab3e6fae2d" alt="Employee Portal" width="1920" height="1080" data-path="docs/mintlify/screenshots/hr/employee-portal.png" />

Employee Portal adalah halaman self-service untuk karyawan. Setiap karyawan bisa melihat slip gaji, memantau kehadiran, dan mengajukan cuti secara mandiri tanpa perlu menghubungi HR.

## Arsitektur

```mermaid theme={null}
flowchart TB
    subgraph TABS["3 Tab Utama"]
        PS[Tab: Payslips<br/>Slip Gaji]
        AT[Tab: Attendance<br/>Kalender Absensi]
        LV[Tab: Leaves<br/>Pengajuan Cuti]
    end

    subgraph SIDEBAR["Profile Sidebar"]
        AV[Avatar & Nama]
        INFO[Posisi, Dept, Role]
        CONTACT[Email, Joined Date]
        HOURS[Working Hours]
        PERM[Permissions]
    end

    subgraph STATS["Statistik Karyawan"]
        PRESENT[Present Days]
        LATE[Late Days]
        ABSENT[Absent Days]
        HRS[Total Hours]
        LVSTAT[Leave Stats]
        INC[YTD Income]
        PAY[Latest Pay]
    end

    subgraph ENTITIES["Entitas"]
        CP[CompanyPayroll]
        CA[CompanyAttendance]
        CL[CompanyLeave]
        CM[CompanyMember]
    end

    PS --> CP
    AT --> CA
    LV --> CL
    SIDEBAR --> CM
```

## Akses Halaman

URL: `/employee-portal`

## Access Policy

Portal menggunakan `resolveUniqueActiveMembership` dari `companyAccessPolicy` untuk menentukan akses:

| Kondisi | Tindakan |
| - | - |
| User memiliki membership aktif | Tampilkan data berdasarkan `company_id` |
| User adalah owner tapi bukan member | Buat mock member data sebagai fallback |
| User tidak memiliki akses | Redirect atau tampilkan pesan error |

## Tiga Tab Utama

| Tab | Label | Fungsi |
| - | - | - |
| `payslips` | Slip Gaji | Lihat dan export slip gaji |
| `attendance` | Kehadiran | Kalender absensi mini |
| `leaves` | Cuti | Ajukan dan pantau cuti |

## Tab Slip Gaji

### Daftar Slip Gaji

Menampilkan seluruh record `CompanyPayroll` milik karyawan:

| Info per Slip | Detail |
| - | - |
| Periode | Bulan/tahun gaji |
| Gaji Pokok | `basic_salary` |
| Tunjangan | `allowances` (array) |
| Potongan | `deductions` (array) |
| Gaji Bersih | `net_salary` |
| Status | draft / approved / paid |
| Tanggal Bayar | `payment_date` |

### Detail Slip Gaji

Dialog detail menampilkan breakdown lengkap:

```
SLIP GAJI — Januari 2026
═══════════════════════════════
PENDAPATAN
  Gaji Pokok          Rp 5.000.000
  Lembur (12 jam)     Rp   375.000
  Bonus KPI           Rp   500.000
  ──────────────────────────────
  Gaji Kotor          Rp 5.875.000

  Tunjangan:
    Transport         Rp   300.000
    Makan             Rp   450.000

POTONGAN
  BPJS Kesehatan      Rp    50.000
  BPJS TK             Rp   100.000
  PPh 21              Rp   287.500
  ──────────────────────────────
  GAJI BERSIH         Rp 6.187.500

Tanggal Bayar: 28 Januari 2026
Status: PAID
```

### Export CSV

Slip gaji bisa diexport ke CSV dengan **BOM (Byte Order Mark)** untuk kompatibilitas Excel:

```javascript theme={null}
// UTF-8 BOM + CSV content
const BOM = '\uFEFF';
const csvContent = BOM + csvData;
```

| Kolom CSV | Field |
| - | - |
| Periode | `period` |
| Gaji Pokok | `basic_salary` |
| Lembur | `overtime_pay` |
| Bonus KPI | `kpi_bonus` |
| Gaji Kotor | `gross_salary` |
| Potongan | `deductions` |
| Gaji Bersih | `net_salary` |
| Tanggal Bayar | `payment_date` |
| Status | `status` |

## Tab Kehadiran: Kalender Mini

Kalender bulanan mini menampilkan status absensi dengan dot indicator:

| Warna Dot | Status | Arti |
| - | - | - |
| Hijau | Present | Hadir |
| Kuning | Late | Terlambat |
| Merah | Absent | Tidak hadir |
| Biru | Leave | Cuti |

## Tab Cuti: Pengajuan Cuti

### Form Pengajuan

Dialog form untuk mengajukan cuti baru:

| Field | Tipe | Deskripsi |
| - | - | - |
| `leave_type` | select | Jenis cuti (6 tipe) |
| `start_date` | date | Tanggal mulai |
| `end_date` | date | Tanggal selesai |
| `reason` | text | Alasan singkat |
| `description` | textarea | Deskripsi detail |

### Auto-Calculate Total Hari

Sistem menghitung otomatis `total_days` dari selisih `end_date - start_date` menggunakan `differenceInDays`.

### Jenis Cuti

| Tipe | Label | Keterangan |
| - | - | - |
| `annual` | Cuti Tahunan | Cuti tahunan reguler |
| `sick` | Cuti Sakit | Dengan surat dokter |
| `unpaid` | Tanpa Bayaran | Di luar tanggungan |
| `maternity` | Melahirkan | Cuti melahirkan |
| `paternity` | Ayah | Cuti kelahiran |
| `emergency` | Darurat | Keadaan darurat |

### Status Pengajuan

| Status | Warna | Keterangan |
| - | - | - |
| `pending` | Kuning | Menunggu persetujuan |
| `approved` | Hijau | Disetujui |
| `rejected` | Merah | Ditolak |
| `cancelled` | Abu-abu | Dibatalkan oleh karyawan |

## Profile Sidebar

### Informasi Profil

| Field | Sumber |
| - | - |
| Avatar | `avatar_url` |
| Nama | `user_name` |
| Posisi | `position` |
| Departemen | `department` |
| Role | `role` |
| Email | `user_email` |
| Tanggal Bergabung | `joined_date` |
| Jam Kerja | `working_hours` |
| Permissions | `permissions` (badge list) |

### Statistik Karyawan

| Metrik | Perhitungan |
| - | - |
| **Present Days** | COUNT attendance `status = 'present'` |
| **Late Days** | COUNT attendance `status = 'late'` |
| **Absent Days** | Hari kerja tanpa record absensi |
| **Total Hours** | SUM `total_hours` |
| **Total Leaves** | COUNT CompanyLeave |
| **Pending Leaves** | COUNT status `pending` |
| **Approved Leaves** | COUNT status `approved` |
| **Total Days Used** | SUM `total_days` cuti approved |
| **YTD Income** | SUM `net_salary` tahun berjalan |
| **Latest Pay** | `net_salary` dari periode terakhir |

## Entitas: CompanyPayroll (Detail)

| Field | Tipe | Deskripsi |
| - | - | - |
| `company_id` | string | ID perusahaan |
| `employee_id` | string | ID karyawan |
| `employee_name` | string | Nama karyawan |
| `period` | string | Periode gaji (YYYY-MM) |
| `basic_salary` | number | Gaji pokok |
| `attendance_days` | number | Hari hadir |
| `working_days` | number | Hari kerja dalam periode |
| `late_count` | number | Jumlah terlambat |
| `overtime_hours` | number | Jam lembur |
| `overtime_pay` | number | Upah lembur |
| `kpi_bonus` | number | Bonus KPI |
| `kpi_score` | number | Skor KPI |
| `gross_salary` | number | Gaji kotor |
| `allowances` | array | Daftar tunjangan `[{name, amount}]` |
| `deductions` | array | Daftar potongan `[{name, amount}]` |
| `net_salary` | number | Gaji bersih |
| `tax_amount` | number | PPh 21 |
| `bpjs_kesehatan_employee` | number | BPJS Kesehatan karyawan |
| `bpjs_kesehatan_company` | number | BPJS Kesehatan perusahaan |
| `bpjs_tk_employee` | number | BPJS TK karyawan |
| `bpjs_tk_company` | number | BPJS TK perusahaan |
| `payment_date` | date | Tanggal pembayaran |
| `notes` | string | Catatan |
| `status` | enum | `draft`, `approved`, `paid` |

***

## Diagram Relasi Entitas Employee Portal (ER Diagram)

Diagram berikut menunjukkan relasi antar-entitas yang diakses melalui Employee Portal. Karyawan berinteraksi dengan `CompanyMember` (profil), `CompanyPayroll` (slip gaji), `CompanyAttendance` (absensi), `CompanyLeave` (cuti), dan `CompanyLoan` (kasbon) — semuanya terfilter berdasarkan `employee_id` milik user yang login.

```mermaid theme={null}
erDiagram
    User ||--|| CompanyMember : "autentikasi → profil"
    CompanyMember ||--|| Employee : "detail personalia"
    CompanyMember ||--o{ CompanyPayroll : "menerima slip gaji"
    CompanyMember ||--o{ CompanyAttendance : "mencatat absensi"
    CompanyMember ||--o{ CompanyLeave : "mengajukan cuti"
    CompanyMember ||--o{ CompanyLoan : "mengajukan kasbon"
    Company ||--|| CompanyMember : "memiliki"
    Company ||--o{ CompanyPayroll : "menghasilkan"
    Company ||--o{ CompanyAttendance : "mencatat"
    Company ||--o{ CompanyLeave : "mengelola"
    Company ||--o{ CompanyLoan : "mengelola"

    User {
        string id PK
        string email
        string name
        string role
    }

    Company {
        string id PK
        string name
    }

    CompanyMember {
        string company_id PK
        string user_id FK
        string user_email
        string user_name
        string role
        string employee_id FK
        string department
        string position
        string status
        number salary
        object permissions
        object working_hours
        date joined_date
    }

    Employee {
        string company_id PK
        string employee_id PK
        string user_id FK
        string full_name
        string email
        string phone
        string department
        string position
        string employment_type
        number salary
        object bank_account
        object emergency_contact
        string address
        date date_of_birth
        string status
        date hire_date
    }

    CompanyPayroll {
        string company_id PK
        string employee_id FK
        string period
        number basic_salary
        number attendance_days
        number overtime_hours
        number kpi_score
        array allowances
        array deductions
        number gross_salary
        number net_salary
        string status
        date payment_date
    }

    CompanyAttendance {
        string company_id PK
        string employee_id FK
        date date
        datetime clock_in_time
        datetime clock_out_time
        string status
        number total_hours
        number overtime_hours
        string shift_id FK
        string location_id FK
    }

    CompanyLeave {
        string company_id PK
        string employee_id FK
        string leave_type
        date start_date
        date end_date
        number total_days
        string reason
        string status
        string approver_id FK
        datetime approved_at
    }

    CompanyLoan {
        string company_id PK
        string employee_id FK
        number amount
        string reason
        number installment_months
        number monthly_deduction
        number remaining_amount
        string status
        string approver_id FK
        datetime disbursed_at
    }
```

### Penjelasan Relasi Utama

| Relasi | Kardinalitas | Keterangan |
| - | - | - |
| `User → CompanyMember` | 1:1 | Setiap user memiliki satu membership aktif di satu perusahaan (via `resolveUniqueActiveMembership`). |
| `CompanyMember → Employee` | 1:1 | `CompanyMember` menyimpan akses sistem; `Employee` menyimpan data personalia lengkap (rekening bank, kontak darurat). |
| `CompanyMember → CompanyPayroll` | 1:N | Satu karyawan memiliki satu slip gaji per periode (YYYY-MM). Karyawan hanya bisa **membaca** slip gaji miliknya. |
| `CompanyMember → CompanyAttendance` | 1:N | Satu karyawan memiliki banyak record absensi. Karyawan bisa **membaca** dan **membuat** (clock-in/out) absensi sendiri. |
| `CompanyMember → CompanyLeave` | 1:N | Satu karyawan bisa mengajukan banyak cuti. Karyawan bisa **membuat** pengajuan baru dan **membaca** status pengajuan sendiri. |
| `CompanyMember → CompanyLoan` | 1:N | Satu karyawan bisa mengajukan kasbon. Karyawan bisa **membuat** pengajuan dan **membaca** status sendiri. |

***

## Entity Schema Tables — Detail Lengkap per Entitas

### Schema: Employee (Data Personalia)

Entitas `Employee` menyimpan data personalia lengkap karyawan. Di Employee Portal, data ini ditampilkan di sidebar profil dan digunakan untuk verifikasi informasi pribadi.

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan pemilik data karyawan |
| `employee_id` | string | Ya | ID unik karyawan (NIP/NIK internal) |
| `user_id` | string | Tidak | Link ke entitas User untuk autentikasi |
| `full_name` | string | Ya | Nama lengkap karyawan sesuai KTP |
| `email` | string | Ya | Email aktif karyawan |
| `phone` | string | Tidak | Nomor telepon/WhatsApp |
| `department` | enum | Ya | Departemen: `Management`, `Sales`, `Marketing`, `Operations`, `Finance`, `IT`, `HR`, `Customer Service` |
| `position` | string | Ya | Jabatan/posisi karyawan |
| `employment_type` | enum | Tidak | Tipe pekerjaan: `full_time` (default), `part_time`, `contract`, `intern` |
| `hire_date` | date | Ya | Tanggal mulai bekerja |
| `salary` | number | Tidak | Gaji bulanan pokok (Rupiah) |
| `bank_account` | object | Tidak | Data rekening bank untuk transfer gaji |
| `bank_account.bank_name` | string | — | Nama bank (BCA, Mandiri, BNI, dll) |
| `bank_account.account_number` | string | — | Nomor rekening |
| `bank_account.account_name` | string | — | Nama pemilik rekening |
| `emergency_contact` | object | Tidak | Kontak darurat karyawan |
| `emergency_contact.name` | string | — | Nama kontak darurat |
| `emergency_contact.relationship` | string | — | Hubungan (suami/istri/orang tua/dll) |
| `emergency_contact.phone` | string | — | Nomor telepon kontak darurat |
| `address` | string | Tidak | Alamat tempat tinggal |
| `date_of_birth` | date | Tidak | Tanggal lahir |
| `status` | enum | Tidak | Status: `active`, `on_leave`, `terminated` (default: `active`) |
| `avatar_url` | string | Tidak | URL foto profil karyawan |
| `description` | string | Tidak | Catatan tambahan (max 1000 karakter) |

### Schema: CompanyMember (Akses & Profil Sistem)

Entitas `CompanyMember` adalah representasi karyawan dalam sistem. Di Employee Portal, entitas ini menjadi sumber data utama untuk sidebar profil, permission, dan statistik.

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `user_id` | string | Tidak | Link ke entitas User |
| `user_email` | string | Ya | Email akun pengguna |
| `user_name` | string | Tidak | Nama lengkap pengguna |
| `role` | enum | Tidak | Role dalam perusahaan: `owner`, `admin`, `supervisor`, `store_admin`, `stock_admin`, `finance_admin`, `hr_admin`, `transaction_admin`, `employee` (default), `production_operator`, `qc_inspector`, `sales_marketing`, `partner_distributor` |
| `employee_id` | string | Tidak | Link ke entitas Employee |
| `department` | string | Tidak | Departemen penempatan |
| `position` | string | Tidak | Jabatan/posisi |
| `status` | enum | Tidak | Status membership: `active`, `inactive`, `pending` (default: `active`) |
| `joined_date` | date | Tidak | Tanggal bergabung di perusahaan |
| `invited_by` | string | Tidak | Email yang mengundang |
| `permissions` | object | Tidak | Hak akses detail per modul (30+ permission flags) |
| `permissions.can_view_dashboard` | boolean | — | Akses dashboard (default: true) |
| `permissions.can_view_tasks` | boolean | — | Lihat task (default: true) |
| `permissions.can_create_tasks` | boolean | — | Buat task (default: true) |
| `permissions.can_edit_tasks` | boolean | — | Edit task (default: true) |
| `permissions.can_delete_tasks` | boolean | — | Hapus task (default: false) |
| `permissions.can_view_notes` | boolean | — | Lihat catatan (default: true) |
| `permissions.can_create_notes` | boolean | — | Buat catatan (default: true) |
| `permissions.can_edit_notes` | boolean | — | Edit catatan (default: true) |
| `permissions.can_delete_notes` | boolean | — | Hapus catatan (default: false) |
| `permissions.can_view_hr` | boolean | — | Lihat data HR (default: false) |
| `permissions.can_edit_hr` | boolean | — | Edit data HR (default: false) |
| `permissions.can_view_finance` | boolean | — | Lihat data keuangan (default: false) |
| `permissions.can_edit_finance` | boolean | — | Edit data keuangan (default: false) |
| `permissions.can_view_inventory` | boolean | — | Lihat inventori (default: false) |
| `permissions.can_edit_inventory` | boolean | — | Edit inventori (default: false) |
| `permissions.can_view_projects` | boolean | — | Lihat proyek (default: false) |
| `permissions.can_edit_projects` | boolean | — | Edit proyek (default: false) |
| `permissions.can_view_pos` | boolean | — | Lihat modul POS (default: false) |
| `permissions.can_use_pos` | boolean | — | Gunakan POS kasir (default: false) |
| `permissions.can_view_reports` | boolean | — | Lihat laporan (default: false) |
| `permissions.can_manage_members` | boolean | — | Kelola anggota (default: false) |
| `permissions.can_manage_roles` | boolean | — | Kelola role (default: false) |
| `permissions.can_view_settings` | boolean | — | Lihat pengaturan (default: false) |
| `permissions.can_edit_settings` | boolean | — | Edit pengaturan (default: false) |
| `permissions.can_manage_cashier_shift` | boolean | — | Kelola shift kasir (default: false) |
| `permissions.can_approve_stock_opname` | boolean | — | Approve stock opname (default: false) |
| `permissions.can_count_stock_opname` | boolean | — | Hitung stock opname (default: false) |
| `permissions.can_transfer_inventory` | boolean | — | Transfer inventori (default: false) |
| `permissions.can_create_production_batch` | boolean | — | Buat batch produksi (default: false) |
| `permissions.can_release_production_qc` | boolean | — | Rilis QC produksi (default: false) |
| `permissions.can_view_hpp` | boolean | — | Lihat HPP (default: false) |
| `permissions.can_manage_channel_pricing` | boolean | — | Kelola harga channel (default: false) |
| `permissions.can_view_distribution` | boolean | — | Lihat distribusi (default: false) |
| `permissions.can_create_distribution_shipment` | boolean | — | Buat pengiriman distribusi (default: false) |
| `permissions.can_confirm_distribution_shipment` | boolean | — | Konfirmasi pengiriman (default: false) |
| `permissions.can_view_b2b_invoices` | boolean | — | Lihat invoice B2B (default: false) |
| `permissions.can_create_b2b_invoice` | boolean | — | Buat invoice B2B (default: false) |
| `permissions.can_verify_b2b_payment` | boolean | — | Verifikasi pembayaran B2B (default: false) |
| `assigned_locations` | array | Tidak | Daftar ID lokasi gudang/outlet yang diizinkan (RBAC-01) |
| `working_hours` | object | Tidak | Jam kerja karyawan |
| `working_hours.start` | string | — | Jam mulai (default: 09:00) |
| `working_hours.end` | string | — | Jam selesai (default: 17:00) |
| `salary` | number | Tidak | Gaji karyawan (opsional) |
| `notes` | string | Tidak | Catatan tambahan |

### Schema: CompanyPayroll (Ringkasan Slip Gaji)

Entitas `CompanyPayroll` menyimpan slip gaji bulanan. Di Employee Portal, karyawan hanya bisa **membaca** slip gaji milik sendiri.

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID karyawan |
| `employee_name` | string | Tidak | Nama karyawan (denormalisasi) |
| `period` | string | Ya | Periode gaji format YYYY-MM |
| `basic_salary` | number | Ya | Gaji pokok |
| `attendance_days` | number | Tidak | Jumlah hari hadir |
| `working_days` | number | Tidak | Total hari kerja di periode |
| `late_count` | number | Tidak | Jumlah hari terlambat (default: 0) |
| `absent_count` | number | Tidak | Jumlah hari absen (default: 0) |
| `overtime_hours` | number | Tidak | Total jam lembur (default: 0) |
| `kpi_score` | number | Tidak | Skor KPI periode ini |
| `allowances` | array | Tidak | Daftar tunjangan `[{name, amount}]` |
| `deductions` | array | Tidak | Daftar potongan `[{name, amount}]` |
| `overtime_pay` | number | Tidak | Upah lembur (default: 0) |
| `kpi_bonus` | number | Tidak | Bonus berdasarkan KPI (default: 0) |
| `gross_salary` | number | Ya | Gaji kotor = basic + allowances + overtime + kpi\_bonus |
| `net_salary` | number | Ya | Gaji bersih = gross - deductions |
| `status` | enum | Tidak | `draft`, `approved`, `paid` (default: `draft`) |
| `payment_date` | date | Tidak | Tanggal pembayaran |
| `payment_method` | enum | Tidak | `bank_transfer`, `cash`, `check` |
| `notes` | string | Tidak | Catatan tambahan |

### Schema: CompanyAttendance (Record Absensi)

Entitas `CompanyAttendance` menyimpan record absensi harian karyawan. Di Employee Portal, karyawan bisa **membaca** absensi sendiri dan **membuat** record clock-in/clock-out.

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID karyawan |
| `employee_name` | string | Tidak | Nama karyawan (denormalisasi) |
| `employee_email` | string | Tidak | Email karyawan (untuk RLS self-access) |
| `date` | date | Ya | Tanggal absensi |
| `shift_id` | string | Tidak | ID shift yang dipilih karyawan |
| `shift_name` | string | Tidak | Nama shift (Pagi/Siang/Malam) |
| `clock_in_time` | datetime | Ya | Waktu clock-in |
| `clock_out_time` | datetime | Tidak | Waktu clock-out |
| `clock_in_photo_url` | string | Tidak | URL foto selfie saat clock-in |
| `clock_out_photo_url` | string | Tidak | URL foto selfie saat clock-out |
| `clock_in_location` | object | Tidak | Koordinat GPS saat clock-in |
| `clock_in_location.latitude` | number | — | Garis lintang lokasi clock-in |
| `clock_in_location.longitude` | number | — | Garis bujur lokasi clock-in |
| `clock_in_location.accuracy` | number | — | Akurasi GPS dalam meter |
| `clock_out_location` | object | Tidak | Koordinat GPS saat clock-out |
| `clock_out_location.latitude` | number | — | Garis lintang lokasi clock-out |
| `clock_out_location.longitude` | number | — | Garis bujur lokasi clock-out |
| `clock_out_location.accuracy` | number | — | Akurasi GPS dalam meter |
| `status` | enum | Tidak | Status absensi: `present` (default), `late`, `absent`, `sick`, `leave`, `wfh` |
| `notes` | string | Tidak | Catatan absensi |
| `total_hours` | number | Tidak | Total jam kerja hari ini |
| `overtime_hours` | number | Tidak | Total 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 terpilih (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 disetujui via koreksi manual berizin (default: false) |
| `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 |

### Schema: CompanyLeave (Pengajuan Cuti)

Entitas `CompanyLeave` menyimpan pengajuan cuti karyawan. Di Employee Portal, karyawan bisa **membuat** pengajuan cuti baru dan **membaca** status pengajuan sendiri.

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID karyawan |
| `employee_name` | string | Tidak | Nama karyawan (denormalisasi) |
| `employee_email` | string | Tidak | Email karyawan (untuk RLS self-access) |
| `leave_type` | enum | Ya | Jenis cuti: `annual` (default), `sick`, `unpaid`, `maternity`, `paternity`, `emergency` |
| `start_date` | date | Ya | Tanggal mulai cuti |
| `end_date` | date | Ya | Tanggal selesai cuti |
| `total_days` | number | Tidak | Jumlah hari cuti (dihitung otomatis dari `end_date - start_date`) |
| `reason` | string | Ya | Alasan singkat pengajuan cuti |
| `description` | string | Tidak | Keterangan tambahan atau dokumen pendukung (max 1000 karakter) |
| `attachment_url` | string | Tidak | URL file pendukung (surat dokter, dll) |
| `status` | enum | Tidak | Status pengajuan: `pending` (default), `approved`, `rejected`, `cancelled` |
| `approver_id` | string | Tidak | ID user yang menyetujui/menolak |
| `approver_notes` | string | Tidak | Catatan dari approver (alasan reject, dll) |
| `approved_at` | datetime | Tidak | Waktu persetujuan/penolakan |

### Schema: CompanyLoan (Pengajuan Kasbon)

Entitas `CompanyLoan` menyimpan pengajuan kasbon/pinjaman karyawan. Di Employee Portal, karyawan bisa **membuat** pengajuan kasbon dan **membaca** status serta sisa pinjaman sendiri.

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `employee_id` | string | Ya | ID karyawan |
| `employee_name` | string | Tidak | Nama karyawan (denormalisasi) |
| `employee_email` | string | Tidak | Email karyawan (untuk RLS self-access) |
| `amount` | number | Ya | Jumlah kasbon yang diajukan (Rupiah) |
| `reason` | string | Ya | Alasan pengajuan kasbon |
| `description` | string | Tidak | Detail tambahan mengenai keperluan kasbon dan rencana pelunasan (max 1000 karakter) |
| `attachment_url` | string | Tidak | URL bukti pendukung |
| `installment_months` | number | Tidak | Jumlah cicilan dalam bulan (default: 1) |
| `monthly_deduction` | number | Tidak | Potongan gaji per bulan untuk cicilan |
| `remaining_amount` | number | Tidak | Sisa pinjaman yang harus dibayar |
| `status` | enum | Tidak | Status pengajuan: `pending` (default), `approved`, `rejected`, `paid_off` |
| `approver_id` | string | Tidak | ID user yang menyetujui/menolak |
| `approver_notes` | string | Tidak | Catatan dari approver |
| `approved_at` | datetime | Tidak | Waktu persetujuan/penolakan |
| `disbursed_at` | datetime | Tidak | Waktu pencairan dana |

### Schema: CompanyAttendanceSettings (Pengaturan Absensi)

Entitas `CompanyAttendanceSettings` menyimpan konfigurasi kebijakan absensi perusahaan. Di Employee Portal, pengaturan ini digunakan untuk validasi clock-in/clock-out (geofence, jam kerja, shift).

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan |
| `description` | string | Tidak | Penjelasan mengenai kebijakan absensi dan pengaturan jam kerja (max 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 fisik kantor |
| `office_radius_meters` | number | Tidak | Radius dari kantor dalam meter untuk validasi absensi (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 | Tidak | Daftar multi-lokasi geofence absensi (default: \[]) |
| `locations[].location_id` | string | Ya | ID unik area/lokasi absensi |
| `locations[].name` | string | Ya | Nama lokasi absensi (misal: Head Office, Outlet 2) |
| `locations[].address` | string | Tidak | Alamat fisik lokasi |
| `locations[].latitude` | number | Ya | Garis lintang lokasi pusat geofence |
| `locations[].longitude` | number | Ya | Garis bujur lokasi pusat geofence |
| `locations[].radius_meters` | number | Tidak | Radius lingkaran geofence dalam meter (default: 100) |
| `locations[].accuracy_tolerance_meters` | number | Tidak | Toleransi akurasi GPS maksimum (default: 150) |
| `locations[].assigned_employee_ids` | array | Tidak | Daftar ID karyawan yang ditugaskan ke lokasi ini (default: \[]) |
| `locations[].is_active` | boolean | Tidak | Status aktif lokasi geofence (default: true) |
| `working_hours` | object | Tidak | Jam kerja default |
| `working_hours.start_time` | string | — | Jam mulai kerja (HH:MM, default: 09:00) |
| `working_hours.end_time` | string | — | Jam selesai kerja (HH:MM, default: 17:00) |
| `working_hours.break_duration_minutes` | number | — | Durasi istirahat dalam menit (default: 60) |
| `shifts` | array | Tidak | Daftar shift kerja yang tersedia (default: \[]) |
| `shifts[].shift_id` | string | Ya | ID unik shift |
| `shifts[].shift_name` | string | Ya | Nama shift (misal: Shift Pagi) |
| `shifts[].start_time` | string | Ya | Jam mulai shift (HH:MM) |
| `shifts[].end_time` | string | Ya | Jam selesai shift (HH:MM) |
| `shifts[].break_duration_minutes` | number | Tidak | Durasi istirahat shift dalam menit (default: 60) |
| `overtime_settings` | object | Tidak | Pengaturan lembur |
| `overtime_settings.enabled` | boolean | — | Fitur 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 terlambat dalam menit (default: 15) |
| `working_days` | array | Tidak | Hari kerja: 1=Senin, 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) |

### Schema: PayslipTemplate (Template Slip Gaji)

Entitas `PayslipTemplate` menyimpan template tampilan slip gaji yang bisa dikustomisasi per perusahaan. Template ini mengatur komponen apa saja yang ditampilkan saat slip gaji diexport atau dicetak.

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

### Schema: User (Akun Pengguna)

Entitas `User` menyimpan akun autentikasi global. Di Employee Portal, entitas User digunakan untuk autentikasi dan menentukan `active_company_id` yang sedang diakses.

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `email` | string | Ya | Email akun pengguna |
| `full_name` | string | Ya | Nama lengkap pengguna |
| `role` | enum | Tidak | Role global: `admin`, `user` |
| `active_company_id` | string | Tidak | ID perusahaan yang sedang aktif diakses |
| `subscription_plan` | enum | Tidak | Plan langganan: `free` (default), `pro`, `business`, `advanced`, `enterprise` |
| `subscription_start` | datetime | Tidak | Tanggal mulai langganan |
| `subscription_end` | datetime | Tidak | Tanggal berakhir langganan |
| `membership_duration_type` | enum | Tidak | Tipe durasi membership: `monthly`, `yearly`, `custom`, `lifetime` |
| `membership_start_date` | date | Tidak | Tanggal mulai membership |
| `membership_end_date` | date | Tidak | Tanggal berakhir membership |
| `is_readonly_mode` | boolean | Tidak | Mode read-only ketika expired dalam grace period 3 hari (default: false) |
| `trial_end` | datetime | Tidak | Tanggal berakhir masa percobaan |
| `company_slots_purchased` | number | Tidak | Slot perusahaan tambahan yang dibeli (default: 0) |
| `admin_type` | enum | Tidak | Tipe admin APLIKASI: `owner` (full access) atau `basic` (hanya transaksi digital) |
| `admin_tier` | enum | Tidak | Tier admin untuk company management: `none` (default), `business`, `advanced`, `enterprise` |
| `balance` | number | Tidak | Saldo deposit untuk membeli membership/addon (default: 0) |
| `commission_balance` | number | Tidak | Saldo komisi dari referral yang dapat ditarik (default: 0) |
| `admin_commission_balance` | number | Tidak | Saldo komisi admin basic dari proses transaksi (default: 0) |
| `total_earnings` | number | Tidak | Total komisi yang pernah diterima (default: 0) |
| `productivity_score` | number | Tidak | Skor produktivitas pengguna (default: 0) |
| `current_streak` | number | Tidak | Streak hari berturut-turut menyelesaikan tugas (default: 0) |
| `longest_streak` | number | Tidak | Streak terpanjang (default: 0) |
| `last_active_date` | date | Tidak | Tanggal terakhir aktif |
| `total_tasks_completed` | number | Tidak | Total tugas yang diselesaikan (default: 0) |
| `total_notes_created` | number | Tidak | Total catatan yang dibuat (default: 0) |
| `achievement_points` | number | Tidak | Poin pencapaian (default: 0) |
| `user_level` | number | Tidak | Level pengguna berdasarkan achievement points (default: 1) |
| `preferences` | object | Tidak | Preferensi pengguna |
| `preferences.theme` | string | — | Tema tampilan |
| `preferences.primary_color` | string | — | Warna utama UI |
| `preferences.wallpaper_url` | string | — | URL wallpaper background |
| `preferences.referral_modal_dismissed` | boolean | — | Modal referral sudah ditutup (default: false) |

### Schema: Company (Data Perusahaan)

Entitas `Company` menyimpan data perusahaan tempat karyawan bekerja. Di Employee Portal, entitas ini menjadi konteks utama filtering data melalui `company_id`.

| Field | Tipe | Required | Deskripsi |
| - | - | - | - |
| `name` | string | Ya | Nama perusahaan |
| `owner_id` | string | Ya | ID user pemilik perusahaan |
| `owner_email` | string | Ya | Email pemilik perusahaan |
| `owner_subscription_plan` | enum | Tidak | Plan membership owner: `free`, `pro`, `business`, `advanced`, `enterprise` |
| `description` | string | Tidak | Deskripsi perusahaan |
| `industry` | enum | Tidak | Industri: `retail`, `manufacturing`, `services`, `technology`, `food_beverage`, `healthcare`, `education`, `other` |
| `address` | string | Tidak | Alamat perusahaan |
| `phone` | string | Tidak | Nomor telepon perusahaan |
| `email` | string | Tidak | Email perusahaan |
| `website` | string | Tidak | Website perusahaan |
| `logo_url` | string | Tidak | URL logo perusahaan |
| `tax_id` | string | Tidak | NPWP perusahaan |
| `employee_count` | number | Tidak | Jumlah karyawan (default: 0) |
| `metadata` | object | Tidak | Data tambahan / legacy |
| `business_type` | string | Tidak | Kategori bisnis yang dipilih saat onboarding |
| `active_modules` | string | Tidak | JSON string berisi array ID modul aktif |
| `settings` | object | Tidak | Pengaturan perusahaan |
| `settings.working_hours_start` | string | — | Jam mulai kerja (default: 09:00) |
| `settings.working_hours_end` | string | — | Jam selesai kerja (default: 17:00) |
| `settings.working_days` | array | — | Hari kerja: 1=Senin, 7=Minggu (default: \[1,2,3,4,5]) |
| `settings.leave_policy` | object | — | Kebijakan cuti perusahaan |
| `settings.tax` | object | — | Konfigurasi pajak (enabled, rate, mode, rounding) |
| `settings.batch_allocation_strategy` | enum | — | Strategi alokasi batch: `fifo` (default), `fefo` |

***

## State Diagram — Siklus Hidup Pengajuan Self-Service

Diagram berikut menunjukkan siklus hidup pengajuan yang bisa dilakukan karyawan melalui Employee Portal: pengajuan cuti dan pengajuan kasbon.

### Status Pengajuan Cuti (CompanyLeave)

```mermaid theme={null}
stateDiagram-v2
    [*] --> Pending : Karyawan mengajukan cuti baru
    Pending --> Approved : HR/Supervisor menyetujui
    Pending --> Rejected : HR/Supervisor menolak + isi approver_notes
    Pending --> Cancelled : Karyawan membatalkan pengajuan

    Approved --> Cancelled : Karyawan cancel cuti yang sudah approved

    Rejected --> [*] : Status final — tidak bisa diubah
    Cancelled --> [*] : Status final — tidak bisa diubah

    note right of Pending
        Pengajuan baru otomatis berstatus pending.
        HR Admin atau Supervisor akan
        mereview dan approve/reject.
    end note

    note right of Approved
        Cuti approved mengurangi jatah cuti
        tahunan dan tercatat di kalender
        absensi sebagai status "leave".
    end note
```

### Status Pengajuan Kasbon (CompanyLoan)

```mermaid theme={null}
stateDiagram-v2
    [*] --> Pending : Karyawan mengajukan kasbon
    Pending --> Approved : Finance/HR menyetujui
    Pending --> Rejected : Pengajuan ditolak

    Approved --> PaidOff : Cicilan lunas (remaining_amount = 0)

    Rejected --> [*] : Status final
    PaidOff --> [*] : Pinjaman selesai, tidak ada tunggakan

    note right of Approved
        Setelah disetujui, dana dicairkan
        (disbursed_at) dan monthly_deduction
        otomatis dipotong dari gaji tiap periode.
    end note
```

### Status Payroll (CompanyPayroll)

```mermaid theme={null}
stateDiagram-v2
    [*] --> Draft : Payroll digenerate (otomatis/manual)
    Draft --> Approved : Finance/HR menyetujui slip gaji
    Draft --> Draft : Revisi/perhitungan ulang

    Approved --> Paid : Gaji ditransfer ke rekening karyawan
    Approved --> Draft : Dikembalikan untuk revisi

    Paid --> [*] : Status final — slip gaji diarsipkan
```

### Status Absensi (CompanyAttendance)

```mermaid theme={null}
stateDiagram-v2
    [*] --> NoRecord : Belum ada record absensi hari ini

    NoRecord --> ClockIn : Karyawan melakukan clock-in
    ClockIn --> Working : Status tercatat, menunggu clock-out
    Working --> ClockOut : Karyawan melakukan clock-out
    ClockOut --> Completed : total_hours dihitung otomatis

    ClockIn --> LateClockIn : Clock-in melewati toleransi terlambat
    LateClockIn --> Working : Status = late, tetap bekerja

    Completed --> ManualOverride : HR approve koreksi manual
    ManualOverride --> Completed : Data absensi diperbarui

    NoRecord --> Absent : Hari kerja berakhir tanpa absensi
    Absent --> [*] : Status final: absent

    note right of ClockIn
        Validasi: GPS dalam radius geofence,
        akurasi GPS < toleransi,
        foto selfie (jika require_photo = true).
    end note

    note right of ManualOverride
        Koreksi manual memerlukan persetujuan
        manager/HR. Flag is_manual_override
        di-set ke true.
    end note
```

***

## Sequence Diagrams — Alur Interaksi Employee Portal

### Alur Melihat Slip Gaji (Payslip View)

Karyawan membuka tab "Slip Gaji" untuk melihat dan mengexport slip gaji bulanan.

```mermaid theme={null}
sequenceDiagram
    participant Karyawan
    participant Portal as Employee Portal
    participant SF as Server Function
    participant DB as Database

    Karyawan->>Portal: Buka tab "Payslips"
    Portal->>SF: loadCompanyPayrolls({ employee_id })
    SF->>SF: resolveUniqueActiveMembership(user)
    SF->>SF: Filter: employee_id = membership.employee_id
    SF->>DB: SELECT * FROM CompanyPayroll WHERE employee_id = ? ORDER BY period DESC
    DB-->>SF: Return payrolls[]
    SF-->>Portal: Return daftar slip gaji

    Portal->>Portal: Render tabel: periode, net_salary, status, payment_date

    alt Karyawan klik detail slip
        Karyawan->>Portal: Klik baris slip gaji
        Portal->>Portal: Buka dialog detail (breakdown gaji)
        Note over Portal: Tampilkan: basic_salary, allowances, deductions, overtime_pay, kpi_bonus, gross_salary, net_salary
    end

    alt Karyawan export CSV
        Karyawan->>Portal: Klik "Export CSV"
        Portal->>Portal: Generate CSV dengan BOM (UTF-8)
        Portal->>Portal: Download file: slip-gaji-{period}.csv
        Note over Portal: BOM \uFEFF ditambahkan agar<br/>Excel bisa membaca karakter UTF-8
    end
```

### Alur Mengajukan Cuti (Leave Request)

Karyawan mengajukan cuti baru melalui tab "Leaves" di Employee Portal.

```mermaid theme={null}
sequenceDiagram
    participant Karyawan
    participant Portal as Employee Portal
    participant SF as Server Function
    participant DB as Database
    participant HR as HR Dashboard
    participant BC as BroadcastChannel

    Karyawan->>Portal: Buka tab "Leaves" > klik "Ajukan Cuti"
    Portal->>Portal: Form dialog: pilih leave_type, start_date, end_date, reason, description
    Karyawan->>Portal: Isi form & submit

    Portal->>SF: createCompanyLeave({ company_id, employee_id, leave_type, start_date, end_date, reason, description })
    SF->>SF: Hitung total_days = differenceInDays(end_date, start_date) + 1
    SF->>SF: Validasi: start_date >= hari ini, end_date >= start_date
    SF->>DB: INSERT INTO CompanyLeave (status: pending)
    DB-->>SF: Return leave record
    SF->>BC: Broadcast new leave request
    SF-->>Portal: Pengajuan cuti berhasil

    BC-->>HR: Realtime refresh (tab Leaves HR Dashboard)
    HR->>HR: Tampilkan pengajuan baru dengan badge "Pending"

    Note over Karyawan: Karyawan bisa memantau status<br/>pengajuan di tab Leaves.<br/>Status: pending → approved/rejected/cancelled
```

### Alur Update Profil (Profile Update)

Karyawan memperbarui informasi profil pribadi melalui sidebar di Employee Portal.

```mermaid theme={null}
sequenceDiagram
    participant Karyawan
    participant Portal as Employee Portal
    participant SF as Server Function
    participant DB as Database

    Karyawan->>Portal: Klik "Edit Profil" di sidebar
    Portal->>Portal: Form edit: phone, address, date_of_birth, emergency_contact, bank_account
    Karyawan->>Portal: Isi perubahan & submit

    Portal->>SF: resolveUniqueActiveMembership(user)
    SF->>SF: Dapatkan employee_id dari membership

    alt Update data Employee
        Portal->>SF: updateEmployee(employee_id, { phone, address, date_of_birth, emergency_contact, bank_account })
        SF->>SF: Validasi RLS: data.user_id = user.id ATAU company_id match
        SF->>DB: UPDATE Employee SET phone, address, ... WHERE employee_id = ?
        DB-->>SF: Return updated record
        SF-->>Portal: Profil berhasil diperbarui
    else Update data CompanyMember
        Portal->>SF: updateCompanyMember(member_id, { notes })
        SF->>SF: Validasi RLS: data.user_id = user.id
        SF->>DB: UPDATE CompanyMember SET notes = ? WHERE id = ?
        DB-->>SF: Return updated record
        SF-->>Portal: Data member berhasil diperbarui
    end

    Portal->>Portal: Refresh sidebar profil
    Portal->>Portal: Tampilkan data terbaru (avatar, nama, posisi, kontak)
```

### Alur Clock-in/Clock-out (Attendance)

Karyawan melakukan absensi masuk dan keluar melalui Employee Portal dengan validasi geofence dan GPS.

```mermaid theme={null}
sequenceDiagram
    participant Karyawan
    participant Portal as Employee Portal
    participant SF as Server Function
    participant CFG as AttendanceSettings
    participant DB as Database

    Karyawan->>Portal: Buka tab "Attendance" > klik "Clock In"
    Portal->>Portal: Ambil lokasi GPS browser (navigator.geolocation)
    Portal->>Portal: Hitung distance dari office geofence

    Portal->>SF: loadCompanyAttendanceSettings(company_id)
    SF->>DB: SELECT * FROM CompanyAttendanceSettings WHERE company_id = ?
    DB-->>SF: Return settings (locations, shifts, working_hours, require_photo)
    SF-->>Portal: Return pengaturan absensi

    alt Multi-lokasi tersedia
        Portal->>Portal: Tampilkan pilihan lokasi absensi
        Karyawan->>Portal: Pilih lokasi & shift
    end

    alt require_photo = true
        Portal->>Portal: Buka kamera > ambil foto selfie
        Portal->>Portal: Upload foto → dapatkan clock_in_photo_url
    end

    Portal->>SF: createCompanyAttendance({ company_id, employee_id, employee_email, date, clock_in_time, clock_in_location, shift_id, location_id, status })
    SF->>SF: Validasi GPS: distance <= radius_meters
    SF->>SF: Validasi akurasi: accuracy <= tolerance
    SF->>SF: Tentukan status: present atau late (berdasarkan late_tolerance_minutes)
    SF->>DB: INSERT INTO CompanyAttendance (status: present/late)
    DB-->>SF: Return attendance record
    SF-->>Portal: Clock-in berhasil

    Note over Karyawan: Karyawan bekerja...

    Karyawan->>Portal: Klik "Clock Out"
    Portal->>Portal: Ambil lokasi GPS & foto selfie (jika wajib)
    Portal->>SF: updateCompanyAttendance(id, { clock_out_time, clock_out_location })
    SF->>SF: Hitung total_hours = clock_out - clock_in - break_duration
    SF->>SF: Hitung overtime_hours (jika melebihi max_overtime_hours_per_day)
    SF->>DB: UPDATE CompanyAttendance SET clock_out_time, total_hours, overtime_hours
    DB-->>SF: Return updated record
    SF-->>Portal: Clock-out berhasil, total jam tercatat
```

***

## Enum Tables

### Enum: `leave_type` (Jenis Cuti)

| Nilai | Label | Keterangan |
| - | - | - |
| `annual` | Cuti Tahunan | Cuti tahunan reguler, mengurangi jatah cuti |
| `sick` | Cuti Sakit | Cuti karena sakit, memerlukan surat dokter |
| `unpaid` | Tanpa Bayaran | Cuti di luar tanggungan, menjadi deduction di payroll |
| `maternity` | Melahirkan | Cuti melahirkan |
| `paternity` | Ayah | Cuti kelahiran untuk ayah |
| `emergency` | Darurat | Cuti keadaan darurat |

### Enum: `approval_status` (Status Pengajuan Self-Service)

| Nilai | Deskripsi | Warna UI | Entitas |
| - | - | - | - |
| `pending` | Menunggu persetujuan HR/Finance | Kuning | CompanyLeave, CompanyLoan |
| `approved` | Disetujui oleh approver | Hijau | CompanyLeave, CompanyLoan |
| `rejected` | Ditolak oleh approver | Merah | CompanyLeave, CompanyLoan |
| `cancelled` | Dibatalkan oleh karyawan | Abu-abu | CompanyLeave |
| `paid_off` | Cicilan pinjaman sudah lunas | Hijau | CompanyLoan |

### Enum: `payroll_status` (Status Slip Gaji)

| Nilai | Deskripsi | Bisa Di-edit |
| - | - | - |
| `draft` | Slip gaji belum diapprove, masih bisa direvisi | Ya |
| `approved` | Slip gaji sudah disetujui Finance/HR | Ya (bisa dikembalikan) |
| `paid` | Gaji sudah ditransfer ke karyawan | Tidak (status final) |

### Enum: `attendance_status` (Status Absensi)

| Nilai | Deskripsi | Warna Dot |
| - | - | - |
| `present` | Hadir tepat waktu | Hijau |
| `late` | Hadir terlambat | Kuning |
| `absent` | Tidak hadir tanpa keterangan | Merah |
| `sick` | Sakit (dengan surat dokter) | Biru |
| `leave` | Cuti (approved) | Biru |
| `wfh` | Work from home | Abu-abu |

### Enum: `employment_type` (Tipe Pekerjaan)

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

### Enum: `employee_status` (Status Karyawan)

| Nilai | Deskripsi |
| - | - |
| `active` | Karyawan aktif bekerja (default) |
| `on_leave` | Karyawan sedang cuti panjang |
| `terminated` | Karyawan sudah tidak bekerja (PHK/resign) |

### Enum: `department` (Departemen)

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

### Enum: `payment_method` (Metode Pembayaran Gaji)

| Nilai | Deskripsi |
| - | - |
| `bank_transfer` | Transfer bank (BCA, Mandiri, BNI, dll) |
| `cash` | Tunai |
| `check` | Cek/giro |

### Enum: `member_role` (Role dalam Perusahaan)

| Nilai | Deskripsi | Scope Akses |
| - | - | - |
| `owner` | Pemilik perusahaan | Full access semua modul dan data |
| `admin` | Administrator global | Full access semua modul (non-owner) |
| `supervisor` | Supervisor tim | Akses tim yang dipimpin |
| `store_admin` | Admin toko/outlet | Akses modul POS dan inventori toko |
| `stock_admin` | Admin gudang | Akses modul inventori dan stock opname |
| `finance_admin` | Admin keuangan | Akses modul keuangan, payroll, kasbon |
| `hr_admin` | Admin HR | Akses modul HR, absensi, cuti, karyawan |
| `transaction_admin` | Admin transaksi | Akses modul transaksi penjualan |
| `employee` | Karyawan biasa | Self-service: profil, slip gaji, absensi, cuti |
| `production_operator` | Operator produksi | Akses modul produksi |
| `qc_inspector` | Inspector QC | Akses modul quality control |
| `sales_marketing` | Sales & Marketing | Akses modul sales dan marketing |
| `partner_distributor` | Partner/Distributor | Akses modul distribusi |

### Enum: `user_role` (Role Global User)

| Nilai | Deskripsi |
| - | - |
| `admin` | Administrator aplikasi (akses penuh ke semua fitur) |
| `user` | Pengguna biasa (akses fitur standar) |

### Enum: `subscription_plan` (Plan Langganan)

| Nilai | Deskripsi |
| - | - |
| `free` | Plan gratis dengan fitur terbatas (default) |
| `pro` | Plan profesional dengan fitur tambahan |
| `business` | Plan bisnis untuk perusahaan menengah |
| `advanced` | Plan advanced dengan fitur lanjutan |
| `enterprise` | Plan enterprise dengan fitur lengkap |

### Enum: `industry` (Industri Perusahaan)

| Nilai | Deskripsi |
| - | - |
| `retail` | Ritel dan perdagangan |
| `manufacturing` | Manufaktur dan pabrik |
| `services` | Jasa dan layanan |
| `technology` | Teknologi dan perangkat lunak |
| `food_beverage` | Makanan dan minuman (F\&B) |
| `healthcare` | Kesehatan dan farmasi |
| `education` | Pendidikan |
| `other` | Industri lainnya |

### Enum: `membership_duration_type` (Tipe Durasi Membership)

| Nilai | Deskripsi |
| - | - |
| `monthly` | Langganan bulanan |
| `yearly` | Langganan tahunan |
| `custom` | Durasi kustom |
| `lifetime` | Akses seumur hidup |

***

## RBAC (Role-Based Access Control) — Employee Portal

Tabel berikut mendefinisikan hak akses per role terhadap fitur-fitur di Employee Portal. Permission disimpan di field `CompanyMember.permissions`.

### Akses Fitur Employee Portal per Role

| Fitur | owner | admin | hr\_admin | finance\_admin | supervisor | employee |
| - | :-: | :-: | :-: | :-: | :-: | :-: |
| **Lihat profil sendiri** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Edit profil sendiri** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (terbatas) |
| **Lihat slip gaji sendiri** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Export slip gaji CSV** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Lihat absensi sendiri** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Clock-in / Clock-out** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Lihat cuti sendiri** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Ajukan cuti baru** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Batalkan cuti pending** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Ajukan kasbon** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Approve cuti karyawan lain** | ✅ | ✅ | ✅ | ❌ | ✅ (tim) | ❌ |
| **Approve kasbon karyawan lain** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ |
| **Lihat absensi tim** | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ |
| **Lihat cuti tim** | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ |

### Akses per Entitas (Employee Portal Scope)

| Entitas | owner | admin | hr\_admin | finance\_admin | supervisor | employee |
| - | :-: | :-: | :-: | :-: | :-: | :-: |
| **CompanyMember** | Full | Full | Read + Edit | Read only | Read (tim) | Read (self) |
| **Employee** | Full | Full | Full | Read only | Read (tim) | Read (self) |
| **CompanyPayroll** | Full | Full | Read only | Full | Read (tim) | Read (self) |
| **CompanyAttendance** | Full | Full | Full | Read only | Read (tim) | Read + Create (self) |
| **CompanyLeave** | Full | Full | Full (approve) | Read only | Read (tim) | Read + Create (self) |
| **CompanyLoan** | Full | Full | Read + Create | Full (approve) | Read (tim) | Read + Create (self) |
| **CompanyAttendanceSettings** | Full | Full | Full | Read only | Read only | Read only |
| **PayslipTemplate** | Full | Full | Read only | Read only | ❌ | ❌ |

### Catatan RLS (Row-Level Security)

Semua entitas di Employee Portal dilindungi oleh Row-Level Security (RLS). Karyawan dengan role `employee` hanya bisa mengakses data milik sendiri melalui kondisi berikut:

| Entitas | Kondisi RLS untuk Self-Access |
| - | - |
| `CompanyMember` | `data.user_id = user.id` ATAU `data.user_email = user.email` |
| `Employee` | `data.user_id = user.id` |
| `CompanyPayroll` | Hanya via `company_id` match (admin generate, employee baca) |
| `CompanyAttendance` | `data.employee_email = user.email` |
| `CompanyLeave` | `data.employee_email = user.email` |
| `CompanyLoan` | `data.employee_email = user.email` |

Admin global (`user_condition: { role: "admin" }`) memiliki akses penuh ke semua data tanpa batasan RLS.

***

## ER Diagram Lengkap — Semua Entitas Employee Portal

Diagram berikut menampilkan relasi lengkap termasuk entitas pendukung: `CompanyAttendanceSettings`, `PayslipTemplate`, dan `Company` dengan field detail.

```mermaid theme={null}
erDiagram
    User ||--|| CompanyMember : "autentikasi → profil"
    CompanyMember ||--|| Employee : "detail personalia"
    CompanyMember ||--o{ CompanyPayroll : "menerima slip gaji"
    CompanyMember ||--o{ CompanyAttendance : "mencatat absensi"
    CompanyMember ||--o{ CompanyLeave : "mengajukan cuti"
    CompanyMember ||--o{ CompanyLoan : "mengajukan kasbon"
    Company ||--|| CompanyMember : "memiliki"
    Company ||--o{ CompanyPayroll : "menghasilkan"
    Company ||--o{ CompanyAttendance : "mencatat"
    Company ||--o{ CompanyLeave : "mengelola"
    Company ||--o{ CompanyLoan : "mengelola"
    Company ||--|| CompanyAttendanceSettings : "konfigurasi absensi"
    Company ||--o{ PayslipTemplate : "template slip gaji"
    CompanyAttendanceSettings ||--o{ CompanyAttendance : "aturan absensi per record"
    PayslipTemplate ||--o{ CompanyPayroll : "format cetak slip"

    User {
        string id PK
        string email UK
        string full_name
        string role
        string active_company_id FK
        string subscription_plan
        datetime subscription_start
        datetime subscription_end
        string membership_duration_type
        date membership_start_date
        date membership_end_date
        boolean is_readonly_mode
        string admin_type
        string admin_tier
        number balance
        number productivity_score
        number user_level
    }

    Company {
        string id PK
        string name
        string owner_id FK
        string owner_email
        string description
        string industry
        string address
        string phone
        string email
        string website
        string logo_url
        string tax_id
        number employee_count
        string business_type
        string active_modules
        object settings
    }

    CompanyMember {
        string id PK
        string company_id FK
        string user_id FK
        string user_email
        string user_name
        string role
        string employee_id FK
        string department
        string position
        string status
        date joined_date
        string invited_by
        object permissions
        array assigned_locations
        object working_hours
        number salary
        string notes
    }

    Employee {
        string id PK
        string company_id FK
        string employee_id
        string user_id FK
        string full_name
        string email
        string phone
        string department
        string position
        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
        string description
    }

    CompanyPayroll {
        string id PK
        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
    }

    CompanyAttendance {
        string id PK
        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 id PK
        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
    }

    CompanyLoan {
        string id PK
        string company_id FK
        string employee_id FK
        string employee_name
        string employee_email
        number amount
        string reason
        string description
        string attachment_url
        number installment_months
        number monthly_deduction
        number remaining_amount
        string status
        string approver_id
        string approver_notes
        datetime approved_at
        datetime disbursed_at
    }

    CompanyAttendanceSettings {
        string id PK
        string company_id FK
        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
    }

    PayslipTemplate {
        string id PK
        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
    }
```

### Ringkasan Kardinalitas Relasi

| Relasi | Kardinalitas | Keterangan |
| - | - | - |
| `Company → CompanyAttendanceSettings` | 1:1 | Setiap perusahaan memiliki satu konfigurasi absensi |
| `Company → PayslipTemplate` | 1:N | Perusahaan bisa memiliki beberapa template slip gaji |
| `CompanyAttendanceSettings → CompanyAttendance` | 1:N | Satu pengaturan absensi berlaku untuk banyak record absensi |
| `PayslipTemplate → CompanyPayroll` | 1:N | Satu template bisa digunakan untuk banyak slip gaji |
| `User → CompanyMember` | 1:1 | Satu user memiliki satu membership aktif |
| `CompanyMember → Employee` | 1:1 | Satu member terhubung ke satu data personalia |
| `CompanyMember → CompanyPayroll` | 1:N | Satu member memiliki banyak slip gaji (per periode) |
| `CompanyMember → CompanyAttendance` | 1:N | Satu member memiliki banyak record absensi |
| `CompanyMember → CompanyLeave` | 1:N | Satu member bisa mengajukan banyak cuti |
| `CompanyMember → CompanyLoan` | 1:N | Satu member bisa mengajukan banyak kasbon |


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