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

# Company attendance

<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: "Company Attendance — Absensi Perusahaan"
description: "Manajemen absensi perusahaan dengan geofence multi-lokasi, shift, mirror camera, dan export CSV di SNISHOP ERP."
------------------------------------------------------------------------------------------------------------------------------

# Company Attendance — Absensi Perusahaan

<img src="https://mintcdn.com/quinnofspicy/e4f_upKhVWcjsUmM/docs/mintlify/screenshots/hr/company-attendance.png?fit=max&auto=format&n=e4f_upKhVWcjsUmM&q=85&s=06aa7591dc807c232672bf651c0a099b" alt="Company Attendance" width="1920" height="1080" data-path="docs/mintlify/screenshots/hr/company-attendance.png" />

Halaman Company Attendance adalah dashboard admin untuk mengelola dan memantau absensi seluruh karyawan. Mendukung geofence multi-lokasi, shift management, mirror camera untuk verifikasi wajah, dan export CSV.

## Arsitektur

```mermaid theme={null}
flowchart TB
    subgraph ADMIN["Admin View"]
        ONE[Tab: One<br/>Absensi per Karyawan]
        ALL[Tab: All<br/>Absensi Semua Karyawan]
        SET[Tab: Settings<br/>Konfigurasi]
    end

    subgraph CLOCK["Clock-In Flow"]
        CAM[Mirror Camera<br/>Face Oval Guide]
        GPS[GPS Acquisition]
        SHIFT[Shift Selector]
        GEO{Geofence<br/>Validation}
        SRV[Server Function<br/>recordAttendance]
        FB[Fallback<br/>Entity Direct]
    end

    subgraph SETTINGS["Attendance Settings"]
        LOC[Multi-Location<br/>Geofence]
        WH[Working Hours<br/>Start/End/Break]
        SH[Shifts<br/>Array]
        OT[Overtime<br/>Settings]
        VER[Verification<br/>Photo/Auto-Clockout]
    end

    ONE --> CAM
    ONE --> GPS
    ONE --> SHIFT
    GPS --> GEO
    GEO -->|Dalam Radius| SRV
    GEO -->|Luar Radius| WARN[Peringatan Lokasi]
    SRV -->|Gagal| FB
    SET --> LOC
    SET --> WH
    SET --> SH
    SET --> OT
    SET --> VER
```

## Akses Halaman

URL: `/company-attendance`

## Tiga Sub-Tab

| Tab | Label | Fungsi |
| - | - | - |
| `one` | Per Karyawan | Clock-in/out untuk satu karyawan dengan mirror camera |
| `all` | Semua Karyawan | Daftar absensi seluruh karyawan dengan filter |
| `settings` | Pengaturan | Konfigurasi geofence, shift, jam kerja |

## Entitas: CompanyAttendance

| Field | Tipe | Deskripsi |
| - | - | - |
| `company_id` | string | ID perusahaan |
| `employee_id` | string | ID karyawan |
| `employee_name` | string | Nama karyawan |
| `employee_email` | string | Email karyawan |
| `date` | date | Tanggal absensi |
| `clock_in_time` | datetime | Waktu clock-in |
| `clock_out_time` | datetime | Waktu clock-out |
| `clock_in_photo_url` | string | URL foto clock-in |
| `clock_out_photo_url` | string | URL foto clock-out |
| `clock_in_location` | object | `{ lat, lng }` GPS clock-in |
| `clock_out_location` | object | `{ lat, lng }` GPS clock-out |
| `status` | enum | `present`, `late`, atau `wfh` |
| `late_minutes` | number | Menit keterlambatan |
| `total_hours` | number | Total jam kerja |
| `overtime_hours` | number | Jam lembur |
| `shift_id` | string | ID shift yang digunakan |
| `notes` | string | Catatan |

## Geofence Validation

Sistem menggunakan **Haversine formula** untuk memvalidasi jarak antara posisi GPS karyawan dan lokasi kantor:

```
distance = 2R × arcsin(√(sin²((lat₂-lat₁)/2) + cos(lat₁)×cos(lat₂)×sin²((lng₂-lng₁)/2)))
```

| Parameter | Sumber |
| - | - |
| `office_location` | `CompanyAttendanceSettings.office_location` (lat/lng) |
| `office_radius_meters` | Radius yang diizinkan (meter) |
| `clock_in_location` | GPS dari browser karyawan |

Jika `distance > office_radius_meters`, sistem menampilkan peringatan lokasi di luar area yang diizinkan.

## Multi-Location Geofence

Perusahaan dengan beberapa lokasi kerja bisa mengkonfigurasi banyak geofence:

| Field per Lokasi | Deskripsi |
| - | - |
| `location_id` | ID unik lokasi |
| `name` | Nama lokasi (misal "Kantor Pusat", "Cabang Surabaya") |
| `address` | Alamat lengkap |
| `latitude` | Latitude GPS |
| `longitude` | Longitude GPS |
| `radius_meters` | Radius geofence (meter) |
| `accuracy_tolerance_meters` | Toleransi akurasi GPS |
| `assigned_employee_ids` | Karyawan yang ditugaskan di lokasi ini |
| `is_active` | Status aktif/nonaktif |

## Shift Management

Shift dikonfigurasi dalam array di `CompanyAttendanceSettings`:

| Field per Shift | Deskripsi |
| - | - |
| `shift_id` | ID unik shift |
| `shift_name` | Nama shift (misal "Pagi", "Siang", "Malam") |
| `start_time` | Jam mulai |
| `end_time` | Jam selesai |
| `break_duration_minutes` | Durasi istirahat (menit) |

Jika shift tersedia, karyawan harus memilih shift sebelum clock-in.

## Mirror Camera

Tab "One" menggunakan mirror camera dengan **face oval guide overlay** untuk verifikasi wajah:

1. Kamera depan aktif dengan tampilan mirror
2. Overlay oval menampilkan posisi wajah yang diharapkan
3. Foto di-capture otomatis saat wajah terdeteksi di posisi yang benar
4. Foto di-compress dan diupload ke Cloudinary

## Photo Detail Dialog

Admin bisa melihat foto clock-in dan clock-out secara berdampingan:

| Info Ditampilkan | Detail |
| - | - |
| Foto Clock-In | Side-by-side dengan foto clock-out |
| Koordinat GPS | Latitude dan longitude |
| Google Maps Link | Klik untuk buka lokasi di Google Maps |
| Jarak Clock-In/Out | Selisih jarak antara lokasi clock-in dan clock-out |

## Server Function: recordAttendance

Absensi perusahaan menggunakan server function sebagai primary path:

```
callRecordAttendanceFunction(payload)
  → base44.functions.invoke('recordAttendance', payload)
  → fallback: base44Raw.entities.CompanyAttendance.create/update
```

## Filter & Export

### Filter

| Filter | Opsi |
| - | - |
| Status | All, Present, Late, WFH |
| Tanggal | Date range picker |
| Karyawan | Dropdown pilih karyawan |

### Export CSV

Data absensi bisa diexport ke CSV dengan filter tanggal. File CSV berisi seluruh record absensi dalam periode yang dipilih.

## Entitas: CompanyAttendanceSettings

| Field | Tipe | Deskripsi |
| - | - | - |
| `company_id` | string | ID perusahaan |
| `require_office_location` | boolean | Wajibkan clock-in di lokasi kantor |
| `office_radius_meters` | number | Radius geofence kantor |
| `office_location` | object | `{ lat, lng, address }` |
| `overtime_settings` | object | `{ enabled, start_after_hours, max_overtime_hours_per_day }` |
| `late_tolerance_minutes` | number | Toleransi terlambat (menit) |
| `working_hours` | object | `{ start_time, end_time, break_duration_minutes }` |
| `shifts` | array | Daftar shift |
| `locations` | array | Daftar lokasi geofence |
| `default_accuracy_tolerance_meters` | number | Toleransi akurasi GPS default |
| `working_days` | array | Hari kerja aktif |
| `auto_clock_out_enabled` | boolean | Auto clock-out otomatis |
| `auto_clock_out_time` | string | Waktu auto clock-out |
| `require_photo` | boolean | Wajibkan foto saat absen |
| `require_notes` | boolean | Wajibkan catatan |

## Live Work Timer

Komponen `LiveWorkTimer` menampilkan durasi kerja secara real-time:

* Update setiap 1 detik
* Pesan motivasional berdasarkan waktu (pagi/siang/sore)
* Progress bar menuju target jam harian
* Indikator status overtime

## Komponen: AttendanceSettingsForm

Form konfigurasi absensi (916 baris) mencakup:

| Section | Konfigurasi |
| - | - |
| Lokasi | Multi-location geofence dengan radius dan employee assignment |
| Jam Kerja | Start/end time, break duration, working days |
| Shift | CRUD shift dengan nama, waktu, dan durasi istirahat |
| Lembur | Enable/disable, start after X jam, max overtime per hari |
| Verifikasi | Require photo, auto clock-out, late tolerance |

***

## Entity Relationship Diagram

```mermaid theme={null}
erDiagram
    Company ||--o{ CompanyAttendance : "memiliki"
    Company ||--|| CompanyAttendanceSettings : "mengkonfigurasi"
    Company ||--o{ CompanyLeave : "mengelola"
    Employee ||--o{ CompanyAttendance : "mencatat"
    Employee }o--|| Company : "bekerja di"
    Employee }o--o| User : "terhubung ke"
    Employee ||--o{ CompanyLeave : "mengajukan"
    Employee ||--o{ AttendanceRecord : "mencatat"
    CompanyLeave }o--|| Employee : "disetujui oleh"
    AttendanceRecord }o--|| User : "dimiliki oleh"

    CompanyAttendanceSettings {
        string company_id PK
        object office_location "lat, lng, address"
        number office_radius_meters
        boolean require_office_location
        number default_accuracy_tolerance_meters
        array locations "multi-geofence"
        object working_hours "start, end, break"
        array shifts "daftar shift"
        object overtime_settings "enabled, threshold, max"
        number late_tolerance_minutes
        array working_days "1=Senin..7=Minggu"
        boolean auto_clock_out_enabled
        string auto_clock_out_time
        boolean require_photo
        boolean require_notes
    }

    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 "lat, lng, accuracy"
        object clock_out_location "lat, lng, accuracy"
        string status "enum"
        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
    }

    AttendanceRecord {
        string user_id FK
        string workspace_id FK
        date date
        datetime clock_in_time
        datetime clock_out_time
        string clock_in_photo_url
        string clock_out_photo_url
        object clock_in_location "lat, lng, address"
        object clock_out_location "lat, lng, address"
        string status "enum"
        string notes
        number total_hours
    }

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

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

    Employee {
        string company_id FK
        string user_id FK
        string employee_id PK
        string full_name
        string email
        string phone
        string department "enum"
        string position
        string employment_type "enum"
        date hire_date
        number salary
        string status "enum"
    }

    Company {
        string name
        string owner_id FK
        string owner_email
        string owner_subscription_plan "enum"
        string industry "enum"
        string address
        string phone
        string email
        string website
        string logo_url
        string tax_id "NPWP"
        number employee_count
        object settings "working_hours, leave_policy, tax"
    }
```

## Tabel Schema Lengkap

### CompanyAttendance (28 field)

| # | Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - | - |
| 1 | `company_id` | string | Ya | — | ID perusahaan pemilik record |
| 2 | `employee_id` | string | Ya | — | ID karyawan yang absen |
| 3 | `employee_name` | string | Tidak | — | Nama lengkap karyawan (denormalized) |
| 4 | `employee_email` | string | Tidak | — | Email karyawan (denormalized) |
| 5 | `date` | date | Ya | — | Tanggal absensi (YYYY-MM-DD) |
| 6 | `shift_id` | string | Tidak | — | ID shift yang dipilih karyawan |
| 7 | `shift_name` | string | Tidak | — | Nama shift (Pagi/Siang/Malam) |
| 8 | `clock_in_time` | datetime | Ya | — | Waktu clock-in (ISO 8601) |
| 9 | `clock_out_time` | datetime | Tidak | — | Waktu clock-out (ISO 8601) |
| 10 | `clock_in_photo_url` | string | Tidak | — | URL foto selfie saat clock-in (Cloudinary) |
| 11 | `clock_out_photo_url` | string | Tidak | — | URL foto selfie saat clock-out (Cloudinary) |
| 12 | `clock_in_location` | object | Tidak | — | GPS clock-in: `{ latitude, longitude, accuracy }` |
| 13 | `clock_out_location` | object | Tidak | — | GPS clock-out: `{ latitude, longitude, accuracy }` |
| 14 | `status` | enum | Tidak | `present` | Status absensi: `present`, `late`, `absent`, `sick`, `leave`, `wfh` |
| 15 | `notes` | string | Tidak | — | Catatan tambahan dari karyawan |
| 16 | `total_hours` | number | Tidak | — | Total jam kerja pada hari tersebut |
| 17 | `overtime_hours` | number | Tidak | `0` | Jam lembur yang tercatat |
| 18 | `distance_from_office` | number | Tidak | — | Jarak dari kantor/geofence pusat dalam meter |
| 19 | `location_id` | string | Tidak | — | ID lokasi geofence absensi terpilih |
| 20 | `location_name` | string | Tidak | — | Nama lokasi geofence terpilih (misal: Head Office) |
| 21 | `clock_in_accuracy` | number | Tidak | — | Akurasi GPS saat clock-in dalam meter |
| 22 | `clock_out_accuracy` | number | Tidak | — | Akurasi GPS saat clock-out dalam meter |
| 23 | `is_manual_override` | boolean | Tidak | `false` | Flag absensi dikoreksi manual berizin |
| 24 | `override_reason` | string | Tidak | — | Alasan pengajuan koreksi manual |
| 25 | `override_approved_by` | string | Tidak | — | ID user/manager yang menyetujui koreksi |
| 26 | `override_approved_at` | datetime | Tidak | — | Waktu persetujuan koreksi manual |
| 27 | `created_by_id` | string | Auto | — | ID user pembuat record (system) |
| 28 | `created_at` | datetime | Auto | — | Timestamp pembuatan record (system) |

### CompanyAttendanceSettings (16 field)

| # | Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - | - |
| 1 | `company_id` | string | Ya | — | ID perusahaan (unique per company) |
| 2 | `description` | string | Tidak | — | Penjelasan kebijakan absensi (max 1000 char) |
| 3 | `office_location` | object | Tidak | — | Lokasi kantor utama: `{ latitude, longitude, address }` |
| 4 | `office_radius_meters` | number | Tidak | `100` | Radius geofence kantor legacy (meter) |
| 5 | `require_office_location` | boolean | Tidak | `false` | Wajibkan clock-in di lokasi kantor |
| 6 | `default_accuracy_tolerance_meters` | number | Tidak | `150` | Toleransi akurasi GPS default (meter) |
| 7 | `locations` | array | Tidak | `[]` | Daftar multi-lokasi geofence absensi |
| 8 | `working_hours` | object | Tidak | — | Jam kerja: `{ start_time, end_time, break_duration_minutes }` |
| 9 | `shifts` | array | Tidak | `[]` | Daftar shift kerja yang tersedia |
| 10 | `overtime_settings` | object | Tidak | — | Pengaturan lembur: `{ enabled, start_after_hours, max_overtime_hours_per_day }` |
| 11 | `late_tolerance_minutes` | number | Tidak | `15` | Toleransi keterlambatan dalam menit |
| 12 | `working_days` | array | Tidak | `[1,2,3,4,5]` | Hari kerja aktif (1=Senin, 7=Minggu) |
| 13 | `auto_clock_out_enabled` | boolean | Tidak | `false` | Otomatis clock-out jika karyawan lupa |
| 14 | `auto_clock_out_time` | string | Tidak | `"18:00"` | Jam auto clock-out (HH:MM) |
| 15 | `require_photo` | boolean | Tidak | `true` | Wajibkan foto selfie saat absen |
| 16 | `require_notes` | boolean | Tidak | `false` | Wajibkan catatan saat absen |

### AttendanceRecord (13 field)

| # | Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - | - |
| 1 | `user_id` | string | Ya | — | ID user pemilik record absensi |
| 2 | `workspace_id` | string | Ya | — | ID workspace tempat absensi dicatat |
| 3 | `date` | date | Ya | — | Tanggal absensi (YYYY-MM-DD) |
| 4 | `clock_in_time` | datetime | Ya | — | Waktu clock-in (ISO 8601) |
| 5 | `clock_out_time` | datetime | Tidak | — | Waktu clock-out (ISO 8601) |
| 6 | `clock_in_photo_url` | string | Tidak | — | URL foto selfie saat clock-in |
| 7 | `clock_out_photo_url` | string | Tidak | — | URL foto selfie saat clock-out |
| 8 | `clock_in_location` | object | Tidak | — | GPS clock-in: `{ latitude, longitude, address }` |
| 9 | `clock_out_location` | object | Tidak | — | GPS clock-out: `{ latitude, longitude, address }` |
| 10 | `status` | enum | Tidak | `present` | Status absensi: `present`, `late`, `absent`, `sick`, `leave` |
| 11 | `notes` | string | Tidak | — | Catatan tambahan dari user |
| 12 | `total_hours` | number | Tidak | — | Total jam kerja pada hari tersebut |
| 13 | `created_by_id` | string | Auto | — | ID user pembuat record (system) |

### CompanyLeave (15 field)

| # | Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - | - |
| 1 | `company_id` | string | Ya | — | ID perusahaan pemilik record |
| 2 | `employee_id` | string | Ya | — | ID karyawan yang mengajukan cuti |
| 3 | `employee_name` | string | Tidak | — | Nama lengkap karyawan (denormalized) |
| 4 | `employee_email` | string | Tidak | — | Email karyawan (denormalized) |
| 5 | `leave_type` | enum | Ya | `annual` | Jenis cuti: `annual`, `sick`, `unpaid`, `maternity`, `paternity`, `emergency` |
| 6 | `start_date` | date | Ya | — | Tanggal mulai cuti (YYYY-MM-DD) |
| 7 | `end_date` | date | Ya | — | Tanggal selesai cuti (YYYY-MM-DD) |
| 8 | `total_days` | number | Tidak | — | Jumlah hari cuti (dihitung otomatis) |
| 9 | `reason` | string | Ya | — | Alasan pengajuan cuti |
| 10 | `description` | string | Tidak | — | Keterangan tambahan atau dokumen pendukung (max 1000 char) |
| 11 | `attachment_url` | string | Tidak | — | URL file lampiran (surat dokter, dll) |
| 12 | `status` | enum | Tidak | `pending` | Status pengajuan: `pending`, `approved`, `rejected`, `cancelled` |
| 13 | `approver_id` | string | Tidak | — | ID approver (manager/admin) yang memproses |
| 14 | `approver_notes` | string | Tidak | — | Catatan dari approver saat menyetujui/menolak |
| 15 | `approved_at` | datetime | Tidak | — | Waktu persetujuan/penolakan (ISO 8601) |

### Leave (13 field)

| # | Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - | - |
| 1 | `company_id` | string | Ya | — | ID perusahaan |
| 2 | `employee_id` | string | Ya | — | ID karyawan yang mengajukan cuti |
| 3 | `employee_name` | string | Tidak | — | Nama lengkap karyawan (denormalized) |
| 4 | `leave_type` | enum | Ya | `annual` | Jenis cuti: `annual`, `sick`, `unpaid`, `maternity`, `paternity`, `emergency` |
| 5 | `start_date` | date | Ya | — | Tanggal mulai cuti (YYYY-MM-DD) |
| 6 | `end_date` | date | Ya | — | Tanggal selesai cuti (YYYY-MM-DD) |
| 7 | `total_days` | number | Tidak | — | Jumlah hari cuti |
| 8 | `reason` | string | Ya | — | Alasan pengajuan cuti |
| 9 | `description` | string | Tidak | — | Detail tambahan mengenai pengajuan cuti (max 1000 char) |
| 10 | `attachment_url` | string | Tidak | — | URL file lampiran (surat dokter, dll) |
| 11 | `status` | enum | Tidak | `pending` | Status pengajuan: `pending`, `approved`, `rejected`, `cancelled` |
| 12 | `approver_id` | string | Tidak | — | ID approver yang memproses |
| 13 | `approver_notes` | string | Tidak | — | Catatan dari approver |
| 14 | `approved_at` | datetime | Tidak | — | Waktu persetujuan/penolakan |

### Employee (18 field)

| # | Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - | - |
| 1 | `company_id` | string | Ya | — | ID perusahaan |
| 2 | `user_id` | string | Tidak | — | Link ke entitas User |
| 3 | `employee_id` | string | Ya | — | ID unik karyawan |
| 4 | `full_name` | string | Ya | — | Nama lengkap karyawan |
| 5 | `email` | string | Ya | — | Email karyawan |
| 6 | `phone` | string | Tidak | — | Nomor telepon |
| 7 | `department` | enum | Ya | — | Departemen: `Management`, `Sales`, `Marketing`, `Operations`, `Finance`, `IT`, `HR`, `Customer Service` |
| 8 | `position` | string | Ya | — | Jabatan karyawan |
| 9 | `description` | string | Tidak | — | Catatan atau deskripsi tambahan mengenai karyawan (max 1000 char) |
| 10 | `employment_type` | enum | Tidak | `full_time` | Tipe employment: `full_time`, `part_time`, `contract`, `intern` |
| 11 | `hire_date` | date | Ya | — | Tanggal mulai bekerja (YYYY-MM-DD) |
| 12 | `salary` | number | Tidak | — | Gaji bulanan karyawan |
| 13 | `bank_account` | object | Tidak | — | Info rekening bank: `{ bank_name, account_number, account_name }` |
| 14 | `emergency_contact` | object | Tidak | — | Kontak darurat: `{ name, relationship, phone }` |
| 15 | `address` | string | Tidak | — | Alamat tempat tinggal karyawan |
| 16 | `date_of_birth` | date | Tidak | — | Tanggal lahir karyawan (YYYY-MM-DD) |
| 17 | `status` | enum | Tidak | `active` | Status karyawan: `active`, `on_leave`, `terminated` |
| 18 | `avatar_url` | string | Tidak | — | URL foto profil/avatar karyawan |

### Company (20 field)

| # | Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - | - |
| 1 | `name` | string | Ya | — | Nama perusahaan |
| 2 | `owner_id` | string | Ya | — | ID owner perusahaan |
| 3 | `owner_email` | string | Ya | — | Email owner |
| 4 | `owner_subscription_plan` | enum | Tidak | — | Plan langganan: `free`, `pro`, `business`, `advanced`, `enterprise` |
| 5 | `description` | string | Tidak | — | Deskripsi perusahaan |
| 6 | `industry` | enum | Tidak | — | Industri: `retail`, `manufacturing`, `services`, `technology`, `food_beverage`, `healthcare`, `education`, `other` |
| 7 | `address` | string | Tidak | — | Alamat perusahaan |
| 8 | `phone` | string | Tidak | — | Nomor telepon perusahaan |
| 9 | `email` | string | Tidak | — | Email perusahaan |
| 10 | `website` | string | Tidak | — | Website perusahaan |
| 11 | `logo_url` | string | Tidak | — | URL logo perusahaan |
| 12 | `tax_id` | string | Tidak | — | NPWP perusahaan |
| 13 | `employee_count` | number | Tidak | `0` | Jumlah karyawan |
| 14 | `metadata` | object | Tidak | — | Data tambahan / legacy |
| 15 | `landing_page_config` | object | Tidak | — | Konfigurasi landing page perusahaan |
| 16 | `business_type` | string | Tidak | — | Kategori bisnis dari onboarding |
| 17 | `active_modules` | string | Tidak | — | JSON string berisi array ID modul aktif |
| 18 | `settings` | object | Tidak | — | Pengaturan perusahaan: working\_hours, leave\_policy, tax, expiry\_thresholds |
| 19 | `created_by_id` | string | Auto | — | ID user pembuat record (system) |
| 20 | `created_at` | datetime | Auto | — | Timestamp pembuatan record (system) |

***

## Schema Nested Object — Detail Sub-Field

### CompanyAttendanceSettings.locations\[] (Sub-Field per Lokasi Geofence)

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

### CompanyAttendanceSettings.shifts\[] (Sub-Field per Shift)

| # | Sub-Field | Tipe | Required | Default | Deskripsi |
| - | - | - | - | - | - |
| 1 | `shift_id` | string | Ya | — | ID unik shift |
| 2 | `shift_name` | string | Ya | — | Nama shift (misal: Shift Pagi, Shift Siang) |
| 3 | `start_time` | string | Ya | — | Jam mulai shift (HH:MM) |
| 4 | `end_time` | string | Ya | — | Jam selesai shift (HH:MM) |
| 5 | `break_duration_minutes` | number | Tidak | `60` | Durasi istirahat shift dalam menit |

### CompanyAttendanceSettings.working\_hours (Sub-Field)

| # | Sub-Field | Tipe | Default | Deskripsi |
| - | - | - | - | - |
| 1 | `start_time` | string | `"09:00"` | Jam mulai kerja (HH:MM) |
| 2 | `end_time` | string | `"17:00"` | Jam selesai kerja (HH:MM) |
| 3 | `break_duration_minutes` | number | `60` | Durasi istirahat dalam menit |

### CompanyAttendanceSettings.overtime\_settings (Sub-Field)

| # | Sub-Field | Tipe | Default | Deskripsi |
| - | - | - | - | - |
| 1 | `enabled` | boolean | `true` | Apakah fitur lembur diaktifkan |
| 2 | `start_after_hours` | number | `8` | Lembur dimulai setelah X jam kerja |
| 3 | `max_overtime_hours_per_day` | number | `4` | Maksimal jam lembur per hari |

### CompanyAttendance.clock\_in\_location / clock\_out\_location (Sub-Field)

| # | Sub-Field | Tipe | Deskripsi |
| - | - | - | - |
| 1 | `latitude` | number | Garis lintang posisi GPS |
| 2 | `longitude` | number | Garis bujur posisi GPS |
| 3 | `accuracy` | number | Akurasi GPS dalam meter |

### AttendanceRecord.clock\_in\_location / clock\_out\_location (Sub-Field)

| # | Sub-Field | Tipe | Deskripsi |
| - | - | - | - |
| 1 | `latitude` | number | Garis lintang posisi GPS |
| 2 | `longitude` | number | Garis bujur posisi GPS |
| 3 | `address` | string | Alamat teks dari reverse geocoding |

### Employee.bank\_account (Sub-Field)

| # | Sub-Field | Tipe | Deskripsi |
| - | - | - | - |
| 1 | `bank_name` | string | Nama bank (misal: BCA, Mandiri, BNI) |
| 2 | `account_number` | string | Nomor rekening bank |
| 3 | `account_name` | string | Nama pemilik rekening |

### Employee.emergency\_contact (Sub-Field)

| # | Sub-Field | Tipe | Deskripsi |
| - | - | - | - |
| 1 | `name` | string | Nama kontak darurat |
| 2 | `relationship` | string | Hubungan dengan karyawan (misal: istri, suami, orang tua) |
| 3 | `phone` | string | Nomor telepon kontak darurat |

### Company.settings (Sub-Field)

| # | Sub-Field | Tipe | Default | Deskripsi |
| - | - | - | - | - |
| 1 | `working_hours_start` | string | `"09:00"` | Jam mulai kerja default |
| 2 | `working_hours_end` | string | `"17:00"` | Jam selesai kerja default |
| 3 | `working_days` | array\[number] | `[1,2,3,4,5]` | Hari kerja aktif (1=Senin, 7=Minggu) |
| 4 | `leave_policy` | object | — | Kebijakan cuti perusahaan |
| 5 | `expiry_thresholds` | object | — | Ambang batas peringatan kedaluwarsa: `{ critical: 7, warning: 30, info: 90 }` (hari) |
| 6 | `batch_allocation_strategy` | enum | `"fifo"` | Strategi alokasi batch: `fifo` atau `fefo` |
| 7 | `tax` | object | — | Konfigurasi pajak: `{ enabled, rate, mode, rounding }` |
| 8 | `spoilage_default_treatment` | enum | `"absorbed_normal"` | Perlakuan spoilage: `absorbed_normal` atau `expense_abnormal` |

***

## State Machine — Status Absensi

```mermaid theme={null}
stateDiagram-v2
    [*] --> present: Clock-in tepat waktu
    [*] --> late: Clock-in melewati toleransi
    [*] --> absent: Tidak ada clock-in & tidak ada keterangan
    [*] --> sick: Pengajuan sakit disetujui
    [*] --> leave: Pengajuan cuti disetujui
    [*] --> wfh: Clock-in remote (WFH) disetujui

    present --> present: Clock-out tercatat
    late --> late: Clock-out tercatat
    wfh --> wfh: Clock-out tercatat

    present --> present: Koreksi manual (is_manual_override=true)
    late --> present: Koreksi manual disetujui
    absent --> present: Koreksi manual disetujui
    sick --> sick: Tetap (tidak berubah)
    leave --> leave: Tetap (tidak berubah)

    note right of present: Status default\njika clock-in\n<= late_tolerance
    note right of late: clock_in_time >\nshift_start +\nlate_tolerance_minutes
    note right of absent: Tidak ada record\nclock-in sama sekali
    note right of wfh: distance_from_office\n> radius ATAU\nstatus remote eksplisit
```

### Transisi Status

| Dari | Ke | Trigger | Kondisi |
| - | - | - | - |
| `[*]` | `present` | Clock-in | `clock_in_time <= shift_start + late_tolerance_minutes` |
| `[*]` | `late` | Clock-in | `clock_in_time > shift_start + late_tolerance_minutes` |
| `[*]` | `absent` | End-of-day | Tidak ada record clock-in untuk hari tersebut |
| `[*]` | `sick` | Approval | Pengajuan sakit disetujui manager |
| `[*]` | `leave` | Approval | Pengajuan cuti disetujui manager |
| `[*]` | `wfh` | Clock-in remote | Karyawan clock-in dari luar geofence dengan izin |
| `late` | `present` | Manual override | `is_manual_override=true` dan `override_approved_by` terisi |
| `absent` | `present` | Manual override | Koreksi manual disetujui admin/manager |

## State Machine — Status Pengajuan Cuti (CompanyLeave)

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending: Karyawan mengajukan cuti

    pending --> approved: Manager/Admin menyetujui
    pending --> rejected: Manager/Admin menolak
    pending --> cancelled: Karyawan membatalkan pengajuan

    approved --> approved: Final (tidak berubah)
    rejected --> rejected: Final (tidak berubah)
    cancelled --> cancelled: Final (tidak berubah)

    note right of pending: Menunggu persetujuan\nmanager/admin
    note right of approved: Cuti disetujui\nstatus absensi = leave\npada tanggal cuti
    note right of rejected: Cuti ditolak\nkaryawan wajib masuk kerja
    note right of cancelled: Dibatalkan oleh karyawan\nsebelum diproses
```

### Transisi Status Cuti

| Dari | Ke | Trigger | Kondisi |
| - | - | - | - |
| `[*]` | `pending` | Submit | Karyawan mengirim pengajuan cuti dengan `reason` dan `start_date`/`end_date` |
| `pending` | `approved` | Approve | Manager/admin menyetujui; `approver_id` dan `approved_at` terisi |
| `pending` | `rejected` | Reject | Manager/admin menolak; `approver_notes` berisi alasan penolakan |
| `pending` | `cancelled` | Cancel | Karyawan membatalkan sebelum diproses |

***

## Sequence Diagrams

### 1. GPS Check-In Flow

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan (Browser)
    participant CAM as Mirror Camera
    participant GPS as GPS API
    participant SRV as Server Function<br/>recordAttendance
    participant DB as CompanyAttendance<br/>Entity

    K->>CAM: Aktifkan kamera depan
    CAM-->>K: Preview + face oval guide
    K->>CAM: Capture foto selfie
    CAM-->>K: Foto di-compress (Blob)
    K->>K: Upload foto ke Cloudinary
    K-->>K: Dapatkan clock_in_photo_url

    K->>GPS: navigator.geolocation.getCurrentPosition()
    GPS-->>K: { latitude, longitude, accuracy }

    K->>SRV: invoke('recordAttendance', payload)
    Note right of SRV: payload: company_id, employee_id,<br/>date, clock_in_time, clock_in_photo_url,<br/>clock_in_location, shift_id

    SRV->>SRV: Hitung distance_from_office<br/>(Haversine formula)
    SRV->>SRV: Tentukan status<br/>(present/late/wfh)

    SRV->>DB: create / update record
    DB-->>SRV: Record tersimpan

    SRV-->>K: Response: { success, record }
    K-->>K: Tampilkan konfirmasi + LiveWorkTimer
```

### 2. Geofence Validation Flow

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan
    participant GEO as Geofence Validator
    participant LOC as locations[]<br/>(Settings)
    participant WARN as Warning UI

    K->>GEO: Submit clock_in_location { lat, lng, accuracy }

    GEO->>LOC: Ambil daftar lokasi geofence aktif
    LOC-->>GEO: locations[] (masing-masing dengan lat, lng, radius_meters)

    loop Untuk setiap lokasi aktif
        GEO->>GEO: haversine(clock_in_location, location)
        GEO->>GEO: Cek: distance <= radius_meters?
        GEO->>GEO: Cek: accuracy <= accuracy_tolerance_meters?
    end

    alt Lokasi ditemukan dalam radius
        GEO-->>K: Valid: clock-in diizinkan
        Note right of GEO: status = present/late<br/>location_id & location_name tercatat
    else Lokasi tidak ditemukan (semua di luar radius)
        GEO->>WARN: Tampilkan peringatan lokasi
        WARN-->>K: "Lokasi di luar area yang diizinkan"
        alt require_office_location = true
            GEO-->>K: Tolak clock-in
        else require_office_location = false
            GEO-->>K: Izinkan dengan warning<br/>status = wfh
        end
    else Akurasi GPS buruk (> tolerance)
        GEO-->>K: "Akurasi GPS terlalu rendah, coba di ruang terbuka"
    end
```

### 3. Overtime Calculation Flow

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan
    participant CLK as Clock-Out Handler
    participant CALC as Overtime Calculator
    participant SET as overtime_settings
    participant DB as CompanyAttendance

    K->>CLK: Clock-out (atau auto clock-out)
    CLK->>CLK: Hitung total_hours =<br/>clock_out - clock_in - break

    CLK->>CALC: Hitung overtime
    CALC->>SET: Baca overtime_settings
    SET-->>CALC: { enabled, start_after_hours, max_overtime_hours_per_day }

    alt overtime_settings.enabled = true
        CALC->>CALC: raw_overtime = total_hours - start_after_hours
        alt raw_overtime > 0
            CALC->>CALC: overtime_hours = MIN(raw_overtime, max_overtime_hours_per_day)
        else raw_overtime <= 0
            CALC->>CALC: overtime_hours = 0
        end
    else overtime_settings.enabled = false
        CALC->>CALC: overtime_hours = 0
    end

    CALC-->>CLK: { total_hours, overtime_hours }
    CLK->>DB: Update record: total_hours, overtime_hours
    DB-->>CLK: Record diperbarui
    CLK-->>K: Tampilkan ringkasan: total jam + lembur
```

### 4. Leave Application & Approval Flow

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan
    participant UI as Form Pengajuan Cuti
    participant DB as CompanyLeave<br/>Entity
    participant MGR as Manager/Admin
    participant ATT as CompanyAttendance

    K->>UI: Isi form: leave_type, start_date, end_date, reason
    K->>UI: Upload lampiran (surat dokter, dll)
    UI->>DB: Create CompanyLeave record
    Note right of DB: status = pending<br/>total_days = hitung selisih tanggal
    DB-->>UI: Record tersimpan
    UI-->>K: Konfirmasi: "Pengajuan cuti terkirim"

    MGR->>DB: Lihat daftar pengajuan cuti pending
    DB-->>MGR: Daftar pengajuan pending

    alt Manager menyetujui
        MGR->>DB: Update: status=approved, approver_id, approved_at
        DB-->>ATT: Sinkronisasi: tanggal cuti → status absent/leave
        ATT->>ATT: Set status = leave pada rentang tanggal cuti
    else Manager menolak
        MGR->>DB: Update: status=rejected, approver_notes
        DB-->>K: Notifikasi: "Pengajuan cuti ditolak"
    end
```

### 5. Auto Clock-Out Flow

```mermaid theme={null}
sequenceDiagram
    participant CRON as Cron Job<br/>(Sistem)
    participant SET as CompanyAttendanceSettings
    participant DB as CompanyAttendance
    participant K as Karyawan (Notifikasi)

    CRON->>CRON: Trigger pada auto_clock_out_time
    CRON->>SET: Baca auto_clock_out_enabled & auto_clock_out_time
    SET-->>CRON: { enabled: true, time: "18:00" }

    CRON->>DB: Cari record dengan clock_in_time ada<br/>tapi clock_out_time = null (hari ini)
    DB-->>CRON: Daftar record yang belum clock-out

    loop Untuk setiap record yang belum clock-out
        CRON->>DB: Update: clock_out_time = auto_clock_out_time
        CRON->>DB: Hitung total_hours & overtime_hours
        DB-->>CRON: Record diperbarui
        CRON->>K: Notifikasi: "Anda belum clock-out, sistem mencatat otomatis"
    end
```

***

## Tabel Enum

### attendance\_status (Status Absensi)

| Nilai | Label (ID) | Deskripsi |
| - | - | - |
| `present` | Hadir | Karyawan clock-in tepat waktu (dalam toleransi keterlambatan) |
| `late` | Terlambat | Karyawan clock-in melewati `late_tolerance_minutes` dari jam mulai shift |
| `absent` | Absen | Tidak ada record clock-in pada hari kerja tersebut |
| `sick` | Sakit | Karyawan mengajukan surat sakit dan disetujui |
| `leave` | Cuti | Karyawan mengajukan cuti dan disetujui |
| `wfh` | Kerja Remote | Karyawan clock-in dari luar geofence dengan izin (Work From Home) |

### check\_type (Tipe Absensi)

| Nilai | Label (ID) | Deskripsi |
| - | - | - |
| `clock_in` | Clock-In | Karyawan mencatat waktu masuk; menghasilkan `clock_in_time`, `clock_in_photo_url`, `clock_in_location` |
| `clock_out` | Clock-Out | Karyawan mencatat waktu keluar; menghasilkan `clock_out_time`, `clock_out_photo_url`, `clock_out_location` |
| `auto_clock_out` | Auto Clock-Out | Sistem otomatis mencatat clock-out pada `auto_clock_out_time` jika karyawan lupa |
| `manual_override` | Koreksi Manual | Admin/manager menyetujui koreksi absensi berizin; men-set `is_manual_override=true` |

### leave\_type (Jenis Cuti)

| Nilai | Label (ID) | Deskripsi |
| - | - | - |
| `annual` | Cuti Tahunan | Cuti tahunan reguler yang dialokasikan perusahaan |
| `sick` | Sakit | Cuti karena sakit dengan surat keterangan dokter |
| `unpaid` | Cuti Tanpa Bayaran | Cuti di luar tanggungan perusahaan |
| `maternity` | Cuti Melahirkan | Cuti melahirkan sesuai ketentuan regulasi |
| `paternity` | Cuti Ayah | Cuti melahirkan untuk ayah |
| `emergency` | Cuti Darurat | Cuti karena keadaan darurat (kematian keluarga, bencana, dll) |

### leave\_status (Status Pengajuan Cuti)

| Nilai | Label (ID) | Deskripsi |
| - | - | - |
| `pending` | Menunggu | Pengajuan cuti belum diproses oleh manager/admin |
| `approved` | Disetujui | Pengajuan cuti disetujui oleh manager/admin |
| `rejected` | Ditolak | Pengajuan cuti ditolak oleh manager/admin |
| `cancelled` | Dibatalkan | Pengajuan cuti dibatalkan oleh karyawan sebelum diproses |

### employment\_type (Tipe Employment Karyawan)

| Nilai | Label (ID) | Deskripsi |
| - | - | - |
| `full_time` | Full-Time | Karyawan tetap dengan jam kerja penuh |
| `part_time` | Part-Time | Karyawan paruh waktu |
| `contract` | Kontrak | Karyawan berbasis kontrak/temporer |
| `intern` | Magang | Karyawan magang/intern |

### employee\_status (Status Karyawan)

| Nilai | Label (ID) | Deskripsi |
| - | - | - |
| `active` | Aktif | Karyawan aktif bekerja |
| `on_leave` | Cuti | Karyawan sedang cuti |
| `terminated` | Nonaktif | Karyawan sudah tidak bekerja |

### department (Departemen)

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

### industry (Industri Perusahaan)

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

### subscription\_plan (Plan Langganan)

| Nilai | Label (ID) | Deskripsi |
| - | - | - |
| `free` | Gratis | Plan dasar tanpa fitur lanjutan |
| `pro` | Profesional | Plan untuk bisnis kecil-menengah |
| `business` | Bisnis | Plan untuk bisnis menengah-besar |
| `advanced` | Lanjutan | Plan dengan fitur lanjutan dan prioritas |
| `enterprise` | Enterprise | Plan khusus perusahaan besar dengan SLA |

### batch\_allocation\_strategy (Strategi Alokasi Batch)

| Nilai | Label (ID) | Deskripsi |
| - | - | - |
| `fifo` | First-In-First-Out | Alokasi batch berdasarkan tanggal penerimaan terlama |
| `fefo` | First-Expiry-First-Out | Alokasi batch berdasarkan tanggal kedaluwarsa terdekat |

### working\_days (Hari Kerja)

| Nilai | Hari (ID) |
| - | - |
| `1` | Senin |
| `2` | Selasa |
| `3` | Rabu |
| `4` | Kamis |
| `5` | Jumat |
| `6` | Sabtu |
| `7` | Minggu |

***

## RBAC — Hak Akses Absensi

| Operasi | Admin | Manager | Employee | Keterangan |
| - | :-: | :-: | :-: | - |
| **CompanyAttendance — Create** | Ya | Ya | Ya (milik sendiri) | Admin & manager bisa buat untuk siapa saja; employee hanya untuk diri sendiri |
| **CompanyAttendance — Read** | Ya | Ya | Ya (milik sendiri) | Admin & manager lihat semua; employee hanya record sendiri |
| **CompanyAttendance — Update** | Ya | Ya | Ya (milik sendiri) | Admin & manager bisa koreksi record karyawan; employee hanya milik sendiri |
| **CompanyAttendance — Delete** | Ya | Ya | Ya (milik sendiri) | Admin & manager bisa hapus; employee hanya record sendiri |
| **CompanyAttendanceSettings — Create** | Ya | Tidak | Tidak | Hanya admin yang bisa membuat pengaturan absensi |
| **CompanyAttendanceSettings — Read** | Ya | Ya | Ya | Semua role bisa membaca pengaturan (untuk validasi clock-in) |
| **CompanyAttendanceSettings — Update** | Ya | Tidak | Tidak | Hanya admin yang bisa mengubah konfigurasi absensi |
| **CompanyAttendanceSettings — Delete** | Ya | Tidak | Tidak | Hanya admin yang bisa menghapus pengaturan |
| **Manual Override (Approve)** | Ya | Ya | Tidak | Admin & manager bisa menyetujui koreksi manual absensi |
| **Export CSV** | Ya | Ya | Tidak | Admin & manager bisa export data absensi |
| **Auto Clock-Out (Cron)** | Sistem | Sistem | — | Dijalankan otomatis oleh sistem pada `auto_clock_out_time` |
| **AttendanceRecord — Create** | Ya | Tidak | Ya (milik sendiri) | Admin bisa buat untuk siapa saja; employee hanya untuk diri sendiri |
| **AttendanceRecord — Read** | Ya | Tidak | Ya (milik sendiri) | Admin lihat semua; employee hanya record sendiri |
| **AttendanceRecord — Update** | Ya | Tidak | Ya (milik sendiri) | Admin bisa koreksi; employee hanya milik sendiri |
| **AttendanceRecord — Delete** | Ya | Tidak | Ya (milik sendiri) | Admin bisa hapus; employee hanya record sendiri |
| **CompanyLeave — Create** | Ya | Ya | Ya (milik sendiri) | Admin & manager bisa buat untuk siapa saja; employee hanya untuk diri sendiri |
| **CompanyLeave — Read** | Ya | Ya | Ya (milik sendiri) | Admin & manager lihat semua pengajuan; employee hanya pengajuan sendiri |
| **CompanyLeave — Update** | Ya | Ya | Ya (milik sendiri) | Admin & manager bisa approve/reject; employee hanya update pengajuan sendiri |
| **CompanyLeave — Delete** | Ya | Ya | Ya (milik sendiri) | Admin & manager bisa hapus; employee hanya pengajuan sendiri |
| **Leave — Create** | Ya | Tidak | Ya (milik sendiri) | Admin bisa buat untuk siapa saja; employee hanya untuk diri sendiri |
| **Leave — Read** | Ya | Tidak | Ya (milik sendiri) | Admin lihat semua; employee hanya record sendiri |
| **Leave — Update** | Ya | Tidak | Ya (milik sendiri) | Admin bisa approve/reject; employee hanya update milik sendiri |
| **Leave — Delete** | Ya | Tidak | Ya (milik sendiri) | Admin bisa hapus; employee hanya record sendiri |
| **Employee — Create** | Ya | Tidak | Tidak | Hanya admin yang bisa menambah data karyawan |
| **Employee — Read** | Ya | Ya | Ya (milik sendiri) | Admin & manager lihat semua; employee hanya data sendiri |
| **Employee — Update** | Ya | Tidak | Tidak | Hanya admin yang bisa mengubah data karyawan |
| **Employee — Delete** | Ya | Tidak | Tidak | Hanya admin yang bisa menghapus data karyawan |

### RLS Policy Summary

```
CompanyAttendance:
  create/read/update/delete:
    OR(
      data.company_id == user.data.active_company_id AND company_id NOT IN [null, ""],
      created_by_id == user.id,
      data.employee_email == user.email,
      user.role == "admin"
    )

CompanyAttendanceSettings:
  create/read/update/delete: {} (policy kosong — dikontrol via UI role check)

AttendanceRecord:
  create/read/update/delete:
    OR(
      data.user_id == user.id,
      created_by_id == user.id,
      user.role == "admin"
    )

CompanyLeave:
  create/read/update/delete:
    OR(
      data.company_id == user.data.active_company_id AND company_id NOT IN [null, ""],
      created_by_id == user.id,
      data.employee_email == user.email,
      user.role == "admin"
    )

Leave:
  create/read/update/delete:
    OR(
      data.company_id == user.data.active_company_id AND company_id NOT IN [null, ""],
      created_by_id == user.id,
      user.role == "admin"
    )

Employee:
  create/read/update/delete:
    OR(
      data.company_id == user.data.active_company_id AND company_id NOT IN [null, ""],
      created_by_id == user.id,
      data.user_id == user.id,
      user.role == "admin"
    )

Company:
  create/read/update/delete: {} (policy kosong — dikontrol via UI role check)
```


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