# API Documentation — Admin Dashboard

**Base URL:** `http://{host}:{port}/api`  
**Versi:** v2.0  
**Update Terakhir:** 2026-02-12

---

## Daftar Isi

1. [Dashboard Statistics](#1-dashboard-statistics)
2. [User Management](#2-user-management)
3. [Device Management](#3-device-management)
4. [Device Reset Requests](#4-device-reset-requests)
5. [Branch / Kantor Management](#5-branch--kantor-management)
6. [Jam Absensi Settings](#6-jam-absensi-settings)
7. [Absensi Data](#7-absensi-data)
8. [Notifications](#8-notifications)
9. [Blog / News](#9-blog--news)
10. [App Version Management](#10-app-version-management)
11. [Response Codes](#11-response-codes)

---

## 1. Dashboard Statistics

### 1.1 `GET /dashboard/summary`

Ringkasan statistik absensi hari ini dan bulan berjalan.

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `date` | string | ❌ | Hari ini | Tanggal (format: `Y-m-d`) |
| `npp` | string | ❌ | - | Filter per karyawan |
| `branch_id` | string | ❌ | - | Filter per cabang |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "date": "2026-02-12",
    "filters": {
        "npp": null,
        "branch_id": null
    },
    "data": {
        "today": {
            "total": 45,
            "on_time": 40,
            "late": 5,
            "complete": 30,
            "checkin_only": 15
        },
        "month": {
            "period": "February 2026",
            "total_records": 800,
            "unique_employees": 50,
            "late_count": 30,
            "early_checkout_count": 10,
            "late_rate": 3.75
        },
        "top_branches": [
            { "branch_id": "001", "total": 300 },
            { "branch_id": "002", "total": 200 }
        ]
    }
}
```

---

### 1.2 `GET /dashboard/monthly-trend`

Trend absensi per bulan selama setahun (12 bulan).

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `year` | int | ❌ | Tahun ini | Tahun yang ingin dilihat |
| `npp` | string | ❌ | - | Filter per karyawan |
| `branch_id` | string | ❌ | - | Filter per cabang |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "year": "2026",
    "filters": { "npp": null, "branch_id": null },
    "data": [
        {
            "month": "Jan",
            "month_num": "01",
            "total": 900,
            "on_time": 800,
            "late": 80,
            "absent": 20
        },
        {
            "month": "Feb",
            "month_num": "02",
            "total": 450,
            "on_time": 400,
            "late": 40,
            "absent": 10
        }
    ]
}
```

---

### 1.3 `GET /dashboard/weekly-pattern`

Pola absensi rata-rata per hari dalam seminggu (Senin–Minggu).

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `start_date` | string | ❌ | 1 Januari tahun ini | Mulai (format: `Y-m-d`) |
| `end_date` | string | ❌ | 31 Desember tahun ini | Akhir (format: `Y-m-d`) |
| `npp` | string | ❌ | - | Filter per karyawan |
| `branch_id` | string | ❌ | - | Filter per cabang |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "date_range": {
        "start_date": "2026-01-01",
        "end_date": "2026-12-31"
    },
    "data": [
        {
            "day": "Minggu",
            "day_num": 0,
            "avg_checkin": null,
            "avg_checkout": null,
            "total_records": 0
        },
        {
            "day": "Senin",
            "day_num": 1,
            "avg_checkin": "07:52",
            "avg_checkout": "17:05",
            "total_records": 42
        }
    ]
}
```

---

### 1.4 `GET /dashboard/hourly-distribution`

Distribusi check-in per jam (00:00–23:00).

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `start_date` | string | ❌ | Awal bulan ini | Mulai |
| `end_date` | string | ❌ | Akhir bulan ini | Akhir |
| `npp` | string | ❌ | - | Filter per karyawan |
| `branch_id` | string | ❌ | - | Filter per cabang |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": [
        { "hour": "06:00", "count": 5 },
        { "hour": "07:00", "count": 120 },
        { "hour": "08:00", "count": 80 },
        { "hour": "09:00", "count": 15 }
    ]
}
```

---

### 1.5 `GET /dashboard/branch-comparison`

Perbandingan absensi antar cabang.

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `start_date` | string | ❌ | Awal bulan ini | Mulai |
| `end_date` | string | ❌ | Akhir bulan ini | Akhir |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "date_range": {
        "start_date": "2026-02-01",
        "end_date": "2026-02-28"
    },
    "data": [
        {
            "branch_id": "001",
            "total": 300,
            "on_time": 275,
            "late": 25,
            "late_rate": 8.33
        },
        {
            "branch_id": "002",
            "total": 200,
            "on_time": 190,
            "late": 10,
            "late_rate": 5.0
        }
    ]
}
```

---

## 2. User Management

### 2.1 `GET /users/list`

Daftar semua user dengan paginasi dan pencarian.

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `search` | string | ❌ | - | Cari berdasarkan NPP atau nama |
| `page` | int | ❌ | 1 | Halaman |
| `per_page` | int | ❌ | 20 | Per halaman (max: 100) |
| `branch_id` | string | ❌ | - | Filter per cabang (kd_unit) |
| `group_id` | int | ❌ | - | Filter per role/group |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": [
        {
            "id": 1,
            "npp": "21002612",
            "nama": "John Doe",
            "email": "john@mail.com",
            "kd_unit": "001",
            "nama_kantor": "Kantor Pusat",
            "id_group_menu": 1,
            "group_name": "Admin",
            "radius": 50,
            "has_device": true,
            "device_id": "abc123def",
            "device_name": "Samsung Galaxy S21",
            "last_login_at": "2026-02-12 09:00:00",
            "created_at": "2025-01-01T00:00:00Z",
            "updated_at": "2026-02-10T08:00:00Z"
        }
    ],
    "pagination": {
        "current_page": 1,
        "per_page": 20,
        "total": 50,
        "last_page": 3,
        "from": 1,
        "to": 20
    }
}
```

---

### 2.2 `GET /users/{npp}/detail`

Detail lengkap user termasuk info cabang, role, device, dan statistik absensi.

**Path Parameters:**

| Parameter | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `npp` | string | ✅ | NPP karyawan |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": {
        "user": {
            "id": 1,
            "npp": "21002612",
            "nama": "John Doe",
            "email": "john@mail.com",
            "kd_unit": "001",
            "radius": 50,
            "id_group_menu": 1,
            "created_at": "2025-01-01T00:00:00Z",
            "updated_at": "2026-02-10T08:00:00Z"
        },
        "branch": {
            "kode_kantor": "001",
            "nama_kantor": "Kantor Pusat",
            "latitude": 5.55,
            "longitude": 95.33,
            "radius": 50
        },
        "role": {
            "group_id": 1,
            "group_name": "Admin"
        },
        "device": {
            "device_id": "abc123def",
            "status": "active",
            "platform": "android",
            "brand": "samsung",
            "model": "SM-G991B",
            "device_name": "Galaxy S21",
            "manufacturer": "Samsung",
            "os_version": "14",
            "sdk_version": "34",
            "is_physical_device": true,
            "app_version": "1.0.2",
            "app_build_number": "5",
            "formatted_name": "Samsung Galaxy S21",
            "registered_at": "2026-01-15T08:00:00Z",
            "last_login_at": "2026-02-12T09:00:00Z",
            "is_registered": true
        },
        "device_stats": {
            "total_devices": 2,
            "reset_count": 1
        },
        "attendance_stats": {
            "total_records": 100,
            "total_check_in": 100,
            "total_check_out": 95,
            "complete_attendance": 95,
            "first_attendance": "2025-01-02",
            "last_attendance": "2026-02-12"
        },
        "this_month": {
            "total_days": 8,
            "check_in_count": 8,
            "check_out_count": 7
        },
        "last_attendances": [
            {
                "tgl_absensi": "2026-02-12",
                "jam_masuk": "07:50:00",
                "jam_pulang": null
            }
        ]
    }
}
```

---

### 2.3 `GET /users/{npp}/devices`

Riwayat semua device yang pernah terdaftar untuk user.

---

### 2.4 `GET /users/{npp}/complete`

Data user paling lengkap (bisa pilih section yang ditampilkan).

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `include` | string | ❌ | all | Comma-separated: `user,device,attendance,notifications,api_activity` |
| `api_activity_limit` | int | ❌ | 10 | Limit log API activity |

---

### 2.5 `GET /users/{npp}/attendance-history`

Riwayat absensi user dengan paginasi dan filter lengkap.

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `page` | int | ❌ | 1 | Halaman |
| `per_page` | int | ❌ | 20 | Per halaman (max: 100) |
| `from` | string | ❌ | - | Dari tanggal (format: `Y-m-d`) |
| `to` | string | ❌ | - | Sampai tanggal (format: `Y-m-d`) |
| `month` | string | ❌ | - | Filter bulan (format: `Y-m`, contoh: `2026-02`) |
| `status` | string | ❌ | - | `complete`, `incomplete`, `check_in_only`, `check_out_only` |
| `sort` | string | ❌ | date | `date`, `check_in`, `check_out` |
| `order` | string | ❌ | desc | `asc`, `desc` |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": {
        "user": { "npp": "21002612", "nama": "John Doe" },
        "filters": {
            "from": null,
            "to": null,
            "month": "2026-02",
            "status": null
        },
        "records": [
            {
                "id": 123,
                "tgl_absensi": "2026-02-12",
                "jam_masuk": "07:50:00",
                "jam_pulang": "17:05:00",
                "branch_id": "001",
                "latitude": 5.55,
                "longitude": 95.33,
                "status": "complete",
                "created_at": "2026-02-12T07:50:00Z"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 20,
            "total": 8,
            "last_page": 1
        }
    }
}
```

---

## 3. Device Management

### 3.1 `POST /admin/delete-device`

Reset device binding user (admin only). Menggunakan soft reset — riwayat device tetap tersimpan.

**Request Body:**
```json
{
    "npp": "21002612",
    "reason": "Device hilang"
}
```

| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `npp` | string | ✅ | NPP karyawan (max: 20 karakter) |
| `reason` | string | ❌ | Alasan reset (max: 255 karakter) |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Device berhasil direset. User dapat login dengan device baru.",
    "data": {
        "npp": "21002612",
        "nama": "John Doe",
        "old_device_id": "abc123def",
        "old_device_name": "Samsung Galaxy S21",
        "reason": "Device hilang",
        "reset_by": "admin_npp",
        "reset_at": "2026-02-12 10:00:00",
        "total_reset_count": 2
    }
}
```

---

## 4. Device Reset Requests

### 4.1 `GET /device-reset`

Semua permintaan reset device.

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `status` | string | ❌ | - | `pending`, `approved`, `rejected` |
| `npp` | string | ❌ | - | Filter per user |
| `start_date` | string | ❌ | - | Filter mulai |
| `end_date` | string | ❌ | - | Filter akhir |
| `per_page` | int | ❌ | 50 | Per halaman |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": {
        "current_page": 1,
        "data": [
            {
                "id": 5,
                "npp": "21002612",
                "reason": "HP rusak",
                "status": "pending",
                "processed_by": null,
                "processed_at": null,
                "admin_notes": null,
                "created_at": "2026-02-12T08:00:00Z",
                "user": {
                    "npp": "21002612",
                    "nama": "John Doe"
                }
            }
        ],
        "total": 5,
        "per_page": 50,
        "last_page": 1
    }
}
```

---

### 4.2 `GET /device-reset/pending`

Hanya request yang masih pending (shortcut).

---

### 4.3 `GET /device-reset/{id}`

Detail satu request reset.

---

### 4.4 `POST /device-reset/{id}/approve`

Approve request reset device. Akan otomatis reset device user dan kirim notifikasi (email + push).

**Request Body:**
```json
{
    "admin_npp": "10001",
    "notes": "Disetujui karena ganti device"
}
```

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Device reset berhasil disetujui. User dapat login dengan device baru.",
    "data": {
        "id": 5,
        "npp": "21002612",
        "status": "approved",
        "processed_by": "10001",
        "processed_at": "2026-02-12T10:00:00Z"
    },
    "device_reset": {
        "device_id": "abc123def",
        "formatted_name": "Samsung Galaxy S21",
        "status": "reset",
        "reset_at": "2026-02-12T10:00:00Z"
    },
    "email_sent": true,
    "push_sent": true
}
```

---

### 4.5 `POST /device-reset/{id}/reject`

Reject request reset device. Akan kirim notifikasi penolakan (email + push).

**Request Body:**
```json
{
    "admin_npp": "10001",
    "reason": "Tidak sesuai prosedur, silakan hubungi HRD"
}
```

| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `reason` | string | ✅ | Alasan penolakan (min: 10 karakter) |
| `admin_npp` | string | ❌ | NPP admin yang memproses |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Device reset ditolak.",
    "data": {
        "id": 5,
        "npp": "21002612",
        "status": "rejected",
        "processed_by": "10001",
        "admin_notes": "Tidak sesuai prosedur"
    },
    "email_sent": true,
    "push_sent": true
}
```

---

## 5. Branch / Kantor Management

### 5.1 `GET /branches`

Daftar semua kantor/lokasi (grouped by kode_kantor, hanya yang aktif).

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `search` | string | ❌ | - | Cari kode, nama kantor, atau nama lokasi |
| `per_page` | int | ❌ | 50 | Per halaman |
| `export` | bool | ❌ | false | Jika `true`, return semua tanpa paginasi |

---

### 5.2 `POST /branches`

Tambah lokasi baru.

**Request Body:**
```json
{
    "kode_kantor": "001",
    "nama_kantor": "Kantor Pusat",
    "nama_lokasi": "Gedung Utama Lt. 1",
    "latitude": 5.5500,
    "longitude": 95.3300,
    "radius": 50
}
```

| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `kode_kantor` | string | ✅ | Kode kantor |
| `nama_kantor` | string | ❌ | Nama kantor |
| `nama_lokasi` | string | ✅ | Nama lokasi spesifik |
| `latitude` | float | ✅ | Latitude |
| `longitude` | float | ✅ | Longitude |
| `radius` | int | ❌ | Radius (meter), default: 50 |

---

### 5.3 `GET /branches/{id}`

Detail satu lokasi.

### 5.4 `PUT /branches/{id}`

Update lokasi.

### 5.5 `DELETE /branches/{id}`

Hapus lokasi.

### 5.6 `POST /branches/by-kode`

Ambil semua lokasi aktif berdasarkan kode_kantor.

**Request Body:**
```json
{ "kode_kantor": "001" }
```

### 5.7 `POST /branches/clear-cache`

Bersihkan cache data kantor (legacy).

---

## 6. Jam Absensi Settings

### 6.1 `GET /jam-absensi`

Daftar semua setting jam absensi.

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": [
        {
            "id": 1,
            "nama": "Jam Kerja Normal",
            "start_jam_masuk": "07:00:00",
            "end_jam_masuk": "09:00:00",
            "start_jam_pulang": "16:00:00",
            "end_jam_pulang": "18:00:00",
            "is_active": true,
            "created_at": "2026-01-01T00:00:00Z",
            "updated_at": "2026-01-01T00:00:00Z"
        },
        {
            "id": 2,
            "nama": "Jam Kerja Shift Malam",
            "start_jam_masuk": "20:00:00",
            "end_jam_masuk": "22:00:00",
            "start_jam_pulang": "04:00:00",
            "end_jam_pulang": "06:00:00",
            "is_active": false,
            "created_at": "2026-01-01T00:00:00Z",
            "updated_at": "2026-01-01T00:00:00Z"
        }
    ]
}
```

---

### 6.2 `GET /jam-absensi/active`

Setting jam yang sedang aktif saat ini.

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": {
        "id": 1,
        "nama": "Jam Kerja Normal",
        "start_jam_masuk": "07:00:00",
        "end_jam_masuk": "09:00:00",
        "start_jam_pulang": "16:00:00",
        "end_jam_pulang": "18:00:00",
        "is_active": true
    }
}
```

---

### 6.3 `POST /jam-absensi`

Tambah setting jam baru.

**Request Body:**
```json
{
    "nama": "Jam Kerja Normal",
    "start_jam_masuk": "07:00:00",
    "end_jam_masuk": "09:00:00",
    "start_jam_pulang": "16:00:00",
    "end_jam_pulang": "18:00:00",
    "is_active": false
}
```

| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `nama` | string | ✅ | Nama setting (max: 100 karakter) |
| `start_jam_masuk` | string | ✅ | Awal waktu check-in (format: `H:i:s`) |
| `end_jam_masuk` | string | ❌ | Akhir waktu check-in (format: `H:i:s`) |
| `start_jam_pulang` | string | ✅ | Awal waktu check-out (format: `H:i:s`) |
| `end_jam_pulang` | string | ❌ | Akhir waktu check-out (format: `H:i:s`) |
| `is_active` | bool | ❌ | Set aktif? (default: false). Jika `true`, setting lain otomatis nonaktif |

**Response 201:**
```json
{
    "rcode": "00",
    "message": "Attendance time setting created successfully",
    "data": {
        "id": 3,
        "nama": "Jam Kerja Normal",
        "start_jam_masuk": "07:00:00",
        "end_jam_masuk": "09:00:00",
        "start_jam_pulang": "16:00:00",
        "end_jam_pulang": "18:00:00",
        "is_active": false
    }
}
```

---

### 6.4 `GET /jam-absensi/{id}`

Detail satu setting berdasarkan ID.

**Response 200:** Sama seperti satu item di response `6.1`.

**Response 404:**
```json
{
    "rcode": "81",
    "message": "Attendance time setting not found"
}
```

---

### 6.5 `PUT /jam-absensi/{id}`

Update setting jam. Hanya kirim field yang ingin diubah (partial update).

**Request Body:**
```json
{
    "nama": "Jam Kerja Baru",
    "start_jam_masuk": "07:30:00",
    "is_active": true
}
```

| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `nama` | string | ❌ | Nama setting (max: 100 karakter) |
| `start_jam_masuk` | string | ❌ | Format: `H:i:s` |
| `end_jam_masuk` | string | ❌ | Format: `H:i:s` |
| `start_jam_pulang` | string | ❌ | Format: `H:i:s` |
| `end_jam_pulang` | string | ❌ | Format: `H:i:s` |
| `is_active` | bool | ❌ | Jika `true`, setting lain otomatis nonaktif |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Attendance time setting updated successfully",
    "data": { /* data yang sudah diupdate */ }
}
```

---

### 6.6 `DELETE /jam-absensi/{id}`

Hapus setting jam.

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Attendance time setting deleted successfully"
}
```

---

### 6.7 `POST /jam-absensi/{id}/set-active`

Set setting ini sebagai jam aktif. Setting lain akan otomatis di-nonaktifkan.

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Attendance time setting activated successfully",
    "data": {
        "id": 1,
        "nama": "Jam Kerja Normal",
        "start_jam_masuk": "07:00:00",
        "end_jam_masuk": "09:00:00",
        "start_jam_pulang": "16:00:00",
        "end_jam_pulang": "18:00:00",
        "is_active": true
    }
}
```

---

## 7. Absensi Data

### 7.1 `GET /absensi-table`

Daftar data absensi dari tbl_absensi (PostgreSQL) dengan filter lengkap. Read-only.

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `start_date` | string | ❌ | - | Dari tanggal (format: `Y-m-d`) |
| `end_date` | string | ❌ | - | Sampai tanggal (format: `Y-m-d`) |
| `date` | string | ❌ | - | Filter satu tanggal spesifik |
| `npp` | string/array | ❌ | - | Filter per NPP (bisa multiple) |
| `branch_id` | string/array | ❌ | - | Filter per cabang (bisa multiple) |
| `status` | string | ❌ | - | `checkin_only`, `complete`, `checkout_only`, `no_checkin` |
| `checkin_start` | string | ❌ | - | Jam masuk mulai (format: `H:i`) |
| `checkin_end` | string | ❌ | - | Jam masuk akhir (format: `H:i`) |
| `checkout_start` | string | ❌ | - | Jam pulang mulai (format: `H:i`) |
| `checkout_end` | string | ❌ | - | Jam pulang akhir (format: `H:i`) |
| `late_only` | bool | ❌ | false | Hanya yang terlambat (> 08:00) |
| `early_checkout_only` | bool | ❌ | false | Hanya yang pulang cepat (< 17:00) |
| `no_absensi` | string | ❌ | - | Filter per nomor absensi |
| `page` | int | ❌ | 1 | Halaman |
| `per_page` | int | ❌ | 50 | Per halaman |
| `export` | bool | ❌ | false | Jika `true`, return semua tanpa paginasi |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": {
        "current_page": 1,
        "data": [
            {
                "id": 123,
                "npp": "21002612",
                "tgl_absensi": "2026-02-12",
                "jam_masuk": "07:50:00",
                "jam_pulang": "17:05:00",
                "branch_id": "001",
                "latitude": 5.55,
                "longitude": 95.33,
                "no_absensi": "ABS-20260212-001",
                "device_info": "Samsung Galaxy S21",
                "cdate": "2026-02-12 07:50:00",
                "duration": "9 jam 15 menit",
                "is_late": false,
                "is_early_checkout": false
            }
        ],
        "total": 100,
        "per_page": 50,
        "last_page": 2
    },
    "filters_applied": {
        "start_date": "2026-02-01",
        "end_date": "2026-02-28",
        "npp": null,
        "branch_id": null,
        "status": null
    },
    "summary": {
        "total_records": 100,
        "page_count": 50
    }
}
```

**Export Mode (`?export=true`):**
```json
{
    "rcode": "00",
    "message": "Export data ready",
    "data": [ /* array semua data tanpa paginasi */ ],
    "total_count": 500,
    "filters_applied": { /* ... */ }
}
```

---

### 7.2 `GET /absensi-table/{id}`

Detail satu record absensi berdasarkan ID.

**Path Parameters:**

| Parameter | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `id` | int | ✅ | ID record absensi |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": {
        "id": 123,
        "npp": "21002612",
        "tgl_absensi": "2026-02-12",
        "jam_masuk": "07:50:00",
        "jam_pulang": "17:05:00",
        "branch_id": "001",
        "latitude": 5.55,
        "longitude": 95.33,
        "no_absensi": "ABS-20260212-001",
        "device_info": "Samsung Galaxy S21",
        "cdate": "2026-02-12 07:50:00",
        "duration": "9 jam 15 menit",
        "is_late": false,
        "is_early_checkout": false
    }
}
```

**Response 404:**
```json
{
    "rcode": "81",
    "message": "Data absensi tidak ditemukan"
}
```

---

### 7.3 `GET /absensi-table/statistics`

Statistik ringkasan data absensi dalam periode tertentu.

**Query Parameters:**

| Parameter | Tipe | Wajib | Default | Keterangan |
|---|---|---|---|---|
| `start_date` | string | ❌ | Awal bulan ini | Dari tanggal (format: `Y-m-d`) |
| `end_date` | string | ❌ | Akhir bulan ini | Sampai tanggal (format: `Y-m-d`) |
| `npp` | string/array | ❌ | - | Filter per NPP |
| `branch_id` | string/array | ❌ | - | Filter per cabang |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": {
        "total_records": 800,
        "unique_employees": 50,
        "checkin_only": 20,
        "complete_attendance": 750,
        "late_checkin": 30,
        "early_checkout": 10,
        "completion_rate": 93.75,
        "late_rate": 3.75,
        "date_range": {
            "start_date": "2026-02-01",
            "end_date": "2026-02-28"
        }
    }
}
```

---

## 8. Notifications

### User Endpoints

| Method | Endpoint | Keterangan |
|---|---|---|
| `GET` | `/notifications` | Daftar notifikasi user (per NPP) |
| `GET` | `/notifications/unread-count` | Jumlah belum dibaca |
| `POST` | `/notifications/{id}/read` | Tandai sudah dibaca |
| `POST` | `/notifications/read-all` | Tandai semua sudah dibaca |
| `DELETE` | `/notifications/{id}` | Hapus notifikasi |

### Admin Endpoints

| Method | Endpoint | Keterangan |
|---|---|---|
| `GET` | `/admin/notifications` | Semua notifikasi (seluruh user) |
| `GET` | `/admin/notifications/stats` | Statistik notifikasi |
| `GET` | `/admin/notifications/{id}` | Detail notifikasi |
| `POST` | `/admin/notifications/{id}/read` | Tandai dibaca |
| `DELETE` | `/admin/notifications/{id}` | Hapus satu |
| `POST` | `/admin/notifications/bulk-delete` | Hapus massal |
| `POST` | `/admin/notifications/bulk-read` | Tandai baca massal |
| `GET` | `/admin/notifications/user/{npp}` | Notifikasi per user |

### Broadcast

#### `POST /notifications/broadcast`

Kirim broadcast notifikasi ke semua user. Push notification akan terkirim ke semua device aktif via FCM.

**Request Body:**
```json
{
    "title": "Pengumuman Penting",
    "body": "Besok tanggal 13 Februari 2026 adalah hari libur nasional",
    "type": "announcement"
}
```

### FCM Token

| Method | Endpoint | Keterangan |
|---|---|---|
| `POST` | `/fcm/register` | Register FCM token |
| `POST` | `/fcm/unregister` | Hapus FCM token (saat logout) |

#### `POST /fcm/register` — Request Body:
```json
{
    "npp": "21002612",
    "fcm_token": "cKPLxxx:APA91bHxxx...",
    "device_type": "android",
    "device_id": "abc123def"
}
```

---

## 9. Blog / News

### Mobile Endpoints

| Method | Endpoint | Keterangan |
|---|---|---|
| `GET` | `/blogs/published` | Blog yang sudah dipublish |
| `GET` | `/blogs/featured` | Blog unggulan |
| `GET` | `/blogs/slug/{slug}` | Blog per slug (deep linking) |

### Admin CRUD Endpoints

| Method | Endpoint | Keterangan |
|---|---|---|
| `GET` | `/blogs` | Semua blog (termasuk draft/archived) |
| `POST` | `/blogs` | Buat blog baru |
| `GET` | `/blogs/{id}` | Detail blog |
| `PUT` | `/blogs/{id}` | Update blog |
| `DELETE` | `/blogs/{id}` | Hapus blog + image + attachments |
| `POST` | `/blogs/{id}/publish` | Publish blog (kirim notifikasi) |
| `POST` | `/blogs/{id}/archive` | Arsipkan blog |
| `POST` | `/blogs/{id}/attachments` | Upload attachment ke blog |
| `DELETE` | `/blogs/attachments/{id}` | Hapus satu attachment |

---

### 9.1 `GET /blogs` — Daftar Blog (Admin)

Menampilkan semua blog dengan pagination, bisa difilter.

**Query Parameters:**

| Parameter | Tipe | Keterangan |
|---|---|---|
| `status` | string | Filter: `draft`, `published`, `archived` |
| `category` | string | Filter: `announcement`, `news`, `event`, `info`, `other` |
| `is_featured` | string | Filter: `true` / `false` |
| `search` | string | Cari di title dan content (case-insensitive) |
| `per_page` | int | Jumlah per halaman (default: 20) |
| `page` | int | Nomor halaman |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": {
        "current_page": 1,
        "data": [
            {
                "id": 1,
                "title": "Pengumuman Libur Nasional",
                "slug": "pengumuman-libur-nasional",
                "excerpt": "Ringkasan singkat untuk preview",
                "content": "<p>Isi blog lengkap...</p>",
                "image": "blogs/pengumuman-libur-nasional-1707800000.jpg",
                "image_thumbnail": "blogs/pengumuman-libur-nasional-1707800000.jpg",
                "category": "announcement",
                "status": "published",
                "is_featured": true,
                "is_pinned": false,
                "author_npp": "10005",
                "author_name": "ADMIN SISTEM",
                "view_count": 42,
                "published_at": "2026-02-10T08:00:00Z",
                "created_at": "2026-02-10T07:30:00Z",
                "updated_at": "2026-02-10T08:00:00Z"
            }
        ],
        "per_page": 20,
        "total": 5,
        "last_page": 1
    }
}
```

---

### 9.2 `GET /blogs/published` — Blog Published (Mobile)

Blog yang sudah dipublish, diurutkan pinned terlebih dahulu, lalu published_at terbaru.

**Query Parameters:**

| Parameter | Tipe | Keterangan |
|---|---|---|
| `category` | string | Filter kategori |
| `limit` | int | Jumlah data (default: 10) |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": [
        {
            "id": 1,
            "title": "Pengumuman Penting",
            "slug": "pengumuman-penting",
            "excerpt": "Ringkasan singkat...",
            "image_thumbnail": "blogs/pengumuman-penting.jpg",
            "category": "announcement",
            "is_featured": true,
            "is_pinned": true,
            "published_at": "2026-02-10T08:00:00Z",
            "view_count": 42
        }
    ]
}
```

---

### 9.3 `GET /blogs/featured` — Blog Unggulan (Mobile)

Blog yang ditandai featured, untuk tampilan carousel di dashboard mobile.

**Query Parameters:**

| Parameter | Tipe | Keterangan |
|---|---|---|
| `limit` | int | Jumlah data (default: 5) |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": [
        {
            "id": 1,
            "title": "Berita Terbaru",
            "slug": "berita-terbaru",
            "excerpt": "Ringkasan...",
            "image": "blogs/berita-terbaru.jpg",
            "image_thumbnail": "blogs/berita-terbaru.jpg",
            "category": "news",
            "published_at": "2026-02-10T08:00:00Z"
        }
    ]
}
```

---

### 9.4 `POST /blogs` — Buat Blog Baru

**Content-Type:** `multipart/form-data` (karena ada upload file)

**Request Body:**

| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `title` | string | ✅ | Judul blog (max: 255) |
| `content` | string | ✅ | Isi blog (HTML) |
| `excerpt` | string | ❌ | Ringkasan (max: 500, auto-generate jika kosong) |
| `category` | string | ❌ | `announcement`, `news`, `event`, `info`, `other` (default: `news`) |
| `status` | string | ❌ | `draft`, `published`, `archived` (default: `draft`) |
| `is_featured` | bool | ❌ | Tandai unggulan (default: false) |
| `is_pinned` | bool | ❌ | Sematkan di atas (default: false) |
| `image` | file | ❌ | Gambar utama (jpeg/png/jpg/gif, max: 2MB) |
| `author_npp` | string | ❌ | NPP penulis |
| `author_name` | string | ❌ | Nama penulis |
| `attachments[]` | file[] | ❌ | Lampiran (pdf/doc/docx/xls/xlsx, max: 10MB/file, max: 10 file) |
| `send_notification` | bool | ❌ | Kirim push notification (default: true, hanya jika status = published) |

> **Note:** `slug` di-generate otomatis dari `title`.

**Response 201:**
```json
{
    "rcode": "00",
    "message": "Blog created successfully",
    "data": {
        "id": 5,
        "title": "Pengumuman Libur Nasional",
        "slug": "pengumuman-libur-nasional",
        "excerpt": "Ringkasan singkat...",
        "content": "<p>Isi blog...</p>",
        "image": "blogs/pengumuman-libur-nasional-1707800000.jpg",
        "category": "announcement",
        "status": "draft",
        "is_featured": false,
        "is_pinned": false,
        "author_npp": "10005",
        "author_name": "ADMIN SISTEM",
        "published_at": null,
        "attachments": [
            {
                "id": 1,
                "file_name": "dokumen-pendukung.pdf",
                "file_path": "blog-attachments/pengumuman-libur-nasional-1707800000-abc123.pdf",
                "file_type": "pdf",
                "file_size": 204800
            }
        ]
    },
    "attachments_uploaded": 1,
    "notification_sent": false,
    "notification_result": null
}
```

---

### 9.5 `GET /blogs/{id}` — Detail Blog

Menampilkan detail blog beserta attachments. Otomatis menambah `view_count`.

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": {
        "id": 1,
        "title": "Pengumuman Penting",
        "slug": "pengumuman-penting",
        "excerpt": "Ringkasan...",
        "content": "<p>Isi blog lengkap...</p>",
        "image": "blogs/pengumuman-penting.jpg",
        "image_thumbnail": "blogs/pengumuman-penting.jpg",
        "category": "announcement",
        "status": "published",
        "is_featured": true,
        "is_pinned": false,
        "author_npp": "10005",
        "author_name": "ADMIN SISTEM",
        "view_count": 43,
        "published_at": "2026-02-10T08:00:00Z",
        "attachments": [
            {
                "id": 1,
                "file_name": "lampiran.pdf",
                "file_path": "blog-attachments/lampiran.pdf",
                "file_type": "pdf",
                "file_size": 102400
            }
        ]
    }
}
```

**Response 404:**
```json
{
    "rcode": "81",
    "message": "Blog not found"
}
```

---

### 9.6 `GET /blogs/slug/{slug}` — Blog by Slug (Mobile Deep Linking)

Sama seperti `GET /blogs/{id}` tapi cari berdasarkan slug. Digunakan untuk deep linking dari notifikasi.

---

### 9.7 `PUT /blogs/{id}` — Update Blog

**Content-Type:** `multipart/form-data`

Semua field sama dengan `POST /blogs` tapi **semuanya opsional**. Hanya kirim field yang ingin diubah.

- Jika `title` berubah, `slug` otomatis di-regenerate
- Jika status berubah ke `published`, `published_at` otomatis di-set
- Upload `image` baru akan menghapus image lama
- Bisa upload `attachments[]` tambahan (ditambahkan, bukan menggantikan)

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Blog updated successfully",
    "data": { /* blog object + attachments */ },
    "attachments_uploaded": 0
}
```

---

### 9.8 `DELETE /blogs/{id}` — Hapus Blog

Menghapus blog beserta image dari storage. Attachments juga ikut terhapus.

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Blog deleted successfully",
    "deleted_title": "Pengumuman Libur Nasional"
}
```

---

### 9.9 `POST /blogs/{id}/publish` — Publish Blog

Publish blog draft dan opsional kirim push notification ke semua user via FCM.

**Request Body (opsional):**

| Field | Tipe | Keterangan |
|---|---|---|
| `send_notification` | bool | Kirim push notification (default: true) |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Blog published successfully",
    "data": { /* blog object dengan status = published */ },
    "notification_sent": true,
    "notification_result": {
        "success": true,
        "message_id": "projects/xxx/messages/123"
    }
}
```

> Notifikasi hanya dikirim jika blog **belum pernah dipublish** (bukan re-publish).

---

### 9.10 `POST /blogs/{id}/archive` — Arsipkan Blog

Ubah status blog menjadi `archived`.

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Blog archived successfully",
    "data": { /* blog object dengan status = archived */ }
}
```

---

### 9.11 `POST /blogs/{id}/attachments` — Upload Attachment

Upload lampiran file ke blog yang sudah ada.

**Content-Type:** `multipart/form-data`

**Request Body:**

| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `attachments[]` | file[] | ✅ | 1-10 file (pdf/doc/docx/xls/xlsx, max: 10MB/file) |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Attachments uploaded successfully",
    "attachments_uploaded": 2,
    "data": [
        {
            "id": 3,
            "blog_id": 1,
            "file_name": "surat-edaran.pdf",
            "file_path": "blog-attachments/surat-edaran.pdf",
            "file_type": "pdf",
            "file_size": 512000
        }
    ]
}
```

---

### 9.12 `DELETE /blogs/attachments/{id}` — Hapus Attachment

Hapus satu file attachment dari blog.

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Attachment deleted successfully",
    "deleted_file": "surat-edaran.pdf"
}
```

**Response 404:**
```json
{
    "rcode": "81",
    "message": "Attachment not found"
}

---

## 10. App Version Management

### 10.1 `GET /app-versions`

Daftar semua konfigurasi versi aplikasi (satu per platform).

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Success",
    "data": [
        {
            "id": 1,
            "platform": "android",
            "version_code": "1.0.2",
            "build_number": 5,
            "min_version_code": "1.0.0",
            "min_build_number": 1,
            "force_update": false,
            "update_message_id": "Silakan update aplikasi ke versi terbaru",
            "update_message_en": "Please update to the latest version",
            "store_url": "https://play.google.com/store/apps/details?id=com.app",
            "changelog": "Bug fixes dan peningkatan performa",
            "maintenance_mode": false,
            "maintenance_message": null,
            "is_active": true,
            "created_at": "2026-01-01T00:00:00Z",
            "updated_at": "2026-02-12T10:00:00Z"
        },
        {
            "id": 2,
            "platform": "ios",
            "version_code": "1.0.1",
            "build_number": 3,
            "min_version_code": "1.0.0",
            "min_build_number": 1,
            "force_update": false,
            "update_message_id": null,
            "update_message_en": null,
            "store_url": "https://apps.apple.com/app/...",
            "changelog": null,
            "maintenance_mode": false,
            "maintenance_message": null,
            "is_active": true,
            "created_at": "2026-01-01T00:00:00Z",
            "updated_at": "2026-02-10T08:00:00Z"
        }
    ]
}
```

---

### 10.2 `GET /app-versions/{id}`

Detail satu konfigurasi versi.

**Response 200:** Sama seperti satu item di response `10.1`.

**Response 404:**
```json
{
    "rcode": "81",
    "message": "Version config not found"
}
```

---

### 10.3 `POST /app-versions`

Buat atau update konfigurasi versi. Menggunakan **upsert** — satu record per platform.

**Request Body:**
```json
{
    "platform": "android",
    "version_code": "1.0.3",
    "build_number": 6,
    "min_version_code": "1.0.0",
    "min_build_number": 1,
    "force_update": false,
    "update_message_id": "Silakan update ke versi terbaru",
    "update_message_en": "Please update to the latest version",
    "store_url": "https://play.google.com/store/apps/details?id=com.app",
    "changelog": "- Fitur baru: notifikasi\n- Bug fixes",
    "maintenance_mode": false,
    "maintenance_message": null
}
```

| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `platform` | string | ✅ | `android` / `ios` |
| `version_code` | string | ✅ | Versi terbaru (e.g., `1.0.3`, max: 20) |
| `build_number` | int | ✅ | Build number terbaru (min: 1) |
| `min_version_code` | string | ✅ | Versi minimum yang diizinkan (max: 20) |
| `min_build_number` | int | ✅ | Build number minimum (min: 1) |
| `force_update` | bool | ❌ | Paksa update? (default: false) |
| `update_message_id` | string | ❌ | Pesan update (Bahasa Indonesia) |
| `update_message_en` | string | ❌ | Pesan update (English) |
| `store_url` | string | ❌ | URL download di Play Store / App Store (max: 500) |
| `changelog` | string | ❌ | Catatan perubahan |
| `maintenance_mode` | bool | ❌ | Mode maintenance? (default: false) |
| `maintenance_message` | string | ❌ | Pesan saat maintenance |

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Version config saved successfully",
    "data": { /* data versi yang disimpan */ }
}
```

---

### 10.4 `PUT /app-versions/{id}`

Update data versi. Field sama dengan `POST /app-versions`.

---

### 10.5 `POST /app-versions/{id}/toggle-force-update`

Toggle on/off force update. Tidak perlu request body.

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Force update enabled",
    "data": {
        "id": 1,
        "platform": "android",
        "force_update": true,
        "version_code": "1.0.3",
        "build_number": 6
    }
}
```

---

### 10.6 `POST /app-versions/{id}/toggle-maintenance`

Toggle on/off maintenance mode. Opsional kirim pesan baru.

**Request Body (opsional):**
```json
{
    "message": "Sedang dalam perbaikan sistem, estimasi 1 jam"
}
```

**Response 200:**
```json
{
    "rcode": "00",
    "message": "Maintenance mode enabled",
    "data": {
        "id": 1,
        "platform": "android",
        "maintenance_mode": true,
        "maintenance_message": "Sedang dalam perbaikan sistem, estimasi 1 jam"
    }
}
```

---

## 11. Response Codes

### Standard Response Codes

| rcode | HTTP Status | Keterangan |
|---|---|---|
| `00` | 200/201 | Berhasil |
| `01` | 422 | Validasi gagal |
| `81` | 404 | Data tidak ditemukan |
| `82` | 400 | Duplikat / konflik |
| `83` | 400 | Business rule error |
| `99` | 500 | Server error |

### Error Response Format

**Validation Error (422):**
```json
{
    "rcode": "01",
    "message": "Validation error",
    "errors": {
        "field_name": ["Pesan error detail"]
    }
}
```

**Not Found (404):**
```json
{
    "rcode": "81",
    "message": "User tidak ditemukan"
}
```

**Server Error (500):**
```json
{
    "rcode": "99",
    "message": "Error: [detail error]"
}
```

---

## Catatan Penting

1. **Header Authorization:**
   ```
   Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGci...
   ```

2. **Content-Type:** Selalu gunakan `application/json` kecuali upload file (`multipart/form-data`).

3. **Paginasi:** Semua endpoint list mendukung `page` dan `per_page` parameter.

4. **Rate Limiting:**
   - Login: 5 request/menit
   - Endpoint lain: 60 request/menit

5. **Image/File URL:** Semua path file harus di-prefix dengan `{base_url}/storage/`
