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

# 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: "Attendance — Absensi Personal"
description: "Absensi personal dengan GPS+foto, kalender bulanan, dan statistik kehadiran di SNISHOP ERP."
----------------------------------------------------------------------------------------------------------

# Attendance — Absensi Personal

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

Halaman Attendance memungkinkan setiap karyawan melakukan clock-in dan clock-out dengan verifikasi foto dan GPS. Sistem mencatat jam kerja, keterlambatan, dan lembur secara otomatis.

## Arsitektur Absensi

```mermaid theme={null}
flowchart TB
    subgraph FLOW["Alur Clock-In"]
        START[Klik Clock In] --> GPS[Ambil GPS<br/>navigator.geolocation]
        START --> CAM[Buka Kamera<br/>capture foto]
        GPS --> COMP[Compress Image<br/>800px / 0.8 quality]
        CAM --> COMP
        COMP --> UPLOAD[Upload ke Cloudinary<br/>snishop_erp/attendance]
        UPLOAD --> CALC[Hitung Status<br/>late threshold 15 menit]
        CALC --> SAVE[Simpan AttendanceRecord]
    end

    subgraph FLOW_OUT["Alur Clock-Out"]
        START2[Klik Clock Out] --> GPS2[Ambil GPS]
        START2 --> CAM2[Capture Foto]
        GPS2 --> COMP2[Compress]
        CAM2 --> COMP2
        COMP2 --> UPLOAD2[Upload]
        UPLOAD2 --> CALC2[Hitung Total Jam<br/>+ Overtime]
        CALC2 --> UPDATE[Update AttendanceRecord]
    end
```

## Akses Halaman

URL: `/attendance`

## Tiga Tab Utama

| Tab | Label | Konten | Komponen |
| - | - | - | - |
| `checkin` | Clock In/Out | Form absensi dengan kamera dan GPS | LiveWorkTimer, Camera |
| `history` | Riwayat | Kalender bulanan dengan status harian | Calendar Grid |
| `stats` | Statistik | Ringkasan kehadiran dan jam kerja | Stats Cards |

## Entitas: AttendanceRecord

| Field | Tipe | Wajib | Deskripsi |
| - | - | - | - |
| `employee_id` | string | Ya | ID karyawan |
| `employee_name` | string | Ya | Nama karyawan (denormalized) |
| `date` | date | Ya | Tanggal absensi |
| `clock_in_time` | datetime | Tidak | Waktu clock-in |
| `clock_out_time` | datetime | Tidak | Waktu clock-out |
| `clock_in_photo_url` | string | Tidak | URL foto clock-in (Cloudinary) |
| `clock_out_photo_url` | string | Tidak | URL foto clock-out (Cloudinary) |
| `clock_in_location` | object | Tidak | `{ lat, lng }` koordinat GPS clock-in |
| `clock_out_location` | object | Tidak | `{ lat, lng }` koordinat GPS clock-out |
| `status` | enum | Auto | `present` atau `late` (dihitung otomatis) |
| `late_minutes` | number | Auto | Menit keterlambatan (0 jika tepat waktu) |
| `total_hours` | number | Auto | Total jam kerja (dihitung saat clock-out) |
| `overtime_hours` | number | Auto | Jam lembur (total\_hours - 9) |
| `notes` | string | Tidak | Catatan tambahan |
| `company_id` | string | Ya | ID perusahaan (multi-tenant) |
| `created_at` | datetime | Auto | Timestamp pembuatan |

## Clock-In: Deteksi Keterlambatan

```mermaid theme={null}
flowchart TD
    A[Clock-In Triggered] --> B[Record clock_in_time]
    B --> C{clock_in_time ≤ 08:15?}
    C -->|Ya| D["status = 'present'"]
    D --> E["late_minutes = 0"]
    C -->|Tidak| F["status = 'late'"]
    F --> G["late_minutes = diff(08:00, clock_in_time)"]
    E --> H[Save to AttendanceRecord]
    G --> H
```

Sistem menggunakan threshold tetap untuk menentukan status kehadiran:

| Konstanta | Nilai | Fungsi |
| - | - | - |
| `WORK_START_HOUR` | 8 (08:00) | Jam mulai kerja standar |
| `LATE_THRESHOLD_MINUTES` | 15 | Toleransi keterlambatan |

```
Jika clock_in_time ≤ 08:15 → status = "present", late_minutes = 0
Jika clock_in_time > 08:15 → status = "late", late_minutes = selisih menit dari 08:00
```

### Contoh Deteksi Keterlambatan

| Clock-In Time | Status | Late Minutes | Penjelasan |
| - | - | - | - |
| 07:55 | present | 0 | Datang sebelum jam kerja |
| 08:00 | present | 0 | Tepat waktu |
| 08:10 | present | 0 | Dalam toleransi 15 menit |
| 08:15 | present | 0 | Batas toleransi |
| 08:20 | late | 20 | Melebihi toleransi 5 menit |
| 08:45 | late | 45 | Terlambat 45 menit |
| 09:30 | late | 90 | Sangat terlambat |

## Clock-Out: Perhitungan Jam Kerja

```mermaid theme={null}
flowchart TD
    A[Clock-Out Triggered] --> B[Record clock_out_time]
    B --> C["total_hours = (clock_out - clock_in) / 3600000"]
    C --> D{total_hours > 9?}
    D -->|Ya| E["overtime_hours = total_hours - 9"]
    D -->|Tidak| F["overtime_hours = 0"]
    E --> G[Update AttendanceRecord]
    F --> G
```

Saat clock-out, sistem menghitung:

| Metrik | Perhitungan | Deskripsi |
| - | - | - |
| `total_hours` | `(clock_out_time - clock_in_time) / 3600000` | Selisih waktu dalam jam |
| `overtime_hours` | `max(0, total_hours - 9)` | Lembur setelah 9 jam kerja |

### Contoh Perhitungan

| Clock-In | Clock-Out | Total Hours | Overtime | Status |
| - | - | - | - | - |
| 08:00 | 17:00 | 9.0 | 0.0 | Normal |
| 08:00 | 18:00 | 10.0 | 1.0 | 1 jam lembur |
| 08:30 | 17:30 | 9.0 | 0.0 | Normal (terlambat 30 menit) |
| 07:00 | 19:00 | 12.0 | 3.0 | 3 jam lembur |
| 08:00 | 12:00 | 4.0 | 0.0 | Setengah hari |

## Tab Riwayat: Kalender Bulanan

```mermaid theme={null}
graph LR
    subgraph "Kalender Grid (Oktober 2026)"
        A[Sen] --- B[Sel] --- C[Rab] --- D[Kam] --- E[Jum] --- F[Sab] --- G[Min]
        H[1<br/>🟢] --- I[2<br/>🟢] --- J[3<br/>🟡] --- K[4<br/>🟢] --- L[5<br/>🟢] --- M[6<br/>⬜] --- N[7<br/>⬜]
        O[8<br/>🟢] --- P[9<br/>🟢] --- Q[10<br/>🟡] --- R[11<br/>🟢] --- S[12<br/>🔵] --- T[13<br/>⬜] --- U[14<br/>⬜]
    end
```

Kalender grid bulanan menampilkan status absensi setiap hari dengan color coding:

| Warna | Ikon | Status | Arti |
| - | - | - | - |
| Hijau | 🟢 | Present | Hadir tepat waktu |
| Kuning | 🟡 | Late | Hadir terlambat |
| Abu-abu | ⬜ | Absent | Tidak hadir / hari libur |
| Biru | 🔵 | Leave | Cuti / izin |

## Tab Statistik

```mermaid theme={null}
flowchart TD
    subgraph "Statistik Kehadiran"
        A[AttendanceRecords<br/>Filtered by period] --> B[Total Hari Kerja]
        A --> C[Count by status]
        C --> C1[Hadir: status=present]
        C --> C2[Terlambat: status=late]
        C --> C3[Absent: no record]
        C --> C4[Cuti: status=leave]
        A --> D[Sum total_hours]
        A --> E[Sum late_minutes]
        A --> F[Sum overtime_hours]
    end
    
    subgraph "Computed Metrics"
        D --> G["Rata-rata Jam/Hari = total_hours / total_hadir"]
        C1 & B --> H["Tingkat Ketepatan = hadir / total_hari × 100%"]
    end
```

| Metrik | Perhitungan | Deskripsi |
| - | - | - |
| **Total Hari** | Jumlah hari kerja dalam periode | Hari kerja (exclude weekend/holiday) |
| **Hadir** | `count(status === 'present')` | Hari hadir tepat waktu |
| **Terlambat** | `count(status === 'late')` | Hari hadir terlambat |
| **Total Jam** | `Σ total_hours` | Akumulasi jam kerja |
| **Rata-rata Jam/Hari** | `total_hours / total_hadir` | Rata-rata durasi kerja per hari |
| **Total Menit Terlambat** | `Σ late_minutes` | Akumulasi keterlambatan |
| **Total Jam Lembur** | `Σ overtime_hours` | Akumulasi lembur |
| **Tingkat Ketepatan Waktu** | `(hadir / total_hari) × 100%` | Persentase ketepatan |

## Upload Foto

```mermaid theme={null}
sequenceDiagram
    participant U as User
    participant C as Camera API
    participant Canvas as Canvas
    participant Cloud as Cloudinary
    
    U->>C: Click "Ambil Foto"
    C->>C: Request camera permission
    C-->>U: Show camera preview
    U->>C: Capture
    C->>Canvas: Draw to canvas
    Canvas->>Canvas: Resize max 800px
    Canvas->>Canvas: Compress quality 0.8
    Canvas-->>U: Preview compressed
    U->>Cloud: Upload
    Note over Cloud: Folder: snishop_erp/attendance
    Cloud-->>U: { secure_url }
```

| Langkah | Detail |
| - | - |
| Capture | Dari kamera depan (selfie) |
| Compress | Resize ke max 800px, kualitas 0.8 |
| Upload | Cloudinary path: `snishop_erp/attendance` |
| Storage | URL disimpan di `clock_in_photo_url` / `clock_out_photo_url` |

## Integrasi dengan Payroll

Data absensi personal mengalir ke perhitungan payroll:

| Data Absensi | Digunakan di Payroll Untuk | Impact |
| - | - | - |
| `attendance_days` | Menghitung hari kerja untuk prorata gaji | 直接影响 gaji pokok |
| `late_count` | Potongan keterlambatan | Mengurangi gaji |
| `overtime_hours` | Perhitungan upah lembur | Menambah gaji |

## Tips Penggunaan

* Pastikan GPS aktif dan izin lokasi browser diberikan sebelum clock-in
* Foto harus jelas terlihat untuk verifikasi — sistem menolak foto blur atau gelap
* Clock-in sebelum 08:15 untuk status "present" (dalam toleransi)
* Jangan lupa clock-out agar total jam kerja tercatat — jika lupa, hubungi HR admin
* Cek tab Statistik secara berkala untuk memantau kehadiran dan lembur
* Jika ada masalah dengan GPS atau kamera, gunakan notes untuk mencatat kendala

***

## Entity Relationship Diagram

```mermaid theme={null}
erDiagram
    Employee ||--o{ CompanyAttendance : "memiliki"
    Employee ||--o{ CompanyLeave : "mengajukan"
    Employee }o--|| User : "terhubung ke"
    CompanyAttendanceSettings ||--o{ CompanyAttendance : "mengatur"
    CompanyAttendanceSettings ||--o{ CompanyLeave : "mengatur kebijakan"

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

    CompanyAttendance {
        string company_id PK
        string employee_id FK
        string employee_name
        string employee_email
        date date
        string shift_id
        string shift_name
        datetime clock_in_time
        datetime clock_out_time
        string clock_in_photo_url
        string clock_out_photo_url
        object clock_in_location
        object clock_out_location
        enum status
        string notes
        number total_hours
        number overtime_hours
        number distance_from_office
        string location_id
        string location_name
        number clock_in_accuracy
        number clock_out_accuracy
        boolean is_manual_override
        string override_reason
        string override_approved_by
        datetime override_approved_at
    }

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

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

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

## Tabel Schema: CompanyAttendance

Entitas absensi tingkat perusahaan yang menyimpan seluruh data clock-in/clock-out karyawan beserta verifikasi GPS dan foto.

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan (multi-tenant) |
| `employee_id` | string | Ya | — | ID karyawan (FK ke Employee) |
| `employee_name` | string | Tidak | — | Nama karyawan (denormalized) |
| `employee_email` | string | Tidak | — | Email karyawan (denormalized) |
| `date` | date | Ya | — | Tanggal absensi |
| `shift_id` | string | Tidak | — | ID shift yang dipilih karyawan |
| `shift_name` | string | Tidak | — | Nama shift (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 clock-in (Cloudinary) |
| `clock_out_photo_url` | string | Tidak | — | URL foto clock-out (Cloudinary) |
| `clock_in_location` | object | Tidak | — | `{ latitude, longitude, accuracy }` GPS clock-in |
| `clock_out_location` | object | Tidak | — | `{ latitude, longitude, accuracy }` GPS clock-out |
| `status` | enum | Tidak | `present` | Status kehadiran: `present`, `late`, `absent`, `sick`, `leave`, `wfh` |
| `notes` | string | Tidak | — | Catatan tambahan dari karyawan |
| `total_hours` | number | Tidak | — | Total jam kerja (dihitung saat clock-out) |
| `overtime_hours` | number | Tidak | `0` | Jam lembur (total\_hours - threshold) |
| `distance_from_office` | number | Tidak | — | Jarak dari kantor/geofence pusat dalam meter |
| `location_id` | string | Tidak | — | ID lokasi geofence absensi terpilih |
| `location_name` | string | Tidak | — | Nama lokasi geofence (misal: Head Office, Outlet 2) |
| `clock_in_accuracy` | number | Tidak | — | Akurasi GPS saat clock-in dalam meter |
| `clock_out_accuracy` | number | Tidak | — | Akurasi GPS saat clock-out dalam meter |
| `is_manual_override` | boolean | Tidak | `false` | Flag absensi ini hasil koreksi manual berizin |
| `override_reason` | string | Tidak | — | Alasan pengajuan koreksi manual absensi |
| `override_approved_by` | string | Tidak | — | ID user/manager yang menyetujui koreksi |
| `override_approved_at` | datetime | Tidak | — | Waktu persetujuan koreksi manual |

## Tabel Schema: CompanyLeave

Entitas pengajuan cuti tingkat perusahaan yang menyimpan data pengajuan, approval, dan dokumen pendukung cuti karyawan.

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan (multi-tenant) |
| `employee_id` | string | Ya | — | ID karyawan (FK ke Employee) |
| `employee_name` | string | Tidak | — | Nama karyawan (denormalized) |
| `employee_email` | string | Tidak | — | Email karyawan (denormalized) |
| `leave_type` | enum | Ya | `annual` | Jenis cuti: `annual`, `sick`, `unpaid`, `maternity`, `paternity`, `emergency` |
| `start_date` | date | Ya | — | Tanggal mulai cuti |
| `end_date` | date | Ya | — | tanggal selesai cuti |
| `total_days` | number | Tidak | — | Jumlah hari cuti (dihitung dari rentang tanggal) |
| `reason` | string | Ya | — | Alasan 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 | `pending` | Status pengajuan: `pending`, `approved`, `rejected`, `cancelled` |
| `approver_id` | string | Tidak | — | ID approver (manager/HR) |
| `approver_notes` | string | Tidak | — | Catatan dari approver saat approve/reject |
| `approved_at` | datetime | Tidak | — | Timestamp keputusan approval |

## State Machine: Siklus Hidup Pengajuan Cuti

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

    pending --> approved: Approver menyetujui
    pending --> rejected: Approver menolak
    pending --> cancelled: Karyawan membatalkan

    approved --> [*]: Cuti selesai dijalankan
    rejected --> [*]: Pengajuan ditolak (final)
    cancelled --> [*]: Dibatalkan oleh karyawan (final)

    state approved {
        [*] --> menunggu_jadwal
        menunggu_jadwal --> dijalankan: Tanggal mulai tiba
        dijalankan --> selesai: Tanggal berakhir tiba
    }
```

### Transisi Status Cuti

| Dari | Ke | Trigger | Kondisi |
| - | - | - | - |
| — | `pending` | Karyawan submit form pengajuan | Field wajib terisi: `company_id`, `employee_id`, `leave_type`, `start_date`, `end_date`, `reason` |
| `pending` | `approved` | Approver klik "Setujui" | Approver memiliki akses ke company yang sama |
| `pending` | `rejected` | Approver klik "Tolak" | Approver wajib mengisi `approver_notes` |
| `pending` | `cancelled` | Karyawan klik "Batalkan" | Hanya bisa dilakukan oleh pembuat pengajuan |
| `approved` | — | Sistem (cron) | Saat tanggal cuti berakhir, status tetap `approved` (histori) |

## Sequence Diagram: Ringkasan Absensi

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan
    participant FE as Frontend
    participant BE as Backend API
    participant DB as Database
    participant GPS as Geolocation API
    participant CAM as Camera API
    participant CDN as Cloudinary

    K->>FE: Buka halaman Attendance
    FE->>BE: GET /company_attendance?employee_id=X&month=Y
    BE->>DB: Query CompanyAttendance (bulan berjalan)
    DB-->>BE: Daftar records absensi
    BE-->>FE: Data absensi bulanan
    FE->>FE: Render kalender grid + statistik

    K->>FE: Klik tab "Statistik"
    FE->>FE: Hitung total_hadir, total_late, total_hours, overtime
    FE-->>K: Tampilkan kartu statistik
```

## Sequence Diagram: Pengajuan Cuti

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan
    participant FE as Frontend
    participant BE as Backend API
    participant DB as Database
    participant Notif as Notifikasi

    K->>FE: Buka form pengajuan cuti
    K->>FE: Isi leave_type, start_date, end_date, reason
    K->>FE: Upload dokumen pendukung (opsional)
    FE->>BE: POST /company_leave
    BE->>BE: Validasi field wajib
    BE->>BE: Hitung total_days = (end_date - start_date) + 1
    BE->>DB: Insert CompanyLeave (status = "pending")
    DB-->>BE: Record tersimpan
    BE->>Notif: Kirim notifikasi ke approver
    Notif-->>BE: Notifikasi terkirim
    BE-->>FE: Response: { success, leave_id }
    FE-->>K: Tampilkan konfirmasi "Pengajuan cuti berhasil"
```

## Sequence Diagram: Approval Cuti

```mermaid theme={null}
sequenceDiagram
    participant M as Manager/HR
    participant FE as Frontend
    participant BE as Backend API
    participant DB as Database
    participant Notif as Notifikasi

    M->>FE: Buka daftar pengajuan cuti pending
    FE->>BE: GET /company_leave?status=pending&company_id=X
    BE->>DB: Query CompanyLeave (pending)
    DB-->>BE: Daftar pengajuan pending
    BE-->>FE: Data pengajuan cuti

    M->>FE: Review detail pengajuan
    alt Setujui
        M->>FE: Klik "Approve" + isi approver_notes
        FE->>BE: PATCH /company_leave/{id} { status: "approved", approver_id, approver_notes, approved_at }
        BE->>DB: Update CompanyLeave → status = "approved"
        DB-->>BE: Updated
        BE->>Notif: Kirim notifikasi ke karyawan
        BE-->>FE: Response: { success }
        FE-->>M: Tampilkan "Pengajuan disetujui"
    else Tolak
        M->>FE: Klik "Reject" + isi approver_notes (wajib)
        FE->>BE: PATCH /company_leave/{id} { status: "rejected", approver_id, approver_notes, approved_at }
        BE->>DB: Update CompanyLeave → status = "rejected"
        DB-->>BE: Updated
        BE->>Notif: Kirim notifikasi penolakan ke karyawan
        BE-->>FE: Response: { success }
        FE-->>M: Tampilkan "Pengajuan ditolak"
    end
```

## Tabel Enum: leave\_type (Jenis Cuti)

| Nilai | Label | Deskripsi |
| - | - | - |
| `annual` | Cuti Tahunan | Cuti reguler yang diambil dari jatah cuti tahunan karyawan |
| `sick` | Cuti Sakit | Cuti karena sakit, umumnya memerlukan surat dokter |
| `unpaid` | Cuti Tanpa Bayaran | Cuti di luar tanggungan perusahaan |
| `maternity` | Cuti Melahirkan | Cuti khusus untuk karyawan yang melahirkan |
| `paternity` | Cuti Ayah | Cuti khusus untuk karyawan yang istrinya melahirkan |
| `emergency` | Cuti Darurat | Cuti mendadak untuk keperluan darurat keluarga |

## Tabel Enum: leave\_status (Status Pengajuan Cuti)

| Nilai | Label | Deskripsi |
| - | - | - |
| `pending` | Menunggu Approval | Pengajuan baru diajukan, menunggu keputusan approver |
| `approved` | Disetujui | Pengajuan disetujui oleh approver |
| `rejected` | Ditolak | Pengajuan ditolak oleh approver |
| `cancelled` | Dibatalkan | Pengajuan dibatalkan oleh karyawan sebelum ada keputusan |

## Tabel Enum: attendance\_status (Status Kehadiran)

| Nilai | Label | Deskripsi |
| - | - | - |
| `present` | Hadir | Karyawan hadir tepat waktu (dalam toleransi) |
| `late` | Terlambat | Karyawan hadir melebihi toleransi keterlambatan |
| `absent` | Absen | Karyawan tidak hadir tanpa keterangan |
| `sick` | Sakit | Karyawan sakit (dengan atau tanpa surat dokter) |
| `leave` | Cuti | Karyawan sedang menjalani cuti yang disetujui |
| `wfh` | Work From Home | Karyawan bekerja dari rumah |

## Tabel Enum: employment\_type (Tipe Karyawan)

| Nilai | Label | Deskripsi |
| - | - | - |
| `full_time` | Penuh Waktu | Karyawan tetap dengan jam kerja penuh |
| `part_time` | Paruh Waktu | Karyawan dengan jam kerja kurang dari penuh |
| `contract` | Kontrak | Karyawan berbasis kontrak jangka waktu tertentu |
| `intern` | Magang | Karyawan magang/pelatihan |

## Tabel Enum: employee\_status (Status Karyawan)

| Nilai | Label | Deskripsi |
| - | - | - |
| `active` | Aktif | Karyawan aktif bekerja |
| `on_leave` | Cuti | Karyawan sedang cuti jangka panjang |
| `terminated` | Nonaktif | Karyawan sudah tidak bekerja |

## Tabel Enum: department (Departemen)

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

## RBAC: Hak Akses Modul Attendance

| Aksi | Admin | Manager/HR | Karyawan (sendiri) |
| - | - | - | - |
| **Clock-in** (create CompanyAttendance) | Ya | Ya | Ya (milik sendiri) |
| **Lihat absensi sendiri** (read) | Ya | Ya (timnya) | Ya (milik sendiri) |
| **Lihat semua absensi perusahaan** (read) | Ya | Ya | Tidak |
| **Koreksi absensi manual** (update) | Ya | Ya (dengan approval) | Tidak |
| **Hapus record absensi** (delete) | Ya | Ya | Tidak |
| **Ajukan cuti** (create CompanyLeave) | Ya | Ya | Ya (milik sendiri) |
| **Lihat cuti sendiri** (read) | Ya | Ya (timnya) | Ya (milik sendiri) |
| **Lihat semua cuti perusahaan** (read) | Ya | Ya | Tidak |
| **Approve/reject cuti** (update) | Ya | Ya | Tidak |
| **Batalkan cuti sendiri** (update → cancelled) | Ya | Ya | Ya (milik sendiri, hanya status pending) |
| **Ubah pengaturan absensi** (CompanyAttendanceSettings) | Ya | Tidak | Tidak |

### Aturan RLS (Row-Level Security)

Semua entitas attendance menggunakan RLS berbasis `company_id` dan `user`:

| Kondisi | Aturan |
| - | - |
| **Multi-tenant isolation** | `data.company_id === user.data.active_company_id` — data hanya bisa diakses dalam scope perusahaan aktif |
| **Owner access** | `created_by_id === user.id` — pembuat record selalu bisa mengakses |
| **Employee self-access** | `data.employee_email === user.email` — karyawan bisa mengakses record atas nama email-nya |
| **Admin override** | `user.role === "admin"` — admin memiliki akses penuh lintas perusahaan |

***

## State Diagram: Siklus Hidup Absensi (Attendance Lifecycle)

```mermaid theme={null}
stateDiagram-v2
    [*] --> belum_absen: Awal hari kerja (00:00)

    belum_absen --> clock_in: Karyawan melakukan clock-in
    belum_absen --> absent: Lewat jam kerja tanpa clock-in (cron)
    belum_absen --> sick: Karyawan input status sakit
    belum_absen --> leave: Karyawan sedang cuti approved
    belum_absen --> wfh: Karyawan input status WFH

    clock_in --> clocked_in: Status tercatat (present/late)
    clocked_in --> clock_out: Karyawan melakukan clock-out
    clocked_in --> manual_override: Karyawan ajukan koreksi absensi

    clock_out --> calculated: Sistem hitung total_hours + overtime
    calculated --> [*]: Record selesai (final)

    manual_override --> pending_approval: Manager/HR terima pengajuan
    pending_approval --> approved_override: Disetujui → update record
    pending_approval --> rejected_override: Ditolak → tetap status semula
    approved_override --> calculated: Record diperbarui
    rejected_override --> clocked_in: Kembali ke status sebelumnya

    absent --> [*]: Tidak ada record (final)
    sick --> [*]: Record sakit (final)
    leave --> [*]: Record cuti (final)
    wfh --> [*]: Record WFH (final)

    state clock_in {
        [*] --> verifikasi_gps
        verifikasi_gps --> capture_foto
        capture_foto --> hitung_status
        hitung_status --> [*]
    }

    state calculated {
        [*] --> hitung_jam
        hitung_jam --> hitung_lembur
        hitung_lembur --> simpan_record
        simpan_record --> [*]
    }
```

### Transisi Status Absensi

| Dari | Ke | Trigger | Kondisi |
| - | - | - | - |
| — | `belum_absen` | Awal hari kerja (midnight cron) | Hari kerja berdasarkan `working_days` di CompanyAttendanceSettings |
| `belum_absen` | `clock_in` | Karyawan klik "Clock In" | GPS valid + foto berhasil diupload |
| `belum_absen` | `absent` | Cron job akhir hari | Tidak ada record clock-in pada hari tersebut |
| `belum_absen` | `sick` | Karyawan/HR input manual | Status sickness dengan atau tanpa surat dokter |
| `belum_absen` | `leave` | Pengajuan cuti approved | CompanyLeave status = `approved` mencakup tanggal tersebut |
| `belum_absen` | `wfh` | Karyawan/HR input manual | Work from home diizinkan oleh kebijakan perusahaan |
| `clocked_in` | `clock_out` | Karyawan klik "Clock Out" | GPS valid + foto berhasil diupload |
| `clocked_in` | `manual_override` | Karyawan ajukan koreksi | Clock-out lupa atau data GPS tidak akurat |
| `manual_override` | `approved_override` | Manager/HR approve | `is_manual_override = true`, `override_approved_by` terisi |
| `manual_override` | `rejected_override` | Manager/HR reject | `override_reason` ditolak dengan catatan |
| `clock_out` | `calculated` | Otomatis setelah clock-out | `total_hours` dan `overtime_hours` dihitung |

***

## Sequence Diagram: Clock-In (Verifikasi GPS + Foto)

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan
    participant FE as Frontend
    participant GPS as Geolocation API
    participant CAM as Camera API
    participant Canvas as Canvas Processor
    participant CDN as Cloudinary
    participant BE as Backend API
    participant DB as Database
    participant Settings as CompanyAttendanceSettings

    K->>FE: Klik "Clock In"
    FE->>GPS: navigator.geolocation.getCurrentPosition()
    GPS-->>FE: { latitude, longitude, accuracy }

    FE->>BE: GET /company_attendance_settings?company_id=X
    BE->>DB: Query CompanyAttendanceSettings
    DB-->>BE: Settings (locations, radius, tolerance)
    BE-->>FE: Pengaturan geofence

    FE->>FE: Hitung distance dari lokasi kantor terdekat
    alt Dalam radius geofence
        FE->>FE: distance ≤ office_radius_meters → Valid
    else Di luar radius
        FE->>FE: distance > office_radius_meters → Peringatan
        FE-->>K: Tampilkan peringatan "Di luar area kantor"
        K->>FE: Konfirmasi lanjutkan (dengan catatan)
    end

    FE->>CAM: Buka kamera (selfie)
    CAM-->>K: Preview kamera
    K->>CAM: Capture foto
    CAM->>Canvas: Draw image ke canvas
    Canvas->>Canvas: Resize max 800px (maintain aspect ratio)
    Canvas->>Canvas: Compress quality 0.8 (JPEG)
    Canvas-->>FE: Blob compressed

    FE->>CDN: Upload image (folder: snishop_erp/attendance)
    CDN-->>FE: { secure_url, public_id }

    FE->>FE: Tentukan status: clock_in_time ≤ 08:15?
    alt Tepat waktu (≤ 08:15)
        FE->>FE: status = "present", late_minutes = 0
    else Terlambat (> 08:15)
        FE->>FE: status = "late", late_minutes = diff(08:00, clock_in_time)
    end

    FE->>BE: POST /company_attendance
    Note over BE: Payload: company_id, employee_id, date,<br/>clock_in_time, clock_in_photo_url,<br/>clock_in_location, status, late_minutes
    BE->>BE: Validasi field wajib
    BE->>DB: Insert CompanyAttendance
    DB-->>BE: Record tersimpan
    BE-->>FE: Response: { success, attendance_id }
    FE-->>K: Tampilkan "Clock-in berhasil"
```

## Sequence Diagram: Clock-Out (Perhitungan Jam + Lembur)

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan
    participant FE as Frontend
    participant GPS as Geolocation API
    participant CAM as Camera API
    participant CDN as Cloudinary
    participant BE as Backend API
    participant DB as Database
    participant Settings as CompanyAttendanceSettings

    K->>FE: Klik "Clock Out"
    FE->>GPS: navigator.geolocation.getCurrentPosition()
    GPS-->>FE: { latitude, longitude, accuracy }

    FE->>CAM: Buka kamera (selfie)
    CAM-->>K: Preview kamera
    K->>CAM: Capture foto
    CAM->>CAM: Compress + resize
    CAM-->>FE: Blob compressed

    FE->>CDN: Upload image (folder: snishop_erp/attendance)
    CDN-->>FE: { secure_url }

    FE->>BE: GET /company_attendance_settings?company_id=X
    BE->>DB: Query overtime_settings
    DB-->>BE: { start_after_hours: 8, max_overtime_hours_per_day: 4 }
    BE-->>FE: Pengaturan lembur

    FE->>FE: Hitung total_hours = (clock_out - clock_in) / 3600000
    FE->>FE: Hitung overtime_hours = max(0, total_hours - start_after_hours)

    alt overtime_hours > max_overtime_hours_per_day
        FE->>FE: overtime_hours = max_overtime_hours_per_day (cap)
    end

    FE->>BE: PATCH /company_attendance/{id}
    Note over BE: Payload: clock_out_time, clock_out_photo_url,<br/>clock_out_location, total_hours, overtime_hours
    BE->>BE: Validasi: clock_out > clock_in
    BE->>DB: Update CompanyAttendance
    DB-->>BE: Record diperbarui
    BE-->>FE: Response: { success, total_hours, overtime_hours }
    FE-->>K: Tampilkan ringkasan: "Total: 9.5 jam, Lembur: 1.5 jam"
```

## Sequence Diagram: Koreksi Manual Absensi (Override)

```mermaid theme={null}
sequenceDiagram
    participant K as Karyawan
    participant FE as Frontend
    participant BE as Backend API
    participant DB as Database
    participant M as Manager/HR
    participant Notif as Notifikasi

    K->>FE: Ajukan koreksi absensi
    K->>FE: Isi override_reason + data koreksi
    FE->>BE: POST /company_attendance/{id}/override
    BE->>BE: Validasi: record ada, milik employee yang sama
    BE->>DB: Update CompanyAttendance → is_manual_override = true, status = "pending_override"
    DB-->>BE: Updated
    BE->>Notif: Kirim notifikasi ke manager/HR
    Notif-->>M: "Ada pengajuan koreksi absensi dari [nama]"

    M->>FE: Review pengajuan koreksi
    alt Setujui
        M->>FE: Klik "Approve Override"
        FE->>BE: PATCH /company_attendance/{id}
        Note over BE: override_approved_by = manager.id<br/>override_approved_at = now()
        BE->>DB: Update CompanyAttendance → approved
        DB-->>BE: Updated
        BE->>Notif: Kirim notifikasi ke karyawan
        Notif-->>K: "Koreksi absensi Anda disetujui"
    else Tolak
        M->>FE: Klik "Reject Override" + catatan
        FE->>BE: PATCH /company_attendance/{id}
        BE->>DB: Revert is_manual_override = false
        DB-->>BE: Reverted
        BE->>Notif: Kirim notifikasi penolakan ke karyawan
        Notif-->>K: "Koreksi absensi Anda ditolak"
    end
```

## Sequence Diagram: Perhitungan Lembur (Overtime)

```mermaid theme={null}
sequenceDiagram
    participant Cron as Cron Job
    participant BE as Backend API
    participant DB as Database
    participant Settings as CompanyAttendanceSettings
    participant Payroll as Payroll Module

    Cron->>BE: Trigger perhitungan lembur harian
    BE->>DB: Query CompanyAttendance WHERE clock_out_time IS NOT NULL AND date = today
    DB-->>BE: Daftar record absensi hari ini

    loop Setiap record
        BE->>Settings: GET overtime_settings
        Settings-->>BE: { start_after_hours: 8, max_overtime_hours_per_day: 4, enabled: true }

        alt overtime_settings.enabled = true
            BE->>BE: total_hours = (clock_out - clock_in) / 3600000
            BE->>BE: raw_overtime = total_hours - start_after_hours

            alt raw_overtime > 0
                BE->>BE: overtime_hours = min(raw_overtime, max_overtime_hours_per_day)
            else raw_overtime ≤ 0
                BE->>BE: overtime_hours = 0
            end

            BE->>DB: UPDATE CompanyAttendance SET overtime_hours = {computed}
        else overtime_settings.enabled = false
            BE->>BE: overtime_hours = 0
            BE->>DB: UPDATE CompanyAttendance SET overtime_hours = 0
        end
    end

    BE->>Payroll: Emit data lembur untuk perhitungan gaji
    Note over Payroll: overtime_hours × tarif_lembur → komponen upah
```

***

## Tabel Schema: CompanyAttendanceSettings

Entitas pengaturan absensi tingkat perusahaan yang menyimpan konfigurasi geofence, jam kerja, shift, lembur, dan kebijakan absensi.

### Grup: Identitas & Lokasi Kantor

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan (multi-tenant) |
| `description` | string | Tidak | — | Penjelasan kebijakan absensi perusahaan (max 1000 karakter) |
| `office_location` | object | Tidak | — | Lokasi kantor utama: `{ latitude, longitude, address }` |
| `office_radius_meters` | number | Tidak | `100` | Radius geofence kantor utama dalam meter (legacy fallback) |
| `require_office_location` | boolean | Tidak | `false` | Wajib absen dari lokasi kantor |
| `default_accuracy_tolerance_meters` | number | Tidak | `150` | Batas toleransi akurasi GPS dalam meter |

### Grup: Multi-Lokasi Geofence (`locations[]`)

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `locations` | array | Tidak | `[]` | Daftar multi-lokasi geofence absensi perusahaan |
| `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 | `100` | Radius lingkaran geofence dalam meter |
| `locations[].accuracy_tolerance_meters` | number | Tidak | `150` | Toleransi akurasi GPS maksimum untuk lokasi ini (meter) |
| `locations[].assigned_employee_ids` | array | Tidak | `[]` | Daftar ID karyawan yang ditugaskan ke lokasi ini (kosong = semua) |
| `locations[].is_active` | boolean | Tidak | `true` | Status aktif lokasi geofence |

### Grup: Jam Kerja (`working_hours`)

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `working_hours` | object | Tidak | — | Konfigurasi jam kerja standar |
| `working_hours.start_time` | string | Tidak | `"09:00"` | Jam mulai kerja (format HH:MM) |
| `working_hours.end_time` | string | Tidak | `"17:00"` | Jam selesai kerja (format HH:MM) |
| `working_hours.break_duration_minutes` | number | Tidak | `60` | Durasi istirahat dalam menit |

### Grup: Shift Kerja (`shifts[]`)

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `shifts` | array | Tidak | `[]` | Daftar shift kerja yang tersedia untuk karyawan |
| `shifts[].shift_id` | string | Ya | — | ID unik shift |
| `shifts[].shift_name` | string | Ya | — | Nama shift (misal: Shift Pagi, Shift Siang, Shift Malam) |
| `shifts[].start_time` | string | Ya | — | Jam mulai shift (format HH:MM) |
| `shifts[].end_time` | string | Ya | — | Jam selesai shift (format HH:MM) |
| `shifts[].break_duration_minutes` | number | Tidak | `60` | Durasi istirahat shift dalam menit |

### Grup: Pengaturan Lembur (`overtime_settings`)

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `overtime_settings` | object | Tidak | — | Konfigurasi perhitungan lembur |
| `overtime_settings.enabled` | boolean | Tidak | `true` | Apakah perhitungan lembur diaktifkan |
| `overtime_settings.start_after_hours` | number | Tidak | `8` | Lembur dimulai setelah X jam kerja |
| `overtime_settings.max_overtime_hours_per_day` | number | Tidak | `4` | Maksimal jam lembur per hari |

### Grup: Kebijakan Absensi

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `late_tolerance_minutes` | number | Tidak | `15` | Toleransi keterlambatan dalam menit |
| `working_days` | array | Tidak | `[1,2,3,4,5]` | Hari kerja (1=Senin, 7=Minggu) |
| `auto_clock_out_enabled` | boolean | Tidak | `false` | Otomatis clock-out jika karyawan lupa |
| `auto_clock_out_time` | string | Tidak | `"18:00"` | Jam auto clock-out (format HH:MM) |
| `require_photo` | boolean | Tidak | `true` | Wajib foto selfie saat absen |
| `require_notes` | boolean | Tidak | `false` | Wajib mengisi catatan saat absen |

***

## Tabel Schema: AttendanceRecord (Absensi Personal)

Entitas absensi personal (non-perusahaan) yang menyimpan data clock-in/clock-out mandiri karyawan.

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `user_id` | string | Ya | — | ID user (PK, pemilik record absensi) |
| `workspace_id` | string | Ya | — | ID workspace |
| `date` | date | Ya | — | Tanggal absensi |
| `clock_in_time` | datetime | Ya | — | Waktu clock-in |
| `clock_out_time` | datetime | Tidak | — | Waktu clock-out |
| `clock_in_photo_url` | string | Tidak | — | URL foto saat clock-in |
| `clock_out_photo_url` | string | Tidak | — | URL foto saat clock-out |
| `clock_in_location` | object | Tidak | — | `{ latitude, longitude, address }` lokasi GPS clock-in |
| `clock_out_location` | object | Tidak | — | `{ latitude, longitude, address }` lokasi GPS clock-out |
| `status` | enum | Tidak | `present` | Status kehadiran: `present`, `late`, `absent`, `sick`, `leave` |
| `notes` | string | Tidak | — | Catatan tambahan |
| `total_hours` | number | Tidak | — | Total jam kerja (dihitung saat clock-out) |

***

## Tabel Schema: Employee (Karyawan)

Entitas karyawan yang menyimpan data profil, jabatan, kompensasi, dan informasi kontak.

### Grup: Identitas & Kontak

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan (multi-tenant) |
| `employee_id` | string | Ya | — | ID unik karyawan (UK) |
| `user_id` | string | Tidak | — | Link ke entitas User (FK) |
| `full_name` | string | Ya | — | Nama lengkap karyawan |
| `email` | string | Ya | — | Email karyawan |
| `phone` | string | Tidak | — | Nomor telepon karyawan |
| `address` | string | Tidak | — | Alamat tempat tinggal |
| `date_of_birth` | date | Tidak | — | Tanggal lahir |
| `avatar_url` | string | Tidak | — | URL foto profil karyawan |

### Grup: Jabatan & Kepegawaian

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `department` | enum | Ya | — | Departemen: `Management`, `Sales`, `Marketing`, `Operations`, `Finance`, `IT`, `HR`, `Customer Service` |
| `position` | string | Ya | — | Jabatan/posisi karyawan |
| `description` | string | Tidak | — | Catatan/deskripsi tambahan termasuk keahlian dan tanggung jawab (max 1000 karakter) |
| `employment_type` | enum | Tidak | `full_time` | Tipe kepegawaian: `full_time`, `part_time`, `contract`, `intern` |
| `hire_date` | date | Ya | — | Tanggal mulai bekerja |
| `status` | enum | Tidak | `active` | Status karyawan: `active`, `on_leave`, `terminated` |

### Grup: Kompensasi

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `salary` | number | Tidak | — | Gaji bulanan karyawan |
| `bank_account` | object | Tidak | — | Data rekening bank: `{ bank_name, account_number, account_name }` |

### Grup: Darurat

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `emergency_contact` | object | Tidak | — | Kontak darurat: `{ name, relationship, phone }` |

***

## Tabel Schema: Leave (Cuti Personal)

Entitas pengajuan cuti personal yang menyimpan data pengajuan cuti mandiri karyawan (non-perusahaan).

| Field | Tipe | Wajib | Default | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | Ya | — | ID perusahaan (multi-tenant) |
| `employee_id` | string | Ya | — | ID karyawan (FK ke Employee) |
| `employee_name` | string | Tidak | — | Nama karyawan (denormalized) |
| `leave_type` | enum | Ya | `annual` | Jenis cuti: `annual`, `sick`, `unpaid`, `maternity`, `paternity`, `emergency` |
| `start_date` | date | Ya | — | Tanggal mulai cuti |
| `end_date` | date | Ya | — | Tanggal selesai cuti |
| `total_days` | number | Tidak | — | Jumlah hari cuti (dihitung dari rentang tanggal) |
| `reason` | string | Ya | — | Alasan pengajuan cuti |
| `description` | string | Tidak | — | Detail tambahan atau keterangan lebih lanjut (max 1000 karakter) |
| `attachment_url` | string | Tidak | — | URL file pendukung (surat dokter, dll) |
| `status` | enum | Tidak | `pending` | Status pengajuan: `pending`, `approved`, `rejected`, `cancelled` |
| `approver_id` | string | Tidak | — | ID approver (manager/HR) |
| `approver_notes` | string | Tidak | — | Catatan dari approver saat approve/reject |
| `approved_at` | datetime | Tidak | — | Timestamp keputusan approval |

***

## Tabel Enum: shift\_name (Nama Shift)

| Nilai | Label | Deskripsi |
| - | - | - |
| `pagi` | Shift Pagi | Shift pagi, umumnya 06:00–14:00 |
| `siang` | Shift Siang | Shift siang, umumnya 14:00–22:00 |
| `malam` | Shift Malam | Shift malam, umumnya 22:00–06:00 |
| `reguler` | Shift Reguler | Shift standar, umumnya 09:00–17:00 |

## Tabel Enum: override\_status (Status Koreksi Manual)

| Nilai | Label | Deskripsi |
| - | - | - |
| `false` | Belum Dikoreksi | Absensi asli tanpa koreksi manual |
| `true` | Sedang/Awal Dikoreksi | Absensi telah diajukan koreksi manual, menunggu approval |
| `approved` | Koreksi Disetujui | Koreksi manual disetujui oleh manager/HR |
| `rejected` | Koreksi Ditolak | Koreksi manual ditolak, record tetap asli |

## Tabel Enum: work\_day (Hari Kerja)

| Nilai Numerik | Hari | Deskripsi |
| - | - | - |
| `1` | Senin | Hari kerja default |
| `2` | Selasa | Hari kerja default |
| `3` | Rabu | Hari kerja default |
| `4` | Kamis | Hari kerja default |
| `5` | Jumat | Hari kerja default |
| `6` | Sabtu | Hari kerja opsional (non-default) |
| `7` | Minggu | Akhir pekan (non-default) |

***

## Matriks RLS Lengkap: Entitas Attendance

### CompanyAttendance — Row-Level Security

| Operasi | Kondisi Akses |
| - | - |
| **Create** | `data.company_id === user.data.active_company_id` (dalam perusahaan aktif) ATAU `created_by_id === user.id` (pembuat) ATAU `data.employee_email === user.email` (email sendiri) ATAU `user.role === "admin"` |
| **Read** | `data.company_id === user.data.active_company_id` ATAU `created_by_id === user.id` ATAU `data.employee_email === user.email` ATAU `user.role === "admin"` |
| **Update** | `data.company_id === user.data.active_company_id` ATAU `created_by_id === user.id` ATAU `data.employee_email === user.email` ATAU `user.role === "admin"` |
| **Delete** | `data.company_id === user.data.active_company_id` ATAU `created_by_id === user.id` ATAU `data.employee_email === user.email` ATAU `user.role === "admin"` |

### CompanyLeave — Row-Level Security

| Operasi | Kondisi Akses |
| - | - |
| **Create** | `data.company_id === user.data.active_company_id` ATAU `created_by_id === user.id` ATAU `data.employee_email === user.email` ATAU `user.role === "admin"` |
| **Read** | `data.company_id === user.data.active_company_id` ATAU `created_by_id === user.id` ATAU `data.employee_email === user.email` ATAU `user.role === "admin"` |
| **Update** | `data.company_id === user.data.active_company_id` ATAU `created_by_id === user.id` ATAU `data.employee_email === user.email` ATAU `user.role === "admin"` |
| **Delete** | `data.company_id === user.data.active_company_id` ATAU `created_by_id === user.id` ATAU `data.employee_email === user.email` ATAU `user.role === "admin"` |

### AttendanceRecord — Row-Level Security

| Operasi | Kondisi Akses |
| - | - |
| **Create** | `data.user_id === user.id` ATAU `created_by_id === user.id` ATAU `user.role === "admin"` |
| **Read** | `data.user_id === user.id` ATAU `created_by_id === user.id` ATAU `user.role === "admin"` |
| **Update** | `data.user_id === user.id` ATAU `created_by_id === user.id` ATAU `user.role === "admin"` |
| **Delete** | `data.user_id === user.id` ATAU `created_by_id === user.id` ATAU `user.role === "admin"` |

### Employee — Row-Level Security

| Operasi | Kondisi Akses |
| - | - |
| **Create** | `data.company_id === user.data.active_company_id` ATAU `created_by_id === user.id` ATAU `data.user_id === user.id` ATAU `user.role === "admin"` |
| **Read** | `data.company_id === user.data.active_company_id` ATAU `created_by_id === user.id` ATAU `data.user_id === user.id` ATAU `user.role === "admin"` |
| **Update** | `data.company_id === user.data.active_company_id` ATAU `created_by_id === user.id` ATAU `data.user_id === user.id` ATAU `user.role === "admin"` |
| **Delete** | `data.company_id === user.data.active_company_id` ATAU `created_by_id === user.id` ATAU `data.user_id === user.id` ATAU `user.role === "admin"` |

### CompanyAttendanceSettings — Row-Level Security

| Operasi | Kondisi Akses |
| - | - |
| **Create** | Tanpa batasan RLS (dikelola oleh sistem) |
| **Read** | Tanpa batasan RLS (dikelola oleh sistem) |
| **Update** | Tanpa batasan RLS (dikelola oleh sistem) |
| **Delete** | Tanpa batasan RLS (dikelola oleh sistem) |

***

## Ringkasan Relasi Entitas Attendance

```
┌─────────────────────────────────────────────────────────────────────────────────┐
│                         ALUR DATA MODUL ATTENDANCE                              │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                 │
│  Employee ──1:N──▶ CompanyAttendance ◀──N:1── CompanyAttendanceSettings        │
│  (Karyawan)         (Absensi Perusahaan)       (Pengaturan Absensi)             │
│       │                    │                         │                          │
│       │                    │                         ├── locations[] (geofence) │
│       │                    │                         ├── shifts[] (shift kerja) │
│       │                    │                         ├── working_hours          │
│       │                    │                         └── overtime_settings      │
│       │                    │                                                  │
│       │                    └──▶ Payroll Module (integrasi gaji)                │
│       │                                                                      │
│       ├──1:N──▶ CompanyLeave (Cuti Perusahaan)                               │
│       │             │                                                        │
│       │             └──▶ Approval Workflow (Manager/HR)                      │
│       │                                                                      │
│       ├──1:N──▶ Leave (Cuti Personal)                                        │
│       │                                                                      │
│       └──1:N──▶ AttendanceRecord (Absensi Personal)                          │
│                                                                                 │
└─────────────────────────────────────────────────────────────────────────────────┘
```


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